Referência

O formato de arquivo .ensobre

Os seus dados são seus. Este documento descreve o arquivo de orçamento do Ensobre (.ensobre; as versões antigas usavam .arca) com detalhe suficiente para que qualquer pessoa consiga lê-lo sem o Ensobre, mesmo que o projeto desaparecesse. Sem truques proprietários, sem nada escondido.

Resumo

Um arquivo de orçamento do Ensobre (.ensobre; as versões antigas usavam .arca) é um banco de dados SQLite completo envolvido por um cabeçalho de 8 bytes. Se o arquivo tem senha, o banco é criptografado com AES-256-GCM, usando uma chave derivada com Argon2id.

  • Sem senha: tire os primeiros 8 bytes e você tem um arquivo SQLite normal, que abre em qualquer ferramenta comum (sqlite3, DB Browser for SQLite, Python, Excel via ODBC…).
  • Com senha: derive a chave (Argon2id), descriptografe (AES-256-GCM) e você obtém o mesmo arquivo SQLite.

O contêiner

Todos os inteiros são little-endian.

Cabeçalho (primeiros 8 bytes, sempre presente)

PosiçãoTamanhoCampo
04Bytes mágicos: ASCII ARCA (nome histórico, mantido por compatibilidade)
42Versão do contêiner (u16) — atualmente 1
62Flags (u16). Bit 0 = criptografado

Sem criptografia (bit 0 = 0)

Da posição 8 até o fim do arquivo está o banco SQLite como ele é — começa com SQLite format 3\0.

Criptografado (bit 0 = 1)

PosiçãoTamanhoCampo
81KDF: 1 = Argon2id v1.3
94m_cost em KiB (u32) — padrão 65.536 (64 MiB)
134t_cost (u32) — padrão 3
174p_cost (u32) — padrão 1
2116Salt (aleatório, renovado sempre que a senha muda)
3712Nonce do AES-GCM (aleatório, novo a cada salvamento)
49…Texto cifrado: o banco SQLite criptografado + a tag GCM de 16 bytes no final
  • Chave = Argon2id(senha em UTF-8, salt, m_cost, t_cost, p_cost), com saída de 32 bytes.
  • O cabeçalho completo de 49 bytes é usado como dado associado (AAD) do AES-GCM: se alguém mexer nos parâmetros, a descriptografia falha de vez em vez de enfraquecer a segurança em silêncio.

Exemplo em Python

É literalmente só isto que você precisa para tirar um arquivo SQLite comum de dentro de um arquivo de orçamento do Ensobre (.ensobre, ou o antigo .arca), com ou sem senha:

from argon2.low_level import hash_secret_raw, Type   # pip install argon2-cffi
from cryptography.hazmat.primitives.ciphers.aead import AESGCM  # pip install cryptography
import struct, sqlite3

data = open("meu orcamento.ensobre", "rb").read()
assert data[:4] == b"ARCA"  # bytes mágicos históricos, mantidos por compatibilidade
flags = struct.unpack_from("<H", data, 6)[0]
if flags & 1:
    m, t, p = struct.unpack_from("<III", data, 9)
    salt, nonce = data[21:37], data[37:49]
    key = hash_secret_raw(b"minha senha", salt, t, m, p, 32, Type.ID)
    db = AESGCM(key).decrypt(nonce, data[49:], data[:49])
else:
    db = data[8:]
open("orcamento.sqlite", "wb").write(db)
print(sqlite3.connect("orcamento.sqlite").execute("select count(*) from transactions").fetchone())

Como é gravado no disco

O Ensobre nunca modifica o arquivo «ao vivo». Ele mantém o banco na memória e, a cada salvamento:

  1. grava o contêiner completo em arquivo.ensobre.tmp (arquivo.arca.tmp nos orçamentos antigos) e força a gravação no disco;
  2. renomeia o arquivo atual para arquivo.ensobre.bak;
  3. renomeia o arquivo .tmp de volta para arquivo.ensobre.

Isso significa que sempre existe pelo menos uma cópia completa no disco, e as pastas sincronizadas (Dropbox, OneDrive, iCloud Drive, Syncthing) nunca veem um arquivo gravado pela metade. Enquanto um orçamento está aberto, também existe um arquivo arquivo.ensobre.lock — um bloqueio para que duas janelas não gravem ao mesmo tempo — que é removido ao fechar.

Se o Ensobre perceber, ao salvar, que o arquivo no disco mudou desde que foi aberto (outro computador, o cliente do Dropbox se atualizando), ele não o sobrescreve: pergunta se você quer manter a sua versão, carregar a que está no disco ou salvar como um novo arquivo.

Esquema SQLite (versão 6)

A tabela meta guarda schema_version, budget_name, base_currency, mode (simple/full), locale e created_at. Ela pode conter outras chaves internas além dessas — pode ignorá-las com segurança. Três são opcionais e informativas, e só existem se o orçamento foi criado mudando a moeda principal: rebased_from (a moeda principal anterior), rebased_rate (quantas unidades da moeda nova valia 1 unidade da antiga: a taxa única usada) e rebased_at (data, YYYY-MM-DD). Outra chave opcional, scheduled_paused:<id do agendamento>, existe enquanto aquela transação agendada está pausada; o valor dela é a data da pausa (YYYY-MM-DD). Uma série pausada mantém a sua frequência e a próxima data, mas não aparece como próxima nem como atrasada e fica de fora das projeções e da exportação para o calendário. Uma chave com valor ilegível, ou de um agendamento que não existe mais, é ignorada. As versões antigas do app não conhecem essa chave, então mostram o agendamento como ativo.

