Files
LibreLedger/README.md
T
LibrePortal 0759c5dbfd License LibreLedger under AGPL-3.0-or-later
The README now says AGPL-3.0-or-later, and the image label changes from
AGPL-3.0-only to match. The published 1.0.0 image keeps the old label; the
new one ships with the next image build (1.0.1). The vendored noble
libraries stay MIT.

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

138 lines
6.5 KiB
Markdown

# 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
- `<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](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.<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
```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 AGPL-3.0-or-later](LICENSE): version 3 of the GNU Affero General Public
License, or (at your option) any later version. The vendored noble libraries
are MIT-licensed (their `LICENSE` files are next to them); bank logos are
trademarks of their owners (`banks/NOTICE.txt`).