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>
138 lines
6.5 KiB
Markdown
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`).
|