TabelaO que guarda
currencies(code, exponent, symbol, name)Moedas usadas no orçamento.
fx_rates(code, as_of, rate, source)Histórico de taxas (source: manual, online ou ecb).
accounts(id, name, kind, currency, closed, sort, note, created_at)Contas. kind: checking, savings, cash, credit, other.
category_groups(id, name, sort, hidden)Grupos de envelopes.
categories(id, group_id, name, icon, sort, hidden, goal_kind, goal_amount, goal_date, debt_account_id)Envelopes e a meta opcional de cada um (target, by_date, monthly; valor na moeda principal). (v4) debt_account_id: quando não é NULL, este é o envelope da dívida daquele cartão de crédito.
assignments(month, category_id, amount)Dinheiro distribuído a um envelope em um determinado mês (moeda principal).
transactions(id, account_id, date, amount, payee, memo, category_id, kind, transfer_id, fx_rate, amount_base, cleared, fitid, import_id, created_at, split_group)Movimentos. Veja abaixo. split_group é (v6).
imports(id, account_id, file_name, imported_at, count)Registro de cada importação.
payee_memory(payee_norm, category_id, income, uses)Último envelope usado com cada estabelecimento (para sugestões).
rules(id, match_kind, pattern, category_id, rename_to, sort)Regras de categorização.
csv_profiles(account_id, mapping)Mapeamento de colunas do CSV lembrado por conta (JSON).
scheduled(id, account_id, payee, memo, amount, kind, category_id, transfer_account_id, frequency, anchor_day, next_date, end_date, created_at)Transações agendadas. frequency: weekly, biweekly, monthly, yearly; anchor_day é o dia original do mês, então «todo dia 31» cai no último dia dos meses curtos. Nunca são lançadas sozinhas — o app as mostra como próximas e você confirma ou pula cada uma.

Convenções

  • Valores: inteiros na menor unidade da moeda (1234 = 12,34 USD; 1500 = 1.500 CLP). O número de casas decimais de cada moeda está em currencies.exponent.
  • Datas: texto ISO YYYY-MM-DD. Meses: YYYY-MM.
  • Ids: UUID v7 como texto.
  • Taxas: texto decimal exato, «unidades da moeda principal por 1 unidade da moeda».

Transações

  • amount: na moeda da própria conta, com sinal (negativo = saída de dinheiro).
  • amount_base: o mesmo movimento na moeda principal, congelado na fx_rate vigente quando foi registrado. Mudar as taxas depois não reescreve a história.
  • kind:
    • normal — um gasto ou um estorno; afeta o envelope de category_id (ou fica «sem envelope» se for NULL).
    • income — receita; vai para «A distribuir».
    • starting — o saldo inicial de uma conta; vai para «A distribuir».
    • transfer — uma das duas pontas de uma transferência entre contas; as duas pontas compartilham transfer_id e têm valores de amount_base exatamente opostos, então nenhum dinheiro é criado nem destruído.
  • split_group (v6): as partes de um gasto dividido entre vários envelopes compartilham este id. Cada parte é uma transação normal ou income completa, com o próprio envelope e valor, então a matemática acima não muda. Todas as partes têm a mesma conta, data, estabelecimento, nota, taxa e estado de conciliação, e o total do gasto é a soma das partes. Só a primeira (a mais antiga) guarda o fitid do banco.

Como funciona a matemática do orçamento

Para cada envelope e mês: available = carryover + assigned + activity, em que activity é a soma de amount_base das transações normal dele naquele mês. Um saldo positivo passa para o mês seguinte; um negativo (gasto a mais) volta a zero e é descontado do «A distribuir» do mês seguinte.

Envelopes de dívida (v4): o saldo inicial de um cartão de crédito (starting) pode ter um category_id apontando para o envelope da dívida dele. Aí ele conta como transação desse envelope, em vez de receita de «A distribuir», e o saldo negativo do envelope passa de um mês para o outro (é dívida anterior sendo paga, não gasto a mais).

A distribuir(mês) = receitas até o mês − distribuído até o mês − distribuído em meses futuros − gasto a mais de meses anteriores + transações sem envelope até o mês.

Isso garante que: A distribuir + Σ disponível = saldo total das contas (nas taxas congeladas) − valor distribuído para meses futuros.

Compatibilidade

  • Os arquivos .arca antigos continuam abrindo normalmente no Ensobre; os orçamentos novos são salvos como .ensobre.
  • Um Ensobre mais novo sempre abre arquivos de versões mais antigas (ele migra o esquema ao salvar).
  • Um Ensobre mais antigo se recusa a abrir um arquivo com schema_version maior do que o que conhece, em vez de arriscar corrompê-lo.

Desde a versão 0.1.3 você nem precisa disto: Configurações → Segurança e backup → Tudo, em SQLite grava o arquivo SQLite comum diretamente — sem contêiner, sem cabeçalho para tirar.