use serde_json::Value; use crate::{api::EmptyResult, db::DbConn, error::MapResult}; db_object! { #[derive(Identifiable, Queryable, Insertable, AsChangeset)] #[table_name = "twofactor"] #[primary_key(uuid)] pub struct TwoFactor { pub uuid: String, pub user_uuid: String, pub atype: i32, pub enabled: bool, pub data: String, pub last_used: i32, } } #[allow(dead_code)] #[derive(num_derive::FromPrimitive)] pub enum TwoFactorType { Authenticator = 0, Email = 1, Duo = 2, YubiKey = 3, U2f = 4, Remember = 5, OrganizationDuo = 6, Webauthn = 7, // These are implementation details U2fRegisterChallenge = 1000, U2fLoginChallenge = 1001, EmailVerificationChallenge = 1002, WebauthnRegisterChallenge = 1003, WebauthnLoginChallenge = 1004, } /// Local methods impl TwoFactor { pub fn new(user_uuid: String, atype: TwoFactorType, data: String) -> Self { Self { uuid: crate::util::get_uuid(), user_uuid, atype: atype as i32, enabled: true, data, last_used: 0, } } pub fn to_json(&self) -> Value { json!({ "Enabled": self.enabled, "Key": "", // This key and value vary "Object": "twoFactorAuthenticator" // This value varies }) } pub fn to_json_provider(&self) -> Value { json!({ "Enabled": self.enabled, "Type": self.atype, "Object": "twoFactorProvider" }) } } /// Database methods impl TwoFactor { pub async fn save(&self, conn: &DbConn) -> EmptyResult { db_run! { conn: sqlite, mysql { match diesel::replace_into(twofactor::table) .values(TwoFactorDb::to_db(self)) .execute(conn) { Ok(_) => Ok(()), // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first. Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => { diesel::update(twofactor::table) .filter(twofactor::uuid.eq(&self.uuid)) .set(TwoFactorDb::to_db(self)) .execute(conn) .map_res("Error saving twofactor") } Err(e) => Err(e.into()), }.map_res("Error saving twofactor") } postgresql { let value = TwoFactorDb::to_db(self); // We need to make sure we're not going to violate the unique constraint on user_uuid and atype. // This happens automatically on other DBMS backends due to replace_into(). PostgreSQL does // not support multiple constraints on ON CONFLICT clauses. diesel::delete(twofactor::table.filter(twofactor::user_uuid.eq(&self.user_uuid)).filter(twofactor::atype.eq(&self.atype))) .execute(conn) .map_res("Error deleting twofactor for insert")?; diesel::insert_into(twofactor::table) .values(&value) .on_conflict(twofactor::uuid) .do_update() .set(&value) .execute(conn) .map_res("Error saving twofactor") } } } pub async fn delete(self, conn: &DbConn) -> EmptyResult { db_run! { conn: { diesel::delete(twofactor::table.filter(twofactor::uuid.eq(self.uuid))) .execute(conn) .map_res("Error deleting twofactor") }} } pub async fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec { db_run! { conn: { twofactor::table .filter(twofactor::user_uuid.eq(user_uuid)) .filter(twofactor::atype.lt(1000)) // Filter implementation types .load::(conn) .expect("Error loading twofactor") .from_db() }} } pub async fn find_by_user_and_type(user_uuid: &str, atype: i32, conn: &DbConn) -> Option { db_run! { conn: { twofactor::table .filter(twofactor::user_uuid.eq(user_uuid)) .filter(twofactor::atype.eq(atype)) .first::(conn) .ok() .from_db() }} } pub async fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> EmptyResult { db_run! { conn: { diesel::delete(twofactor::table.filter(twofactor::user_uuid.eq(user_uuid))) .execute(conn) .map_res("Error deleting twofactors") }} } pub async fn migrate_u2f_to_webauthn(conn: &DbConn) -> EmptyResult { let u2f_factors = db_run! { conn: { twofactor::table .filter(twofactor::atype.eq(TwoFactorType::U2f as i32)) .load::(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::proto::*; for mut u2f in u2f_factors { let mut regs: Vec = 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, y, }), }; let new_reg = WebauthnRegistration { id: reg.id, migrated: true, name: reg.name.clone(), credential: Credential { counter: reg.counter, verified: false, cred: key, cred_id: reg.reg.key_handle.clone(), registration_policy: UserVerificationPolicy::Discouraged, }, }; webauthn_regs.push(new_reg); reg.migrated = Some(true); } u2f.data = serde_json::to_string(®s)?; u2f.save(conn).await?; TwoFactor::new(u2f.user_uuid.clone(), TwoFactorType::Webauthn, serde_json::to_string(&webauthn_regs)?) .save(conn) .await?; } Ok(()) } }