1use crate::error::DpapiError;
17
18pub fn extract_key_material(profile_xml: &str) -> Option<&str> {
24 const OPEN: &str = "<keyMaterial>";
25 const CLOSE: &str = "</keyMaterial>";
26 let start = profile_xml.find(OPEN)? + OPEN.len();
27 let rest = profile_xml.get(start..)?;
28 let end = rest.find(CLOSE)?;
29 Some(rest.get(..end)?.trim())
30}
31
32pub fn decrypt_wlan_key_material(
42 key_material_hex: &str,
43 master_key: &[u8],
44) -> Result<String, DpapiError> {
45 let blob_bytes = hex_decode(key_material_hex)?;
46 let blob = crate::blob::parse_dpapi_blob(&blob_bytes)?;
47 let cleartext = crate::decrypt::decrypt_dpapi_blob(&blob, master_key, None)?;
48 let trimmed = cleartext
50 .iter()
51 .position(|&b| b == 0)
52 .map_or(cleartext.as_slice(), |nul| &cleartext[..nul]);
53 String::from_utf8(trimmed.to_vec()).map_err(|_| DpapiError::Utf16Error)
54}
55
56fn hex_decode(s: &str) -> Result<Vec<u8>, DpapiError> {
59 let bytes = s.as_bytes();
60 if bytes.len() % 2 != 0 {
61 return Err(DpapiError::InvalidHex(format!(
62 "odd length ({} chars)",
63 bytes.len()
64 )));
65 }
66 let mut out = Vec::with_capacity(bytes.len() / 2);
67 let mut i = 0;
68 while i < bytes.len() {
69 let hi = hex_nibble(bytes[i])?;
70 let lo = hex_nibble(bytes[i + 1])?;
71 out.push((hi << 4) | lo);
72 i += 2;
73 }
74 Ok(out)
75}
76
77fn hex_nibble(b: u8) -> Result<u8, DpapiError> {
79 match b {
80 b'0'..=b'9' => Ok(b - b'0'),
81 b'a'..=b'f' => Ok(b - b'a' + 10),
82 b'A'..=b'F' => Ok(b - b'A' + 10),
83 other => Err(DpapiError::InvalidHex(format!("non-hex byte {other:#04x}"))),
84 }
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90
91 const MASTER_KEY_HEX: &str = "9828d9873735439e823dbd216205ff88266d28ad685a413970c640d5ee943154bbade31fada673d542c72d707a163bb3d1bceb0c50465b359ae06998481b0ce3";
94 const KEY_MATERIAL_HEX: &str = "01000000D08C9DDF0115D1118C7A00C04FC297EB0100000033F19F5EE340BE4A8A2E2B4E62BD0CC600000000020000000000106600000001000020000000DEADBEEFCAFEBABE0011223344556677DEADBEEFCAFEBABE0011223344556677000000000E8000000002000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000051E2C9E20723EF48230A95FCEBBA31AF8BC567EDABBBD958F6E42E4CCE9236C240000000538815E34921B886D09A9CEAD4024E596A73C9C3B53E37A4481D05D7097751049323C613F78C8BD0D8A3AAAB8BF9FBC966E87526245734D0C781DFE0214B1D70";
96 const EXPECT_PSK: &str = "CorrectHorseBatteryStaple";
97
98 fn hex(s: &str) -> Vec<u8> {
99 (0..s.len())
100 .step_by(2)
101 .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
102 .collect()
103 }
104
105 #[test]
107 fn decrypt_wlan_key_material_matches_impacket() {
108 let mk = hex(MASTER_KEY_HEX);
109 let psk = decrypt_wlan_key_material(KEY_MATERIAL_HEX, &mk).expect("decrypt ok");
110 assert_eq!(psk, EXPECT_PSK);
111 }
112
113 #[test]
115 fn extract_key_material_from_profile_xml() {
116 let xml = format!(
117 "<WLANProfile><MSM><security><sharedKey>\
118 <keyMaterial>{KEY_MATERIAL_HEX}</keyMaterial>\
119 </sharedKey></security></MSM></WLANProfile>"
120 );
121 let km = extract_key_material(&xml).expect("keyMaterial present");
122 assert_eq!(km, KEY_MATERIAL_HEX);
123 }
124
125 #[test]
127 fn end_to_end_profile_xml_to_psk() {
128 let xml = format!(
129 "<WLANProfile><MSM><security><sharedKey>\
130 <keyMaterial>\n {KEY_MATERIAL_HEX}\n </keyMaterial>\
131 </sharedKey></security></MSM></WLANProfile>"
132 );
133 let km = extract_key_material(&xml).expect("keyMaterial present");
134 let mk = hex(MASTER_KEY_HEX);
135 let psk = decrypt_wlan_key_material(km, &mk).expect("decrypt ok");
136 assert_eq!(psk, EXPECT_PSK);
137 }
138
139 #[test]
141 fn no_usable_master_key_refuses_rather_than_fabricates() {
142 let bad_mk = [0u8; 64];
143 let result = decrypt_wlan_key_material(KEY_MATERIAL_HEX, &bad_mk);
144 assert!(
145 result.is_err(),
146 "must error on an unusable master key, never fabricate a PSK"
147 );
148 }
149}