mirror of
https://github.com/dani-garcia/vaultwarden
synced 2024-12-13 17:22:58 +01:00
Implemented multiple U2f keys, key names, and compromised checks
This commit is contained in:
parent
6027b969f5
commit
a744b9437a
3 changed files with 135 additions and 83 deletions
|
@ -119,7 +119,7 @@ fn disable_twofactor(data: JsonUpcase<DisableTwoFactorData>, headers: Headers, c
|
||||||
err!("Invalid password");
|
err!("Invalid password");
|
||||||
}
|
}
|
||||||
|
|
||||||
let type_ = data.Type.into_i32().expect("Invalid type");
|
let type_ = data.Type.into_i32()?;
|
||||||
|
|
||||||
if let Some(twofactor) = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn) {
|
if let Some(twofactor) = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn) {
|
||||||
twofactor.delete(&conn)?;
|
twofactor.delete(&conn)?;
|
||||||
|
@ -174,10 +174,7 @@ fn activate_authenticator(data: JsonUpcase<EnableAuthenticatorData>, headers: He
|
||||||
let data: EnableAuthenticatorData = data.into_inner().data;
|
let data: EnableAuthenticatorData = data.into_inner().data;
|
||||||
let password_hash = data.MasterPasswordHash;
|
let password_hash = data.MasterPasswordHash;
|
||||||
let key = data.Key;
|
let key = data.Key;
|
||||||
let token = match data.Token.into_i32() {
|
let token = data.Token.into_i32()? as u64;
|
||||||
Some(n) => n as u64,
|
|
||||||
None => err!("Malformed token"),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut user = headers.user;
|
let mut user = headers.user;
|
||||||
|
|
||||||
|
@ -244,19 +241,18 @@ fn generate_u2f(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn)
|
||||||
if !CONFIG.domain_set() {
|
if !CONFIG.domain_set() {
|
||||||
err!("`DOMAIN` environment variable is not set. U2F disabled")
|
err!("`DOMAIN` environment variable is not set. U2F disabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
let data: PasswordData = data.into_inner().data;
|
let data: PasswordData = data.into_inner().data;
|
||||||
let user = headers.user;
|
|
||||||
|
|
||||||
if !user.check_valid_password(&data.MasterPasswordHash) {
|
if !headers.user.check_valid_password(&data.MasterPasswordHash) {
|
||||||
err!("Invalid password");
|
err!("Invalid password");
|
||||||
}
|
}
|
||||||
|
|
||||||
let u2f_type = TwoFactorType::U2f as i32;
|
let (enabled, keys) = get_u2f_registrations(&headers.user.uuid, &conn)?;
|
||||||
let enabled = TwoFactor::find_by_user_and_type(&user.uuid, u2f_type, &conn).is_some();
|
let keys_json: Vec<Value> = keys.iter().map(|r| r.to_json()).collect();
|
||||||
|
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
"Enabled": enabled,
|
"Enabled": enabled,
|
||||||
|
"Keys": keys_json,
|
||||||
"Object": "twoFactorU2f"
|
"Object": "twoFactorU2f"
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
@ -264,18 +260,16 @@ fn generate_u2f(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn)
|
||||||
#[post("/two-factor/get-u2f-challenge", data = "<data>")]
|
#[post("/two-factor/get-u2f-challenge", data = "<data>")]
|
||||||
fn generate_u2f_challenge(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
|
fn generate_u2f_challenge(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||||
let data: PasswordData = data.into_inner().data;
|
let data: PasswordData = data.into_inner().data;
|
||||||
let user = headers.user;
|
|
||||||
|
|
||||||
if !user.check_valid_password(&data.MasterPasswordHash) {
|
if !headers.user.check_valid_password(&data.MasterPasswordHash) {
|
||||||
err!("Invalid password");
|
err!("Invalid password");
|
||||||
}
|
}
|
||||||
|
|
||||||
let user_uuid = &user.uuid;
|
let _type = TwoFactorType::U2fRegisterChallenge;
|
||||||
|
let challenge = _create_u2f_challenge(&headers.user.uuid, _type, &conn).challenge;
|
||||||
let challenge = _create_u2f_challenge(user_uuid, TwoFactorType::U2fRegisterChallenge, &conn).challenge;
|
|
||||||
|
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
"UserId": user.uuid,
|
"UserId": headers.user.uuid,
|
||||||
"AppId": APP_ID.to_string(),
|
"AppId": APP_ID.to_string(),
|
||||||
"Challenge": challenge,
|
"Challenge": challenge,
|
||||||
"Version": U2F_VERSION,
|
"Version": U2F_VERSION,
|
||||||
|
@ -291,6 +285,37 @@ struct EnableU2FData {
|
||||||
DeviceResponse: 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>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
struct U2FRegistration {
|
||||||
|
id: i32,
|
||||||
|
name: String,
|
||||||
|
#[serde(with = "RegistrationDef")]
|
||||||
|
reg: Registration,
|
||||||
|
counter: u32,
|
||||||
|
compromised: 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
|
// This struct is copied from the U2F lib
|
||||||
// to add an optional error code
|
// to add an optional error code
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
@ -303,8 +328,8 @@ struct RegisterResponseCopy {
|
||||||
pub error_code: Option<NumberOrString>,
|
pub error_code: Option<NumberOrString>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RegisterResponseCopy {
|
impl Into<RegisterResponse> for RegisterResponseCopy {
|
||||||
fn into_response(self) -> RegisterResponse {
|
fn into(self) -> RegisterResponse {
|
||||||
RegisterResponse {
|
RegisterResponse {
|
||||||
registration_data: self.registration_data,
|
registration_data: self.registration_data,
|
||||||
version: self.version,
|
version: self.version,
|
||||||
|
@ -331,9 +356,9 @@ fn activate_u2f(data: JsonUpcase<EnableU2FData>, headers: Headers, conn: DbConn)
|
||||||
let challenge: Challenge = serde_json::from_str(&tf_challenge.data)?;
|
let challenge: Challenge = serde_json::from_str(&tf_challenge.data)?;
|
||||||
tf_challenge.delete(&conn)?;
|
tf_challenge.delete(&conn)?;
|
||||||
|
|
||||||
let response_copy: RegisterResponseCopy = serde_json::from_str(&data.DeviceResponse)?;
|
let response: RegisterResponseCopy = serde_json::from_str(&data.DeviceResponse)?;
|
||||||
|
|
||||||
let error_code = response_copy
|
let error_code = response
|
||||||
.error_code
|
.error_code
|
||||||
.clone()
|
.clone()
|
||||||
.map_or("0".into(), NumberOrString::into_string);
|
.map_or("0".into(), NumberOrString::into_string);
|
||||||
|
@ -342,30 +367,27 @@ fn activate_u2f(data: JsonUpcase<EnableU2FData>, headers: Headers, conn: DbConn)
|
||||||
err!("Error registering U2F token")
|
err!("Error registering U2F token")
|
||||||
}
|
}
|
||||||
|
|
||||||
let response = response_copy.into_response();
|
let registration = U2F.register_response(challenge.clone(), response.into())?;
|
||||||
|
let full_registration = U2FRegistration {
|
||||||
|
id: data.Id.into_i32()?,
|
||||||
|
name: data.Name,
|
||||||
|
reg: registration,
|
||||||
|
compromised: false,
|
||||||
|
counter: 0,
|
||||||
|
};
|
||||||
|
|
||||||
let registration = U2F.register_response(challenge.clone(), response)?;
|
let mut regs = get_u2f_registrations(&user.uuid, &conn)?.1;
|
||||||
// TODO: Allow more than one U2F device
|
|
||||||
let mut registrations = Vec::new();
|
|
||||||
registrations.push(registration);
|
|
||||||
|
|
||||||
let tf_registration = TwoFactor::new(
|
// TODO: Check that there is no repeat Id
|
||||||
user.uuid.clone(),
|
regs.push(full_registration);
|
||||||
TwoFactorType::U2f,
|
save_u2f_registrations(&user.uuid, ®s, &conn)?;
|
||||||
serde_json::to_string(®istrations).unwrap(),
|
|
||||||
);
|
|
||||||
tf_registration.save(&conn)?;
|
|
||||||
|
|
||||||
_generate_recover_code(&mut user, &conn);
|
_generate_recover_code(&mut user, &conn);
|
||||||
|
|
||||||
|
let keys_json: Vec<Value> = regs.iter().map(|r| r.to_json()).collect();
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
"Enabled": true,
|
"Enabled": true,
|
||||||
"Challenge": {
|
"Keys": keys_json,
|
||||||
"UserId": user.uuid,
|
|
||||||
"AppId": APP_ID.to_string(),
|
|
||||||
"Challenge": challenge,
|
|
||||||
"Version": U2F_VERSION,
|
|
||||||
},
|
|
||||||
"Object": "twoFactorU2f"
|
"Object": "twoFactorU2f"
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
@ -385,52 +407,76 @@ fn _create_u2f_challenge(user_uuid: &str, type_: TwoFactorType, conn: &DbConn) -
|
||||||
challenge
|
challenge
|
||||||
}
|
}
|
||||||
|
|
||||||
// This struct is copied from the U2F lib
|
fn save_u2f_registrations(user_uuid: &str, regs: &Vec<U2FRegistration>, conn: &DbConn) -> EmptyResult {
|
||||||
// because it doesn't implement Deserialize
|
TwoFactor::new(user_uuid.into(), TwoFactorType::U2f, serde_json::to_string(regs)?).save(&conn)
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
struct RegistrationCopy {
|
|
||||||
pub key_handle: Vec<u8>,
|
|
||||||
pub pub_key: Vec<u8>,
|
|
||||||
pub attestation_cert: Option<Vec<u8>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Into<Registration> for RegistrationCopy {
|
fn get_u2f_registrations(user_uuid: &str, conn: &DbConn) -> Result<(bool, Vec<U2FRegistration>), Error> {
|
||||||
fn into(self) -> Registration {
|
let type_ = TwoFactorType::U2f as i32;
|
||||||
Registration {
|
let (enabled, regs) = match TwoFactor::find_by_user_and_type(user_uuid, type_, conn) {
|
||||||
key_handle: self.key_handle,
|
Some(tf) => (tf.enabled, tf.data),
|
||||||
pub_key: self.pub_key,
|
None => return Ok((false, Vec::new())), // If no data, return empty list
|
||||||
attestation_cert: self.attestation_cert,
|
};
|
||||||
|
|
||||||
|
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,
|
||||||
|
}];
|
||||||
|
|
||||||
|
// Save new format
|
||||||
|
save_u2f_registrations(user_uuid, &new_regs, &conn)?;
|
||||||
|
|
||||||
|
new_regs
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
Ok((enabled, data))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn _parse_registrations(registations: &str) -> Vec<Registration> {
|
fn _old_parse_registrations(registations: &str) -> Vec<Registration> {
|
||||||
let registrations_copy: Vec<RegistrationCopy> =
|
#[derive(Deserialize)]
|
||||||
serde_json::from_str(registations).expect("Can't parse RegistrationCopy data");
|
struct Helper(#[serde(with = "RegistrationDef")] Registration);
|
||||||
|
|
||||||
registrations_copy.into_iter().map(Into::into).collect()
|
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 fn generate_u2f_login(user_uuid: &str, conn: &DbConn) -> ApiResult<U2fSignRequest> {
|
pub fn generate_u2f_login(user_uuid: &str, conn: &DbConn) -> ApiResult<U2fSignRequest> {
|
||||||
let challenge = _create_u2f_challenge(user_uuid, TwoFactorType::U2fLoginChallenge, conn);
|
let challenge = _create_u2f_challenge(user_uuid, TwoFactorType::U2fLoginChallenge, conn);
|
||||||
|
|
||||||
let type_ = TwoFactorType::U2f as i32;
|
let registrations: Vec<_> = get_u2f_registrations(user_uuid, conn)?
|
||||||
let twofactor = match TwoFactor::find_by_user_and_type(user_uuid, type_, conn) {
|
.1
|
||||||
Some(tf) => tf,
|
.into_iter()
|
||||||
None => err!("No U2F devices registered"),
|
.map(|r| r.reg)
|
||||||
};
|
.collect();
|
||||||
|
|
||||||
let registrations = _parse_registrations(&twofactor.data);
|
if registrations.is_empty() {
|
||||||
let signed_request: U2fSignRequest = U2F.sign_request(challenge, registrations);
|
err!("No U2F devices registered")
|
||||||
|
}
|
||||||
|
|
||||||
Ok(signed_request)
|
Ok(U2F.sign_request(challenge, registrations))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn validate_u2f_login(user_uuid: &str, response: &str, conn: &DbConn) -> EmptyResult {
|
pub fn validate_u2f_login(user_uuid: &str, response: &str, conn: &DbConn) -> EmptyResult {
|
||||||
let challenge_type = TwoFactorType::U2fLoginChallenge as i32;
|
let challenge_type = TwoFactorType::U2fLoginChallenge as i32;
|
||||||
let u2f_type = TwoFactorType::U2f as i32;
|
|
||||||
|
|
||||||
let tf_challenge = TwoFactor::find_by_user_and_type(user_uuid, challenge_type, &conn);
|
let tf_challenge = TwoFactor::find_by_user_and_type(user_uuid, challenge_type, &conn);
|
||||||
|
|
||||||
let challenge = match tf_challenge {
|
let challenge = match tf_challenge {
|
||||||
|
@ -441,27 +487,29 @@ pub fn validate_u2f_login(user_uuid: &str, response: &str, conn: &DbConn) -> Emp
|
||||||
}
|
}
|
||||||
None => err!("Can't recover login challenge"),
|
None => err!("Can't recover login challenge"),
|
||||||
};
|
};
|
||||||
|
|
||||||
let twofactor = match TwoFactor::find_by_user_and_type(user_uuid, u2f_type, conn) {
|
|
||||||
Some(tf) => tf,
|
|
||||||
None => err!("No U2F devices registered"),
|
|
||||||
};
|
|
||||||
|
|
||||||
let registrations = _parse_registrations(&twofactor.data);
|
|
||||||
|
|
||||||
let response: SignResponse = serde_json::from_str(response)?;
|
let response: SignResponse = serde_json::from_str(response)?;
|
||||||
|
let mut registrations = get_u2f_registrations(user_uuid, conn)?.1;
|
||||||
|
if registrations.is_empty() {
|
||||||
|
err!("No U2F devices registered")
|
||||||
|
}
|
||||||
|
|
||||||
let mut _counter: u32 = 0;
|
for reg in &mut registrations {
|
||||||
for registration in registrations {
|
let response = U2F.sign_response(challenge.clone(), reg.reg.clone(), response.clone(), reg.counter);
|
||||||
let response = U2F.sign_response(challenge.clone(), registration, response.clone(), _counter);
|
|
||||||
match response {
|
match response {
|
||||||
Ok(new_counter) => {
|
Ok(new_counter) => {
|
||||||
_counter = new_counter;
|
reg.counter = new_counter;
|
||||||
info!("O {:#}", new_counter);
|
save_u2f_registrations(user_uuid, ®istrations, &conn)?;
|
||||||
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
Err(u2f::u2ferror::U2fError::CounterTooLow) => {
|
||||||
|
reg.compromised = true;
|
||||||
|
save_u2f_registrations(user_uuid, ®istrations, &conn)?;
|
||||||
|
|
||||||
|
err!("This device might be compromised!");
|
||||||
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
info!("E {:#}", e);
|
warn!("E {:#}", e);
|
||||||
// break;
|
// break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,10 +47,13 @@ impl NumberOrString {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn into_i32(self) -> Option<i32> {
|
fn into_i32(self) -> ApiResult<i32> {
|
||||||
|
use std::num::ParseIntError as PIE;
|
||||||
match self {
|
match self {
|
||||||
NumberOrString::Number(n) => Some(n),
|
NumberOrString::Number(n) => Ok(n),
|
||||||
NumberOrString::String(s) => s.parse().ok(),
|
NumberOrString::String(s) => s
|
||||||
|
.parse()
|
||||||
|
.map_err(|e: PIE| crate::Error::new("Can't convert to number", e.to_string())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,6 +38,7 @@ mod mail;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
pub use config::CONFIG;
|
pub use config::CONFIG;
|
||||||
|
pub use error::{Error, MapResult};
|
||||||
|
|
||||||
fn launch_rocket() {
|
fn launch_rocket() {
|
||||||
// Create Rocket object, this stores current log level and sets it's own
|
// Create Rocket object, this stores current log level and sets it's own
|
||||||
|
|
Loading…
Reference in a new issue