vw_small

Hardened fork of Vaultwarden (https://github.com/dani-garcia/vaultwarden) with fewer features.
git clone https://git.philomathiclife.com/repos/vw_small
Log | Files | Refs | README

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 }