Skip to main content

dpapi_core/
chrome.rs

1use forensicnomicon::dpapi::{CHROME_COOKIE_V10, CHROME_COOKIE_V20};
2
3use crate::error::DpapiError;
4
5/// How a Chrome/Chromium cookie value is encoded in heap memory.
6#[derive(Debug, PartialEq)]
7pub enum ChromeCookieEncoding {
8    /// Plaintext — no encryption prefix detected.
9    Raw,
10    /// Classic DPAPI blob (prefix `DPAPI`, 5 bytes). Windows 7 / no App-Bound.
11    DpapiBlob(Vec<u8>),
12    /// AES-256-GCM v10: `v10` + 12-byte nonce + ciphertext + 16-byte tag.
13    V10 {
14        nonce: [u8; 12],
15        ciphertext: Vec<u8>,
16    },
17    /// AES-256-GCM v20 (Chrome 127+): same wire format as v10.
18    V20 {
19        nonce: [u8; 12],
20        ciphertext: Vec<u8>,
21    },
22}
23
24/// Detect the encoding of a raw `encrypted_value` blob from Chrome's Cookies DB.
25pub fn detect_chrome_cookie_encoding(data: &[u8]) -> ChromeCookieEncoding {
26    // v10/v20 require at least 3 (prefix) + 12 (nonce) = 15 bytes
27    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
51/// Decrypt a v10/v20 AES-256-GCM cookie value.
52/// `key` is the 32-byte AES key from Chrome's `Local State` (already decrypted).
53pub fn decrypt_v10_cookie(
54    nonce: &[u8; 12],
55    ciphertext: &[u8],
56    key: &[u8; 32],
57) -> Result<Vec<u8>, DpapiError> {
58    #[allow(deprecated)]
59    // from_slice deprecated in generic-array 1.x; aes-gcm 0.10 still uses 0.14
60    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
72#[cfg(test)]
73mod tests {
74    use super::*;
75
76    #[test]
77    fn detect_v10_prefix() {
78        let mut data = vec![0u8; 20];
79        data[0..3].copy_from_slice(b"v10");
80        let enc = detect_chrome_cookie_encoding(&data);
81        assert!(matches!(enc, ChromeCookieEncoding::V10 { .. }));
82    }
83
84    #[test]
85    fn detect_v20_prefix() {
86        let mut data = vec![0u8; 20];
87        data[0..3].copy_from_slice(b"v20");
88        let enc = detect_chrome_cookie_encoding(&data);
89        assert!(matches!(enc, ChromeCookieEncoding::V20 { .. }));
90    }
91
92    #[test]
93    fn detect_dpapi_prefix() {
94        let data = b"DPAPI\x00\x01\x02\x03".to_vec();
95        let enc = detect_chrome_cookie_encoding(&data);
96        assert!(matches!(enc, ChromeCookieEncoding::DpapiBlob(_)));
97    }
98
99    #[test]
100    fn detect_plaintext_is_raw() {
101        let enc = detect_chrome_cookie_encoding(b"plaintext_value");
102        assert_eq!(enc, ChromeCookieEncoding::Raw);
103    }
104
105    #[test]
106    #[allow(deprecated)]
107    fn decrypt_v10_roundtrip() {
108        use aes_gcm::{
109            aead::{Aead, Nonce},
110            Aes256Gcm, KeyInit,
111        };
112        let key = [0x42u8; 32];
113        let nonce_bytes = [0x11u8; 12];
114        let plaintext = b"session_token_value";
115        let cipher = Aes256Gcm::new_from_slice(&key).unwrap();
116        #[allow(deprecated)]
117        let nonce = Nonce::<Aes256Gcm>::from_slice(&nonce_bytes);
118        let ciphertext = cipher.encrypt(nonce, plaintext.as_ref()).unwrap();
119        let recovered = decrypt_v10_cookie(&nonce_bytes, &ciphertext, &key).expect("ok");
120        assert_eq!(recovered, plaintext);
121    }
122}