Skip to main content

xsrf/
lib.rs

1//! A library to provide Cross-site request forgery protection.
2//!
3//! Getting this right can be tricky, and this library aims to provide the
4//! primitives to be able to do this without making it too easy to get it
5//! wrong. Remember though, this needs to be coupled with the HTTP layer
6//! correctly as well in order to ensure it provide protection.
7//!
8//! # Warning
9//!
10//! This library provides primitives, and is meant to be used as a building
11//! block. The suggested way to use this is to write a library to integrate
12//! this with your favorite HTTP stack. For example, if you're using
13//! [actix](https://actix.rs/) then don't use this directly but instead go use
14//! [actix-xsrf](https://docs.rs/actix-xsrf).
15//!
16//! # Usage
17//!
18//! The library uses what seems to now be the standard method used by various
19//! popular frameworks.
20//! - A `CookieToken` is issued and stored in the cookie or the session.
21//!   Remember to use a secure signed cookie.
22//! - From this `CookieToken`, one or more `RequestToken`s can be issued.
23//!   You can issue one per request, or multiple. Any number of them can be
24//!   validated against the original `CookieToken`.
25//! - The `RequestToken` should either be embedded in your HTML form, or sent
26//!   via a HTTP header (often the case for requests initiated in JavaScript).
27//! - The server side should validate this under the right circumstances.
28//!
29//! # Notes
30//! - [`rand`](https://docs.rs/rand) is used to generate cryptographically
31//!   secure tokens.
32//! - `RequestToken`s use a one-time-pad and are xor-ed with the `CookieToken`
33//!    to protect against [BREACH](http://breachattack.com/).
34//! - [`subtle`](https://docs.rs/subtle) is used to protect against timing
35//!   attacks.
36use rand::{thread_rng, Rng};
37use subtle::ConstantTimeEq;
38
39const TOKEN_LEN: usize = 32;
40const ENCODED_LEN: usize = 44;
41static BC: base64::Config = base64::URL_SAFE;
42
43#[derive(thiserror::Error, Debug)]
44pub enum Error {
45    #[error("invalid xsrf token")]
46    InvalidToken,
47    #[error("xsrf token mismatch")]
48    TokenMismatch,
49}
50
51pub type Result<T> = std::result::Result<T, Error>;
52
53pub struct CookieToken {
54    data: [u8; TOKEN_LEN],
55}
56
57impl ToString for CookieToken {
58    fn to_string(&self) -> String {
59        base64::encode_config(&self.data, BC)
60    }
61}
62
63impl std::convert::TryFrom<&str> for CookieToken {
64    type Error = Error;
65
66    fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
67        if value.len() != ENCODED_LEN {
68            return Err(Error::InvalidToken);
69        }
70        let mut t = Self {
71            data: [0; TOKEN_LEN],
72        };
73        if base64::decode_config_slice(value, BC, &mut t.data).is_err() {
74            return Err(Error::InvalidToken);
75        }
76        Ok(t)
77    }
78}
79
80impl CookieToken {
81    pub fn new() -> CookieToken {
82        let mut t = Self {
83            data: [0; TOKEN_LEN],
84        };
85        thread_rng().fill(&mut t.data);
86        t
87    }
88
89    pub fn gen_req_token(&self) -> RequestToken {
90        let mut t = RequestToken {
91            otp: [0; TOKEN_LEN],
92            mask: [0; TOKEN_LEN],
93        };
94        thread_rng().fill(&mut t.otp);
95        xor_into(&t.otp, &self.data, &mut t.mask);
96        t
97    }
98
99    pub fn verify_req_token(&self, token: RequestToken) -> Result<()> {
100        let mut expected = [0; TOKEN_LEN];
101        xor_into(&token.otp, &token.mask, &mut expected);
102        let eq: bool = expected.ct_eq(&self.data).into();
103        if eq {
104            Ok(())
105        } else {
106            Err(Error::TokenMismatch)
107        }
108    }
109}
110
111pub struct RequestToken {
112    otp: [u8; TOKEN_LEN],
113    mask: [u8; TOKEN_LEN],
114}
115
116impl ToString for RequestToken {
117    fn to_string(&self) -> String {
118        let mut s = String::with_capacity(ENCODED_LEN * 2);
119        base64::encode_config_buf(self.otp, BC, &mut s);
120        base64::encode_config_buf(self.mask, BC, &mut s);
121        s
122    }
123}
124
125impl std::convert::TryFrom<&str> for RequestToken {
126    type Error = Error;
127
128    fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
129        if value.len() != ENCODED_LEN * 2 {
130            return Err(Error::InvalidToken);
131        }
132        let mut t = Self {
133            otp: [0; TOKEN_LEN],
134            mask: [0; TOKEN_LEN],
135        };
136        if base64::decode_config_slice(&value[..ENCODED_LEN], BC, &mut t.otp).is_err() {
137            return Err(Error::InvalidToken);
138        }
139        if base64::decode_config_slice(&value[ENCODED_LEN..], BC, &mut t.mask).is_err() {
140            return Err(Error::InvalidToken);
141        }
142        Ok(t)
143    }
144}
145
146fn xor_into(a: &[u8], b: &[u8], into: &mut [u8]) {
147    let l = a.len();
148    debug_assert_eq!(b.len(), l);
149    debug_assert_eq!(into.len(), l);
150    a.iter()
151        .zip(b.iter())
152        .enumerate()
153        .for_each(|(index, (a, b))| into[index] = a ^ b)
154}
155
156#[cfg(test)]
157mod tests {
158    use super::{CookieToken, RequestToken, ENCODED_LEN};
159    use std::convert::TryInto;
160
161    #[test]
162    fn cookie_token_to_from_string() {
163        let original = CookieToken::new();
164        let s = original.to_string();
165        assert_eq!(s.len(), ENCODED_LEN);
166        let decoded: CookieToken = s.as_str().try_into().unwrap();
167        assert_eq!(original.data, decoded.data);
168    }
169
170    #[test]
171    fn request_token_to_from_string() {
172        let ct = CookieToken::new();
173        let original = ct.gen_req_token();
174        let s = original.to_string();
175        assert_eq!(s.len(), ENCODED_LEN * 2);
176        let decoded: RequestToken = s.as_str().try_into().unwrap();
177        assert_eq!(original.otp, decoded.otp);
178        assert_eq!(original.mask, decoded.mask);
179    }
180
181    #[test]
182    fn gen_and_verify_req_token() {
183        let ct = CookieToken::new();
184        let rt = ct.gen_req_token();
185        ct.verify_req_token(rt).unwrap();
186    }
187}