commit a07c213b3eeb957a0550e6acb687439279523d7a
parent ed72741f480bb2b400c84e357cd4c218e4745132
Author: Daniel GarcĂa <dani-garcia@users.noreply.github.com>
Date: Sun, 27 Mar 2022 18:43:09 +0200
Merge pull request #2398 from BlackDex/remove-u2f
Remove u2f implementation
Diffstat:
8 files changed, 32 insertions(+), 414 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -186,12 +186,6 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
@@ -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]]
@@ -3550,23 +3544,6 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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",
diff --git a/Cargo.toml b/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
diff --git a/src/api/core/two_factor/mod.rs b/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());
diff --git a/src/api/core/two_factor/u2f.rs b/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, ®s, &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(®s) {
- Ok(d) => d,
- Err(_) => {
- // If error, try old format
- let mut old_regs = _old_parse_registrations(®s);
-
- 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, ®istrations, conn).await?;
-
- return Ok(());
- }
- Err(u2f::u2ferror::U2fError::CounterTooLow) => {
- reg.compromised = true;
- save_u2f_registrations(user_uuid, ®istrations, conn).await?;
-
- err!("This device might be compromised!");
- }
- Err(e) => {
- warn!("E {:#}", e);
- // break;
- }
- }
- }
- err!("error verifying response")
-}
diff --git a/src/api/core/two_factor/webauthn.rs b/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"),
diff --git a/src/api/identity.rs b/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;
diff --git a/src/db/models/two_factor.rs b/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::*;
diff --git a/src/error.rs b/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,