Skip to content

Saavi v0.3.0

Choose a tag to compare

@github-actions github-actions released this 26 Aug 10:42

Security (external audit + review response)

  • Signature verdicts on every unseal, both keyrings. decryptText and
    decryptBytes now 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_key and gpg_delete_public show 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_FAILED alongside DECRYPTION_OKAY now
    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). buildEncryptedMessage assembles the complete outer
    RFC 5322 + RFC 3156 multipart/encrypted message 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 no gpg on
    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 .sig first — that is the check this dialog stands in for.
  • Windows (.msi or -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-----