113 lines
6.2 KiB
Markdown
113 lines
6.2 KiB
Markdown
# 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.
|