1use forensicnomicon::dpapi::{hash_alg_info, PROVIDER_GUID_BYTES};
2
3use crate::error::DpapiError;
4
5pub use forensicnomicon::dpapi::HashAlgInfo as HashAlg;
16
17pub fn hash_alg(alg_id_hash: u32) -> HashAlg {
25 hash_alg_info(alg_id_hash).unwrap_or(HashAlg {
26 is_sha512: false,
27 digest_len: 20,
28 derive_block_len: 64,
29 hash_block_len: 64,
30 })
31}
32
33#[derive(Debug, Clone)]
41pub struct DpapiBlob {
42 pub version: u32,
43 pub master_key_guid: [u8; 16],
44 pub description: String,
45 pub alg_id_encrypt: u32,
46 pub alg_id_hash: u32,
47 pub salt: Vec<u8>,
48 pub hmac_key: Vec<u8>,
49 pub hmac: Vec<u8>,
50 pub ciphertext: Vec<u8>,
51 pub sign: Vec<u8>,
52 pub to_sign: Vec<u8>,
53}
54
55fn read_len_prefixed<'a>(data: &'a [u8], pos: &mut usize) -> Result<&'a [u8], DpapiError> {
57 let len = read_u32(data, pos) as usize;
58 let slice = data.get(*pos..*pos + len).ok_or(DpapiError::TooShort {
59 needed: *pos + len,
60 got: data.len(),
61 })?;
62 *pos += len;
63 Ok(slice)
64}
65
66pub fn parse_dpapi_blob(data: &[u8]) -> Result<DpapiBlob, DpapiError> {
67 if data.len() < 48 {
70 return Err(DpapiError::TooShort {
71 needed: 48,
72 got: data.len(),
73 });
74 }
75
76 let mut pos = 0usize;
77
78 let version = read_u32(data, &mut pos);
79 if version != 1 && version != 2 {
80 return Err(DpapiError::UnsupportedVersion(version));
81 }
82 let provider_guid: [u8; 16] =
85 data[pos..pos + 16]
86 .try_into()
87 .map_err(|_| DpapiError::TooShort {
88 needed: pos + 16,
89 got: data.len(),
90 })?;
91 if provider_guid != PROVIDER_GUID_BYTES {
92 use core::fmt::Write as _;
93 let hex = provider_guid.iter().fold(String::new(), |mut s, b| {
94 let _ = write!(s, "{b:02x}");
95 s
96 });
97 return Err(DpapiError::NotDpapiProvider(hex));
98 }
99 pos += 16;
100 let _mk_version = read_u32(data, &mut pos);
101 let master_key_guid: [u8; 16] =
102 data[pos..pos + 16]
103 .try_into()
104 .map_err(|_| DpapiError::TooShort {
105 needed: pos + 16,
106 got: data.len(),
107 })?;
108 pos += 16;
109 let _flags = read_u32(data, &mut pos);
110
111 let desc_bytes = read_len_prefixed(data, &mut pos)?;
112 let description = decode_utf16le(desc_bytes);
113
114 let alg_id_encrypt = read_u32(data, &mut pos);
115 let _crypt_algo_len = read_u32(data, &mut pos);
116
117 let salt = read_len_prefixed(data, &mut pos)?.to_vec();
118 let hmac_key = read_len_prefixed(data, &mut pos)?.to_vec();
119
120 let alg_id_hash = read_u32(data, &mut pos);
121 let _hash_algo_len = read_u32(data, &mut pos);
122
123 let hmac = read_len_prefixed(data, &mut pos)?.to_vec();
124 let ciphertext = read_len_prefixed(data, &mut pos)?.to_vec();
125
126 let sign_len_pos = pos;
128 let sign = read_len_prefixed(data, &mut pos)?.to_vec();
129 if sign_len_pos < 20 {
130 return Err(DpapiError::TooShort {
131 needed: 20,
132 got: sign_len_pos,
133 });
134 }
135 let to_sign = data[20..sign_len_pos].to_vec();
136
137 Ok(DpapiBlob {
138 version,
139 master_key_guid,
140 description,
141 alg_id_encrypt,
142 alg_id_hash,
143 salt,
144 hmac_key,
145 hmac,
146 ciphertext,
147 sign,
148 to_sign,
149 })
150}
151
152fn decode_utf16le(bytes: &[u8]) -> String {
154 if bytes.len() < 2 {
155 return String::new();
156 }
157 let words: Vec<u16> = bytes
158 .chunks_exact(2)
159 .map(|c| u16::from_le_bytes([c[0], c[1]]))
160 .collect();
161 String::from_utf16_lossy(&words)
162 .trim_end_matches('\0')
163 .to_string()
164}
165
166#[inline]
170fn read_u32(data: &[u8], pos: &mut usize) -> u32 {
171 let v = data
172 .get(*pos..*pos + 4)
173 .and_then(|s| s.try_into().ok())
174 .map_or(0, u32::from_le_bytes);
175 *pos += 4;
176 v
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 fn hex(s: &str) -> Vec<u8> {
184 (0..s.len())
185 .step_by(2)
186 .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
187 .collect()
188 }
189
190 fn make_blob(
192 crypt_algo: u32,
193 hash_algo: u32,
194 salt: &[u8],
195 hmac_key: &[u8],
196 hmac: &[u8],
197 data: &[u8],
198 sign: &[u8],
199 ) -> Vec<u8> {
200 let mut v = Vec::new();
201 v.extend_from_slice(&2u32.to_le_bytes()); v.extend_from_slice(&PROVIDER_GUID_BYTES); v.extend_from_slice(&0u32.to_le_bytes()); v.extend_from_slice(&[0xAAu8; 16]); v.extend_from_slice(&0u32.to_le_bytes()); v.extend_from_slice(&0u32.to_le_bytes()); v.extend_from_slice(&crypt_algo.to_le_bytes());
208 v.extend_from_slice(&256u32.to_le_bytes()); v.extend_from_slice(&(salt.len() as u32).to_le_bytes());
210 v.extend_from_slice(salt);
211 v.extend_from_slice(&(hmac_key.len() as u32).to_le_bytes());
212 v.extend_from_slice(hmac_key);
213 v.extend_from_slice(&hash_algo.to_le_bytes());
214 v.extend_from_slice(&512u32.to_le_bytes()); v.extend_from_slice(&(hmac.len() as u32).to_le_bytes());
216 v.extend_from_slice(hmac);
217 v.extend_from_slice(&(data.len() as u32).to_le_bytes());
218 v.extend_from_slice(data);
219 v.extend_from_slice(&(sign.len() as u32).to_le_bytes());
220 v.extend_from_slice(sign);
221 v
222 }
223
224 const REAL_BLOB_HEX: &str = "01000000d08c9ddf0115d1118c7a00c04fc297eb0100000033f19f5ee340be4a8a2e2b4e62bd0cc6000000000200000000001066000000010000200000000d1af96e5e102266fd36d96ac7d1595552e5a4e972463f77e6e227f22d5fc8df000000000e8000000002000020000000834f3c5710c8a7474f7dbcea8ba28ab8e4d4443f50a0c63ff4eba1cce485295f20000000b61d7576c0c6caf3690edb247bde3f7edaa59580e3b4be1265ea78e8c1b8a61d400000001c03ab807147742649b6bdfd1c1344d178bb163842d70abacfd51233af909cb81a677ec05d8db996f587ef5ac410dc189beda756eb0d1b6ee376823e80968538";
227
228 #[test]
229 fn parse_rejects_too_short() {
230 assert!(parse_dpapi_blob(&[0u8; 10]).is_err());
231 }
232
233 #[test]
234 fn parse_rejects_non_dpapi_provider_guid() {
235 let mut blob = make_blob(
238 0x6610,
239 0x8004,
240 &[0xEEu8; 16],
241 &[],
242 &[0xCCu8; 20],
243 &[0xDDu8; 16],
244 &[0xCCu8; 20],
245 );
246 blob[4..20].copy_from_slice(&[0x11u8; 16]); match parse_dpapi_blob(&blob) {
248 Err(DpapiError::NotDpapiProvider(guid)) => {
249 assert!(guid.contains("11"), "error must surface the bytes: {guid}");
250 }
251 other => panic!("expected NotDpapiProvider, got {other:?}"),
252 }
253 }
254
255 #[test]
256 fn parse_extracts_master_key_guid() {
257 let blob = make_blob(
258 0x6610,
259 0x8004,
260 &[0xEEu8; 16],
261 &[],
262 &[0xCCu8; 20],
263 &[0xDDu8; 16],
264 &[0xCCu8; 20],
265 );
266 let result = parse_dpapi_blob(&blob).expect("should parse");
267 assert_eq!(result.master_key_guid, [0xAA; 16]);
268 }
269
270 #[test]
271 fn parse_extracts_alg_id_encrypt() {
272 let blob = make_blob(
273 0x6610,
274 0x8004,
275 &[0xEEu8; 16],
276 &[],
277 &[0xCCu8; 20],
278 &[0xDDu8; 16],
279 &[0xCCu8; 20],
280 );
281 let result = parse_dpapi_blob(&blob).expect("should parse");
282 assert_eq!(result.alg_id_encrypt, 0x6610);
283 }
284
285 #[test]
286 fn parse_extracts_salt_and_ciphertext() {
287 let salt = [0xEEu8; 32];
288 let data = [0xDDu8; 32];
289 let blob = make_blob(
290 0x6610,
291 0x8004,
292 &salt,
293 &[],
294 &[0xCCu8; 20],
295 &data,
296 &[0xCCu8; 20],
297 );
298 let result = parse_dpapi_blob(&blob).expect("should parse");
299 assert_eq!(result.salt, salt);
300 assert_eq!(result.ciphertext, data);
301 }
302
303 #[test]
305 fn parse_real_blob_matches_impacket_fields() {
306 let result = parse_dpapi_blob(&hex(REAL_BLOB_HEX)).expect("should parse");
307 assert_eq!(result.alg_id_encrypt, 0x6610);
308 assert_eq!(result.alg_id_hash, 0x800E);
309 assert_eq!(
310 result.salt,
311 hex("0d1af96e5e102266fd36d96ac7d1595552e5a4e972463f77e6e227f22d5fc8df")
312 );
313 assert!(result.hmac_key.is_empty());
314 assert_eq!(
315 result.hmac,
316 hex("834f3c5710c8a7474f7dbcea8ba28ab8e4d4443f50a0c63ff4eba1cce485295f")
317 );
318 assert_eq!(
319 result.ciphertext,
320 hex("b61d7576c0c6caf3690edb247bde3f7edaa59580e3b4be1265ea78e8c1b8a61d")
321 );
322 assert_eq!(result.sign.len(), 64);
323 }
324}