Initial commit: Bitwarden export to PDF renderer

Adds the render.py WeasyPrint renderer, the pod-based bitwarden-print.sh
wrapper, a Containerfile, a sample export (JSON + PDF), README and LICENSE.
This commit is contained in:
juergen committed 2026-11-13 14:09:10 +01:00
commit 41385b35e6
8 files changed
+1041

No files matched your search

+117
View File
@@ -0,0 +1,117 @@
# bitwarden-print
Render a Bitwarden `.json` export as a dense PDF: a summary, a page-numbered
index, and one compact block per vault item that prints **every** value in the
export — known fields first (username, password, TOTP, card, identity, SSH key,
custom fields, notes, history, attachments), then everything else
alphabetically, including unknown/extra keys and empty objects/lists.
Everything runs in a throwaway Podman pod on this machine; the vault never
leaves it.
## Requirements
- Podman (tested with 6.x)
- On macOS: `podman machine start` (the default machine is used)
## Quick start
```sh
./bitwarden-print.sh ~/Downloads/bitwarden_export_20261113.json
# -> ~/Downloads/bitwarden_export_20261113.pdf
```
Custom output and options:
```sh
./bitwarden-print.sh vault.json -o ~/Desktop/vault.pdf \
--paper letter --sort name --title "Vault 2026-11-13"
```
Mask secrets instead of printing them:
```sh
./bitwarden-print.sh vault.json --mask-secrets
```
Options:
| Option | Meaning |
| --- | --- |
| `-o, --output FILE` | output PDF (default: next to the export, same basename) |
| `--sort folder\|name\|type\|as-is` | item order (default: `folder`, then name) |
| `--title TEXT` | document title (also the page footer) |
| `--paper a4\|letter` | page size (default: `a4`) |
| `--mask-secrets` | mask `login.password`, `login.totp`, `card.number`, `card.code`, `identity.ssn`, `sshKey.privateKey`, …; values keep a length hint |
## What the PDF contains
1. **Header** — source file, item count, value count, timestamp.
2. **Summary** — export metadata, item/type/folder counts, custom field,
attachment and password-history totals, created/revised date ranges, plus a
collections table if the export has one.
3. **Index** — one line per item (type, name, folder, username) with a real
page number, clickable in the PDF.
4. **Items** — for each item a heading (number, type badge, name, folder) and a
two-column table of `json.path` → value. Values wrap, newlines in notes are
preserved, and empty values (`null`, `""`, `{}`, `[]`) are shown dimmed so
nothing silently disappears.
Items are not truncated: an export with many entries produces many pages, but
each line is only a few millimetres tall.
## How it runs
```
./bitwarden-print.sh
-> podman build -t localhost/bitwarden-print:latest . # Python + WeasyPrint image
-> podman pod create --name bitwarden-print-pod
-> podman create --pod bitwarden-print-pod ... bitwarden-print-run
-> podman cp export.json bitwarden-print-run:/export.json # no bind mounts
-> podman start -a bitwarden-print-run # render.py -> /output.pdf
-> podman cp bitwarden-print-run:/output.pdf <output>
-> pod, container and temp file are removed
```
The input is copied into the container instead of bind-mounted, so arbitrary
host paths work (including `/tmp`, which the Podman machine does not share) and
no SELinux relabelling is required.
Manual equivalent:
```sh
podman build -t localhost/bitwarden-print:latest .
podman pod create --name bitwarden-print-pod
podman create --pod bitwarden-print-pod --name bw localhost/bitwarden-print:latest \
/export.json -o /output.pdf
podman cp vault.json bw:/export.json
podman start -a bw
podman cp bw:/output.pdf ./vault.pdf
podman rm -f bw && podman pod rm -f bitwarden-print-pod
```
## Files
- `bitwarden-print.sh` — CLI wrapper that builds the image and drives the pod
- `Containerfile` — `python:3.12-slim-bookworm` + WeasyPrint (Pango/Cairo) + DejaVu fonts
- `render.py` — JSON → HTML → PDF, no data leaves the container
- `examples/sample-export.json` — synthetic vault used for testing (all values fake)
Run the renderer directly on a host that has WeasyPrint installed:
```sh
pip install weasyprint==62.3 pydyf==0.10.0
./render.py examples/sample-export.json -o /tmp/sample.pdf --html-out /tmp/sample.html
```
## Security notes
- The generated PDF contains your vault in **plaintext** unless you pass
`--mask-secrets`. Treat the PDF like the export itself: keep it local, and
delete it when you are done.
- `--mask-secrets` only masks well-known secret paths. Secrets stored inside
custom fields (`fields[i].value`), notes or file names are printed as-is.
- Encrypted exports (`"encrypted": true`) are rejected with a hint: re-export
from Bitwarden as `.json` without a file password first.
- The container is removed after each run, but the image
(`localhost/bitwarden-print:latest`) stays cached for the next run.