# Embedded OpenPGP compatibility

All password-store cryptography lives in `crates/storage`. IronStorage does not
read a user's GnuPG keyring and does not launch `gpg`, `gpg-agent`, `pass`, or a
pinentry process. Applications import explicit exported key files and supply a
protected key's unlock secret through the `SecretProvider` interface.

## Backend decision

IronStorage uses [`pgp` 0.20](https://crates.io/crates/pgp/0.20.0) with default
features disabled. The crate is `MIT OR Apache-2.0`, is implemented in Rust,
and does not introduce a native OpenPGP library or runtime helper process.
`rand` 0.8 supplies `OsRng` for session keys and signatures. The alternatives
and their license consequences are recorded in `DEPENDENCIES.md`.

The storage API imports binary transferable keys and ASCII-armored public or
secret keys. Self-signatures and subkey bindings are verified before a key is
committed to the in-memory store. An import stream is applied transactionally,
duplicate public/secret exports are merged by primary fingerprint, and file and
key-count limits bound untrusted input. Directory loading rejects symbolic
links and non-regular files.

## Identity and recipient behavior

The resolver accepts:

- a full primary or subkey fingerprint;
- an 8- or 16-hex-digit primary or subkey key ID, with optional `0x` prefix;
- an exact UTF-8 user ID.

Short identifiers and user IDs must identify exactly one primary certificate.
Missing and ambiguous identities are separate errors. `.gpg-id` parsing follows
upstream `pass`: text after `#` is ignored, surrounding whitespace is removed,
blank lines are skipped, and duplicate resolved recipients are coalesced.

Encryption selects a non-revoked encryption-capable subkey, falling back to an
encryption-capable primary key. Output uses AES-256 in a version 1
symmetrically-encrypted integrity-protected data packet and deliberately does
not add a compression packet, matching upstream `pass --compress-algo=none`.
Every resolved recipient receives a public-key encrypted session-key packet.
IronStorage continues to produce this packet-type-18 SEIPDv1/MDC profile so
existing output remains unchanged.

Decryption first examines those recipient packets. A secret is requested only
for imported protected keys that can match the message; unavailable identities
are skipped so any one recipient of a multi-recipient entry can decrypt it.
Cancellation stops immediately. Wrong secrets, malformed messages, and a lack
of matching secret keys remain distinct failures.

For compatibility with password stores written by modern GnuPG, decryption
also accepts GnuPG's packet-type-20 OCB AEAD extension; the checked-in
GnuPG 2.4.8 compatibility case uses AES-256.
This is an explicit read-compatibility profile: it does not enable historical
unauthenticated encrypted-data packets, accept unsupported packet-20 modes, or
change the format IronStorage writes. AEAD authentication must succeed before
the provider is told that an interactively supplied passphrase was accepted;
wrong passphrases and tampered ciphertext therefore cannot be persisted.

### Upstream `pass` format boundary

The format boundary was re-audited against the current upstream tools on
2026-08-10. [`pass` 1.7.4 delegates reads and writes directly to
GnuPG](https://git.zx2c4.com/password-store/plain/src/password-store.sh): reads
use `gpg -d`, while writes use `gpg -e` with `--compress-algo=none`; `pass`
does not select or parse an encrypted-data packet itself. The current
[Homebrew `pass` formula](https://formulae.brew.sh/formula/pass) packages that
same release with GnuPG 2.5.21. GnuPG's
[OpenPGP options](https://www.gnupg.org/documentation/manuals/gnupg/OpenPGP-Options.html)
state that public-key encryption selects CFB+MDC or OCB from recipient key
preferences, which explains why an ordinary current `pass` installation can
write packet type 20 without a `pass` option requesting it.

Default `pass` does not opt into unauthenticated legacy decryption. GnuPG made
missing-MDC messages a hard failure in 2.2.8 and requires the explicit,
dangerous `--ignore-mdc-error` override to recover plaintext from them. The
[GnuPG 2.2.8 security release](https://lists.gnupg.org/pipermail/gnupg-users/2018-June/060644.html)
warns against using that override unconditionally. IronStorage therefore keeps
legacy packet-type-9 decryption disabled: its default behavior matches default
`pass` while preserving the authenticated-decryption guarantee.

## Secret lifetime and recipient signatures

Decrypted data and provider-returned unlock secrets use `SecretBytes`. Its
debug representation is redacted and its allocation is zeroed on drop. The
OpenPGP backend's password type also zeroes its owned storage. Plaintext enters
the message encoder through an owning reader instead of being copied into an
ordinary intermediate buffer.
The production provider is the storage-owned native secret store documented in
[`secure-secret-storage.md`](secure-secret-storage.md); protected-key
passphrases are addressed only by their primary fingerprint.

Detached `.gpg-id.sig` files use binary-document signatures with SHA-256 and
issuer fingerprint/key-ID metadata. Verification succeeds only when the valid
signature belongs to an explicitly allowed primary fingerprint, which is the
`PASSWORD_STORE_SIGNING_KEY` trust rule; a cryptographically valid signature
from another imported key is rejected.

## Compatibility evidence

`crates/storage/tests/fixtures/compatibility` contains protected public and
secret exports, single- and multi-recipient `.gpg` entries, signed recipient
files, and a GnuPG 2.4.8-produced packet-type-20 AES-256/OCB entry. The fixture
audit established that GnuPG decrypts the packet profile used by the generator.
Production tests then exercise that same uncompressed `MessageBuilder` profile
through `KeyStore::encrypt`, independently parse and decrypt its output, decrypt
every checked-in GnuPG-audited entry through the storage API, and verify both
checked-in and newly generated recipient signatures. Packet-type-20 tests also
cover the exact encryption subkey, wrong passphrases, authentication failure,
missing secret keys, unsupported modes, and deferred secret persistence.

Tests never use a real user keyring and application runtime never executes an
external cryptographic tool.
