salvo_csrf/
bcrypt_cipher.rs1use base64::engine::general_purpose::URL_SAFE_NO_PAD;
2use base64::Engine;
3
4use super::CsrfCipher;
5
6#[derive(Debug, Clone)]
8pub struct BcryptCipher {
9 cost: u32,
10 token_size: usize,
11}
12impl Default for BcryptCipher {
13 fn default() -> Self {
14 Self::new()
15 }
16}
17
18impl BcryptCipher {
19 #[inline]
21 #[must_use]
22 pub fn new() -> Self {
23 Self {
24 cost: 8,
25 token_size: 32,
26 }
27 }
28
29 #[inline]
31 #[must_use]
32 pub fn token_size(mut self, token_size: usize) -> Self {
33 assert!((8..=72).contains(&token_size), "length must be between 8 and 72");
34 self.token_size = token_size;
35 self
36 }
37
38 #[inline]
40 #[must_use]
41 pub fn cost(mut self, cost: u32) -> Self {
42 assert!((4..=31).contains(&cost), "cost must be between 4 and 31");
43 self.cost = cost;
44 self
45 }
46}
47
48impl CsrfCipher for BcryptCipher {
49 fn verify(&self, token: &str, proof: &str) -> bool {
50 let token_bytes = URL_SAFE_NO_PAD
53 .decode(token.as_bytes())
54 .unwrap_or_else(|_| vec![0u8; self.token_size]);
55
56 let proof = proof.replace('_', "/");
59
60 bcrypt::verify(&token_bytes, &proof).unwrap_or(false)
62 }
63 fn generate(&self) -> (String, String) {
64 let token = self.random_bytes(self.token_size);
65 let proof = bcrypt::hash(&token, self.cost)
67 .expect("bcrypt hash failed")
68 .replace('/', "_");
69
70 (URL_SAFE_NO_PAD.encode(token), proof)
71 }
72}
73
74#[cfg(test)]
75mod tests {
76 use base64::engine::general_purpose::URL_SAFE_NO_PAD;
77 use base64::Engine;
78
79 use super::*;
80
81 #[test]
82 fn test_bcrypt_cipher_new() {
83 let cipher = BcryptCipher::new();
84 assert_eq!(cipher.cost, 8);
85 assert_eq!(cipher.token_size, 32);
86 }
87
88 #[test]
89 fn test_bcrypt_cipher_with_token_size() {
90 let cipher = BcryptCipher::new().token_size(16);
91 assert_eq!(cipher.token_size, 16);
92 }
93
94 #[test]
95 fn test_bcrypt_cipher_with_min_token_size() {
96 let cipher = BcryptCipher::new().token_size(8);
97 assert_eq!(cipher.token_size, 8);
98 }
99
100 #[test]
101 #[should_panic(expected = "length must be between 8 and 72")]
102 fn test_bcrypt_cipher_with_too_small_token_size() {
103 let _ = BcryptCipher::new().token_size(7);
104 }
105
106 #[test]
107 #[should_panic(expected = "length must be between 8 and 72")]
108 fn test_bcrypt_cipher_with_too_large_token_size() {
109 let _ = BcryptCipher::new().token_size(73);
110 }
111
112 #[test]
113 fn test_bcrypt_cipher_with_cost() {
114 let cipher = BcryptCipher::new().cost(10);
115 assert_eq!(cipher.cost, 10);
116 }
117
118 #[test]
119 #[should_panic(expected = "cost must be between 4 and 31")]
120 fn test_bcrypt_cipher_with_invalid_cost() {
121 let _ = BcryptCipher::new().cost(32);
122 }
123
124 #[test]
125 fn test_bcrypt_cipher_verify_and_generate() {
126 let cipher = BcryptCipher::new();
127 let (token, proof) = cipher.generate();
128 assert!(cipher.verify(&token, &proof));
129 }
130
131 #[test]
132 fn test_bcrypt_cipher_verify_invalid_token() {
133 let cipher = BcryptCipher::new();
134 let (token, proof) = cipher.generate();
135 let invalid_token = URL_SAFE_NO_PAD.encode(vec![0; token.len()]);
136 assert!(!cipher.verify(&invalid_token, &proof));
137 }
138}