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