From 22010f0d69045c3e5d8530dffbf06a6ce78c6b5b Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Sat, 17 Oct 2026 17:17:10 +0200 Subject: [PATCH] Treat passphrases that differ only in HMAC-padded zero bytes as the same key --- .../rekey/sqlcipher-trailing-zero-passphrases | Bin 0 -> 185 bytes fuzz/src/input.rs | 15 ++++++ fuzz/src/run.rs | 43 +++++++++++++----- 3 files changed, 46 insertions(+), 12 deletions(-) create mode 100644 fuzz/corpus/rekey/sqlcipher-trailing-zero-passphrases diff --git a/fuzz/corpus/rekey/sqlcipher-trailing-zero-passphrases b/fuzz/corpus/rekey/sqlcipher-trailing-zero-passphrases new file mode 100644 index 0000000000000000000000000000000000000000..73e912d1175d6d6283da91a4f7a4c04a36bdd208 GIT binary patch literal 185 zcmb2^n9#85VpWxk!;Pw240r((M0Uj+Kb^Su4`v0*?8?;GuxE1By-&S5&)q*%L^iLw qG7$viAZCJ%#HIQ_9F!Enng9R4M`ANDAUI%i!Gx@)&U#~|eRBZ0LSk6} literal 0 HcmV?d00001 diff --git a/fuzz/src/input.rs b/fuzz/src/input.rs index d8b9143..d19bf9e 100644 --- a/fuzz/src/input.rs +++ b/fuzz/src/input.rs @@ -372,6 +372,21 @@ impl NativeConfig { Cipher::SqlCipher => self.hmac_use.unwrap_or(true), } } + + /// The block size of the HMAC a passphrase keys in this configuration's PBKDF2. HMAC pads a + /// key no longer than its block with zeros, so passphrases that differ only in trailing zero + /// bytes within it derive the same key. + #[must_use] + pub const fn passphrase_hmac_block(&self) -> Option { + match self.cipher { + Cipher::ChaCha20 => Some(64), + Cipher::SqlCipher => match self.kdf_algorithm { + Some(Algorithm::Sha1 | Algorithm::Sha256) => Some(64), + Some(Algorithm::Sha512) | None => Some(128), + }, + Cipher::Aes128 | Cipher::Aes256 | Cipher::Ascon128 | Cipher::Aegis => None, + } + } } /// One native-scheme execution: write, round-trip, a wrong key, and, where authenticated, damage. diff --git a/fuzz/src/run.rs b/fuzz/src/run.rs index 0360ddd..cdeb6ce 100644 --- a/fuzz/src/run.rs +++ b/fuzz/src/run.rs @@ -86,7 +86,8 @@ pub fn run_native(case: &NativeCase) { // wrong key equivalent to the real one (same material, or a `kdf_iter` the cipher ignores) // proves nothing either. let key_len = cipher.key_len(case.config.aegis_algorithm_256); - if !native_keys_equivalent(&case.wrong_key, &case.config.key, cipher, key_len) && own.len() > 1 + if !native_keys_equivalent(&case.wrong_key, &case.config.key, &case.config, key_len) + && own.len() > 1 { let mut wrong_config = case.config.clone(); wrong_config.key = case.wrong_key.clone(); @@ -223,7 +224,9 @@ pub fn run_rekey(case: &RekeyCase) { // (see run_native's own identical guard), and a pre-rekey key equivalent to the new one // proves nothing either. if let RekeyAction::ChangeKey(new_key) = &case.action { - if !native_keys_equivalent(&case.config.key, new_key, cipher, key_len) && own.len() > 1 { + if !native_keys_equivalent(&case.config.key, new_key, &case.config, key_len) + && own.len() > 1 + { let old = dump_native(&clean, &case.config); assert_ne!( old, @@ -750,22 +753,38 @@ fn native_key_bytes(key: &NativeKey, key_len: usize) -> Vec { /// The bytes that actually determine `key`'s derived key, ignoring parts that influence only what /// gets stored for reopening (the embedded salt in [`NativeKey::RawWithSalt`], never consulted /// for the key itself since a raw key bypasses derivation entirely), a requested `kdf_iter` a -/// cipher's own `GenerateKey*Cipher` never reads ([`Cipher::has_kdf_iter`] false), or passphrase -/// bytes beyond [`Cipher::passphrase_prefix_len`] that the same function never reads either. -fn native_key_material(key: &NativeKey, cipher: Cipher, key_len: usize) -> Vec { +/// cipher's own `GenerateKey*Cipher` never reads ([`Cipher::has_kdf_iter`] false), passphrase +/// bytes beyond [`Cipher::passphrase_prefix_len`] that the same function never reads either, or +/// trailing zero bytes HMAC pads anyway ([`NativeConfig::passphrase_hmac_block`]). +fn native_key_material(key: &NativeKey, config: &NativeConfig, key_len: usize) -> Vec { match key { NativeKey::Raw(key) | NativeKey::RawWithSalt { key, .. } => key[..key_len].to_vec(), - NativeKey::Passphrase { bytes, .. } => cipher.passphrase_prefix_len().map_or_else( - || bytes.clone(), - |limit| bytes[..bytes.len().min(limit)].to_vec(), - ), + NativeKey::Passphrase { bytes, .. } => { + let read = config + .cipher + .passphrase_prefix_len() + .map_or(bytes.as_slice(), |limit| &bytes[..bytes.len().min(limit)]); + match config.passphrase_hmac_block() { + Some(block) if read.len() <= block => { + let end = read.iter().rposition(|&b| b != 0).map_or(0, |i| i + 1); + read[..end].to_vec() + } + _ => read.to_vec(), + } + } } } -/// Whether `a` and `b` derive the same key for `cipher`, so a round-trip or wrong-key oracle +/// Whether `a` and `b` derive the same key under `config`, so a round-trip or wrong-key oracle /// comparing them would prove nothing. -fn native_keys_equivalent(a: &NativeKey, b: &NativeKey, cipher: Cipher, key_len: usize) -> bool { - if native_key_material(a, cipher, key_len) != native_key_material(b, cipher, key_len) { +fn native_keys_equivalent( + a: &NativeKey, + b: &NativeKey, + config: &NativeConfig, + key_len: usize, +) -> bool { + let cipher = config.cipher; + if native_key_material(a, config, key_len) != native_key_material(b, config, key_len) { return false; } match (a, b) {