coil_core/browser/
csrf.rs1use super::BrowserSecurityError;
2use super::support::{sign_payload, verify_payload};
3use crate::*;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub struct CsrfProtection {
7 pub enabled: bool,
8 pub field_name: String,
9 pub header_name: String,
10}
11
12impl CsrfProtection {
13 pub fn from_config(config: &HttpCsrfConfig) -> Self {
14 Self {
15 enabled: config.enabled,
16 field_name: config.field_name.clone(),
17 header_name: config.header_name.clone(),
18 }
19 }
20
21 pub fn issue_token(
22 &self,
23 secret: &[u8],
24 session_id: &str,
25 action: &str,
26 ) -> Result<String, BrowserSecurityError> {
27 if !self.enabled {
28 return Err(BrowserSecurityError::CsrfDisabled);
29 }
30
31 let mut nonce = [0u8; 16];
32 OsRng.fill_bytes(&mut nonce);
33 let nonce = URL_SAFE_NO_PAD.encode(nonce);
34 let payload = format!("{session_id}:{action}:{nonce}");
35 let signature = sign_payload(secret, payload.as_bytes())?;
36 Ok(format!("v1.{nonce}.{signature}"))
37 }
38
39 pub fn verify_token(
40 &self,
41 secret: &[u8],
42 session_id: &str,
43 action: &str,
44 token: &str,
45 ) -> Result<bool, BrowserSecurityError> {
46 if !self.enabled {
47 return Ok(true);
48 }
49
50 let mut parts = token.split('.');
51 let version = parts.next();
52 let nonce = parts.next();
53 let signature = parts.next();
54
55 if version != Some("v1") || parts.next().is_some() {
56 return Err(BrowserSecurityError::InvalidCsrfTokenFormat);
57 }
58
59 let nonce = nonce.ok_or(BrowserSecurityError::InvalidCsrfTokenFormat)?;
60 let signature = signature.ok_or(BrowserSecurityError::InvalidCsrfTokenFormat)?;
61 let payload = format!("{session_id}:{action}:{nonce}");
62
63 Ok(verify_payload(secret, payload.as_bytes(), signature).is_ok())
64 }
65}