vw_small

Hardened fork of Vaultwarden (https://github.com/dani-garcia/vaultwarden) with fewer features.
git clone https://git.philomathiclife.com/repos/vw_small
Log | Files | Refs | README

commit fb0c23b71fe8ff95bc421a8e6dbbc6d4a46a38d5
parent 6643e83b61bccefc24d7265bd1bbbcd9a786f85e
Author: BlackDex <black.dex@gmail.com>
Date:   Sun, 27 Mar 2022 17:25:04 +0200

Remove u2f implementation

For a while now WebAuthn has replaced u2f.
And since web-vault v2.27.0 the connector files for u2f have been removed.
Also, on the official bitwarden server the endpoint to `/two-factor/get-u2f` results in a 404.

- Removed all u2f code except the migration code from u2f to WebAuthn

Diffstat:
MCargo.lock | 40++++++++--------------------------------
MCargo.toml | 3+--
Msrc/api/core/two_factor/mod.rs | 2--
Dsrc/api/core/two_factor/u2f.rs | 353-------------------------------------------------------------------------------
Msrc/api/core/two_factor/webauthn.rs | 23++++++++++++++++++++++-
Msrc/api/identity.rs | 21---------------------
Msrc/db/models/two_factor.rs | 2+-
Msrc/error.rs | 2--
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, &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") -} 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,