coil_core/browser/
cookie.rs1use 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}