WIP - ActixWeb multi-tenant blog and newsletter API server. Originally forked from LukeMathWalker/zero-to-production.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 171 lines 5.5 kB view raw
1use aes_gcm::aead::{Aead, KeyInit, OsRng}; 2use aes_gcm::{AeadCore, Aes256Gcm, Key, Nonce}; 3use anyhow::Context; 4use base64::Engine; 5use base64::engine::general_purpose::STANDARD; 6use captcha::Captcha; 7use captcha::filters::{Dots, Grid, Noise, Wave}; 8use secrecy::{ExposeSecret, SecretString}; 9use serde::{Deserialize, Serialize}; 10 11#[derive(Deserialize, Debug)] 12pub struct Base64Challenger { 13 pub base64_image: String, 14 answer: String, 15 secret: SecretString, 16} 17 18impl Base64Challenger { 19 pub fn new(secret: SecretString) -> Result<Self, anyhow::Error> { 20 if secret.expose_secret().len() != 32 { 21 anyhow::bail!("Secret must be 32 bytes in length.") 22 } 23 24 let mut captcha = Captcha::new(); 25 captcha 26 .add_chars(6) 27 .view(320, 120) 28 .set_color([23, 23, 23]) 29 .apply_filter(Dots::new(10)) 30 .apply_filter(Wave::new(1.5, 40.0)) 31 .apply_filter(Noise::new(0.1)) 32 .apply_filter(Grid::new(20, 10)); 33 let answer = captcha.chars_as_string(); 34 let base64_image = captcha 35 .as_base64() 36 .context("Failed to generate base64 string from image.")?; 37 38 Ok(Self { 39 answer, 40 base64_image, 41 secret, 42 }) 43 } 44 45 pub fn encrypt(&self) -> Result<String, anyhow::Error> { 46 // Create cipher from 32-byte key. (Panics if key is not 32 bytes) 47 let key = Key::<Aes256Gcm>::from_slice(self.secret.expose_secret().as_bytes()); 48 let cipher = Aes256Gcm::new(key); 49 // Nonce is a 12-byte value. 50 let nonce = Aes256Gcm::generate_nonce(&mut OsRng); 51 let ciphertext = match cipher.encrypt(&nonce, self.answer.as_bytes()) { 52 Ok(cipher) => cipher, 53 Err(_) => anyhow::bail!("Failed to encrypt challenge."), 54 }; 55 56 // Base64 encode nonce + ciphertext for output. 57 let mut out = Vec::with_capacity(nonce.as_slice().len() + ciphertext.len()); 58 out.extend_from_slice(nonce.as_slice()); 59 out.extend_from_slice(&ciphertext); 60 61 Ok(STANDARD.encode(out)) 62 } 63 64 pub fn decrypt(encoded: &str, secret: SecretString) -> Result<String, anyhow::Error> { 65 let data = STANDARD 66 .decode(encoded) 67 .context("Failed to base64 decode challenge.")?; 68 69 if data.len() < 12 { 70 anyhow::bail!("Ciphertext is too short.") 71 } 72 73 // Split nonce and ciphertext for decryption. 74 let (nonce_bytes, ciphertext) = data.split_at(12); 75 let nonce = Nonce::from_slice(nonce_bytes); 76 let key = Key::<Aes256Gcm>::from_slice(secret.expose_secret().as_bytes()); 77 let cipher = Aes256Gcm::new(key); 78 79 match cipher.decrypt(nonce, ciphertext.as_ref()) { 80 Ok(bytes_vec) => { 81 String::from_utf8(bytes_vec).context("Failed to convert answer to string.") 82 } 83 Err(_) => anyhow::bail!("Failed to decrypt answer."), 84 } 85 } 86 87 pub fn verify( 88 encoded: &str, 89 answer: String, 90 secret: SecretString, 91 ) -> Result<(), anyhow::Error> { 92 if Self::decrypt(encoded, secret)? == answer { 93 Ok(()) 94 } else { 95 anyhow::bail!("Incorrect answer.") 96 } 97 } 98} 99 100#[derive(Serialize, Debug)] 101pub struct CaptchaResponse { 102 pub challenge_image: String, 103 pub challenge: String, 104} 105 106#[cfg(test)] 107mod tests { 108 use crate::challenge::Base64Challenger; 109 use claims::{assert_err, assert_ok}; 110 use secrecy::SecretString; 111 112 #[test] 113 fn secret_must_be_32_bytes_in_length() { 114 let secret = SecretString::from("W81lMp7E1J0569L2Z1ERpeX8XDiYn11"); 115 let challenge = Base64Challenger::new(secret); 116 117 assert_err!(challenge); 118 } 119 120 #[test] 121 fn can_create_challenge_image() { 122 let secret = SecretString::from("w8ar9i496zulwEayDG828Y67i09IfwWC"); 123 let challenge = Base64Challenger::new(secret); 124 125 assert_ok!(challenge); 126 } 127 128 #[test] 129 fn can_encrypt_challenge() { 130 let secret = SecretString::from("njE17BV5QLYO82V3UWoa22ZwwdiD40l2"); 131 let challenge = Base64Challenger::new(secret).expect("Creating challenge."); 132 133 assert_ok!(challenge.encrypt()); 134 } 135 136 #[test] 137 fn can_decrypt_challenge() { 138 let secret = SecretString::from("tNuS550e9os25IFZxw518GlNSK3ouiY1"); 139 let challenge = Base64Challenger::new(secret).expect("Creating challenge."); 140 let encrypted = challenge.encrypt().unwrap(); 141 let decrypted = Base64Challenger::decrypt(&encrypted, challenge.secret).unwrap(); 142 143 assert_eq!(challenge.answer, decrypted); 144 } 145 146 #[test] 147 fn can_verify_correct_answer() { 148 let secret = SecretString::from("7LphV05vqV3oxYj831j97H3vs2g5wP89"); 149 let challenge = Base64Challenger::new(secret).expect("Creating challenge."); 150 let encrypted = challenge.encrypt().unwrap(); 151 152 assert_ok!(Base64Challenger::verify( 153 &encrypted, 154 challenge.answer, 155 challenge.secret 156 )); 157 } 158 159 #[test] 160 fn can_reject_incorrect_answer() { 161 let secret = SecretString::from("Zh20YpU56L5Ces0VffGl31rb2Km4k7Gr"); 162 let challenge = Base64Challenger::new(secret).expect("Creating challenge."); 163 let encrypted = challenge.encrypt().unwrap(); 164 165 assert_err!(Base64Challenger::verify( 166 &encrypted, 167 String::from("badanswer"), 168 challenge.secret 169 )); 170 } 171}