1use crate::blob::decode_utf16le;
16use crate::error::DpapiError;
17
18#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct Credential {
24 pub target: String,
26 pub username: String,
28 pub secret: String,
30 pub last_written: u64,
32}
33
34pub fn parse_credential_file(data: &[u8]) -> Result<Vec<u8>, DpapiError> {
40 const HEADER_LEN: usize = 12;
43 let size = read_u32(data, 4) as usize;
44 let end = HEADER_LEN.checked_add(size).ok_or(DpapiError::TooShort {
45 needed: usize::MAX,
46 got: data.len(),
47 })?;
48 let blob = data.get(HEADER_LEN..end).ok_or(DpapiError::TooShort {
49 needed: end,
50 got: data.len(),
51 })?;
52 Ok(blob.to_vec())
53}
54
55pub fn decrypt_credential(blob_bytes: &[u8], master_key: &[u8]) -> Result<Credential, DpapiError> {
65 let blob = crate::blob::parse_dpapi_blob(blob_bytes)?;
66 let cleartext = crate::decrypt::decrypt_dpapi_blob(&blob, master_key, None)?;
67 parse_credential_blob(&cleartext)
68}
69
70fn parse_credential_blob(data: &[u8]) -> Result<Credential, DpapiError> {
78 const HEADER_LEN: usize = 48;
79 if data.len() < HEADER_LEN {
80 return Err(DpapiError::TooShort {
81 needed: HEADER_LEN,
82 got: data.len(),
83 });
84 }
85 let last_written = read_u64(data, 20);
87
88 let mut pos = HEADER_LEN;
89 let target = read_len_prefixed_utf16(data, &mut pos)?;
90 let _target_alias = read_len_prefixed_utf16(data, &mut pos)?;
91 let _description = read_len_prefixed_utf16(data, &mut pos)?;
92 let secret = read_len_prefixed_utf16(data, &mut pos)?;
93 let username = read_len_prefixed_utf16(data, &mut pos)?;
94
95 Ok(Credential {
96 target,
97 username,
98 secret,
99 last_written,
100 })
101}
102
103fn read_len_prefixed_utf16(data: &[u8], pos: &mut usize) -> Result<String, DpapiError> {
105 let len = read_u32(data, *pos) as usize;
106 let start = pos.checked_add(4).ok_or(DpapiError::TooShort {
107 needed: usize::MAX,
108 got: data.len(),
109 })?;
110 let end = start.checked_add(len).ok_or(DpapiError::TooShort {
111 needed: usize::MAX,
112 got: data.len(),
113 })?;
114 let slice = data.get(start..end).ok_or(DpapiError::TooShort {
115 needed: end,
116 got: data.len(),
117 })?;
118 *pos = end;
119 Ok(decode_utf16le(slice))
120}
121
122#[inline]
124fn read_u32(data: &[u8], off: usize) -> u32 {
125 data.get(off..off + 4)
126 .and_then(|s| s.try_into().ok())
127 .map_or(0, u32::from_le_bytes)
128}
129
130#[inline]
132fn read_u64(data: &[u8], off: usize) -> u64 {
133 data.get(off..off + 8)
134 .and_then(|s| s.try_into().ok())
135 .map_or(0, u64::from_le_bytes)
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 fn hex(s: &str) -> Vec<u8> {
143 (0..s.len())
144 .step_by(2)
145 .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
146 .collect()
147 }
148
149 const MASTER_KEY_HEX: &str = "9828d9873735439e823dbd216205ff88266d28ad685a413970c640d5ee943154bbade31fada673d542c72d707a163bb3d1bceb0c50465b359ae06998481b0ce3";
153 const CRED_FILE_HEX: &str = "01000000b60100000000000001000000d08c9ddf0115d1118c7a00c04fc297eb0100000033f19f5ee340be4a8a2e2b4e62bd0cc600000000020000000000106600000001000020000000aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899000000000e800000000200004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0000000e2d6d8670704ca1daecd786fe94c133a68fd50708f3ed0ca7013b5e0bc5f61296b5a32b935d6b5404a2162bc26cf561cb7b45f58c7cc8d18305c9dd068860bd4f6cea89ea34db4acde8ebae4606ec1261e8006b104d96eb42975e0df1042aa1161e6c70af5530507238141080d7d7ea1f16a9609963b296143504a4af284826e1436641c74c6dc00d0b1731794887426fc4e4f4d440416c1874aaf34b6a74411d9ed966d73b6a8d05c8546329e7bb4222d2518ab8e2e7d8c47624ec64ecc8a0040000000e0585a675fef9ed63f72673bd9408684dc7fc86ad4926a76c432af933aeab68447e56860b1715cff46516cf38433a856b28a5d0653313a11664b98f2361e8cca";
155 const CRED_BLOB_HEX: &str = "01000000d08c9ddf0115d1118c7a00c04fc297eb0100000033f19f5ee340be4a8a2e2b4e62bd0cc600000000020000000000106600000001000020000000aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899000000000e800000000200004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0000000e2d6d8670704ca1daecd786fe94c133a68fd50708f3ed0ca7013b5e0bc5f61296b5a32b935d6b5404a2162bc26cf561cb7b45f58c7cc8d18305c9dd068860bd4f6cea89ea34db4acde8ebae4606ec1261e8006b104d96eb42975e0df1042aa1161e6c70af5530507238141080d7d7ea1f16a9609963b296143504a4af284826e1436641c74c6dc00d0b1731794887426fc4e4f4d440416c1874aaf34b6a74411d9ed966d73b6a8d05c8546329e7bb4222d2518ab8e2e7d8c47624ec64ecc8a0040000000e0585a675fef9ed63f72673bd9408684dc7fc86ad4926a76c432af933aeab68447e56860b1715cff46516cf38433a856b28a5d0653313a11664b98f2361e8cca";
157 const EXPECT_TARGET: &str = "Domain:target=TERMSRV/fileserver01";
159 const EXPECT_USERNAME: &str = "CORP\\jdoe";
160 const EXPECT_SECRET: &str = "S3cr3t-P@ssw0rd!";
161
162 #[test]
164 fn credential_file_wrapper_yields_inner_blob() {
165 let blob = parse_credential_file(&hex(CRED_FILE_HEX)).expect("strip ok");
166 assert_eq!(blob, hex(CRED_BLOB_HEX));
167 }
168
169 #[test]
171 fn decrypt_credential_matches_impacket() {
172 let blob = hex(CRED_BLOB_HEX);
173 let mk = hex(MASTER_KEY_HEX);
174 let cred = decrypt_credential(&blob, &mk).expect("decrypt ok");
175 assert_eq!(cred.target, EXPECT_TARGET);
176 assert_eq!(cred.username, EXPECT_USERNAME);
177 assert_eq!(cred.secret, EXPECT_SECRET);
178 }
179
180 #[test]
182 fn end_to_end_credential_file_to_fields() {
183 let blob = parse_credential_file(&hex(CRED_FILE_HEX)).expect("strip ok");
184 let mk = hex(MASTER_KEY_HEX);
185 let cred = decrypt_credential(&blob, &mk).expect("decrypt ok");
186 assert_eq!(cred.target, EXPECT_TARGET);
187 assert_eq!(cred.username, EXPECT_USERNAME);
188 assert_eq!(cred.secret, EXPECT_SECRET);
189 }
190
191 #[test]
194 fn no_usable_master_key_refuses_rather_than_fabricates() {
195 let blob = hex(CRED_BLOB_HEX);
196 let bad_mk = [0u8; 64];
197 let result = decrypt_credential(&blob, &bad_mk);
198 assert!(
199 result.is_err(),
200 "must error on an unusable master key, never fabricate a credential"
201 );
202 }
203}