1use crate::blob::decode_utf16le;
23use crate::error::DpapiError;
24use crate::error::DpapiError::TooShort;
25
26#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct VaultVpolKeys {
32 pub key1: Vec<u8>,
33 pub key2: Vec<u8>,
34}
35
36#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct VcrdAttribute {
42 pub id: u32,
43 pub iv: Vec<u8>,
44 pub data: Vec<u8>,
45}
46
47#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct WebCredential {
50 pub username: String,
51 pub resource: String,
52 pub password: String,
53}
54
55pub fn parse_vpol_file(data: &[u8]) -> Result<Vec<u8>, DpapiError> {
61 let mut pos = 0usize;
62 let _version = read_u32(data, &mut pos);
63 pos = pos.saturating_add(16); let desc_len = read_u32(data, &mut pos) as usize;
65 pos = advance(pos, desc_len, data.len())?; pos = advance(pos, 12, data.len())?; let _size = read_u32(data, &mut pos); pos = advance(pos, 32, data.len())?; let key_size = read_u32(data, &mut pos) as usize;
70 let end = pos.checked_add(key_size).ok_or(too_short(data.len()))?;
71 let blob = data.get(pos..end).ok_or(TooShort {
72 needed: end,
73 got: data.len(),
74 })?;
75 Ok(blob.to_vec())
76}
77
78pub fn decrypt_vpol_keys(vpol_blob: &[u8], master_key: &[u8]) -> Result<VaultVpolKeys, DpapiError> {
84 let blob = crate::blob::parse_dpapi_blob(vpol_blob)?;
85 let cleartext = crate::decrypt::decrypt_dpapi_blob(&blob, master_key, None)?;
86 let mut pos = 0usize;
88 let key1 = read_bcrypt_key(&cleartext, &mut pos)?;
89 let key2 = read_bcrypt_key(&cleartext, &mut pos)?;
90 Ok(VaultVpolKeys { key1, key2 })
91}
92
93fn read_bcrypt_key(data: &[u8], pos: &mut usize) -> Result<Vec<u8>, DpapiError> {
102 let _size = read_u32(data, pos);
103 let _version = read_u32(data, pos);
104 let _unknown2 = read_u32(data, pos);
105 let _magic = read_u32(data, pos);
107 let _hver = read_u32(data, pos);
108 let cb = read_u32(data, pos) as usize;
109 let key_end = pos.checked_add(cb).ok_or(too_short(data.len()))?;
110 let key = data.get(*pos..key_end).ok_or(TooShort {
111 needed: key_end,
112 got: data.len(),
113 })?;
114 *pos = key_end;
115 Ok(key.to_vec())
116}
117
118pub fn parse_vcrd_attributes(vcrd: &[u8]) -> Result<Vec<VcrdAttribute>, DpapiError> {
120 let mut pos = 0usize;
123 pos = advance(pos, 16 + 4 + 8 + 4 + 4, vcrd.len())?;
124 let fn_len = read_u32(vcrd, &mut pos) as usize;
125 pos = advance(pos, fn_len, vcrd.len())?;
126 let maps_size = read_u32(vcrd, &mut pos) as usize;
127 let maps_start = pos;
128 let maps_end = maps_start
129 .checked_add(maps_size)
130 .ok_or(too_short(vcrd.len()))?;
131 const MAP_ENTRY: usize = 12;
134 if maps_size % MAP_ENTRY != 0 {
135 return Err(too_short(vcrd.len()));
136 }
137 let count = maps_size / MAP_ENTRY;
138 let mut offsets: Vec<(u32, usize)> = Vec::with_capacity(count);
139 let mut mp = maps_start;
140 for _ in 0..count {
141 let id = read_u32(vcrd, &mut mp);
142 let offset = read_u32(vcrd, &mut mp) as usize;
143 let _unknown1 = read_u32(vcrd, &mut mp);
144 offsets.push((id, offset));
145 }
146 if mp > maps_end {
147 return Err(too_short(vcrd.len()));
148 }
149
150 let mut attrs = Vec::with_capacity(count);
151 for i in 0..count {
152 let (id, start) = offsets[i];
153 let end = offsets.get(i + 1).map_or(vcrd.len(), |&(_, o)| o);
154 let attr_bytes = vcrd.get(start..end).ok_or(TooShort {
155 needed: end,
156 got: vcrd.len(),
157 })?;
158 if let Some((iv, payload)) = parse_attribute_payload(attr_bytes) {
159 attrs.push(VcrdAttribute {
160 id,
161 iv,
162 data: payload,
163 });
164 }
165 }
166 Ok(attrs)
167}
168
169fn parse_attribute_payload(attr: &[u8]) -> Option<(Vec<u8>, Vec<u8>)> {
177 if attr.len() <= 28 {
178 return None;
179 }
180 let id = u32::from_le_bytes(attr.get(0..4)?.try_into().ok()?);
181 let mut pos = 16usize;
182 if attr.get(16..22)? == [0u8; 6] {
183 pos += 6; }
185 if id >= 100 {
186 pos += 4; }
188 let size = u32::from_le_bytes(attr.get(pos..pos + 4)?.try_into().ok()?) as usize;
189 pos += 4;
190 let iv_present = *attr.get(pos)?;
191 pos += 1;
192 let iv_size = u32::from_le_bytes(attr.get(pos..pos + 4)?.try_into().ok()?) as usize;
193 pos += 4;
194 let (iv, data_len) = if iv_present != 0 {
195 let iv = attr.get(pos..pos + iv_size)?.to_vec();
196 pos += iv_size;
197 (iv, size.checked_sub(iv_size + 5)?)
198 } else {
199 (Vec::new(), size.checked_sub(1)?)
200 };
201 let data = attr.get(pos..pos + data_len)?.to_vec();
202 Some((iv, data))
203}
204
205pub fn decrypt_vcrd_attribute(
211 attr: &VcrdAttribute,
212 vpol_key: &[u8],
213) -> Result<Vec<u8>, DpapiError> {
214 use aes::Aes256;
215 use cbc::Decryptor as CbcDec;
216 use cipher::{block_padding::NoPadding, BlockDecryptMut, KeyIvInit};
217
218 let zero_iv = [0u8; 16];
219 let iv: &[u8] = if attr.iv.len() == 16 {
220 &attr.iv
221 } else {
222 &zero_iv
223 };
224 let mut buf = attr.data.clone();
225 let dec = CbcDec::<Aes256>::new_from_slices(vpol_key, iv)
226 .map_err(|_| DpapiError::InvalidKeyLength)?;
227 let out = dec
228 .decrypt_padded_mut::<NoPadding>(&mut buf)
229 .map_err(|_| DpapiError::DecryptionFailed)?;
230 Ok(out.to_vec())
231}
232
233pub fn parse_internet_explorer(cleartext: &[u8]) -> Result<WebCredential, DpapiError> {
238 let mut pos = 12usize; let username = read_id_len_utf16(cleartext, &mut pos)?;
240 let resource = read_id_len_utf16(cleartext, &mut pos)?;
241 let password = read_id_len_utf16(cleartext, &mut pos)?;
242 Ok(WebCredential {
243 username,
244 resource,
245 password,
246 })
247}
248
249fn read_id_len_utf16(data: &[u8], pos: &mut usize) -> Result<String, DpapiError> {
251 let _id = read_u32(data, pos);
252 let len = read_u32(data, pos) as usize;
253 let end = pos.checked_add(len).ok_or(too_short(data.len()))?;
254 let slice = data.get(*pos..end).ok_or(TooShort {
255 needed: end,
256 got: data.len(),
257 })?;
258 *pos = end;
259 Ok(decode_utf16le(slice))
260}
261
262#[inline]
264fn read_u32(data: &[u8], pos: &mut usize) -> u32 {
265 let v = data
266 .get(*pos..*pos + 4)
267 .and_then(|s| s.try_into().ok())
268 .map_or(0, u32::from_le_bytes);
269 *pos = pos.saturating_add(4);
270 v
271}
272
273fn advance(pos: usize, n: usize, len: usize) -> Result<usize, DpapiError> {
275 let end = pos.checked_add(n).ok_or(too_short(len))?;
276 if end > len {
277 return Err(TooShort {
278 needed: end,
279 got: len,
280 });
281 }
282 Ok(end)
283}
284
285#[inline]
287fn too_short(got: usize) -> DpapiError {
288 TooShort {
289 needed: usize::MAX,
290 got,
291 }
292}
293
294#[cfg(test)]
295mod tests {
296 use super::*;
297
298 fn hex(s: &str) -> Vec<u8> {
299 (0..s.len())
300 .step_by(2)
301 .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
302 .collect()
303 }
304
305 const MASTER_KEY_HEX: &str = "9828d9873735439e823dbd216205ff88266d28ad685a413970c640d5ee943154bbade31fada673d542c72d707a163bb3d1bceb0c50465b359ae06998481b0ce3";
308 const VPOL_FILE_HEX: &str = "01000000000000000000000000000000000000000a000000760070006f006c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007601000001000000d08c9ddf0115d1118c7a00c04fc297eb0100000033f19f5ee340be4a8a2e2b4e62bd0cc6000000000200000000001066000000010000200000001122334455667788112233445566778811223344556677881122334455667788000000000e8000000002000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000065a02b0acc4ef1fb8f698d7e6c9214d19144177556a964a02b9c13445d8cb6b640779e5cf1e273f2f8ede6445631e12c28c81e907373f71b9d26ffca96bba17b5147feb3b70fb9ebe9aa4150a1ec63c1f4e7356e600ed1bb5c15b2f850b8f21b3fac4e658821cf41f8b779baa815284576fa06d4aa59509eb7541d331958e7ca4000000057da67b85c3f524f46e18fd82b0bcfedba543efe03877d8f7d81daef61e311df7f8509d7ee7cd25becdfac53fa47c20a4150417065518659c72f200da584e654";
309 const VPOL_BLOB_HEX: &str = "01000000d08c9ddf0115d1118c7a00c04fc297eb0100000033f19f5ee340be4a8a2e2b4e62bd0cc6000000000200000000001066000000010000200000001122334455667788112233445566778811223344556677881122334455667788000000000e8000000002000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000065a02b0acc4ef1fb8f698d7e6c9214d19144177556a964a02b9c13445d8cb6b640779e5cf1e273f2f8ede6445631e12c28c81e907373f71b9d26ffca96bba17b5147feb3b70fb9ebe9aa4150a1ec63c1f4e7356e600ed1bb5c15b2f850b8f21b3fac4e658821cf41f8b779baa815284576fa06d4aa59509eb7541d331958e7ca4000000057da67b85c3f524f46e18fd82b0bcfedba543efe03877d8f7d81daef61e311df7f8509d7ee7cd25becdfac53fa47c20a4150417065518659c72f200da584e654";
311 const VPOL_KEY1_HEX: &str = "101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f";
312 const VPOL_KEY2_HEX: &str = "404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f";
313 const VCRD_FILE_HEX: &str = "0000000000000000000000000000000000000000000000000000d801000000000000000040000000570069006e0064006f007700730020005700650062002000500061007300730077006f00720064002000430072006500640065006e007400690061006c0000000c0000006400000078000000000000006400000000000000000000000000000000000000000000000000a500000001100000000f1e2d3c4b5a69788796a5b4c3d2e1f0d3183526451c5a94e61918a73ef697b98b4aeee92c9d96997727f81033c97368e2dc5fde038d14400f40259325c614d74e6309d7ee0b222a3f172b072b06118cb34acbb407d6a7d73c4f2d02034715c3ca8eea7993ab826edc8ddcf92bb9a9a3176e4af211ae8d18fbbe9a9574d01d67a98ec41b9e743d3ddf616db94c33c77a00a7abd0cffb59455947816dc11ff61c";
314 const ATTR_IV_HEX: &str = "0f1e2d3c4b5a69788796a5b4c3d2e1f0";
315 const EXPECT_USER: &str = "alice@example.com";
316 const EXPECT_RES: &str = "https://portal.example.com";
317 const EXPECT_PWD: &str = "V@ultP4ss!";
318
319 #[test]
321 fn vpol_wrapper_yields_inner_blob() {
322 let blob = parse_vpol_file(&hex(VPOL_FILE_HEX)).expect("strip ok");
323 assert_eq!(blob, hex(VPOL_BLOB_HEX));
324 }
325
326 #[test]
328 fn decrypt_vpol_keys_matches_impacket() {
329 let mk = hex(MASTER_KEY_HEX);
330 let keys = decrypt_vpol_keys(&hex(VPOL_BLOB_HEX), &mk).expect("vpol ok");
331 assert_eq!(keys.key1, hex(VPOL_KEY1_HEX));
332 assert_eq!(keys.key2, hex(VPOL_KEY2_HEX));
333 }
334
335 #[test]
337 fn vcrd_parses_attribute_with_iv() {
338 let attrs = parse_vcrd_attributes(&hex(VCRD_FILE_HEX)).expect("parse ok");
339 assert_eq!(attrs.len(), 1);
340 assert_eq!(attrs[0].iv, hex(ATTR_IV_HEX));
341 assert!(!attrs[0].data.is_empty());
342 }
343
344 #[test]
347 fn end_to_end_vault_web_credential() {
348 let mk = hex(MASTER_KEY_HEX);
349 let vpol_blob = parse_vpol_file(&hex(VPOL_FILE_HEX)).expect("strip ok");
350 let keys = decrypt_vpol_keys(&vpol_blob, &mk).expect("vpol ok");
351 let attrs = parse_vcrd_attributes(&hex(VCRD_FILE_HEX)).expect("parse ok");
352 let cleartext = decrypt_vcrd_attribute(&attrs[0], &keys.key1).expect("attr ok");
353 let cred = parse_internet_explorer(&cleartext).expect("ie ok");
354 assert_eq!(cred.username, EXPECT_USER);
355 assert_eq!(cred.resource, EXPECT_RES);
356 assert_eq!(cred.password, EXPECT_PWD);
357 }
358
359 #[test]
362 fn no_usable_master_key_refuses_rather_than_fabricates() {
363 let bad_mk = [0u8; 64];
364 let result = decrypt_vpol_keys(&hex(VPOL_BLOB_HEX), &bad_mk);
365 assert!(
366 result.is_err(),
367 "must error when the VPOL key can't be derived, never fabricate keys"
368 );
369 }
370}