Skip to main content

coil_core/browser/
cookie.rs

1use super::BrowserSecurityError;
2use super::support::{cipher_for_secret, ensure_cookie_protection, sign_payload, verify_payload};
3use crate::*;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum CookieProtection {
7    Signed,
8    Encrypted,
9}
10
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct CookiePolicy {
13    pub name: String,
14    pub domain: Option<String>,
15    pub path: String,
16    pub same_site: SameSitePolicy,
17    pub secure: bool,
18    pub http_only: bool,
19    pub protection: CookieProtection,
20}
21
22impl CookiePolicy {
23    pub fn from_config(config: &HttpCookieConfig) -> Self {
24        Self {
25            name: config.name.clone(),
26            domain: config.domain.clone(),
27            path: config.path.clone(),
28            same_site: config.same_site,
29            secure: config.secure,
30            http_only: config.http_only,
31            protection: match config.protection {
32                ConfigCookieProtection::Signed => CookieProtection::Signed,
33                ConfigCookieProtection::Encrypted => CookieProtection::Encrypted,
34            },
35        }
36    }
37
38    pub fn protect(&self, secret: &[u8], value: &str) -> Result<String, BrowserSecurityError> {
39        match self.protection {
40            CookieProtection::Signed => CookieSigner::new(self.clone()).sign(secret, value),
41            CookieProtection::Encrypted => CookieSealer::new(self.clone()).seal(secret, value),
42        }
43    }
44
45    pub fn unprotect(&self, secret: &[u8], encoded: &str) -> Result<String, BrowserSecurityError> {
46        match self.protection {
47            CookieProtection::Signed => CookieSigner::new(self.clone()).verify(secret, encoded),
48            CookieProtection::Encrypted => CookieSealer::new(self.clone()).open(secret, encoded),
49        }
50    }
51
52    pub fn render_set_cookie(&self, value: &str, max_age: Option<Duration>) -> String {
53        let mut attributes = vec![format!("{}={value}", self.name)];
54        attributes.push(format!("Path={}", self.path));
55
56        if let Some(domain) = &self.domain {
57            attributes.push(format!("Domain={domain}"));
58        }
59
60        if let Some(max_age) = max_age {
61            attributes.push(format!("Max-Age={}", max_age.as_secs()));
62        }
63
64        attributes.push(format!(
65            "SameSite={}",
66            match self.same_site {
67                SameSitePolicy::Lax => "Lax",
68                SameSitePolicy::Strict => "Strict",
69                SameSitePolicy::None => "None",
70            }
71        ));
72
73        if self.secure {
74            attributes.push("Secure".to_string());
75        }
76
77        if self.http_only {
78            attributes.push("HttpOnly".to_string());
79        }
80
81        attributes.join("; ")
82    }
83}
84
85#[derive(Debug, Clone, PartialEq, Eq)]
86pub struct CookieSigner {
87    pub policy: CookiePolicy,
88}
89
90impl CookieSigner {
91    pub fn new(policy: CookiePolicy) -> Self {
92        Self { policy }
93    }
94
95    pub fn sign(&self, secret: &[u8], value: &str) -> Result<String, BrowserSecurityError> {
96        ensure_cookie_protection(&self.policy, CookieProtection::Signed)?;
97        let payload = URL_SAFE_NO_PAD.encode(value.as_bytes());
98        let signature = sign_payload(secret, payload.as_bytes())?;
99        Ok(format!("v1.{payload}.{signature}"))
100    }
101
102    pub fn verify(&self, secret: &[u8], encoded: &str) -> Result<String, BrowserSecurityError> {
103        ensure_cookie_protection(&self.policy, CookieProtection::Signed)?;
104        let mut parts = encoded.split('.');
105        let version = parts.next();
106        let payload = parts.next();
107        let signature = parts.next();
108
109        if version != Some("v1") || parts.next().is_some() {
110            return Err(BrowserSecurityError::InvalidCookieFormat);
111        }
112
113        let payload = payload.ok_or(BrowserSecurityError::InvalidCookieFormat)?;
114        let signature = signature.ok_or(BrowserSecurityError::InvalidCookieFormat)?;
115        verify_payload(secret, payload.as_bytes(), signature)?;
116        let bytes = URL_SAFE_NO_PAD
117            .decode(payload)
118            .map_err(|_| BrowserSecurityError::InvalidCookieFormat)?;
119
120        String::from_utf8(bytes).map_err(|_| BrowserSecurityError::InvalidCookieFormat)
121    }
122}
123
124#[derive(Debug, Clone, PartialEq, Eq)]
125pub struct CookieSealer {
126    pub policy: CookiePolicy,
127}
128
129impl CookieSealer {
130    pub fn new(policy: CookiePolicy) -> Self {
131        Self { policy }
132    }
133
134    pub fn seal(&self, secret: &[u8], value: &str) -> Result<String, BrowserSecurityError> {
135        ensure_cookie_protection(&self.policy, CookieProtection::Encrypted)?;
136        if secret.is_empty() {
137            return Err(BrowserSecurityError::EmptySecret);
138        }
139
140        let cipher = cipher_for_secret(secret);
141        let mut nonce = [0u8; 12];
142        OsRng.fill_bytes(&mut nonce);
143        let nonce = Nonce::from(nonce);
144        let ciphertext = cipher
145            .encrypt(&nonce, value.as_bytes())
146            .map_err(|_| BrowserSecurityError::InvalidEncryptedCookiePayload)?;
147
148        Ok(format!(
149            "v1.{}.{}",
150            URL_SAFE_NO_PAD.encode(nonce),
151            URL_SAFE_NO_PAD.encode(ciphertext)
152        ))
153    }
154
155    pub fn open(&self, secret: &[u8], encoded: &str) -> Result<String, BrowserSecurityError> {
156        ensure_cookie_protection(&self.policy, CookieProtection::Encrypted)?;
157        if secret.is_empty() {
158            return Err(BrowserSecurityError::EmptySecret);
159        }
160
161        let mut parts = encoded.split('.');
162        let version = parts.next();
163        let nonce = parts.next();
164        let ciphertext = parts.next();
165
166        if version != Some("v1") || parts.next().is_some() {
167            return Err(BrowserSecurityError::InvalidEncryptedCookieFormat);
168        }
169
170        let nonce = nonce.ok_or(BrowserSecurityError::InvalidEncryptedCookieFormat)?;
171        let ciphertext = ciphertext.ok_or(BrowserSecurityError::InvalidEncryptedCookieFormat)?;
172        let nonce = URL_SAFE_NO_PAD
173            .decode(nonce)
174            .map_err(|_| BrowserSecurityError::InvalidEncryptedCookieFormat)?;
175        let ciphertext = URL_SAFE_NO_PAD
176            .decode(ciphertext)
177            .map_err(|_| BrowserSecurityError::InvalidEncryptedCookieFormat)?;
178        let nonce: [u8; 12] = nonce
179            .try_into()
180            .map_err(|_| BrowserSecurityError::InvalidEncryptedCookieFormat)?;
181
182        let cipher = cipher_for_secret(secret);
183        let nonce = Nonce::from(nonce);
184        let plaintext = cipher
185            .decrypt(&nonce, ciphertext.as_ref())
186            .map_err(|_| BrowserSecurityError::InvalidEncryptedCookiePayload)?;
187
188        String::from_utf8(plaintext)
189            .map_err(|_| BrowserSecurityError::InvalidEncryptedCookiePayload)
190    }
191}