Referencia

El formato de archivo .arca

Tus datos son tuyos. Este documento describe el archivo .arca con suficiente detalle para que cualquier persona pueda leerlo sin Arca, incluso si el proyecto dejara de existir. Nada propietario, nada oculto.

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)

OffsetTamañoCampo
04Magia: los bytes ASCII ARCA
42Versión del contenedor (u16) — actualmente 1
62Banderas (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)

OffsetTamañoCampo
81KDF: 1 = Argon2id v1.3
94m_cost en KiB (u32) — por defecto 65 536 (64 MiB)
134t_cost (u32) — por defecto 3
174p_cost (u32) — por defecto 1
2116Sal (aleatoria, se renueva al cambiar la contraseña)
3712Nonce 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:

  1. escribe el contenedor completo en archivo.arca.tmp y lo sincroniza a disco;
  2. renombra el .arca actual a archivo.arca.bak;
  3. renombra el .tmp a archivo.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.

TablaQué 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á en currencies.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 tasa fx_rate vigente al registrarlo. Cambiar tasas después no reescribe la historia.
  • kind:
    • normal — gasto o reembolso; afecta al sobre category_id (o queda "sin sobre" si es NULL).
    • 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 comparten transfer_id y tienen amount_base exactamente 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_version mayor 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.