authenticator.rs (4028B)
1 use crate::{ 2 api::{EmptyResult, JsonResult, PasswordOrOtpData}, 3 auth::{ClientIp, Headers}, 4 crypto, 5 db::{models::Totp, DbConn}, 6 error::Error, 7 util::NumberOrString, 8 }; 9 use data_encoding::BASE32; 10 use rocket::{serde::json::Json, Route}; 11 12 pub fn routes() -> Vec<Route> { 13 routes![ 14 activate_authenticator, 15 activate_authenticator_put, 16 generate_authenticator, 17 ] 18 } 19 20 #[post("/two-factor/get-authenticator", data = "<data>")] 21 async fn generate_authenticator( 22 data: Json<PasswordOrOtpData>, 23 headers: Headers, 24 conn: DbConn, 25 ) -> JsonResult { 26 let data: PasswordOrOtpData = data.into_inner(); 27 let user = headers.user; 28 data.validate(&user)?; 29 let totp = Totp::find_by_user(&user.uuid, &conn).await?; 30 let (enabled, key) = match totp { 31 Some(t) => (true, t.token), 32 _ => (false, crypto::encode_random_bytes::<20>(&BASE32)), 33 }; 34 Ok(Json(json!({ 35 "enabled": enabled, 36 "key": key, 37 "object": "twoFactorAuthenticator" 38 }))) 39 } 40 41 #[derive(Deserialize)] 42 #[serde(rename_all = "camelCase")] 43 struct EnableAuthenticatorData { 44 key: String, 45 token: NumberOrString, 46 master_password_hash: Option<String>, 47 otp: Option<String>, 48 } 49 50 #[post("/two-factor/authenticator", data = "<data>")] 51 async fn activate_authenticator( 52 data: Json<EnableAuthenticatorData>, 53 headers: Headers, 54 conn: DbConn, 55 ) -> JsonResult { 56 let data: EnableAuthenticatorData = data.into_inner(); 57 let key = data.key; 58 let token = data.token.into_string(); 59 let user = headers.user; 60 PasswordOrOtpData { 61 master_password_hash: data.master_password_hash, 62 otp: data.otp, 63 } 64 .validate(&user)?; 65 // Validate key as base32 and 20 bytes length 66 let decoded_key: Vec<u8> = match BASE32.decode(key.as_bytes()) { 67 Ok(decoded) => decoded, 68 _ => err!("Invalid totp secret"), 69 }; 70 if decoded_key.len() != 20 { 71 err!("Invalid key length") 72 } 73 // Validate the token provided with the key, and save new twofactor 74 validate_totp_code( 75 user.uuid.as_str(), 76 &token, 77 key.to_uppercase(), 78 &headers.ip, 79 &conn, 80 ) 81 .await?; 82 Ok(Json(json!({ 83 "enabled": true, 84 "key": key, 85 "object": "twoFactorAuthenticator" 86 }))) 87 } 88 89 #[put("/two-factor/authenticator", data = "<data>")] 90 async fn activate_authenticator_put( 91 data: Json<EnableAuthenticatorData>, 92 headers: Headers, 93 conn: DbConn, 94 ) -> JsonResult { 95 activate_authenticator(data, headers, conn).await 96 } 97 98 pub async fn validate_totp_code_str( 99 user_uuid: &str, 100 totp_code: &str, 101 secret: String, 102 ip: &ClientIp, 103 conn: &DbConn, 104 ) -> EmptyResult { 105 if !totp_code.chars().all(char::is_numeric) { 106 err!("TOTP code is not a number"); 107 } 108 validate_totp_code(user_uuid, totp_code, secret, ip, conn).await 109 } 110 #[allow( 111 clippy::integer_division, 112 clippy::integer_division_remainder_used, 113 clippy::redundant_else 114 )] 115 async fn validate_totp_code( 116 user_uuid: &str, 117 totp_code: &str, 118 secret: String, 119 ip: &ClientIp, 120 conn: &DbConn, 121 ) -> EmptyResult { 122 use totp_lite::{totp_custom, Sha1}; 123 let Ok(decoded_secret) = BASE32.decode(secret.as_bytes()) else { 124 err!("Invalid TOTP secret") 125 }; 126 let mut totp = Totp::find_by_user(user_uuid, conn) 127 .await? 128 .unwrap_or_else(|| Totp::new(user_uuid.to_owned(), secret)); 129 let current_time = chrono::Utc::now(); 130 let current_timestamp = u64::try_from(current_time.timestamp()).expect("underflow"); 131 let time_step = current_timestamp / 30u64; 132 if time_step > totp.get_last_used() 133 && totp_custom::<Sha1>(30, 6, &decoded_secret, current_timestamp) == totp_code 134 { 135 totp.set_last_used(time_step); 136 totp.replace(conn).await 137 } else { 138 Err(Error::from(format!( 139 "Invalid TOTP code! Server time: {} IP: {}", 140 current_time.format("%F %T UTC"), 141 ip.ip 142 ))) 143 } 144 }