/// AES-128-CBC decryption for XEX2 session key derivation and payload decryption. use aes::Aes128; use cbc::cipher::{BlockDecryptMut, KeyIvInit}; use crate::error::{Result, Xex2Error}; type Aes128CbcDec = cbc::Decryptor; /// Well-known XEX2 retail AES-128 master key. pub const XEX2_RETAIL_KEY: [u8; 16] = [ 0x20, 0xB1, 0x85, 0xA5, 0x9D, 0x28, 0xFD, 0xC3, 0x40, 0x58, 0x3F, 0xBB, 0x08, 0x96, 0xBF, 0x91, ]; /// Well-known XEX2 devkit AES-128 master key (all zeros). pub const XEX2_DEVKIT_KEY: [u8; 16] = [0u8; 16]; /// Well-known XEX1 retail AES-128 master key. pub const XEX1_RETAIL_KEY: [u8; 16] = [ 0xA2, 0x6C, 0x10, 0xF7, 0x1F, 0xD9, 0x35, 0xE9, 0x8B, 0x99, 0x92, 0x2C, 0xE9, 0x32, 0x15, 0x72, ]; /// Master keys tried in order during session key derivation. const MASTER_KEYS: &[[u8; 16]] = &[XEX2_RETAIL_KEY, XEX2_DEVKIT_KEY, XEX1_RETAIL_KEY]; /// Zero IV used for all XEX2 AES-128-CBC operations. const ZERO_IV: [u8; 16] = [0u8; 16]; /// Decrypts a 16-byte block using AES-128-CBC with a zero IV. fn aes128_cbc_decrypt_block(key: &[u8; 16], encrypted: &[u8; 16]) -> [u8; 16] { let mut block = encrypted.to_owned(); let decryptor = Aes128CbcDec::new(key.into(), &ZERO_IV.into()); decryptor .decrypt_padded_mut::(&mut block) .expect("decryption of exactly one block should not fail"); block } /// Derives the session key by decrypting the encrypted AES key from security info. /// /// Tries master keys in order: XEX2 retail, XEX2 devkit, XEX1 retail. /// Returns the session key derived using the first master key (retail by default). /// Actual validation of which key is correct happens later when checking for a valid PE header. pub fn derive_session_key(encrypted_key: &[u8; 16]) -> [u8; 16] { // For now, always use retail key. Key trial with validation will be added in M6. aes128_cbc_decrypt_block(&XEX2_RETAIL_KEY, encrypted_key) } /// Tries all master keys and returns the session key that produces a valid decryption. /// /// `validator` is called with the derived session key and should return `true` if /// the key produces valid output (e.g., decrypted data starts with MZ signature). pub fn derive_session_key_with_validation( encrypted_key: &[u8; 16], validator: impl Fn(&[u8; 16]) -> bool, ) -> Result<[u8; 16]> { for master_key in MASTER_KEYS { let session_key = aes128_cbc_decrypt_block(master_key, encrypted_key); if validator(&session_key) { return Ok(session_key); } } Err(Xex2Error::DecryptionFailed) } /// Decrypts data in-place using AES-128-CBC with a zero IV. /// /// The data length must be a multiple of 16 bytes (AES block size). /// Any trailing bytes that don't fill a complete block are left unchanged. pub fn decrypt_in_place(session_key: &[u8; 16], data: &mut [u8]) { let block_len = data.len() - (data.len() % 16); if block_len == 0 { return; } let decryptor = Aes128CbcDec::new(session_key.into(), &ZERO_IV.into()); decryptor .decrypt_padded_mut::(&mut data[..block_len]) .expect("decryption with NoPadding on aligned data should not fail"); } #[cfg(test)] mod tests { use super::*; #[test] fn test_master_key_constants() { // Verify retail key starts with expected bytes assert_eq!(XEX2_RETAIL_KEY[0], 0x20); assert_eq!(XEX2_RETAIL_KEY[15], 0x91); // Devkit key is all zeros assert!(XEX2_DEVKIT_KEY.iter().all(|&b| b == 0)); // XEX1 key starts with expected bytes assert_eq!(XEX1_RETAIL_KEY[0], 0xA2); assert_eq!(XEX1_RETAIL_KEY[15], 0x72); } #[test] fn test_decrypt_block_deterministic() { let input = [0u8; 16]; let result1 = aes128_cbc_decrypt_block(&XEX2_RETAIL_KEY, &input); let result2 = aes128_cbc_decrypt_block(&XEX2_RETAIL_KEY, &input); assert_eq!(result1, result2); } #[test] fn test_decrypt_block_different_keys_differ() { let input = [0x42u8; 16]; let retail = aes128_cbc_decrypt_block(&XEX2_RETAIL_KEY, &input); let devkit = aes128_cbc_decrypt_block(&XEX2_DEVKIT_KEY, &input); assert_ne!(retail, devkit); } #[test] fn test_derive_session_key_from_sample() { // The sample file's encrypted AES key starts with 0xEACB let encrypted: [u8; 16] = [ 0xEA, 0xCB, 0x4C, 0x2E, 0x0D, 0xBA, 0x85, 0x36, 0xCF, 0xB2, 0x65, 0x3C, 0xBB, 0xBF, 0x2E, 0xFC, ]; let session_key = derive_session_key(&encrypted); // Session key should be non-zero (decryption worked) assert!(!session_key.iter().all(|&b| b == 0)); // Session key should differ from input assert_ne!(&session_key[..], &encrypted[..]); } #[test] fn test_derive_session_key_with_validation_finds_key() { let encrypted = [0x42u8; 16]; // Validator that always accepts retail key result let retail_result = aes128_cbc_decrypt_block(&XEX2_RETAIL_KEY, &encrypted); let result = derive_session_key_with_validation(&encrypted, |key| *key == retail_result); assert!(result.is_ok()); assert_eq!(result.unwrap(), retail_result); } #[test] fn test_derive_session_key_with_validation_fails() { let encrypted = [0x42u8; 16]; let result = derive_session_key_with_validation(&encrypted, |_| false); assert!(result.is_err()); } #[test] fn test_decrypt_in_place_roundtrip() { // Encrypt then decrypt should give back original // Since we only have decrypt, verify it's deterministic let key = [0x01u8; 16]; let mut data = [0xABu8; 32]; let original = data; decrypt_in_place(&key, &mut data); // Decrypted data should differ from encrypted assert_ne!(data, original); // Decrypting again should give different result (CBC is not self-inverse) let decrypted_once = data; decrypt_in_place(&key, &mut data); assert_ne!(data, decrypted_once); } #[test] fn test_decrypt_in_place_empty() { let key = [0u8; 16]; let mut data: [u8; 0] = []; decrypt_in_place(&key, &mut data); // Should not panic } #[test] fn test_decrypt_in_place_partial_block() { let key = [0u8; 16]; let mut data = [0xFFu8; 20]; // 16 + 4 trailing bytes decrypt_in_place(&key, &mut data); // Last 4 bytes should be unchanged assert_eq!(data[16..], [0xFF, 0xFF, 0xFF, 0xFF]); } }