Saavi v0.3.0
Security (external audit + review response)
-
Signature verdicts on every unseal, both keyrings.
decryptTextand
decryptBytesnow classify EVERY signature (good / bad / expired / revoked
/ unknown-key / unsigned) against candidate keys and return a worst-first
summary — a bad signature can never hide behind a good one. The Saavi-store
unseal shows the same Unsigned / Signed-by / trusted verdicts the system
GnuPG path always did; an unknown signer is looked up by key ID on
keys.openpgp.org (as an untrusted candidate — it can name a signer, never
vouch for one). (audit M1) -
"Your key" is a fingerprint comparison, never a UID substring. The
Verify and unseal trust badges compare the signer's fingerprint against
this device's keys; a stranger's key whose user ID embeds your address can
no longer render as "trusted key". (audit M2) -
Corrupt store records are quarantined, not silently dropped. A ring
that fails to parse is parked under a quarantine key and surfaced as a loud
alert in the key table, instead of the key simply vanishing. (audit M3) -
Imported keys are re-locked with our S2K. A cleartext or weak-S2K
export is re-encrypted under the current passphrase on import. (audit I3) -
System-keyring trust changes now confirm natively.
gpg_import,
gpg_set_ownertrust,gpg_recv_keyandgpg_delete_publicshow a native
OK/Cancel dialog naming the fingerprint before touching~/.gnupg, so a
webview alone cannot poison the keyring every other tool reads. (audit L1) -
Keychain keys unlock lazily. Remembered keys are no longer unlocked on
every key-list refresh (which undid Lock and the idle timer); the table
shows "remembered" from a keychain probe, and a key is decrypted only when
actually used. A hidden window now also auto-locks. (audit L2, L9) -
Decryption belt:
DECRYPTION_FAILEDalongsideDECRYPTION_OKAYnow
counts as failure.human()prefers an error/failure line over gpg's
trailing "not certified" warning. Locked-key detection is structural (key
IDs) with the error-string match only as a fallback. (audit L4, L8, hardening) -
Encrypt-to-self in the sealer so the sender keeps a readable record of
what they sent. (audit L6) -
WKD/VKS hardening: the domain is validated before URL construction, and
the size cap is enforced WHILE streaming (a chunked response with no
Content-Length can no longer defeat it). The update manifest read is
capped the same way. (audit L7, 04) -
MIME layer for PGP/MIME letters (
src/mime.ts): builds and parses the
inner MIME entity that gets encrypted — text + HTML alternatives,
attachments, and the real Subject as a protected header
(protected-headers="v1", the Thunderbird/LAMPS convention) so the
visible subject can stay "...". Base64 leaf parts throughout; the parser
also reads foreign mail (quoted-printable, RFC 2047/2231 filenames,
LF-only input).buildEncryptedMessageassembles the complete outer
RFC 5322 + RFC 3156multipart/encryptedmessage around an armored
ciphertext — the exact wire bytes (webmail imports and submits them; a
desktop export can write them as .eml). -
GnuPG interop tests (
tests/interop.test.ts, skipped when nogpgon
PATH): real GnuPG decrypts our sealed MIME letters and reports GOODSIG on
our signatures; we decrypt GnuPG's ciphertext. Documented finding: OpenPGP
text-mode literals canonicalise line endings, so nothing may depend on
CRLF surviving decryption — the MIME parser is line-ending-agnostic. -
Sealer: the To field accepts addresses separated by commas, semicolons,
spaces or new lines (before, anything but a comma made one unusable
address). When no key is found the message now says why, per address:
the domain publishes none over WKD and keys.openpgp.org has none —
or the domain could not be reached at all.
Install
These builds are GPG-signed (see below) but not yet code-signed with
Apple or Microsoft certificates, so each OS will warn on first launch.
- macOS (
.dmg, universal): drag Saavi to Applications. The build is
not yet notarized, so on first open macOS says it "could not verify that
Saavi is free of malware" and offers only Done / Move to Bin. Click
Done, then System Settings → Privacy & Security, scroll to "Saavi was
blocked", click Open Anyway. Or, in Terminal:
xattr -d com.apple.quarantine /Applications/Saavi.app. Either way,
verify the.sigfirst — that is the check this dialog stands in for. - Windows (
.msior-setup.exe): SmartScreen will show "Windows
protected your PC" — click More info → Run anyway. The installer
fetches Microsoft's WebView2 runtime if it is missing (Windows 10/11 ship it). - Linux:
sudo apt install ./saavi_*_amd64.deb, or
chmod +x saavi_*.AppImage && ./saavi_*.AppImage.
Your keys live in the app's own store (passphrase-locked) — nothing is
uploaded anywhere. Backups are plain text files you save yourself.
Verify this release
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA512
Verifying Saavi v0.3.0
1. Download the asset you want, its detached signature (.sig), and
saavi_pubkey.gpg from this release.
2. Import the key:
gpg --import saavi_pubkey.gpg
3. Check the fingerprint:
gpg --fingerprint "Saavi release signing"
It must read, exactly:
DCF5 773B 84E9 AABA 785F D5A8 4D2A ECE6 8A95 3F46
Compare it against https://kaditham.ie/saavi/ — published outside
GitHub, so a compromised repository cannot swap the key and the
binaries together. If it differs, stop and write to
security@kaditham.me (see SECURITY.md in the repository).
4. Verify the asset:
gpg --verify <asset>.sig <asset>
Expect: Good signature from "Saavi release signing
<webmaster@kaditham.me>". A "key is not certified" warning is
normal here; step 3 is what establishes the trust.
This message is clearsigned by the same release key: save it to a
file and run gpg --verify on it to check it has not been altered.
-----BEGIN PGP SIGNATURE-----
iHUEARYKAB0WIQTc9Xc7hOmqunhf1ahNKuzmipU/RgUCao7ARgAKCRBNKuzmipU/
RgTWAP0S68RW4+0h3yHne92WH6XjVp2il3ys3p8jDVvgBoWrVQD9E6y3SUR54XSG
kD3Y3KukCGQJAVi0v8pf3bvA6QVhRgo=
=aWaN
-----END PGP SIGNATURE-----