Daniel García
3 years ago
committed by
GitHub
8 changed files with 32 additions and 414 deletions
@ -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") |
|
||||
} |
|
Loading…
Reference in new issue