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ção | Tamanho | Campo |
|---|---|---|
| 0 | 4 | Bytes mágicos: ASCII ARCA (nome histórico, mantido por compatibilidade) |
| 4 | 2 | Versão do contêiner (u16) — atualmente 1 |
| 6 | 2 | Flags (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ção | Tamanho | Campo |
|---|---|---|
| 8 | 1 | KDF: 1 = Argon2id v1.3 |
| 9 | 4 | m_cost em KiB (u32) — padrão 65.536 (64 MiB) |
| 13 | 4 | t_cost (u32) — padrão 3 |
| 17 | 4 | p_cost (u32) — padrão 1 |
| 21 | 16 | Salt (aleatório, renovado sempre que a senha muda) |
| 37 | 12 | Nonce 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:
- grava o contêiner completo em
arquivo.ensobre.tmp(arquivo.arca.tmpnos orçamentos antigos) e força a gravação no disco; - renomeia o arquivo atual para
arquivo.ensobre.bak; - renomeia o arquivo
.tmpde volta paraarquivo.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.
| Tabela | O 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á emcurrencies.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 nafx_ratevigente quando foi registrado. Mudar as taxas depois não reescreve a história.kind:normal— um gasto ou um estorno; afeta o envelope decategory_id(ou fica «sem envelope» se forNULL).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 compartilhamtransfer_ide têm valores deamount_baseexatamente 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çãonormalouincomecompleta, 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 ofitiddo 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
.arcaantigos 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_versionmaior 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.