Skip to main content

dpapi_core/
decrypt.rs

1use aes::Aes256;
2use cbc::Decryptor;
3use cipher::{block_padding::Pkcs7, BlockDecryptMut, KeyIvInit};
4use hmac::{Hmac, Mac};
5use sha1::{Digest, Sha1};
6use sha2::Sha512;
7
8use forensicnomicon::dpapi::{cipher_alg_info, CALG_AES_256};
9
10use crate::blob::{hash_alg, HashAlg};
11use crate::error::DpapiError;
12
13/// Decrypt an AES-256-CBC ciphertext.
14pub fn decrypt_aes256_cbc(key: &[u8], iv: &[u8], ciphertext: &[u8]) -> Result<Vec<u8>, DpapiError> {
15    let mut buf = ciphertext.to_vec();
16    let decryptor =
17        Decryptor::<Aes256>::new_from_slices(key, iv).map_err(|_| DpapiError::InvalidKeyLength)?;
18    let plaintext = decryptor
19        .decrypt_padded_mut::<Pkcs7>(&mut buf)
20        .map_err(|_| DpapiError::DecryptionFailed)?;
21    Ok(plaintext.to_vec())
22}
23
24/// Verify HMAC-SHA1 over `data` using `key`; return Err if mismatch.
25pub fn verify_hmac_sha1(key: &[u8], data: &[u8], expected: &[u8]) -> Result<(), DpapiError> {
26    let mut mac = Hmac::<Sha1>::new_from_slice(key).map_err(|_| DpapiError::InvalidKeyLength)?;
27    mac.update(data);
28    mac.verify_slice(expected)
29        .map_err(|_| DpapiError::HmacMismatch)
30}
31
32/// Keyed HMAC over `msg` selecting SHA1 or SHA512 by `alg`.
33fn hmac_hash(alg: HashAlg, key: &[u8], msg: &[u8]) -> Result<Vec<u8>, DpapiError> {
34    if alg.is_sha512 {
35        let mut mac =
36            Hmac::<Sha512>::new_from_slice(key).map_err(|_| DpapiError::InvalidKeyLength)?;
37        mac.update(msg);
38        Ok(mac.finalize().into_bytes().to_vec())
39    } else {
40        let mut mac =
41            Hmac::<Sha1>::new_from_slice(key).map_err(|_| DpapiError::InvalidKeyLength)?;
42        mac.update(msg);
43        Ok(mac.finalize().into_bytes().to_vec())
44    }
45}
46
47/// Plain (unkeyed) hash digest selecting SHA1 or SHA512 by `alg`.
48fn plain_hash(alg: HashAlg, msg: &[u8]) -> Vec<u8> {
49    if alg.is_sha512 {
50        Sha512::digest(msg).to_vec()
51    } else {
52        Sha1::digest(msg).to_vec()
53    }
54}
55
56/// impacket `DPAPI_BLOB.deriveKey`: derive the cipher key from the session key.
57///
58/// When the session key is longer than the hash's derive block, it is re-MACed;
59/// when the resulting key is shorter than the cipher key length, it is expanded
60/// via the ipad/opad construction (with DES parity fix-up applied to the bytes).
61fn derive_key(alg: HashAlg, session_key: &[u8], cipher_key_len: usize) -> Vec<u8> {
62    let mut derived = if session_key.len() > alg.derive_block_len {
63        // HMAC with an empty message, keyed by the session key.
64        hmac_hash(alg, session_key, &[]).unwrap_or_default()
65    } else {
66        session_key.to_vec()
67    };
68
69    if derived.len() < cipher_key_len {
70        let mut padded = derived.clone();
71        padded.resize(alg.derive_block_len, 0);
72        let ipad: Vec<u8> = padded
73            .iter()
74            .take(alg.derive_block_len)
75            .map(|b| b ^ 0x36)
76            .collect();
77        let opad: Vec<u8> = padded
78            .iter()
79            .take(alg.derive_block_len)
80            .map(|b| b ^ 0x5c)
81            .collect();
82        let mut out = plain_hash(alg, &ipad);
83        out.extend_from_slice(&plain_hash(alg, &opad));
84        fixparity(&mut out);
85        derived = out;
86    }
87
88    derived
89}
90
91/// DES odd-parity fix-up: set each byte's low bit so the byte has odd parity,
92/// matching impacket's `fixparity`.
93fn fixparity(key: &mut [u8]) {
94    for b in key.iter_mut() {
95        let high7 = *b >> 1;
96        let ones = high7.count_ones();
97        *b = (high7 << 1) | u8::from(ones % 2 == 0);
98    }
99}
100
101/// Decrypt a DPAPI blob with the provided master-key bytes (and optional entropy).
102///
103/// Implements impacket's `DPAPI_BLOB.decrypt`:
104/// `keyHash = SHA1(master_key)`; `sessionKey = HMAC_H(keyHash, salt[||entropy])`
105/// where `H` is SHA1 for `algId` 0x8004 and SHA512 for 0x8009/0x800e;
106/// the cipher key is `deriveKey(sessionKey)`; the IV is all zeros. The trailing
107/// `Sign` HMAC is verified (either impacket integrity formula) before returning.
108pub fn decrypt_dpapi_blob(
109    blob: &crate::blob::DpapiBlob,
110    master_key: &[u8],
111    entropy: Option<&[u8]>,
112) -> Result<Vec<u8>, DpapiError> {
113    let alg = hash_alg(blob.alg_id_hash);
114    // Cipher key/IV sizes are knowledge (forensicnomicon); the IV is always
115    // zero-filled (impacket: `iv=b'\x00'*IVLen`). 3DES is the only non-AES-256
116    // cipher DPAPI uses, so the AES-256 algId selects the AES path.
117    let cipher = cipher_alg_info(blob.alg_id_encrypt)
118        .ok_or(DpapiError::UnsupportedAlgId(blob.alg_id_encrypt))?;
119    let is_aes256 = blob.alg_id_encrypt == CALG_AES_256;
120
121    // keyHash = SHA1(master_key) — always SHA1, even for SHA512 blobs.
122    let key_hash = Sha1::digest(master_key).to_vec();
123
124    // sessionKey = HMAC_H(keyHash, salt [|| entropy])
125    let mut salt_msg = blob.salt.clone();
126    if let Some(e) = entropy {
127        salt_msg.extend_from_slice(e);
128    }
129    let session_key = hmac_hash(alg, &key_hash, &salt_msg)?;
130
131    let derived = derive_key(alg, &session_key, cipher.key_len);
132    if derived.len() < cipher.key_len {
133        return Err(DpapiError::InvalidKeyLength);
134    }
135    let iv = vec![0u8; cipher.iv_len];
136
137    let cleartext = if is_aes256 {
138        decrypt_aes256_cbc(&derived[..cipher.key_len], &iv, &blob.ciphertext)?
139    } else {
140        decrypt_3des_cbc(&derived[..cipher.key_len], &iv, &blob.ciphertext)?
141    };
142
143    verify_blob_signature(&alg, &key_hash, blob, entropy)?;
144    Ok(cleartext)
145}
146
147/// Verify the blob's trailing `Sign` HMAC against impacket's two accepted forms.
148fn verify_blob_signature(
149    alg: &HashAlg,
150    key_hash: &[u8],
151    blob: &crate::blob::DpapiBlob,
152    entropy: Option<&[u8]>,
153) -> Result<(), DpapiError> {
154    // Form 1: manual ipad/opad over keyHash padded to the hash block size.
155    let mut key_hash2 = key_hash.to_vec();
156    key_hash2.resize(key_hash.len() + alg.hash_block_len, 0);
157    let ipad: Vec<u8> = key_hash2
158        .iter()
159        .take(alg.hash_block_len)
160        .map(|b| b ^ 0x36)
161        .collect();
162    let opad: Vec<u8> = key_hash2
163        .iter()
164        .take(alg.hash_block_len)
165        .map(|b| b ^ 0x5c)
166        .collect();
167
168    let mut inner = ipad;
169    inner.extend_from_slice(&blob.hmac);
170    let inner_digest = plain_hash(*alg, &inner);
171
172    let mut outer = opad;
173    outer.extend_from_slice(&inner_digest);
174    if let Some(e) = entropy {
175        outer.extend_from_slice(e);
176    }
177    outer.extend_from_slice(&blob.to_sign);
178    let calc1 = plain_hash(*alg, &outer);
179
180    // Form 2: standard HMAC_H(keyHash, HMac [|| entropy] || toSign).
181    let mut msg2 = blob.hmac.clone();
182    if let Some(e) = entropy {
183        msg2.extend_from_slice(e);
184    }
185    msg2.extend_from_slice(&blob.to_sign);
186    let calc2 = hmac_hash(*alg, key_hash, &msg2)?;
187
188    if calc1 == blob.sign || calc2 == blob.sign {
189        Ok(())
190    } else {
191        Err(DpapiError::HmacMismatch)
192    }
193}
194
195fn decrypt_3des_cbc(key: &[u8], iv: &[u8], ciphertext: &[u8]) -> Result<Vec<u8>, DpapiError> {
196    use cbc::Decryptor as CbcDec;
197    use cipher::block_padding::NoPadding;
198    use des::TdesEde3;
199
200    let mut buf = ciphertext.to_vec();
201    let dec =
202        CbcDec::<TdesEde3>::new_from_slices(key, iv).map_err(|_| DpapiError::InvalidKeyLength)?;
203    let out = dec
204        .decrypt_padded_mut::<NoPadding>(&mut buf)
205        .map_err(|_| DpapiError::DecryptionFailed)?;
206
207    // impacket unpads with the cipher block size; mirror that here.
208    let unpadded = pkcs_unpad(out, 8)?;
209    Ok(unpadded)
210}
211
212/// PKCS#7-style unpad for a given block size (impacket's `unpad`).
213fn pkcs_unpad(data: &[u8], block_len: usize) -> Result<Vec<u8>, DpapiError> {
214    let pad = *data.last().ok_or(DpapiError::DecryptionFailed)? as usize;
215    if pad == 0 || pad > block_len || pad > data.len() {
216        return Err(DpapiError::DecryptionFailed);
217    }
218    if data[data.len() - pad..].iter().any(|&b| b as usize != pad) {
219        return Err(DpapiError::DecryptionFailed);
220    }
221    Ok(data[..data.len() - pad].to_vec())
222}
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227    use aes::Aes256;
228    use cbc::Encryptor;
229    use cipher::{block_padding::Pkcs7, BlockEncryptMut, KeyIvInit};
230
231    fn aes256_cbc_encrypt(key: &[u8; 32], iv: &[u8; 16], plaintext: &[u8]) -> Vec<u8> {
232        let enc = Encryptor::<Aes256>::new_from_slices(key, iv).unwrap();
233        let mut buf = plaintext.to_vec();
234        // pad to 16-byte boundary
235        let pad_len = 16 - (buf.len() % 16);
236        buf.extend(std::iter::repeat_n(pad_len as u8, pad_len));
237        enc.encrypt_padded_mut::<Pkcs7>(&mut buf, plaintext.len())
238            .unwrap()
239            .to_vec()
240    }
241
242    #[test]
243    fn decrypt_aes256_cbc_roundtrip() {
244        let key = [0x42u8; 32];
245        let iv = [0x11u8; 16];
246        let plaintext = b"hello DPAPI world!";
247        let ciphertext = aes256_cbc_encrypt(&key, &iv, plaintext);
248        let recovered = decrypt_aes256_cbc(&key, &iv, &ciphertext).expect("decrypt ok");
249        assert_eq!(&recovered[..plaintext.len()], plaintext);
250    }
251
252    #[test]
253    fn verify_hmac_sha1_correct_passes() {
254        use hmac::{Hmac, Mac};
255        use sha1::Sha1;
256        let key = b"secretkey";
257        let data = b"some data to mac";
258        let mut mac = Hmac::<Sha1>::new_from_slice(key).unwrap();
259        mac.update(data);
260        let expected = mac.finalize().into_bytes();
261        assert!(verify_hmac_sha1(key, data, &expected).is_ok());
262    }
263
264    #[test]
265    fn verify_hmac_sha1_wrong_key_fails() {
266        use hmac::{Hmac, Mac};
267        use sha1::Sha1;
268        let data = b"data";
269        let mut mac = Hmac::<Sha1>::new_from_slice(b"key1").unwrap();
270        mac.update(data);
271        let expected = mac.finalize().into_bytes();
272        assert!(verify_hmac_sha1(b"key2", data, &expected).is_err());
273    }
274
275    fn hex(s: &str) -> Vec<u8> {
276        (0..s.len())
277            .step_by(2)
278            .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
279            .collect()
280    }
281
282    // Tier-1 vector: blob minted on Windows, master key recovered with mimikatz,
283    // plaintext authored & confirmed by impacket 0.12.0 (DPAPI_BLOB.decrypt).
284    // hashAlgo=0x800e (SHA512), cryptAlgo=0x6610 (AES-256-CBC), no entropy.
285    const MASTER_KEY_HEX: &str = "9828d9873735439e823dbd216205ff88266d28ad685a413970c640d5ee943154bbade31fada673d542c72d707a163bb3d1bceb0c50465b359ae06998481b0ce3";
286    const VECTOR1_BLOB_HEX: &str = "01000000d08c9ddf0115d1118c7a00c04fc297eb0100000033f19f5ee340be4a8a2e2b4e62bd0cc6000000000200000000001066000000010000200000000d1af96e5e102266fd36d96ac7d1595552e5a4e972463f77e6e227f22d5fc8df000000000e8000000002000020000000834f3c5710c8a7474f7dbcea8ba28ab8e4d4443f50a0c63ff4eba1cce485295f20000000b61d7576c0c6caf3690edb247bde3f7edaa59580e3b4be1265ea78e8c1b8a61d400000001c03ab807147742649b6bdfd1c1344d178bb163842d70abacfd51233af909cb81a677ec05d8db996f587ef5ac410dc189beda756eb0d1b6ee376823e80968538";
287
288    // Vector 2: same key, hashAlgo=0x800e/AES-256, WITH entropy b"Some entropy".
289    const VECTOR2_BLOB_HEX: &str = "01000000d08c9ddf0115d1118c7a00c04fc297eb0100000033f19f5ee340be4a8a2e2b4e62bd0cc600000000020000000000106600000001000020000000f239c0018e71b33bef9a6299675c7e209eef1f6447bd578d19c7973548737545000000000e80000000020000200000009d9ef33e15ffb1b310a13ecec39b1c02adc39e8d40a7162f9f9bb3170c699a812000000040e820259332c47af42e5f9de629e109d1504641aad853f3818c40ac311cf24a4000000010f01a84a5cc0393d3ea44cc3a8ff00ca4d02fcabc7c353a6823c53e4e719c9b398282a06b8878250205160ed79fef8b026093ad5a467594953d6de28d71f8c9";
290
291    #[test]
292    fn decrypt_sha512_aes256_blob_no_entropy() {
293        let blob = crate::blob::parse_dpapi_blob(&hex(VECTOR1_BLOB_HEX)).expect("parse");
294        let mk = hex(MASTER_KEY_HEX);
295        let pt = decrypt_dpapi_blob(&blob, &mk, None).expect("decrypt");
296        assert_eq!(pt, b"Some test string");
297    }
298
299    #[test]
300    fn decrypt_sha512_aes256_blob_with_entropy() {
301        let blob = crate::blob::parse_dpapi_blob(&hex(VECTOR2_BLOB_HEX)).expect("parse");
302        let mk = hex(MASTER_KEY_HEX);
303        let pt = decrypt_dpapi_blob(&blob, &mk, Some(b"Some entropy")).expect("decrypt");
304        assert_eq!(pt, b"Some test string");
305    }
306
307    #[test]
308    fn decrypt_sha512_blob_wrong_entropy_fails_integrity() {
309        let blob = crate::blob::parse_dpapi_blob(&hex(VECTOR2_BLOB_HEX)).expect("parse");
310        let mk = hex(MASTER_KEY_HEX);
311        // Wrong/missing entropy must not silently return garbage: the Sign HMAC
312        // check rejects it.
313        assert!(decrypt_dpapi_blob(&blob, &mk, None).is_err());
314    }
315}