LibrePortal 2cffc3ab07 Start LibreLedger: an encrypted money ledger that stores only ciphertext
LibreLedger is a single-person money ledger. The browser encrypts
everything (PBKDF2-HMAC-SHA256 with 250k iterations, then AES-256-GCM),
and a small standard-library Python server stores only the resulting
{v, salt, iv, ct} blob. Ledger files from the earlier Money Ledger version
open unchanged.

- On plain http to a LAN or VPN address, where browsers hide Web Crypto,
  the app switches to vendored @noble/hashes and @noble/ciphers 2.2.0 and
  says so on the lock screen. tests/crypto-interop.test.mjs shows both
  paths read each other's files.
- The server hands out only the app's own files, sends a self-only CSP,
  nosniff, frame denial and no-referrer, checks the shape of each blob,
  refuses cross-site writes and writes atomically. It keeps the last 10
  backups plus one per day for 30 days.
- The container runs that server from a digest-pinned python alpine
  image, as an unprivileged user, with its data in /data and a health
  check on /api/health.

Assisted-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 01:17:16 +01:00

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 and @noble/ciphers libraries, vendored and pinned in vendor/. 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

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

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

  • <data-dir>/ledger.enc is the ledger, written atomically on every change.
  • <data-dir>/backups/ledger-<UTC time>.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 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.<your domain> 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

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. The vendored noble libraries are MIT-licensed (their LICENSE files are next to them); bank logos are trademarks of their owners (banks/NOTICE.txt).

S
Description
Encrypted personal money ledger: your data is encrypted in your browser; the server never sees it.
Readme AGPL-3.0
246 KiB
Languages
JavaScript 61.3%
CSS 26.9%
Python 8.9%
HTML 2%
Dockerfile 0.9%