Browse Source

Merge pull request #2398 from BlackDex/remove-u2f

Remove u2f implementation
Daniel García 3 years ago
parent
commit
a07c213b3e

+ 8 - 32
Cargo.lock

@@ -184,12 +184,6 @@ version = "0.2.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a4521f3e3d031370679b3b140beb36dfe4801b09ac77e30c61941f97df3ef28b"
 
-[[package]]
-name = "base64"
-version = "0.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7"
-
 [[package]]
 name = "base64"
 version = "0.13.0"
@@ -435,7 +429,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "94d4706de1b0fa5b132270cddffa8585166037822e260a944fe161acd137ca05"
 dependencies = [
  "aes-gcm",
- "base64 0.13.0",
+ "base64",
  "hkdf",
  "hmac 0.12.1",
  "percent-encoding 2.1.0",
@@ -1404,7 +1398,7 @@ version = "8.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "012bb02250fdd38faa5feee63235f7a459974440b9b57593822414c31f92839e"
 dependencies = [
- "base64 0.13.0",
+ "base64",
  "pem",
  "ring",
  "serde",
@@ -1440,7 +1434,7 @@ version = "0.10.0-rc.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "71d8da8f34d086b081c9cc3b57d3bb3b51d16fc06b5c848a188e2f14d58ac2a5"
 dependencies = [
- "base64 0.13.0",
+ "base64",
  "fastrand",
  "hostname",
  "httpdate",
@@ -2087,7 +2081,7 @@ version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e9a3b09a20e374558580a4914d3b7d89bd61b954a5a5e1dcbea98753addb1947"
 dependencies = [
- "base64 0.13.0",
+ "base64",
 ]
 
 [[package]]
@@ -2543,7 +2537,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "46a1f7aa4f35e5e8b4160449f51afc758f0ce6454315a9fa7d0d113e958c41eb"
 dependencies = [
  "async-compression",
- "base64 0.13.0",
+ "base64",
  "bytes 1.1.0",
  "cookie 0.15.1",
  "cookie_store",
@@ -2738,7 +2732,7 @@ version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1ee86d63972a7c661d1536fefe8c3c8407321c3df668891286de28abcd087360"
 dependencies = [
- "base64 0.13.0",
+ "base64",
 ]
 
 [[package]]
@@ -3549,23 +3543,6 @@ version = "1.15.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
 
-[[package]]
-name = "u2f"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2f285392366190c4d46823458f4543ac0f35174759c78e80c5baa39e1f7aa4f"
-dependencies = [
- "base64 0.11.0",
- "byteorder",
- "bytes 0.4.12",
- "chrono",
- "openssl",
- "serde",
- "serde_derive",
- "serde_json",
- "time 0.1.43",
-]
-
 [[package]]
 name = "ubyte"
 version = "0.10.1"
@@ -3718,7 +3695,6 @@ dependencies = [
  "tokio",
  "totp-lite",
  "tracing",
- "u2f",
  "url 2.2.2",
  "uuid",
  "webauthn-rs",
@@ -3858,7 +3834,7 @@ version = "0.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "90b266eccb4b32595876f5c73ea443b0516da0b1df72ca07bc08ed9ba7f96ec1"
 dependencies = [
- "base64 0.13.0",
+ "base64",
  "nom 7.1.1",
  "openssl",
  "rand 0.8.5",
@@ -4013,7 +3989,7 @@ version = "0.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4d3c3f584739059f479ca4de114cbfe032315752abb3be60afb30db40a802169"
 dependencies = [
- "base64 0.13.0",
+ "base64",
  "crypto-mac 0.10.1",
  "futures",
  "hmac 0.10.1",

+ 1 - 2
Cargo.toml

@@ -100,8 +100,7 @@ totp-lite = "1.0.3"
 # Yubico Library
 yubico = { version = "0.10.0", features = ["online-tokio"], default-features = false }
 
-# U2F libraries
-u2f = "0.2.0"
+# WebAuthn libraries
 webauthn-rs = "0.3.2"
 
 # Handling of URL's for WebAuthn

+ 0 - 2
src/api/core/two_factor/mod.rs

@@ -15,7 +15,6 @@ use crate::{
 pub mod authenticator;
 pub mod duo;
 pub mod email;
-pub mod u2f;
 pub mod webauthn;
 pub mod yubikey;
 
@@ -25,7 +24,6 @@ pub fn routes() -> Vec<Route> {
     routes.append(&mut authenticator::routes());
     routes.append(&mut duo::routes());
     routes.append(&mut email::routes());
-    routes.append(&mut u2f::routes());
     routes.append(&mut webauthn::routes());
     routes.append(&mut yubikey::routes());
 

+ 0 - 353
src/api/core/two_factor/u2f.rs

@@ -1,353 +0,0 @@
-use once_cell::sync::Lazy;
-use rocket::serde::json::Json;
-use rocket::Route;
-use serde_json::Value;
-use u2f::{
-    messages::{RegisterResponse, SignResponse, U2fSignRequest},
-    protocol::{Challenge, U2f},
-    register::Registration,
-};
-
-use crate::{
-    api::{
-        core::two_factor::_generate_recover_code, ApiResult, EmptyResult, JsonResult, JsonUpcase, NumberOrString,
-        PasswordData,
-    },
-    auth::Headers,
-    db::{
-        models::{TwoFactor, TwoFactorType},
-        DbConn,
-    },
-    error::Error,
-    CONFIG,
-};
-
-const U2F_VERSION: &str = "U2F_V2";
-
-static APP_ID: Lazy<String> = Lazy::new(|| format!("{}/app-id.json", &CONFIG.domain()));
-static U2F: Lazy<U2f> = Lazy::new(|| U2f::new(APP_ID.clone()));
-
-pub fn routes() -> Vec<Route> {
-    routes![generate_u2f, generate_u2f_challenge, activate_u2f, activate_u2f_put, delete_u2f,]
-}
-
-#[post("/two-factor/get-u2f", data = "<data>")]
-async fn generate_u2f(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
-    if !CONFIG.domain_set() {
-        err!("`DOMAIN` environment variable is not set. U2F disabled")
-    }
-    let data: PasswordData = data.into_inner().data;
-
-    if !headers.user.check_valid_password(&data.MasterPasswordHash) {
-        err!("Invalid password");
-    }
-
-    let (enabled, keys) = get_u2f_registrations(&headers.user.uuid, &conn).await?;
-    let keys_json: Vec<Value> = keys.iter().map(U2FRegistration::to_json).collect();
-
-    Ok(Json(json!({
-        "Enabled": enabled,
-        "Keys": keys_json,
-        "Object": "twoFactorU2f"
-    })))
-}
-
-#[post("/two-factor/get-u2f-challenge", data = "<data>")]
-async fn generate_u2f_challenge(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
-    let data: PasswordData = data.into_inner().data;
-
-    if !headers.user.check_valid_password(&data.MasterPasswordHash) {
-        err!("Invalid password");
-    }
-
-    let _type = TwoFactorType::U2fRegisterChallenge;
-    let challenge = _create_u2f_challenge(&headers.user.uuid, _type, &conn).await.challenge;
-
-    Ok(Json(json!({
-        "UserId": headers.user.uuid,
-        "AppId": APP_ID.to_string(),
-        "Challenge": challenge,
-        "Version": U2F_VERSION,
-    })))
-}
-
-#[derive(Deserialize, Debug)]
-#[allow(non_snake_case)]
-struct EnableU2FData {
-    Id: NumberOrString,
-    // 1..5
-    Name: String,
-    MasterPasswordHash: String,
-    DeviceResponse: String,
-}
-
-// This struct is referenced from the U2F lib
-// because it doesn't implement Deserialize
-#[derive(Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-#[serde(remote = "Registration")]
-struct RegistrationDef {
-    key_handle: Vec<u8>,
-    pub_key: Vec<u8>,
-    attestation_cert: Option<Vec<u8>>,
-    device_name: Option<String>,
-}
-
-#[derive(Serialize, Deserialize)]
-pub struct U2FRegistration {
-    pub id: i32,
-    pub name: String,
-    #[serde(with = "RegistrationDef")]
-    pub reg: Registration,
-    pub counter: u32,
-    compromised: bool,
-    pub migrated: Option<bool>,
-}
-
-impl U2FRegistration {
-    fn to_json(&self) -> Value {
-        json!({
-            "Id": self.id,
-            "Name": self.name,
-            "Compromised": self.compromised,
-        })
-    }
-}
-
-// This struct is copied from the U2F lib
-// to add an optional error code
-#[derive(Deserialize)]
-#[serde(rename_all = "camelCase")]
-struct RegisterResponseCopy {
-    pub registration_data: String,
-    pub version: String,
-    pub client_data: String,
-
-    pub error_code: Option<NumberOrString>,
-}
-
-impl From<RegisterResponseCopy> for RegisterResponse {
-    fn from(r: RegisterResponseCopy) -> RegisterResponse {
-        RegisterResponse {
-            registration_data: r.registration_data,
-            version: r.version,
-            client_data: r.client_data,
-        }
-    }
-}
-
-#[post("/two-factor/u2f", data = "<data>")]
-async fn activate_u2f(data: JsonUpcase<EnableU2FData>, headers: Headers, conn: DbConn) -> JsonResult {
-    let data: EnableU2FData = data.into_inner().data;
-    let mut user = headers.user;
-
-    if !user.check_valid_password(&data.MasterPasswordHash) {
-        err!("Invalid password");
-    }
-
-    let tf_type = TwoFactorType::U2fRegisterChallenge as i32;
-    let tf_challenge = match TwoFactor::find_by_user_and_type(&user.uuid, tf_type, &conn).await {
-        Some(c) => c,
-        None => err!("Can't recover challenge"),
-    };
-
-    let challenge: Challenge = serde_json::from_str(&tf_challenge.data)?;
-    tf_challenge.delete(&conn).await?;
-
-    let response: RegisterResponseCopy = serde_json::from_str(&data.DeviceResponse)?;
-
-    let error_code = response.error_code.clone().map_or("0".into(), NumberOrString::into_string);
-
-    if error_code != "0" {
-        err!("Error registering U2F token")
-    }
-
-    let registration = U2F.register_response(challenge, response.into())?;
-    let full_registration = U2FRegistration {
-        id: data.Id.into_i32()?,
-        name: data.Name,
-        reg: registration,
-        compromised: false,
-        counter: 0,
-        migrated: None,
-    };
-
-    let mut regs = get_u2f_registrations(&user.uuid, &conn).await?.1;
-
-    // TODO: Check that there is no repeat Id
-    regs.push(full_registration);
-    save_u2f_registrations(&user.uuid, &regs, &conn).await?;
-
-    _generate_recover_code(&mut user, &conn).await;
-
-    let keys_json: Vec<Value> = regs.iter().map(U2FRegistration::to_json).collect();
-    Ok(Json(json!({
-        "Enabled": true,
-        "Keys": keys_json,
-        "Object": "twoFactorU2f"
-    })))
-}
-
-#[put("/two-factor/u2f", data = "<data>")]
-async fn activate_u2f_put(data: JsonUpcase<EnableU2FData>, headers: Headers, conn: DbConn) -> JsonResult {
-    activate_u2f(data, headers, conn).await
-}
-
-#[derive(Deserialize, Debug)]
-#[allow(non_snake_case)]
-struct DeleteU2FData {
-    Id: NumberOrString,
-    MasterPasswordHash: String,
-}
-
-#[delete("/two-factor/u2f", data = "<data>")]
-async fn delete_u2f(data: JsonUpcase<DeleteU2FData>, headers: Headers, conn: DbConn) -> JsonResult {
-    let data: DeleteU2FData = data.into_inner().data;
-
-    let id = data.Id.into_i32()?;
-
-    if !headers.user.check_valid_password(&data.MasterPasswordHash) {
-        err!("Invalid password");
-    }
-
-    let type_ = TwoFactorType::U2f as i32;
-    let mut tf = match TwoFactor::find_by_user_and_type(&headers.user.uuid, type_, &conn).await {
-        Some(tf) => tf,
-        None => err!("U2F data not found!"),
-    };
-
-    let mut data: Vec<U2FRegistration> = match serde_json::from_str(&tf.data) {
-        Ok(d) => d,
-        Err(_) => err!("Error parsing U2F data"),
-    };
-
-    data.retain(|r| r.id != id);
-
-    let new_data_str = serde_json::to_string(&data)?;
-
-    tf.data = new_data_str;
-    tf.save(&conn).await?;
-
-    let keys_json: Vec<Value> = data.iter().map(U2FRegistration::to_json).collect();
-
-    Ok(Json(json!({
-        "Enabled": true,
-        "Keys": keys_json,
-        "Object": "twoFactorU2f"
-    })))
-}
-
-async fn _create_u2f_challenge(user_uuid: &str, type_: TwoFactorType, conn: &DbConn) -> Challenge {
-    let challenge = U2F.generate_challenge().unwrap();
-
-    TwoFactor::new(user_uuid.into(), type_, serde_json::to_string(&challenge).unwrap())
-        .save(conn)
-        .await
-        .expect("Error saving challenge");
-
-    challenge
-}
-
-async fn save_u2f_registrations(user_uuid: &str, regs: &[U2FRegistration], conn: &DbConn) -> EmptyResult {
-    TwoFactor::new(user_uuid.into(), TwoFactorType::U2f, serde_json::to_string(regs)?).save(conn).await
-}
-
-async fn get_u2f_registrations(user_uuid: &str, conn: &DbConn) -> Result<(bool, Vec<U2FRegistration>), Error> {
-    let type_ = TwoFactorType::U2f as i32;
-    let (enabled, regs) = match TwoFactor::find_by_user_and_type(user_uuid, type_, conn).await {
-        Some(tf) => (tf.enabled, tf.data),
-        None => return Ok((false, Vec::new())), // If no data, return empty list
-    };
-
-    let data = match serde_json::from_str(&regs) {
-        Ok(d) => d,
-        Err(_) => {
-            // If error, try old format
-            let mut old_regs = _old_parse_registrations(&regs);
-
-            if old_regs.len() != 1 {
-                err!("The old U2F format only allows one device")
-            }
-
-            // Convert to new format
-            let new_regs = vec![U2FRegistration {
-                id: 1,
-                name: "Unnamed U2F key".into(),
-                reg: old_regs.remove(0),
-                compromised: false,
-                counter: 0,
-                migrated: None,
-            }];
-
-            // Save new format
-            save_u2f_registrations(user_uuid, &new_regs, conn).await?;
-
-            new_regs
-        }
-    };
-
-    Ok((enabled, data))
-}
-
-fn _old_parse_registrations(registations: &str) -> Vec<Registration> {
-    #[derive(Deserialize)]
-    struct Helper(#[serde(with = "RegistrationDef")] Registration);
-
-    let regs: Vec<Value> = serde_json::from_str(registations).expect("Can't parse Registration data");
-
-    regs.into_iter().map(|r| serde_json::from_value(r).unwrap()).map(|Helper(r)| r).collect()
-}
-
-pub async fn generate_u2f_login(user_uuid: &str, conn: &DbConn) -> ApiResult<U2fSignRequest> {
-    let challenge = _create_u2f_challenge(user_uuid, TwoFactorType::U2fLoginChallenge, conn).await;
-
-    let registrations: Vec<_> = get_u2f_registrations(user_uuid, conn).await?.1.into_iter().map(|r| r.reg).collect();
-
-    if registrations.is_empty() {
-        err!("No U2F devices registered")
-    }
-
-    Ok(U2F.sign_request(challenge, registrations))
-}
-
-pub async fn validate_u2f_login(user_uuid: &str, response: &str, conn: &DbConn) -> EmptyResult {
-    let challenge_type = TwoFactorType::U2fLoginChallenge as i32;
-    let tf_challenge = TwoFactor::find_by_user_and_type(user_uuid, challenge_type, conn).await;
-
-    let challenge = match tf_challenge {
-        Some(tf_challenge) => {
-            let challenge: Challenge = serde_json::from_str(&tf_challenge.data)?;
-            tf_challenge.delete(conn).await?;
-            challenge
-        }
-        None => err!("Can't recover login challenge"),
-    };
-    let response: SignResponse = serde_json::from_str(response)?;
-    let mut registrations = get_u2f_registrations(user_uuid, conn).await?.1;
-    if registrations.is_empty() {
-        err!("No U2F devices registered")
-    }
-
-    for reg in &mut registrations {
-        let response = U2F.sign_response(challenge.clone(), reg.reg.clone(), response.clone(), reg.counter);
-        match response {
-            Ok(new_counter) => {
-                reg.counter = new_counter;
-                save_u2f_registrations(user_uuid, &registrations, conn).await?;
-
-                return Ok(());
-            }
-            Err(u2f::u2ferror::U2fError::CounterTooLow) => {
-                reg.compromised = true;
-                save_u2f_registrations(user_uuid, &registrations, conn).await?;
-
-                err!("This device might be compromised!");
-            }
-            Err(e) => {
-                warn!("E {:#}", e);
-                // break;
-            }
-        }
-    }
-    err!("error verifying response")
-}

+ 22 - 1
src/api/core/two_factor/webauthn.rs

@@ -21,6 +21,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>,
+}
+
 struct WebauthnConfig {
     url: String,
     origin: Url,
@@ -306,7 +328,6 @@ async fn delete_webauthn(data: JsonUpcase<DeleteU2FData>, headers: Headers, conn
     // 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, &conn).await
     {
-        use crate::api::core::two_factor::u2f::U2FRegistration;
         let mut data: Vec<U2FRegistration> = match serde_json::from_str(&u2f.data) {
             Ok(d) => d,
             Err(_) => err!("Error parsing U2F data"),

+ 0 - 21
src/api/identity.rs

@@ -314,7 +314,6 @@ async fn twofactor_auth(
         Some(TwoFactorType::Authenticator) => {
             _tf::authenticator::validate_totp_code_str(user_uuid, twofactor_code, &selected_data?, ip, conn).await?
         }
-        Some(TwoFactorType::U2f) => _tf::u2f::validate_u2f_login(user_uuid, twofactor_code, conn).await?,
         Some(TwoFactorType::Webauthn) => {
             _tf::webauthn::validate_webauthn_login(user_uuid, twofactor_code, conn).await?
         }
@@ -372,26 +371,6 @@ async fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &DbConn)
         match TwoFactorType::from_i32(*provider) {
             Some(TwoFactorType::Authenticator) => { /* Nothing to do for TOTP */ }
 
-            Some(TwoFactorType::U2f) if CONFIG.domain_set() => {
-                let request = two_factor::u2f::generate_u2f_login(user_uuid, conn).await?;
-                let mut challenge_list = Vec::new();
-
-                for key in request.registered_keys {
-                    challenge_list.push(json!({
-                        "appId": request.app_id,
-                        "challenge": request.challenge,
-                        "version": key.version,
-                        "keyHandle": key.key_handle,
-                    }));
-                }
-
-                let challenge_list_str = serde_json::to_string(&challenge_list).unwrap();
-
-                result["TwoFactorProviders2"][provider.to_string()] = json!({
-                    "Challenges": challenge_list_str,
-                });
-            }
-
             Some(TwoFactorType::Webauthn) if CONFIG.domain_set() => {
                 let request = two_factor::webauthn::generate_webauthn_login(user_uuid, conn).await?;
                 result["TwoFactorProviders2"][provider.to_string()] = request.0;

+ 1 - 1
src/db/models/two_factor.rs

@@ -157,7 +157,7 @@ impl TwoFactor {
                 .from_db()
         }};
 
-        use crate::api::core::two_factor::u2f::U2FRegistration;
+        use crate::api::core::two_factor::webauthn::U2FRegistration;
         use crate::api::core::two_factor::webauthn::{get_webauthn_registrations, WebauthnRegistration};
         use webauthn_rs::proto::*;
 

+ 0 - 2
src/error.rs

@@ -49,7 +49,6 @@ use rocket::error::Error as RocketErr;
 use serde_json::{Error as SerdeErr, Value};
 use std::io::Error as IoErr;
 use std::time::SystemTimeError as TimeErr;
-use u2f::u2ferror::U2fError as U2fErr;
 use webauthn_rs::error::WebauthnError as WebauthnErr;
 use yubico::yubicoerror::YubicoError as YubiErr;
 
@@ -70,7 +69,6 @@ make_error! {
     Json(Value):     _no_source,  _serialize,
     Db(DieselErr):   _has_source, _api_error,
     R2d2(R2d2Err):   _has_source, _api_error,
-    U2f(U2fErr):     _has_source, _api_error,
     Serde(SerdeErr): _has_source, _api_error,
     JWt(JwtErr):     _has_source, _api_error,
     Handlebars(HbErr): _has_source, _api_error,