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>
This commit is contained in:
LibrePortal
2026-09-17 01:17:16 +01:00
commit 2cffc3ab07
45 changed files with 11114 additions and 0 deletions
+44
View File
@@ -0,0 +1,44 @@
# LibreLedger — the app plus its persistence server (server.py, standard
# library only). The browser encrypts; the container only stores ciphertext.
#
# Build: docker build -t libreledger .
# Run: docker run --rm -p 8080:8080 -v libreledger-data:/data libreledger
# Open: http://localhost:8080
#
# Base image pinned by version AND index digest (multi-arch). Bump both together.
FROM python:3.14.7-alpine3.24@sha256:c6ead215bfd31f1e433d968853b7a769989117115b728874824e6c0a27cb96fc
ARG VERSION=1.0.0
LABEL org.opencontainers.image.title="LibreLedger" \
org.opencontainers.image.description="Encrypted personal money ledger: your data is encrypted in your browser; the server never sees it." \
org.opencontainers.image.version="${VERSION}" \
org.opencontainers.image.source="https://git.libreportal.org/LibrePortal/LibreLedger" \
org.opencontainers.image.licenses="AGPL-3.0-only"
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
LIBRELEDGER_HOST=0.0.0.0 \
LIBRELEDGER_PORT=8080 \
LIBRELEDGER_DATA_DIR=/data
# Unprivileged by default. LibrePortal may override the uid so the process
# owns its bind mount (under rootless Docker that is container root, which is
# the unprivileged install user on the host).
RUN addgroup -S -g 10001 libreledger \
&& adduser -S -D -H -u 10001 -G libreledger -s /sbin/nologin libreledger \
&& mkdir -p /data \
&& chown libreledger:libreledger /data \
&& chmod 700 /data
WORKDIR /app
COPY --chmod=0444 server.py index.html app.js theme.js crypto-fallback.js styles.css favicon.svg ./
COPY --chmod=0444 banks/*.svg ./banks/
COPY --chmod=0444 vendor/ ./vendor/
RUN find /app -type d -exec chmod 0555 {} +
USER libreledger
EXPOSE 8080
VOLUME ["/data"]
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
CMD ["python3", "-c", "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8080/api/health', timeout=4).status == 200 else 1)"]
CMD ["python3", "/app/server.py"]