Compare commits

...

2 Commits

Author SHA1 Message Date
Stefan Melmuk 0020d36c24
fix u2f migration 2024-04-23 08:02:24 +02:00
Stefan Melmuk 54d4612fc8
Revert "remove u2f to webauthn migration"
This reverts commit 21b8a0a334.
2024-04-22 19:57:37 +02:00
5 changed files with 115 additions and 4 deletions

1
Cargo.lock generated
View File

@ -4133,6 +4133,7 @@ dependencies = [
"url",
"uuid",
"webauthn-rs",
"webauthn-rs-core",
"which",
"yubico",
]

View File

@ -109,7 +109,8 @@ totp-lite = "2.0.1"
yubico = { version = "0.11.0", features = ["online-tokio"], default-features = false }
# WebAuthn libraries
webauthn-rs = { version = "0.4.8", features = ["danger-allow-state-serialisation"] }
webauthn-rs = { version = "0.4.8", features = ["danger-allow-state-serialisation", "danger-credential-internals", "resident-key-support"] }
webauthn-rs-core = { version = "0.4.9" }
# Handling of URL's for WebAuthn and favicons
url = "2.5.0"

View File

@ -22,6 +22,28 @@ pub fn routes() -> Vec<Route> {
routes![get_webauthn, generate_webauthn_challenge, activate_webauthn, activate_webauthn_put, delete_webauthn,]
}
// Some old u2f structs still needed for migrating from u2f to WebAuthn
// Both `struct Registration` and `struct U2FRegistration` can be removed if we remove the u2f to WebAuthn migration
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Registration {
pub key_handle: Vec<u8>,
pub pub_key: Vec<u8>,
pub attestation_cert: Option<Vec<u8>>,
pub device_name: Option<String>,
}
#[derive(Serialize, Deserialize)]
pub struct U2FRegistration {
pub id: i32,
pub name: String,
#[serde(with = "Registration")]
pub reg: Registration,
pub counter: u32,
compromised: bool,
pub migrated: Option<bool>,
}
#[post("/two-factor/get-webauthn", data = "<data>")]
async fn get_webauthn(data: JsonUpcase<PasswordOrOtpData>, headers: Headers, mut conn: DbConn) -> JsonResult {
if !CONFIG.domain_set() {
@ -168,11 +190,27 @@ async fn delete_webauthn(data: JsonUpcase<DeleteU2FData>, headers: Headers, mut
None => err!("Webauthn entry not found"),
};
let _removed_item = data.remove(item_pos);
let removed_item = data.remove(item_pos);
tf.data = serde_json::to_string(&data)?;
tf.save(&mut conn).await?;
drop(tf);
// If entry is migrated from u2f, delete the u2f entry as well
if let Some(mut u2f) =
TwoFactor::find_by_user_and_type(&headers.user.uuid, TwoFactorType::U2f as i32, &mut conn).await
{
let mut data: Vec<U2FRegistration> = match serde_json::from_str(&u2f.data) {
Ok(d) => d,
Err(_) => err!("Error parsing U2F data"),
};
data.retain(|r| r.reg.key_handle != removed_item.security_key.cred_id().0);
let new_data_str = serde_json::to_string(&data)?;
u2f.data = new_data_str;
u2f.save(&mut conn).await?;
}
let keys_json: Vec<Value> = data.iter().map(WebauthnRegistration::to_json).collect();
Ok(Json(json!({
@ -183,7 +221,7 @@ async fn delete_webauthn(data: JsonUpcase<DeleteU2FData>, headers: Headers, mut
}
#[derive(Debug, Serialize, Deserialize)]
struct WebauthnRegistration {
pub struct WebauthnRegistration {
pub id: i32,
pub name: String,
pub migrated: bool,
@ -201,7 +239,7 @@ impl WebauthnRegistration {
}
}
async fn get_webauthn_registrations(
pub async fn get_webauthn_registrations(
user_uuid: &str,
conn: &mut DbConn,
) -> Result<(bool, Vec<WebauthnRegistration>), Error> {

View File

@ -147,4 +147,74 @@ impl TwoFactor {
.map_res("Error deleting twofactors")
}}
}
pub async fn migrate_u2f_to_webauthn(conn: &mut DbConn) -> EmptyResult {
let u2f_factors = db_run! { conn: {
twofactor::table
.filter(twofactor::atype.eq(TwoFactorType::U2f as i32))
.load::<TwoFactorDb>(conn)
.expect("Error loading twofactor")
.from_db()
}};
use crate::api::core::two_factor::webauthn::U2FRegistration;
use crate::api::core::two_factor::webauthn::{get_webauthn_registrations, WebauthnRegistration};
use webauthn_rs_core::proto::*;
for mut u2f in u2f_factors {
let mut regs: Vec<U2FRegistration> = serde_json::from_str(&u2f.data)?;
// If there are no registrations or they are migrated (we do the migration in batch so we can consider them all migrated when the first one is)
if regs.is_empty() || regs[0].migrated == Some(true) {
continue;
}
let (_, mut webauthn_regs) = get_webauthn_registrations(&u2f.user_uuid, conn).await?;
// If the user already has webauthn registrations saved, don't overwrite them
if !webauthn_regs.is_empty() {
continue;
}
for reg in &mut regs {
let x: [u8; 32] = reg.reg.pub_key[1..33].try_into().unwrap();
let y: [u8; 32] = reg.reg.pub_key[33..65].try_into().unwrap();
let key = COSEKey {
type_: COSEAlgorithm::ES256,
key: COSEKeyType::EC_EC2(COSEEC2Key {
curve: ECDSACurve::SECP256R1,
x: x.to_vec().into(),
y: y.to_vec().into(),
}),
};
let credential = CredentialV3 {
counter: reg.counter,
verified: false,
cred: key,
cred_id: reg.reg.key_handle.clone(),
registration_policy: UserVerificationPolicy::Preferred,
};
let new_reg = WebauthnRegistration {
id: reg.id,
migrated: true,
name: reg.name.clone(),
security_key: (Credential::from(credential).into()),
};
webauthn_regs.push(new_reg);
reg.migrated = Some(true);
}
u2f.data = serde_json::to_string(&regs)?;
u2f.save(conn).await?;
TwoFactor::new(u2f.user_uuid.clone(), TwoFactorType::Webauthn, serde_json::to_string(&webauthn_regs)?)
.save(conn)
.await?;
}
Ok(())
}
}

View File

@ -88,6 +88,7 @@ async fn main() -> Result<(), Error> {
let pool = create_db_pool().await;
schedule_jobs(pool.clone());
crate::db::models::TwoFactor::migrate_u2f_to_webauthn(&mut pool.get().await.unwrap()).await.unwrap();
launch_rocket(pool, extra_debug).await // Blocks until program termination.
}