Implement embedded OpenPGP compatibility (#4)
This commit is contained in:
73
docs/cryptography.md
Normal file
73
docs/cryptography.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# 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.
|
||||
|
||||
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.
|
||||
|
||||
## 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.
|
||||
|
||||
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, and signed
|
||||
recipient files. 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, and
|
||||
verify both checked-in and newly generated recipient signatures.
|
||||
|
||||
Tests never use a real user keyring and application runtime never executes an
|
||||
external cryptographic tool.
|
||||
Reference in New Issue
Block a user