Files
IronStorage/crates/storage/tests/crypto_compatibility.rs
2026-08-09 21:59:29 +00:00

383 lines
12 KiB
Rust

#![forbid(unsafe_code)]
mod support;
use std::{collections::BTreeMap, io::Cursor, path::Path};
use ironstorage::{
crypto::{
CryptoError, DetachedSignatureBytes, KeyInfo, KeyStore, SecretProvider, SecretProviderError,
},
repository::{EncryptedEntry, SecretBytes},
};
use pgp::{
composed::{EncryptionCaps, KeyType, Message, SecretKeyParamsBuilder, SubkeyParamsBuilder},
crypto::{ecc_curve::ECCCurve, hash::HashAlgorithm, sym::SymmetricKeyAlgorithm},
ser::Serialize as _,
types::{Password, Timestamp},
};
use rand_chacha::{ChaCha20Rng, rand_core::SeedableRng};
use smallvec::smallvec;
use support::compatibility::{FixtureSet, TestResult};
#[derive(Default)]
struct FixtureSecrets {
values: BTreeMap<String, Vec<u8>>,
requests: Vec<String>,
}
impl FixtureSecrets {
fn all(fixture: &FixtureSet) -> Self {
Self {
values: fixture
.generated
.keys
.iter()
.map(|key| {
(
key.primary_fingerprint.clone(),
key.passphrase.as_bytes().to_vec(),
)
})
.collect(),
requests: Vec::new(),
}
}
fn one(fingerprint: &str, passphrase: impl AsRef<[u8]>) -> Self {
Self {
values: BTreeMap::from([(fingerprint.to_owned(), passphrase.as_ref().to_vec())]),
requests: Vec::new(),
}
}
}
impl SecretProvider for FixtureSecrets {
fn secret_for(&mut self, key: &KeyInfo) -> Result<SecretBytes, SecretProviderError> {
let fingerprint = key.fingerprint().as_str().to_owned();
self.requests.push(fingerprint.clone());
self.values
.get(&fingerprint)
.cloned()
.map(SecretBytes::new)
.ok_or(SecretProviderError::Unavailable)
}
}
struct CancelledProvider;
impl SecretProvider for CancelledProvider {
fn secret_for(&mut self, _key: &KeyInfo) -> Result<SecretBytes, SecretProviderError> {
Err(SecretProviderError::Cancelled)
}
}
#[test]
fn imports_armored_and_binary_public_and_protected_secret_keys() -> TestResult {
let fixture = FixtureSet::load()?;
let alice = fixture.key("alice")?;
let bob = fixture.key("bob")?;
let mut keys = KeyStore::new();
keys.import(&fixture.read(&alice.public_armor)?)?;
keys.import(&fixture.read(&alice.secret_binary)?)?;
keys.import(&fixture.read(&bob.public_binary)?)?;
keys.import(&fixture.read(&bob.secret_armor)?)?;
assert_eq!(keys.len(), 2);
let infos = keys.infos().collect::<Vec<_>>();
assert!(infos.iter().all(KeyInfo::has_secret));
assert!(infos.iter().all(KeyInfo::can_encrypt));
assert!(infos.iter().all(KeyInfo::can_sign));
assert!(format!("{keys:?}").contains("key_count: 2"));
assert!(!format!("{keys:?}").contains(&alice.passphrase));
let loaded = KeyStore::load(fixture.path("keys"))?;
assert_eq!(loaded.len(), 2, "duplicate armor/binary exports are merged");
Ok(())
}
#[test]
fn resolves_pass_identities_and_recipient_file_rules() -> TestResult {
let fixture = FixtureSet::load()?;
let keys = KeyStore::load(fixture.path("keys"))?;
let alice = fixture.key("alice")?;
let info = keys
.infos()
.find(|info| info.fingerprint().as_str() == alice.primary_fingerprint)
.expect("Alice info");
for identity in [
alice.primary_fingerprint.as_str(),
&alice.primary_fingerprint[24..],
&alice.primary_fingerprint[32..],
info.key_id(),
alice.user_id.as_str(),
alice.encryption_subkey_fingerprint.as_str(),
&alice.encryption_subkey_fingerprint[24..],
] {
assert_eq!(
keys.resolve(identity)?.fingerprint().as_str(),
alice.primary_fingerprint
);
}
let recipients = keys.resolve_recipients(
format!(
" {} # primary recipient\n\n{} # duplicate\n",
alice.primary_fingerprint, alice.user_id
)
.as_bytes(),
)?;
assert_eq!(recipients.len(), 1);
assert!(matches!(
keys.resolve("missing@example.invalid"),
Err(CryptoError::MissingIdentity { .. })
));
assert!(matches!(
keys.resolve_recipients(b" # comments only\n"),
Err(CryptoError::MissingRecipients)
));
assert!(matches!(
keys.resolve_recipients(&[0xff]),
Err(CryptoError::InvalidRecipientFile)
));
Ok(())
}
#[test]
fn reports_exact_user_id_ambiguity() -> TestResult {
let fixture = FixtureSet::load()?;
let alice = fixture.key("alice")?;
let mut keys = KeyStore::new();
keys.import(&fixture.read(&alice.public_binary)?)?;
keys.import(&duplicate_user_id_key(&alice.user_id)?)?;
assert!(matches!(
keys.resolve(&alice.user_id),
Err(CryptoError::AmbiguousIdentity { .. })
));
Ok(())
}
#[test]
fn decrypts_every_gnupg_audited_pass_fixture() -> TestResult {
let fixture = FixtureSet::load()?;
let keys = KeyStore::load(fixture.path("keys"))?;
let mut provider = FixtureSecrets::all(&fixture);
for entry in &fixture.generated.entries {
let ciphertext = fixture.read(Path::new("stores").join(&entry.store).join(&entry.path))?;
let expected = fixture.read(
Path::new("expected")
.join(&entry.store)
.join(&entry.plaintext),
)?;
let plaintext = keys.decrypt(&EncryptedEntry::new(ciphertext), &mut provider)?;
assert_eq!(
plaintext.expose(),
expected,
"fixture {}/{}",
entry.store,
entry.path
);
}
assert!(!provider.requests.is_empty());
Ok(())
}
#[test]
fn encrypts_for_one_or_many_recipients_without_compression() -> TestResult {
let fixture = FixtureSet::load()?;
let keys = KeyStore::load(fixture.path("keys"))?;
let alice = fixture.key("alice")?;
let bob = fixture.key("bob")?;
let recipients = keys.resolve_recipients(
format!(
"{}\n{}\n",
alice.primary_fingerprint, bob.primary_fingerprint
)
.as_bytes(),
)?;
let expected = b"generated-by-ironstorage\nlogin: fixture\n";
let ciphertext = keys.encrypt(SecretBytes::new(expected.to_vec()), &recipients)?;
let parsed = Message::from_bytes(Cursor::new(ciphertext.as_bytes()))?;
assert!(parsed.is_encrypted());
for key in [alice, bob] {
let mut provider = FixtureSecrets::one(&key.primary_fingerprint, &key.passphrase);
let plaintext = keys.decrypt(&ciphertext, &mut provider)?;
assert_eq!(plaintext.expose(), expected);
assert_eq!(provider.requests.last(), Some(&key.primary_fingerprint));
}
let independent = fixture.parse_secret_key(alice)?;
assert_eq!(
decrypt_independently(ciphertext.as_bytes(), &independent, &alice.passphrase)?,
expected
);
assert!(matches!(
keys.encrypt(SecretBytes::new(Vec::new()), &[]),
Err(CryptoError::MissingRecipients)
));
Ok(())
}
#[test]
fn protected_key_failures_are_typed_and_redacted() -> TestResult {
let fixture = FixtureSet::load()?;
let alice = fixture.key("alice")?;
let entry = fixture
.generated
.entries
.iter()
.find(|entry| entry.recipients == [alice.primary_fingerprint.clone()])
.expect("Alice fixture");
let ciphertext = EncryptedEntry::new(
fixture.read(Path::new("stores").join(&entry.store).join(&entry.path))?,
);
let keys = KeyStore::load(fixture.path(&alice.secret_armor))?;
let mut wrong = FixtureSecrets::one(&alice.primary_fingerprint, b"very-secret-wrong-value");
let error = keys.decrypt(&ciphertext, &mut wrong).unwrap_err();
assert_eq!(error, CryptoError::DecryptionFailed);
assert!(!format!("{error:?}").contains("very-secret-wrong-value"));
assert!(!error.to_string().contains("very-secret-wrong-value"));
let public_only = KeyStore::load(fixture.path(&alice.public_armor))?;
let mut unavailable = FixtureSecrets::default();
assert!(matches!(
public_only.decrypt(&ciphertext, &mut unavailable),
Err(CryptoError::MissingSecretKey)
));
assert!(unavailable.requests.is_empty());
let mut cancelled = FixtureSecrets::default();
let error = keys.decrypt(&ciphertext, &mut cancelled).unwrap_err();
assert!(matches!(
error,
CryptoError::SecretProvider {
reason: SecretProviderError::Unavailable,
..
}
));
assert!(matches!(
keys.decrypt(&ciphertext, &mut CancelledProvider),
Err(CryptoError::SecretProvider {
reason: SecretProviderError::Cancelled,
..
})
));
Ok(())
}
#[test]
fn signs_and_verifies_gpg_id_with_an_explicit_trust_set() -> TestResult {
let fixture = FixtureSet::load()?;
let keys = KeyStore::load(fixture.path("keys"))?;
let alice = fixture.key("alice")?;
let bob = fixture.key("bob")?;
let alice_handle = keys.resolve(&alice.primary_fingerprint)?;
let bob_handle = keys.resolve(&bob.primary_fingerprint)?;
let recipient_file = fixture.read("stores/basic/.gpg-id")?;
let checked_in = DetachedSignatureBytes::new(fixture.read("stores/basic/.gpg-id.sig")?);
assert_eq!(
keys.verify(
&recipient_file,
&checked_in,
std::slice::from_ref(&alice_handle)
)?,
alice_handle
);
assert_eq!(
keys.verify(&recipient_file, &checked_in, &[bob_handle]),
Err(CryptoError::InvalidSignature)
);
assert_eq!(
keys.verify(
b"tampered\n",
&checked_in,
std::slice::from_ref(&alice_handle)
),
Err(CryptoError::InvalidSignature)
);
assert_eq!(
keys.verify(
&recipient_file,
&DetachedSignatureBytes::new(vec![0xff]),
std::slice::from_ref(&alice_handle),
),
Err(CryptoError::CorruptSignature)
);
let mut provider = FixtureSecrets::one(&alice.primary_fingerprint, &alice.passphrase);
let generated = keys.sign(&recipient_file, &alice_handle, &mut provider)?;
assert_eq!(
keys.verify(
&recipient_file,
&generated,
std::slice::from_ref(&alice_handle)
)?,
alice_handle
);
let mut wrong = FixtureSecrets::one(&alice.primary_fingerprint, b"wrong");
assert_eq!(
keys.sign(&recipient_file, &alice_handle, &mut wrong),
Err(CryptoError::SigningFailed)
);
Ok(())
}
#[test]
fn rejects_corrupt_keys_messages_and_signatures() -> TestResult {
let fixture = FixtureSet::load()?;
let keys = KeyStore::load(fixture.path("keys"))?;
let mut provider = FixtureSecrets::all(&fixture);
let mut empty = KeyStore::new();
assert_eq!(
empty.import(b"not an OpenPGP key"),
Err(CryptoError::CorruptKeyMaterial)
);
assert!(empty.is_empty());
assert!(matches!(
keys.decrypt(&EncryptedEntry::new(vec![0xff, 0x00]), &mut provider),
Err(CryptoError::CorruptMessage)
));
Ok(())
}
fn duplicate_user_id_key(user_id: &str) -> TestResult<Vec<u8>> {
let created_at = Timestamp::from_secs(1_704_067_201);
let subkey = SubkeyParamsBuilder::default()
.key_type(KeyType::ECDH(ECCCurve::Curve25519Legacy))
.can_encrypt(EncryptionCaps::All)
.created_at(created_at)
.build()?;
let mut rng = ChaCha20Rng::from_seed([0x55; 32]);
let secret = SecretKeyParamsBuilder::default()
.key_type(KeyType::Ed25519Legacy)
.can_certify(true)
.can_sign(true)
.created_at(created_at)
.primary_user_id(user_id.to_owned())
.preferred_symmetric_algorithms(smallvec![SymmetricKeyAlgorithm::AES256])
.preferred_hash_algorithms(smallvec![HashAlgorithm::Sha256])
.preferred_compression_algorithms(smallvec![])
.subkey(subkey)
.build()?
.generate(&mut rng)?;
let mut bytes = Vec::new();
secret.to_public_key().to_writer(&mut bytes)?;
Ok(bytes)
}
fn decrypt_independently(
ciphertext: &[u8],
secret: &pgp::composed::SignedSecretKey,
passphrase: &str,
) -> TestResult<Vec<u8>> {
let message = Message::from_bytes(Cursor::new(ciphertext))?;
let mut decrypted = message.decrypt(&Password::from(passphrase), secret)?;
Ok(decrypted.as_data_vec()?)
}