FastAPI app for OpenPGP encrypt, decrypt, sign, and verify, with an integrated web UI (tabbed workspace) and a JSON API (/docs).
Configure keys in .env:
| Variable | Purpose |
|---|---|
OPENPGP_PRIVATE_KEY |
Armored secret key — decrypt, sign |
OPENPGP_PUBLIC_KEY |
Armored public key (same keypair) — encrypt to yourself when recipient_mode is self |
OPENPGP_KEY_PASSPHRASE |
Optional, if the secret key is passphrase-protected |
Never commit .env. Copy .env.example and fill in real values. Export your public key once, e.g. gpg --armor --export YOUR_KEY_ID, and paste it into OPENPGP_PUBLIC_KEY.
- Python 3.11+
- GnuPG (
gpg) on yourPATH
Create the environment once from the project root:
cd /path/to/VaultPGP
python3 -m venv .venvActivate it every time you open a new terminal before running pip or uvicorn:
| Shell / OS | Command |
|---|---|
| macOS / Linux (bash, zsh) | source .venv/bin/activate |
| fish | source .venv/bin/activate.fish |
| Windows (cmd) | .venv\Scripts\activate.bat |
| Windows (PowerShell) | .venv\Scripts\Activate.ps1 |
To leave the environment: deactivate
With the venv activated:
pip install -e ".[dev]"
cp .env.example .envEdit .env (use double quotes around multiline key blocks). Start the server:
uvicorn app.main:app --reload --host 127.0.0.1 --port 8000Open http://127.0.0.1:8000.
Install without dev dependencies: pip install -e .
The home page is a single workspace with:
- Tabs — Encrypt, Decrypt, Sign, Verify (one action visible at a time).
- Status chips — whether GnuPG is available and whether secret/public keys were loaded from
.env. - After each form submit — the tab that handled the request stays selected so you see results immediately.
- URL hash — switching tabs updates the address (e.g.
#verify). You can bookmark or sharehttp://127.0.0.1:8000#decrypt. - Copy — buttons on outputs use the clipboard when the browser allows it.
- Verify — large field for a full clearsigned block; optional signer's public key when the signature is from someone else (otherwise GnuPG returns “no public key”). Detached verify is under an expandable section.
- Sign — default output is signed + encrypted to
OPENPGP_PUBLIC_KEY(armoredBEGIN PGP MESSAGE) so you can paste it into Decrypt. Clearsigned and detached-only modes are available as radio options.
A short footer links to OpenAPI at /docs.
Four POST endpoints. Use JSON (Content-Type: application/json) with the same field names as the forms, or call from scripts.
- If you POST JSON and do not send
Accept: application/json(typicalcurldefault isAccept: */*), encrypt, decrypt, and sign return raw armored text (text/plain) with real line breaks — safe to redirect to a file or pipe togpg. - To get a JSON object, send
Accept: application/json. That response includes human-readable fields plusciphertext_b64,plaintext_b64,signature_b64(UTF-8 armored content, base64-encoded) for one-line copy; decode withbase64 -d. ?format=plainorAccept: text/plainalso forces a plain armored body.
Python requests: requests.post(..., json=...) often omits Accept: application/json, so you receive plain armored text, not JSON. Use headers={"Accept": "application/json"} if you call .json() on the response.
| Endpoint | JSON body |
|---|---|
/encrypt |
plaintext, recipient_mode ("self" | "other"), and recipient_public_key when recipient_mode is "other" |
/decrypt |
ciphertext (pasted ciphertext is normalized: literal \n, %0A, etc.) |
/sign |
message, output: encrypted_signed (default, signed+encrypted armored PGP MESSAGE for OPENPGP_PUBLIC_KEY — paste into Decrypt), clearsigned, or detached. Legacy: detached boolean still maps to output when output is omitted. |
/verify |
Clearsigned: clearsigned or signed_message — full block through END PGP SIGNATURE. Detached: message + signature. signer_public_key (optional armored public key) — use when GnuPG reports NO_PUBKEY / “Can't check signature: No public key” (the app’s keyring only has your .env keys unless you supply the signer’s key). |
Interactive docs: http://127.0.0.1:8000/docs
Encrypt to yourself (plain armored response with default curl):
curl -s -H "Content-Type: application/json" \
-d '{"plaintext":"note to self","recipient_mode":"self"}' \
http://127.0.0.1:8000/encrypt > message.ascEncrypt to someone else:
curl -s -H "Content-Type: application/json" \
-d '{"plaintext":"hello","recipient_mode":"other","recipient_public_key":"'"$(cat their-pubkey.asc)"'"}' \
http://127.0.0.1:8000/encryptJSON envelope (for jq or apps):
curl -s -H "Content-Type: application/json" -H "Accept: application/json" \
-d '{"plaintext":"secret","recipient_mode":"self"}' \
http://127.0.0.1:8000/encrypt | jq -r .ciphertextJSON string values escape newlines as \n. Do not paste raw JSON into GnuPG — you may see “illegal base64 data at input byte 0” or similar.
- Prefer plain
curloutput (noAccept: application/json) and save to.asc, or usejq -r .ciphertext, orciphertext_b64+base64 -d. - Decrypting inside VaultPGP normalizes common paste issues before calling GnuPG.
GET /health — reports whether gpg is available.
.envholds key material in plaintext on disk; restrict permissions and keep it out of version control.- Bind to 127.0.0.1 for local use only. Do not expose the app on a network without authentication and TLS.
