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

webauthn.rs (6376B)


      1 use crate::{
      2     api::{EmptyResult, JsonResult, PasswordOrOtpData},
      3     auth::Headers,
      4     config,
      5     db::{
      6         models::{WebAuthn, WebAuthnAuth, WebAuthnChallenge, WebAuthnInfo, WebAuthnReg},
      7         DbConn,
      8     },
      9     error::Error,
     10 };
     11 use rocket::serde::json::Json;
     12 use rocket::Route;
     13 use url::Url;
     14 use webauthn_rs::prelude::{
     15     PublicKeyCredential, RegisterPublicKeyCredential, Uuid, Webauthn, WebauthnBuilder,
     16     WebauthnError,
     17 };
     18 
     19 pub fn routes() -> Vec<Route> {
     20     routes![
     21         activate_webauthn,
     22         activate_webauthn_put,
     23         delete_webauthn,
     24         generate_webauthn_challenge,
     25         get_webauthn,
     26     ]
     27 }
     28 fn build_webauthn() -> Result<Webauthn, WebauthnError> {
     29     WebauthnBuilder::new(
     30         config::get_config().domain(),
     31         &Url::parse(&config::get_config().domain_origin()).expect("a valid URL"),
     32     )?
     33     .build()
     34 }
     35 
     36 #[post("/two-factor/get-webauthn", data = "<data>")]
     37 async fn get_webauthn(data: Json<PasswordOrOtpData>, headers: Headers, conn: DbConn) -> JsonResult {
     38     let data: PasswordOrOtpData = data.into_inner();
     39     let user = headers.user;
     40     data.validate(&user)?;
     41     let keys = WebAuthnInfo::get_all_by_user(&user.uuid, &conn).await?;
     42     Ok(Json(json!({
     43         "enabled": !keys.is_empty(),
     44         "keys": keys,
     45         "object": "twoFactorWebAuthn"
     46     })))
     47 }
     48 #[post("/two-factor/get-webauthn-challenge", data = "<data>")]
     49 async fn generate_webauthn_challenge(
     50     data: Json<PasswordOrOtpData>,
     51     headers: Headers,
     52     conn: DbConn,
     53 ) -> JsonResult {
     54     let data: PasswordOrOtpData = data.into_inner();
     55     let user = headers.user;
     56     data.validate(&user)?;
     57     let (challenge, registration) = build_webauthn()?.start_securitykey_registration(
     58         Uuid::try_parse(user.uuid.as_str()).expect("unable to create UUID"),
     59         user.email.as_str(),
     60         user.name.as_str(),
     61         Some(WebAuthn::get_all_credentials_by_user(&user.uuid, &conn).await?),
     62         None,
     63         None,
     64     )?;
     65     WebAuthnChallenge::Reg(WebAuthnReg::new(user.uuid, &registration)?)
     66         .replace(&conn)
     67         .await?;
     68     let mut challenge_value = serde_json::to_value(challenge.public_key)?;
     69     challenge_value["status"] = "ok".into();
     70     challenge_value["errorMessage"] = "".into();
     71     Ok(Json(challenge_value))
     72 }
     73 
     74 #[derive(Deserialize)]
     75 #[serde(rename_all = "camelCase")]
     76 struct EnableWebauthnData {
     77     id: i64,
     78     name: String,
     79     device_response: RegisterPublicKeyCredential,
     80     master_password_hash: String,
     81 }
     82 
     83 #[post("/two-factor/webauthn", data = "<data>")]
     84 async fn activate_webauthn(
     85     data: Json<EnableWebauthnData>,
     86     headers: Headers,
     87     conn: DbConn,
     88 ) -> JsonResult {
     89     let data = data.into_inner();
     90     let user = headers.user;
     91     PasswordOrOtpData {
     92         master_password_hash: Some(data.master_password_hash),
     93         otp: None,
     94     }
     95     .validate(&user)?;
     96     // Retrieve and delete the saved challenge state
     97     let chall = WebAuthnReg::find_by_user(&user.uuid, &conn)
     98         .await?
     99         .ok_or_else(|| Error::from(String::from("no webauthn challenge")))?;
    100     let registration = chall.security_key_reg()?;
    101     WebAuthnChallenge::Reg(chall).delete(&conn).await?;
    102     // Verify the credentials with the saved state
    103     let security_key =
    104         build_webauthn()?.finish_securitykey_registration(&data.device_response, &registration)?;
    105     WebAuthn::new(user.uuid.clone(), data.id, data.name, &security_key)?
    106         .insert(&conn)
    107         .await?;
    108     let keys = WebAuthnInfo::get_all_by_user(user.uuid.as_str(), &conn).await?;
    109     Ok(Json(json!({
    110         "enabled": !keys.is_empty(),
    111         "keys": keys,
    112         "object": "twoFactorWebAuthn"
    113     })))
    114 }
    115 
    116 #[put("/two-factor/webauthn", data = "<data>")]
    117 async fn activate_webauthn_put(
    118     data: Json<EnableWebauthnData>,
    119     headers: Headers,
    120     conn: DbConn,
    121 ) -> JsonResult {
    122     activate_webauthn(data, headers, conn).await
    123 }
    124 
    125 #[derive(Deserialize)]
    126 #[serde(rename_all = "camelCase")]
    127 struct DeleteU2FData {
    128     id: i64,
    129     master_password_hash: String,
    130 }
    131 
    132 #[delete("/two-factor/webauthn", data = "<data>")]
    133 async fn delete_webauthn(data: Json<DeleteU2FData>, headers: Headers, conn: DbConn) -> JsonResult {
    134     if !headers
    135         .user
    136         .check_valid_password(&data.master_password_hash)
    137     {
    138         err!("Invalid password");
    139     }
    140     WebAuthn::delete_by_user_uuid_and_id(&headers.user.uuid, data.id, &conn).await?;
    141     let keys = WebAuthnInfo::get_all_by_user(&headers.user.uuid, &conn).await?;
    142     Ok(Json(json!({
    143         "enabled": !keys.is_empty(),
    144         "keys": keys,
    145         "object": "twoFactorWebAuthn"
    146     })))
    147 }
    148 
    149 pub async fn generate_webauthn_login(user_uuid: &str, conn: &DbConn) -> JsonResult {
    150     let keys = WebAuthn::get_all_security_keys(user_uuid, conn).await?;
    151     if keys.is_empty() {
    152         err!("No WebAuthn devices registered")
    153     }
    154     let (challenge, auth) = build_webauthn()?.start_securitykey_authentication(keys.as_slice())?;
    155     WebAuthnChallenge::Auth(WebAuthnAuth::new(user_uuid.to_owned(), &auth)?)
    156         .replace(conn)
    157         .await?;
    158     Ok(Json(serde_json::to_value(challenge.public_key)?))
    159 }
    160 
    161 pub async fn validate_webauthn_login(
    162     user_uuid: &str,
    163     response: &str,
    164     conn: &DbConn,
    165 ) -> EmptyResult {
    166     let chall = WebAuthnAuth::find_by_user(user_uuid, conn)
    167         .await?
    168         .ok_or_else(|| Error::from(String::from("no webauthn challenge")))?;
    169     let security_key_authentication = chall.security_key_auth()?;
    170     WebAuthnChallenge::Auth(chall).delete(conn).await?;
    171     let resp = serde_json::from_str::<PublicKeyCredential>(response)?;
    172     let auth =
    173         build_webauthn()?.finish_securitykey_authentication(&resp, &security_key_authentication)?;
    174     let mut web = WebAuthn::get_by_cred_id(&resp.id, conn)
    175         .await?
    176         .ok_or_else(|| Error::from(String::from("no matching webauthn entry")))?;
    177     if auth.needs_update() {
    178         let mut sec_key = web.security_key()?;
    179         if let Some(update) = sec_key.update_credential(&auth) {
    180             if update {
    181                 web.set_security_key(&sec_key)?;
    182                 web.update(conn).await?;
    183                 Ok(())
    184             } else {
    185                 unreachable!("webauthn credential no longer needs to be updated")
    186             }
    187         } else {
    188             unreachable!("webauthn credential no longer matches challenge")
    189         }
    190     } else {
    191         Ok(())
    192     }
    193 }