1use 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}