#![expect(
clippy::indexing_slicing,
reason = "keyblob_decrypt indexes fixed-size arrays with inspection-obvious bounds"
)]
use cbc::Decryptor;
use cbc::cipher::block_padding::Pkcs7;
use cbc::cipher::{BlockModeDecrypt, KeyIvInit};
use des::TdesEde3;
use sha1::Sha1;
use crate::error::{Error, Result};
pub(crate) const BLOCK_SIZE: usize = 8;
pub(crate) const KEY_LENGTH: usize = 24;
pub(crate) const PBKDF2_ITER: u32 = 1000;
pub(crate) const MAGIC_CMS_IV: [u8; 8] = [0x4a, 0xdd, 0xa2, 0x2c, 0x79, 0xe8, 0x21, 0x05];
type TripleDesCbcDec = Decryptor<TdesEde3>;
pub(crate) fn kc_decrypt(key: &[u8], iv: &[u8], data: &[u8]) -> Result<Vec<u8>> {
if data.is_empty() {
return Err(Error::Cipher("ciphertext is empty"));
}
if !data.len().is_multiple_of(BLOCK_SIZE) {
return Err(Error::Cipher("ciphertext not aligned to 8-byte block"));
}
if key.len() != KEY_LENGTH {
return Err(Error::Cipher("invalid 3DES key length"));
}
if iv.len() != BLOCK_SIZE {
return Err(Error::Cipher("invalid IV length"));
}
let mut buf = data.to_vec();
let plain = TripleDesCbcDec::new_from_slices(key, iv)
.map_err(|_| Error::Cipher("invalid key or IV length"))?
.decrypt_padded::<Pkcs7>(&mut buf)
.map_err(|_| Error::Cipher("padding verification failed"))?;
Ok(plain.to_vec())
}
pub(crate) fn keyblob_decrypt(blob: &[u8], iv: &[u8], db_key: &[u8]) -> Result<Vec<u8>> {
let plain = kc_decrypt(db_key, &MAGIC_CMS_IV, blob)
.map_err(|e| Error::ParseFailed(format!("key unwrap stage 1: {e}")))?;
if plain.len() < 32 {
return Err(Error::ParseFailed(
"unwrapped blob too short for symmetric stage 2".into(),
));
}
let mut rev = [0_u8; 32];
for (i, dst) in rev.iter_mut().enumerate() {
*dst = plain[31 - i];
}
let final_plain = kc_decrypt(db_key, iv, &rev)
.map_err(|e| Error::ParseFailed(format!("key unwrap stage 2: {e}")))?;
if final_plain.len() != 28 {
return Err(Error::ParseFailed(format!(
"invalid unwrapped key length: got {}, want 28",
final_plain.len()
)));
}
Ok(final_plain[4..28].to_vec())
}
pub(crate) fn private_key_decrypt(blob: &[u8], iv: &[u8], db_key: &[u8]) -> Result<Vec<u8>> {
let plain = kc_decrypt(db_key, &MAGIC_CMS_IV, blob)
.map_err(|e| Error::ParseFailed(format!("private key unwrap stage 1: {e}")))?;
let rev: Vec<u8> = plain.iter().rev().copied().collect();
let final_plain = kc_decrypt(db_key, iv, &rev)
.map_err(|e| Error::ParseFailed(format!("private key unwrap stage 2: {e}")))?;
Ok(final_plain)
}
pub(crate) fn generate_master_key(password: &str, salt: &[u8]) -> [u8; KEY_LENGTH] {
let mut out = [0_u8; KEY_LENGTH];
pbkdf2::pbkdf2_hmac::<Sha1>(password.as_bytes(), salt, PBKDF2_ITER, &mut out);
out
}
#[cfg(test)]
mod tests {
#![expect(
clippy::unwrap_used,
clippy::panic,
reason = "test code uses direct unwrap and panic assertions"
)]
use cbc::Encryptor;
use cbc::cipher::BlockModeEncrypt;
use cbc::cipher::block_padding::Pkcs7 as Pkcs7Pad;
use super::*;
type TripleDesCbcEnc = Encryptor<TdesEde3>;
#[test]
fn pbkdf2_hmac_sha1_rfc6070_vector_1() {
let mut out = [0_u8; 20];
pbkdf2::pbkdf2_hmac::<Sha1>(b"password", b"salt", 1, &mut out);
assert_eq!(hex::encode(out), "0c60c80f961f0e71f3a9b524af6012062fe037a6");
}
#[test]
fn pbkdf2_hmac_sha1_rfc6070_vector_2() {
let mut out = [0_u8; 20];
pbkdf2::pbkdf2_hmac::<Sha1>(b"password", b"salt", 2, &mut out);
assert_eq!(hex::encode(out), "ea6c014dc72d6f8ccd1ed92ace1d41f0d8de8957");
}
#[test]
fn generate_master_key_keychainbreaker_test_vector() {
let salt = hex::decode("fc143c45cce245f3e54fbb39141a894e2870dd85").unwrap();
let key = generate_master_key("keychainbreaker-test", &salt);
assert_eq!(
hex::encode(key),
"4557eb716bbf20200945109cf3b884af9aca72e890e47c07"
);
}
#[test]
fn kc_decrypt_round_trip() {
let key = [0xAB_u8; KEY_LENGTH];
let iv = [0x12_u8; BLOCK_SIZE];
let plaintext: &[u8] = b"keychainbreaker test plaintext";
let mut buf = vec![0_u8; plaintext.len() + BLOCK_SIZE];
buf[..plaintext.len()].copy_from_slice(plaintext);
let ct = TripleDesCbcEnc::new_from_slices(&key, &iv)
.unwrap()
.encrypt_padded::<Pkcs7Pad>(&mut buf, plaintext.len())
.unwrap();
let ciphertext = ct.to_vec();
let recovered = kc_decrypt(&key, &iv, &ciphertext).unwrap();
assert_eq!(recovered, plaintext);
}
#[test]
fn kc_decrypt_rejects_short_inputs() {
let key = [0_u8; KEY_LENGTH];
let iv = [0_u8; BLOCK_SIZE];
assert!(kc_decrypt(&key, &iv, &[]).is_err());
assert!(kc_decrypt(&key, &iv, &[0_u8; 7]).is_err());
assert!(kc_decrypt(&[0_u8; 16], &iv, &[0_u8; 8]).is_err());
assert!(kc_decrypt(&key, &[0_u8; 4], &[0_u8; 8]).is_err());
}
#[test]
fn kc_decrypt_rejects_bad_padding() {
let key = [0_u8; KEY_LENGTH];
let iv = [0_u8; BLOCK_SIZE];
let bogus = [0xFF_u8; 16];
assert!(matches!(
kc_decrypt(&key, &iv, &bogus),
Err(Error::Cipher(_))
));
}
#[test]
fn magic_cms_iv_is_constant() {
assert_eq!(MAGIC_CMS_IV.len(), BLOCK_SIZE);
assert_eq!(MAGIC_CMS_IV[0], 0x4a);
assert_eq!(MAGIC_CMS_IV[7], 0x05);
}
#[test]
fn keyblob_decrypt_rejects_unwrapped_key_with_wrong_length() {
let db_key = [0xAB_u8; KEY_LENGTH];
let iv = [0x12_u8; BLOCK_SIZE];
let stage2_plain = [0x55_u8; 24];
let mut s2_buf = [0_u8; 32];
s2_buf[..24].copy_from_slice(&stage2_plain);
let stage2_ct = TripleDesCbcEnc::new_from_slices(&db_key, &iv)
.unwrap()
.encrypt_padded::<Pkcs7Pad>(&mut s2_buf, 24)
.unwrap()
.to_vec();
assert_eq!(stage2_ct.len(), 32);
let mut stage1_plain = [0_u8; 32];
for (i, byte) in stage2_ct.iter().enumerate() {
stage1_plain[31 - i] = *byte;
}
let mut s1_buf = [0_u8; 40];
s1_buf[..32].copy_from_slice(&stage1_plain);
let blob = TripleDesCbcEnc::new_from_slices(&db_key, &MAGIC_CMS_IV)
.unwrap()
.encrypt_padded::<Pkcs7Pad>(&mut s1_buf, 32)
.unwrap()
.to_vec();
let err = keyblob_decrypt(&blob, &iv, &db_key).unwrap_err();
match err {
Error::ParseFailed(msg) => {
assert!(
msg.contains("invalid unwrapped key length"),
"unexpected message: {msg}"
);
}
other => panic!("expected ParseFailed, got {other:?}"),
}
}
}