# LibreLedger An encrypted personal money ledger: **your data is encrypted in your browser; the server never sees it.** One screen, one continuous running balance: - **Balances** at the top: your accounts and their balances, with a grand total. That total is where the ledger *opens*. - **Monthly ledger** below: one block per month, a table of `Date · Type · Description · In · Out · Balance`. The balance runs on from your account balances and **carries from each month into the next**. - **Type** comes from the row: money *in* shows a green **Money** badge, money *out* an amber **Cost** badge. - Each row has a **category** (emoji and colour) for tagging entries at a glance. - Insert or remove rows anywhere, and add or remove whole months. - **Recurring** bills and income (rent, phone, salary…), each with an amount, a category and a schedule: - **Monthly** on a chosen day, **every N weeks** from an anchor date, or **yearly** on a month and day. - **🔁 Add recurring** drops every matching occurrence into a month as real, editable rows; **+ Add month** moves to the next calendar month and pre-fills it. - **Totals**, **Savings** and **Housing affordability** summaries, several **budgets** in one ledger, CSV and PDF export. - **Dark / light theme**, dark by default. ## Zero-knowledge encryption - Everything is encrypted **in the browser** with **AES-256-GCM**. The key is derived from your passphrase with **PBKDF2-HMAC-SHA256, 250,000 iterations** and a random 16-byte salt; each save uses a fresh random 12-byte IV. - The server (`server.py`) only stores and returns the resulting blob, `{ "v": 1, "salt": …, "iv": …, "ct": … }` (base64). It never receives the passphrase, the key or any figure from the ledger. - There is no account and no recovery. **Lose the passphrase and the data is gone**; that is the point. - The page loads nothing from anywhere else. The server sends a strict Content-Security-Policy (`'self'` only), `nosniff`, frame denial and `no-referrer`, and serves only the app's own files. ### Plain http on your LAN or VPN Browsers only give pages their built-in encryption (Web Crypto) in a *secure context*: HTTPS or `http://localhost`. Opened as `http://192.168.x.x:port` or over a VPN address, LibreLedger switches to its **built-in fallback**: the same PBKDF2-SHA256 and AES-256-GCM from the audited [@noble/hashes](https://github.com/paulmillr/noble-hashes) and [@noble/ciphers](https://github.com/paulmillr/noble-ciphers) libraries, vendored and pinned in [`vendor/`](vendor/README.md). The files it writes are identical in format, so a ledger saved one way opens the other way. The lock screen says when the fallback is in use. Unlocking takes a few seconds longer, and the key sits in page memory rather than in the browser's key store, so **prefer HTTPS** whenever you have it. Plain http also means anyone on the network path can see (encrypted) traffic and could tamper with the page itself, so only use it on a network you trust. ## Self-hosting ### Docker ```bash docker run -d --name libreledger -p 127.0.0.1:8080:8080 \ -v libreledger-data:/data \ git.libreportal.org/libreportal/libreledger:1.0.0 # open http://localhost:8080 ``` Or build it yourself: `docker build -t libreledger .` The image runs `server.py` on port 8080 as an unprivileged user, keeps its data in the `/data` volume and has a health check on `/api/health`. Put HTTPS in front of it (Caddy, Traefik, nginx…) when it is reachable from anywhere but your own machine. ### Without Docker ```bash python3 server.py # http://127.0.0.1:8080, data in ./data python3 server.py --host 0.0.0.0 --port 8080 --data-dir /srv/libreledger ``` Python 3.9 or newer, standard library only. | Option | Environment | Default | | --- | --- | --- | | `--host` | `LIBRELEDGER_HOST` | `127.0.0.1` | | `--port` | `LIBRELEDGER_PORT` | `8080` | | `--data-dir` | `LIBRELEDGER_DATA_DIR` | `./data` | | `--backup-interval` | `LIBRELEDGER_BACKUP_INTERVAL` | `600` seconds (`0`: every save) | | `--backup-keep-recent` | `LIBRELEDGER_BACKUP_KEEP_RECENT` | `10` | | `--backup-keep-daily` | `LIBRELEDGER_BACKUP_KEEP_DAILY` | `30` | ### Your data - `/ledger.enc` is the ledger, written atomically on every change. - `/backups/ledger-.enc` are earlier versions: the newest 10, plus the last one of each of the most recent 30 days. At most one is taken per backup interval. - All of them are encrypted with the passphrase that was current when they were written. To restore one, stop the server and copy it over `ledger.enc`, or use **Restore** in the app. - To move a ledger to another server, copy `ledger.enc` into its data folder and unlock it with your passphrase. Files from the earlier "Money Ledger" version of this app work as they are. The server has no login of its own: anyone who can reach it can fetch the encrypted file or overwrite it (the backups cover the latter). Keep it on a private network, a VPN, or behind an authenticating proxy. ### Install on LibrePortal LibreLedger is in the [LibrePortal](https://git.libreportal.org/LibrePortal/LibrePortal) app catalog: `libreportal app install libreledger`, or **Apps → LibreLedger** in the WebUI. It installs *private* (reachable only from your trusted places), with its data included in LibrePortal's backups, and on `https://ledger.` when a domain is set up. ## In the app - **💾 Saved to disk**: the encrypted ledger is saving to the server. - **🔗 Link file** *(Chrome/Edge desktop, when opened without the server)*: auto-save to an encrypted file you choose. - **Export CSV / PDF**: plaintext exports of the decrypted data. Handle them with care. - **Backup / Restore**: download or load an *encrypted* copy. - **Change passphrase**: re-encrypts with a new passphrase. Older backups keep the old one. - **Lock**: forgets the key and the data; the passphrase is needed again. ## Development ```bash python3 tests/test_server.py # server: allowlist, headers, checks, backups node tests/crypto-interop.test.mjs # Web Crypto <-> fallback interop (Node 22.12+) ``` Files: `index.html` (markup and lock screen), `styles.css`, `app.js` (state, rendering and the crypto layer), `theme.js`, `crypto-fallback.js`, `server.py`, `banks/` (logos, see `banks/NOTICE.txt`), `vendor/`. ## Licence [GNU AGPLv3](LICENSE). The vendored noble libraries are MIT-licensed (their `LICENSE` files are next to them); bank logos are trademarks of their owners (`banks/NOTICE.txt`).