From b04f0856eac02fe378cd019e9f6a6bf453e851a8 Mon Sep 17 00:00:00 2001 From: zUnixorn Date: Fri, 6 Jun 2025 05:30:16 +0200 Subject: [PATCH] add basic migration impl --- src/api/core/two_factor/webauthn.rs | 21 ++++++++----- src/db/models/two_factor.rs | 47 ++++++++++++++++++++++++++++- 2 files changed, 59 insertions(+), 9 deletions(-) diff --git a/src/api/core/two_factor/webauthn.rs b/src/api/core/two_factor/webauthn.rs index 97355fad..19a79c7c 100644 --- a/src/api/core/two_factor/webauthn.rs +++ b/src/api/core/two_factor/webauthn.rs @@ -133,12 +133,15 @@ async fn generate_webauthn_challenge(data: Json, headers: Hea Some(registrations), )?; - // TODO is there a nicer way to do this? // this is done since `start_passkey_registration()` always sets this to `Required` which shouldn't be needed for 2FA - challenge.public_key.authenticator_selection = challenge.public_key.authenticator_selection.map(|mut a| { - a.user_verification = UserVerificationPolicy::Discouraged_DO_NOT_USE; - a - }); + challenge.public_key.extensions = None; + if let Some(asc) = challenge.public_key.authenticator_selection.as_mut() { + asc.user_verification = UserVerificationPolicy::Discouraged_DO_NOT_USE; + } + + let mut state = serde_json::to_value(&state)?; + state["rs"]["policy"] = Value::String("discouraged".to_string()); + state["rs"]["extensions"].as_object_mut().unwrap().clear(); let type_ = TwoFactorType::WebauthnRegisterChallenge; TwoFactor::new(user.uuid.clone(), type_, serde_json::to_string(&state)?).save(&mut conn).await?; @@ -368,10 +371,12 @@ pub async fn generate_webauthn_login(user_id: &UserId, webauthn: Webauthn2FaConf // Generate a challenge based on the credentials let (mut response, state) = webauthn.start_passkey_authentication(&creds)?; + + // Modify to discourage user verification + let mut state = serde_json::to_value(&state)?; + state["ast"]["policy"] = Value::String("discouraged".to_string()); + state["ast"]["appid"] = Value::String(format!("{}/app-id.json", &CONFIG.domain())); response.public_key.user_verification = UserVerificationPolicy::Discouraged_DO_NOT_USE; - - // TODO does the appid extension matter? As far as I understand, this was only put into the authentication state anyway - // let ext = RequestAuthenticationExtensions::builder().appid(format!("{}/app-id.json", &CONFIG.domain())).build(); // Save the challenge state for later validation TwoFactor::new(user_id.clone(), TwoFactorType::WebauthnLoginChallenge, serde_json::to_string(&state)?) diff --git a/src/db/models/two_factor.rs b/src/db/models/two_factor.rs index f945bd29..d43dda33 100644 --- a/src/db/models/two_factor.rs +++ b/src/db/models/two_factor.rs @@ -3,6 +3,7 @@ use webauthn_rs::prelude::{Credential, ParsedAttestation}; use webauthn_rs_proto::{AttestationFormat, RegisteredExtensions}; use super::UserId; use crate::{api::EmptyResult, db::DbConn, error::MapResult}; +use crate::api::core::two_factor::webauthn::WebauthnRegistration; db_object! { #[derive(Identifiable, Queryable, Insertable, AsChangeset)] @@ -45,7 +46,27 @@ mod webauthn_0_3 { use webauthn_rs::prelude::ParsedAttestation; use webauthn_rs_proto::{AttestationFormat, RegisteredExtensions}; + #[derive(Deserialize)] + pub struct WebauthnRegistration { + pub id: i32, + pub name: String, + pub migrated: bool, + pub credential: Credential, + } + + impl From for crate::api::core::two_factor::webauthn::WebauthnRegistration { + fn from(value: WebauthnRegistration) -> Self { + Self { + id: value.id, + name: value.name, + migrated: value.migrated, + credential: webauthn_rs::prelude::Credential::from(value.credential).into(), + } + } + } + // Copied from https://docs.rs/webauthn-rs/0.3.2/src/webauthn_rs/proto.rs.html#316-339 + #[derive(Deserialize)] pub struct Credential { pub cred_id: Vec, pub cred: COSEKey, @@ -329,7 +350,31 @@ impl TwoFactor { } pub async fn migrate_credential_to_passkey(conn: &mut DbConn) -> EmptyResult { - todo!() + let webauthn_factors = db_run! { conn: { + twofactor::table + .filter(twofactor::atype.eq(TwoFactorType::Webauthn as i32)) + .load::(conn) + .expect("Error loading twofactor") + .from_db() + }}; + + for webauthn_factor in webauthn_factors { + // assume that a failure to parse into the old struct, means that it was already converted + // alternatively this could also be checked via an extra field in the db + let Ok(regs) = serde_json::from_str::>(&webauthn_factor.data) else { + continue; + }; + + let regs = regs.into_iter() + .map(|r| r.into()) + .collect::>(); + + TwoFactor::new(webauthn_factor.user_uuid.clone(), TwoFactorType::Webauthn, serde_json::to_string(®s)?) + .save(conn) + .await?; + } + + Ok(()) } }