Files
IronStorage/docs/cryptography.md
2026-08-10 00:23:40 +00:00

3.9 KiB

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 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. The production provider is the storage-owned native secret store documented in 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, 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.