Files
juergen 41385b35e6 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.
2026-11-13 14:09:10 +01:00

4.4 KiB

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

./bitwarden-print.sh ~/Downloads/bitwarden_export_20261113.json
# -> ~/Downloads/bitwarden_export_20261113.pdf

Custom output and options:

./bitwarden-print.sh vault.json -o ~/Desktop/vault.pdf \
  --paper letter --sort name --title "Vault 2026-11-13"

Mask secrets instead of printing them:

./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:

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:

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.