1use forensicnomicon::dpapi::{CHROME_COOKIE_V10, CHROME_COOKIE_V20};
2
3use crate::error::DpapiError;
4
5#[derive(Debug, PartialEq)]
7pub enum ChromeCookieEncoding {
8 Raw,
10 DpapiBlob(Vec<u8>),
12 V10 {
14 nonce: [u8; 12],
15 ciphertext: Vec<u8>,
16 },
17 V20 {
19 nonce: [u8; 12],
20 ciphertext: Vec<u8>,
21 },
22}
23
24pub fn detect_chrome_cookie_encoding(data: &[u8]) -> ChromeCookieEncoding {
26 if data.len() > 15 {
28 if data.starts_with(CHROME_COOKIE_V20) {
29 let mut nonce = [0u8; 12];
30 nonce.copy_from_slice(&data[3..15]);
31 return ChromeCookieEncoding::V20 {
32 nonce,
33 ciphertext: data[15..].to_vec(),
34 };
35 }
36 if data.starts_with(CHROME_COOKIE_V10) {
37 let mut nonce = [0u8; 12];
38 nonce.copy_from_slice(&data[3..15]);
39 return ChromeCookieEncoding::V10 {
40 nonce,
41 ciphertext: data[15..].to_vec(),
42 };
43 }
44 }
45 if data.starts_with(b"DPAPI") {
46 return ChromeCookieEncoding::DpapiBlob(data[5..].to_vec());
47 }
48 ChromeCookieEncoding::Raw
49}
50
51pub fn decrypt_v10_cookie(
54 nonce: &[u8; 12],
55 ciphertext: &[u8],
56 key: &[u8; 32],
57) -> Result<Vec<u8>, DpapiError> {
58 #[allow(deprecated)]
59 use aes_gcm::{
61 aead::{Aead, Nonce},
62 Aes256Gcm, KeyInit,
63 };
64 let cipher = Aes256Gcm::new_from_slice(key).map_err(|_| DpapiError::InvalidKeyLength)?;
65 #[allow(deprecated)]
66 let nonce_ga = Nonce::<Aes256Gcm>::from_slice(nonce);
67 cipher
68 .decrypt(nonce_ga, ciphertext)
69 .map_err(|_| DpapiError::DecryptionFailed)
70}
71
72pub fn parse_local_state_encrypted_key(encrypted_key_b64: &[u8]) -> Result<Vec<u8>, DpapiError> {
84 use base64::{engine::general_purpose::STANDARD, Engine as _};
85
86 use std::fmt::Write as _;
87
88 let decoded = STANDARD
89 .decode(encrypted_key_b64)
90 .map_err(|_| DpapiError::Base64Error)?;
91 if let Some(blob) = decoded.strip_prefix(b"DPAPI") {
92 Ok(blob.to_vec())
93 } else {
94 let mut shown = String::new();
96 for b in decoded.iter().take(8) {
97 let _ = write!(shown, "{b:02x}");
98 }
99 Err(DpapiError::MissingDpapiPrefix(shown))
100 }
101}
102
103pub fn decrypt_local_state_key(
114 blob_bytes: &[u8],
115 master_key: &[u8],
116) -> Result<[u8; 32], DpapiError> {
117 let blob = crate::blob::parse_dpapi_blob(blob_bytes)?;
118 let plaintext = crate::decrypt::decrypt_dpapi_blob(&blob, master_key, None)?;
121 plaintext
122 .as_slice()
123 .try_into()
124 .map_err(|_| DpapiError::UnexpectedKeyLength {
125 expected: 32,
126 got: plaintext.len(),
127 })
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133
134 fn hex(s: &str) -> Vec<u8> {
136 (0..s.len())
137 .step_by(2)
138 .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
139 .collect()
140 }
141
142 const MASTER_KEY_HEX: &str = "9828d9873735439e823dbd216205ff88266d28ad685a413970c640d5ee943154bbade31fada673d542c72d707a163bb3d1bceb0c50465b359ae06998481b0ce3";
145 const LOCAL_STATE_BLOB_HEX: &str = "01000000d08c9ddf0115d1118c7a00c04fc297eb0100000033f19f5ee340be4a8a2e2b4e62bd0cc60000000002000000000010660000000100002000000000112233445566778899aabbccddeeff00112233445566778899aabbccddeeff000000000e80000000020000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000030000000fadff9dbad57d443d7a0d77cf9fbfdd68827ce91b2ca6ff533bf2f7467ce329865d99ceb7b841f271e14d1f508ce3a2c40000000fe727776259faf7d3100849fb4ea49fc69fc16bebd1ec98b5a5227b4cfafbce2983ff94a57afd2f2ba1f9afa32ae3aa2148c10f2f3016ccabc3e71c6f26dd6c0";
148 const ENCRYPTED_KEY_B64: &str = "RFBBUEkBAAAA0Iyd3wEV0RGMegDAT8KX6wEAAAAz8Z9e40C+SoouK05ivQzGAAAAAAIAAAAAABBmAAAAAQAAIAAAAAARIjNEVWZ3iJmqu8zd7v8AESIzRFVmd4iZqrvM3e7/AAAAAA6AAAAAAgAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAA+t/5261X1EPXoNd8+fv91ognzpGyym/1M78vdGfOMphl2Zzre4QfJx4U0fUIzjosQAAAAP5yd3Yln699MQCEn7TqSfxp/Ba+vR7Ji1pSJ7TPr7zimD/5Slev0vK6H5r6Mq46ohSMEPLzAWzKvD5xxvJt1sA=";
150 const COOKIE_KEY_HEX: &str = "202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f";
152 const V10_COOKIE_HEX: &str = "7631300102030405060708090a0b0c1b5af334ffe7a1fe676c5ab453c8848232ab94aa630c69bae71883958ba23e4dfe4cc5faff526ce54b";
154 const V10_PLAINTEXT: &[u8] = b"forensic-session-token-42";
155
156 #[test]
158 fn local_state_b64_strips_dpapi_prefix_to_blob() {
159 let blob =
160 parse_local_state_encrypted_key(ENCRYPTED_KEY_B64.as_bytes()).expect("decode ok");
161 assert_eq!(blob, hex(LOCAL_STATE_BLOB_HEX));
162 }
163
164 #[test]
166 fn decrypt_local_state_key_matches_impacket() {
167 let blob = hex(LOCAL_STATE_BLOB_HEX);
168 let mk = hex(MASTER_KEY_HEX);
169 let key = decrypt_local_state_key(&blob, &mk).expect("decrypt ok");
170 assert_eq!(key.to_vec(), hex(COOKIE_KEY_HEX));
171 }
172
173 #[test]
175 fn end_to_end_local_state_then_v10_cookie() {
176 let blob =
177 parse_local_state_encrypted_key(ENCRYPTED_KEY_B64.as_bytes()).expect("decode ok");
178 let mk = hex(MASTER_KEY_HEX);
179 let key = decrypt_local_state_key(&blob, &mk).expect("decrypt ok");
180
181 let raw = hex(V10_COOKIE_HEX);
182 let enc = detect_chrome_cookie_encoding(&raw);
183 let ChromeCookieEncoding::V10 { nonce, ciphertext } = enc else {
184 panic!("expected v10 encoding");
185 };
186 let plaintext = decrypt_v10_cookie(&nonce, &ciphertext, &key).expect("gcm ok");
187 assert_eq!(plaintext, V10_PLAINTEXT);
188 }
189
190 #[test]
193 fn no_usable_master_key_refuses_rather_than_fabricates() {
194 let blob = hex(LOCAL_STATE_BLOB_HEX);
195 let bad_mk = [0u8; 64];
196 let result = decrypt_local_state_key(&blob, &bad_mk);
197 assert!(
198 result.is_err(),
199 "must error on an unusable master key, never fabricate a cookie key"
200 );
201 }
202
203 #[test]
204 fn detect_v10_prefix() {
205 let mut data = vec![0u8; 20];
206 data[0..3].copy_from_slice(b"v10");
207 let enc = detect_chrome_cookie_encoding(&data);
208 assert!(matches!(enc, ChromeCookieEncoding::V10 { .. }));
209 }
210
211 #[test]
212 fn detect_v20_prefix() {
213 let mut data = vec![0u8; 20];
214 data[0..3].copy_from_slice(b"v20");
215 let enc = detect_chrome_cookie_encoding(&data);
216 assert!(matches!(enc, ChromeCookieEncoding::V20 { .. }));
217 }
218
219 #[test]
220 fn detect_dpapi_prefix() {
221 let data = b"DPAPI\x00\x01\x02\x03".to_vec();
222 let enc = detect_chrome_cookie_encoding(&data);
223 assert!(matches!(enc, ChromeCookieEncoding::DpapiBlob(_)));
224 }
225
226 #[test]
227 fn detect_plaintext_is_raw() {
228 let enc = detect_chrome_cookie_encoding(b"plaintext_value");
229 assert_eq!(enc, ChromeCookieEncoding::Raw);
230 }
231
232 #[test]
233 #[allow(deprecated)]
234 fn decrypt_v10_roundtrip() {
235 use aes_gcm::{
236 aead::{Aead, Nonce},
237 Aes256Gcm, KeyInit,
238 };
239 let key = [0x42u8; 32];
240 let nonce_bytes = [0x11u8; 12];
241 let plaintext = b"session_token_value";
242 let cipher = Aes256Gcm::new_from_slice(&key).unwrap();
243 #[allow(deprecated)]
244 let nonce = Nonce::<Aes256Gcm>::from_slice(&nonce_bytes);
245 let ciphertext = cipher.encrypt(nonce, plaintext.as_ref()).unwrap();
246 let recovered = decrypt_v10_cookie(&nonce_bytes, &ciphertext, &key).expect("ok");
247 assert_eq!(recovered, plaintext);
248 }
249}