Resumen
Un archivo .arca es una base de datos SQLite completa envuelta en una cabecera de 8 bytes. Si el archivo está protegido con contraseña, la base va cifrada con AES-256-GCM usando una clave derivada con Argon2id.
- Sin contraseña: quita los primeros 8 bytes y tendrás un archivo SQLite normal que abre cualquier herramienta (
sqlite3, DB Browser for SQLite, Python, Excel vía ODBC…). - Con contraseña: deriva la clave (Argon2id), descifra (AES-256-GCM) y obtendrás el mismo archivo SQLite.
El contenedor
Todos los enteros son little-endian.
Cabecera (primeros 8 bytes, siempre presente)
| Offset | Tamaño | Campo |
|---|---|---|
| 0 | 4 | Magia: los bytes ASCII ARCA |
| 4 | 2 | Versión del contenedor (u16) — actualmente 1 |
| 6 | 2 | Banderas (u16). Bit 0 = cifrado |
Sin cifrar (bit 0 = 0)
Desde el offset 8 hasta el final del archivo está la base SQLite tal cual — empieza con SQLite format 3\0.
Cifrado (bit 0 = 1)
| Offset | Tamaño | Campo |
|---|---|---|
| 8 | 1 | KDF: 1 = Argon2id v1.3 |
| 9 | 4 | m_cost en KiB (u32) — por defecto 65 536 (64 MiB) |
| 13 | 4 | t_cost (u32) — por defecto 3 |
| 17 | 4 | p_cost (u32) — por defecto 1 |
| 21 | 16 | Sal (aleatoria, se renueva al cambiar la contraseña) |
| 37 | 12 | Nonce de AES-GCM (aleatorio, nuevo en cada guardado) |
| 49 | … | Texto cifrado: base SQLite cifrada + etiqueta GCM de 16 bytes al final |
- Clave =
Argon2id(contraseña en UTF-8, sal, m_cost, t_cost, p_cost), salida de 32 bytes. - Los 49 bytes de cabecera completos se usan como datos asociados (AAD) de AES-GCM: si alguien altera los parámetros, el descifrado falla directamente en vez de degradar la seguridad en silencio.
Ejemplo en Python
Esto es literalmente todo lo que hace falta para sacar un SQLite normal de un archivo .arca, con o sin contraseña:
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("mi presupuesto.arca", "rb").read()
assert data[:4] == b"ARCA"
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"mi contraseña", salt, t, m, p, 32, Type.ID)
db = AESGCM(key).decrypt(nonce, data[49:], data[:49])
else:
db = data[8:]
open("presupuesto.sqlite", "wb").write(db)
print(sqlite3.connect("presupuesto.sqlite").execute("select count(*) from transactions").fetchone())
Cómo se escribe en disco
Arca nunca modifica el archivo "en vivo". Mantiene la base en memoria y, en cada guardado:
- escribe el contenedor completo en
archivo.arca.tmpy lo sincroniza a disco; - renombra el
.arcaactual aarchivo.arca.bak; - renombra el
.tmpaarchivo.arca.
Por eso siempre existe al menos una copia completa en disco, y las carpetas sincronizadas (Dropbox, OneDrive, iCloud Drive, Syncthing) nunca ven un archivo a medio escribir. Mientras el presupuesto está abierto también existe un archivo archivo.arca.lock — un candado para que dos ventanas no escriban a la vez — que se borra al cerrar.
Si al guardar Arca detecta que el archivo en disco cambió desde que lo abrió (otra computadora, el cliente de Dropbox poniéndose al día), no lo sobrescribe: pregunta si quieres conservar tu versión, cargar la del disco o guardar como archivo nuevo.
Esquema SQLite (versión 5)
La tabla meta guarda schema_version, budget_name, base_currency, mode (simple/full), locale y created_at. Puede tener otras claves internas además de esas — se pueden ignorar sin problema.
| Tabla | Qué guarda |
|---|---|
currencies(code, exponent, symbol, name) | Monedas usadas en el presupuesto. |
fx_rates(code, as_of, rate, source) | Historial de tasas (source: manual, online o ecb). |
accounts(id, name, kind, currency, closed, sort, note, created_at) | Cuentas. kind: checking, savings, cash, credit, other. |
category_groups(id, name, sort, hidden) | Grupos de sobres. |
categories(id, group_id, name, icon, sort, hidden, goal_kind, goal_amount, goal_date, debt_account_id) | Sobres y su meta opcional (target, by_date, monthly; monto en moneda base). (v4) debt_account_id: si no es NULL, es el sobre de deuda de esa tarjeta de crédito. |
assignments(month, category_id, amount) | Dinero asignado a un sobre en un mes (moneda base). |
transactions(id, account_id, date, amount, payee, memo, category_id, kind, transfer_id, fx_rate, amount_base, cleared, fitid, import_id, created_at) | Movimientos. Ver abajo. |
imports(id, account_id, file_name, imported_at, count) | Registro de cada importación. |
payee_memory(payee_norm, category_id, income, uses) | Último sobre usado por comercio (para sugerir). |
rules(id, match_kind, pattern, category_id, rename_to, sort) | Reglas de categorización. |
csv_profiles(account_id, mapping) | Mapeo de columnas CSV recordado por cuenta (JSON). |
scheduled(id, account_id, payee, memo, amount, kind, category_id, transfer_account_id, frequency, anchor_day, next_date, end_date, created_at) | Movimientos programados. frequency: weekly, biweekly, monthly, yearly; anchor_day es el día original del mes, así que "cada 31" cae el último día de los meses cortos. Nunca se registran solos — la app los muestra como próximos y tú los confirmas o los saltas. |
Convenciones
- Montos: enteros en unidades menores de su moneda (
1234= 12,34 USD;1500= 1 500 CLP). La cantidad de decimales de cada moneda está encurrencies.exponent. - Fechas: texto ISO
AAAA-MM-DD. Meses:AAAA-MM. - Ids: UUID v7 en texto.
- Tasas: texto decimal exacto, "unidades de la moneda base por 1 unidad de la moneda."
Movimientos
amount: en la moneda de la cuenta, con signo (negativo = salida).amount_base: el mismo movimiento en la moneda base, congelado con la tasafx_ratevigente al registrarlo. Cambiar tasas después no reescribe la historia.kind:normal— gasto o reembolso; afecta al sobrecategory_id(o queda "sin sobre" si esNULL).income— ingreso; va a "Por asignar."starting— saldo inicial de una cuenta; va a "Por asignar."transfer— una de las dos patas de una transferencia entre cuentas; ambas compartentransfer_idy tienenamount_baseexactamente opuestos, así que no crean ni destruyen dinero.
Cómo se calcula el presupuesto
Para cada sobre y mes: disponible = arrastre + asignado + actividad, donde actividad es la suma de amount_base de sus movimientos normal del mes. Un saldo positivo pasa al mes siguiente; uno negativo (sobregasto) se reinicia a cero y se descuenta de "Por asignar" del mes siguiente.
Sobres de deuda (v4): el saldo inicial (starting) de una tarjeta puede tener category_id apuntando a su sobre de deuda. En ese caso cuenta como actividad del sobre y no como ingreso de "Por asignar", y el saldo negativo del sobre sí pasa al mes siguiente (es deuda previa por cubrir, no sobregasto).
Por asignar(mes) = ingresos hasta el mes − asignado hasta el mes − asignado en meses futuros − sobregastos de meses anteriores + movimientos sin sobre hasta el mes.
Esto garantiza: Por asignar + Σ disponible = saldo total de las cuentas (a tasas congeladas) − asignado a futuro.
Compatibilidad
- Un Arca más nuevo siempre abre archivos de versiones anteriores (migra el esquema al guardar).
- Un Arca más viejo se niega a abrir un archivo con
schema_versionmayor al que conoce, en lugar de arriesgarse a dañarlo.
Desde la versión 0.1.3 ni siquiera hace falta: Ajustes → Seguridad y respaldo → Todo, como SQLite escribe el archivo SQLite plano directamente, sin contenedor ni cabecera que quitar.