Support GnuPG AEAD password entries
This commit is contained in:
@@ -8,15 +8,22 @@ use ironstorage::{
|
||||
crypto::{
|
||||
CryptoError, DetachedSignatureBytes, KeyInfo, KeyStore, SecretProvider, SecretProviderError,
|
||||
},
|
||||
repository::{EncryptedEntry, SecretBytes},
|
||||
read::{ShowResult, VaultReader},
|
||||
repository::{EncryptedEntry, Repository, SecretBytes},
|
||||
};
|
||||
use pgp::{
|
||||
composed::{EncryptionCaps, KeyType, Message, SecretKeyParamsBuilder, SubkeyParamsBuilder},
|
||||
crypto::{ecc_curve::ECCCurve, hash::HashAlgorithm, sym::SymmetricKeyAlgorithm},
|
||||
composed::{
|
||||
Edata, EncryptionCaps, Esk, KeyType, Message, SecretKeyParamsBuilder, SubkeyParamsBuilder,
|
||||
},
|
||||
crypto::{
|
||||
aead::AeadAlgorithm, ecc_curve::ECCCurve, hash::HashAlgorithm, sym::SymmetricKeyAlgorithm,
|
||||
},
|
||||
packet::{ProtectedDataConfig, SymEncryptedProtectedDataConfig},
|
||||
ser::Serialize as _,
|
||||
types::{Password, Timestamp},
|
||||
};
|
||||
use rand_chacha::{ChaCha20Rng, rand_core::SeedableRng};
|
||||
use sha2::{Digest as _, Sha256};
|
||||
use smallvec::smallvec;
|
||||
use support::compatibility::{FixtureSet, TestResult};
|
||||
|
||||
@@ -24,6 +31,8 @@ use support::compatibility::{FixtureSet, TestResult};
|
||||
struct FixtureSecrets {
|
||||
values: BTreeMap<String, Vec<u8>>,
|
||||
requests: Vec<String>,
|
||||
accepted: Vec<String>,
|
||||
rejected: Vec<String>,
|
||||
}
|
||||
|
||||
impl FixtureSecrets {
|
||||
@@ -41,6 +50,8 @@ impl FixtureSecrets {
|
||||
})
|
||||
.collect(),
|
||||
requests: Vec::new(),
|
||||
accepted: Vec::new(),
|
||||
rejected: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +59,8 @@ impl FixtureSecrets {
|
||||
Self {
|
||||
values: BTreeMap::from([(fingerprint.to_owned(), passphrase.as_ref().to_vec())]),
|
||||
requests: Vec::new(),
|
||||
accepted: Vec::new(),
|
||||
rejected: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,6 +75,15 @@ impl SecretProvider for FixtureSecrets {
|
||||
.map(SecretBytes::new)
|
||||
.ok_or(SecretProviderError::Unavailable)
|
||||
}
|
||||
|
||||
fn secret_accepted(&mut self, key: &KeyInfo) -> Result<(), SecretProviderError> {
|
||||
self.accepted.push(key.fingerprint().as_str().to_owned());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn secret_rejected(&mut self, key: &KeyInfo) {
|
||||
self.rejected.push(key.fingerprint().as_str().to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
struct CancelledProvider;
|
||||
@@ -186,6 +208,127 @@ fn decrypts_every_gnupg_audited_pass_fixture() -> TestResult {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decrypts_gnupg_packet_20_aes256_ocb_through_the_vault_reader() -> TestResult {
|
||||
let fixture = FixtureSet::load()?;
|
||||
let aead = &fixture.gnupg_aead;
|
||||
let alice = fixture.key("alice")?;
|
||||
assert_eq!(aead.format, 1);
|
||||
assert_eq!(aead.producer, "GnuPG 2.4.8");
|
||||
assert!(aead.command_profile.contains("--force-ocb"));
|
||||
assert_eq!(aead.packet_tag, 20);
|
||||
assert_eq!(aead.cipher, "AES256");
|
||||
assert_eq!(aead.aead_mode, "OCB");
|
||||
assert_eq!(
|
||||
aead.recipient_primary_fingerprint,
|
||||
alice.primary_fingerprint
|
||||
);
|
||||
assert_eq!(
|
||||
aead.recipient_encryption_subkey_fingerprint,
|
||||
alice.encryption_subkey_fingerprint
|
||||
);
|
||||
|
||||
let ciphertext = fixture.read(&aead.entry)?;
|
||||
assert_eq!(
|
||||
hex::encode(Sha256::digest(&ciphertext)),
|
||||
aead.ciphertext_sha256
|
||||
);
|
||||
let message = Message::from_bytes(Cursor::new(&ciphertext))?;
|
||||
let Message::Encrypted { esk, edata, .. } = &message else {
|
||||
panic!("GnuPG AEAD fixture must be encrypted");
|
||||
};
|
||||
let [Esk::PublicKeyEncryptedSessionKey(pkesk)] = esk.as_slice() else {
|
||||
panic!("GnuPG AEAD fixture must have one public-key recipient");
|
||||
};
|
||||
let secret = fixture.parse_secret_key(alice)?;
|
||||
assert!(pkesk.match_identity(&secret.secret_subkeys[0].key));
|
||||
assert!(!pkesk.match_identity(&secret.primary_key));
|
||||
let Edata::GnupgAeadData { reader } = edata else {
|
||||
panic!("GnuPG fixture must use packet type 20");
|
||||
};
|
||||
let ProtectedDataConfig::GnupgAead(config) = reader.config() else {
|
||||
panic!("packet type 20 must carry GnuPG AEAD configuration");
|
||||
};
|
||||
assert_eq!(config.sym_alg, SymmetricKeyAlgorithm::AES256);
|
||||
assert_eq!(config.aead, AeadAlgorithm::Ocb);
|
||||
|
||||
let repository = Repository::open(fixture.path(format!("gnupg-aead/{}", aead.store)))?;
|
||||
let keys = KeyStore::load(fixture.path("keys"))?;
|
||||
let reader = VaultReader::new(&repository, &keys);
|
||||
let mut provider = FixtureSecrets::one(&alice.primary_fingerprint, &alice.passphrase);
|
||||
let ShowResult::Entry(plaintext) = reader.show(Some("paypal"), &mut provider)? else {
|
||||
panic!("paypal must resolve as an entry");
|
||||
};
|
||||
assert_eq!(plaintext.expose(), fixture.read(&aead.plaintext)?);
|
||||
assert_eq!(
|
||||
provider.requests,
|
||||
std::slice::from_ref(&alice.primary_fingerprint)
|
||||
);
|
||||
assert_eq!(
|
||||
provider.accepted,
|
||||
std::slice::from_ref(&alice.primary_fingerprint)
|
||||
);
|
||||
assert!(provider.rejected.is_empty());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_gnupg_packet_20_inputs_without_accepting_secrets() -> TestResult {
|
||||
let fixture = FixtureSet::load()?;
|
||||
let aead = &fixture.gnupg_aead;
|
||||
let alice = fixture.key("alice")?;
|
||||
let ciphertext = fixture.read(&aead.entry)?;
|
||||
let keys = KeyStore::load(fixture.path(&alice.secret_armor))?;
|
||||
|
||||
let mut wrong = FixtureSecrets::one(&alice.primary_fingerprint, b"wrong-aead-passphrase");
|
||||
assert!(matches!(
|
||||
keys.decrypt(&EncryptedEntry::new(ciphertext.clone()), &mut wrong),
|
||||
Err(CryptoError::DecryptionFailed)
|
||||
));
|
||||
assert!(wrong.accepted.is_empty());
|
||||
assert_eq!(
|
||||
wrong.rejected,
|
||||
std::slice::from_ref(&alice.primary_fingerprint)
|
||||
);
|
||||
|
||||
let mut tampered = ciphertext.clone();
|
||||
*tampered.last_mut().expect("non-empty AEAD fixture") ^= 0x01;
|
||||
let mut correct = FixtureSecrets::one(&alice.primary_fingerprint, &alice.passphrase);
|
||||
assert!(matches!(
|
||||
keys.decrypt(&EncryptedEntry::new(tampered), &mut correct),
|
||||
Err(CryptoError::DecryptionFailed)
|
||||
));
|
||||
assert!(correct.accepted.is_empty());
|
||||
assert_eq!(
|
||||
correct.rejected,
|
||||
std::slice::from_ref(&alice.primary_fingerprint)
|
||||
);
|
||||
|
||||
let public_only = KeyStore::load(fixture.path(&alice.public_armor))?;
|
||||
let mut unavailable = FixtureSecrets::default();
|
||||
assert!(matches!(
|
||||
public_only.decrypt(&EncryptedEntry::new(ciphertext.clone()), &mut unavailable),
|
||||
Err(CryptoError::MissingSecretKey)
|
||||
));
|
||||
assert!(unavailable.requests.is_empty());
|
||||
|
||||
let mut unsupported = ciphertext;
|
||||
let config = unsupported
|
||||
.windows(3)
|
||||
.position(|window| window == [1, 9, 2])
|
||||
.expect("packet-20 version, AES-256, and OCB configuration");
|
||||
unsupported[config + 2] = 1;
|
||||
let mut unused = FixtureSecrets::one(&alice.primary_fingerprint, &alice.passphrase);
|
||||
assert!(matches!(
|
||||
keys.decrypt(&EncryptedEntry::new(unsupported), &mut unused),
|
||||
Err(CryptoError::CorruptMessage)
|
||||
));
|
||||
assert!(unused.requests.is_empty());
|
||||
assert!(unused.accepted.is_empty());
|
||||
assert!(unused.rejected.is_empty());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encrypts_for_one_or_many_recipients_without_compression() -> TestResult {
|
||||
let fixture = FixtureSet::load()?;
|
||||
@@ -203,7 +346,16 @@ fn encrypts_for_one_or_many_recipients_without_compression() -> TestResult {
|
||||
let ciphertext = keys.encrypt(SecretBytes::new(expected.to_vec()), &recipients)?;
|
||||
|
||||
let parsed = Message::from_bytes(Cursor::new(ciphertext.as_bytes()))?;
|
||||
assert!(parsed.is_encrypted());
|
||||
let Message::Encrypted { edata, .. } = &parsed else {
|
||||
panic!("IronStorage output must be encrypted");
|
||||
};
|
||||
let Edata::SymEncryptedProtectedData { reader } = edata else {
|
||||
panic!("IronStorage output must remain packet type 18");
|
||||
};
|
||||
assert_eq!(
|
||||
reader.config(),
|
||||
&ProtectedDataConfig::Seipd(SymEncryptedProtectedDataConfig::V1)
|
||||
);
|
||||
for key in [alice, bob] {
|
||||
let mut provider = FixtureSecrets::one(&key.primary_fingerprint, &key.passphrase);
|
||||
let plaintext = keys.decrypt(&ciphertext, &mut provider)?;
|
||||
|
||||
Reference in New Issue
Block a user