| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037 |
- use data_encoding::{BASE32, BASE64};
- use rocket_contrib::json::Json;
- use serde_json;
- use serde_json::Value;
- use crate::api::{ApiResult, EmptyResult, JsonResult, JsonUpcase, NumberOrString, PasswordData};
- use crate::auth::Headers;
- use crate::crypto;
- use crate::db::{
- models::{TwoFactor, TwoFactorType, User},
- DbConn,
- };
- use crate::error::{Error, MapResult};
- use rocket::Route;
- pub fn routes() -> Vec<Route> {
- routes![
- get_twofactor,
- get_recover,
- recover,
- disable_twofactor,
- disable_twofactor_put,
- generate_authenticator,
- activate_authenticator,
- activate_authenticator_put,
- generate_u2f,
- generate_u2f_challenge,
- activate_u2f,
- activate_u2f_put,
- generate_yubikey,
- activate_yubikey,
- activate_yubikey_put,
- get_duo,
- activate_duo,
- activate_duo_put,
- ]
- }
- #[get("/two-factor")]
- fn get_twofactor(headers: Headers, conn: DbConn) -> JsonResult {
- let twofactors = TwoFactor::find_by_user(&headers.user.uuid, &conn);
- let twofactors_json: Vec<Value> = twofactors.iter().map(TwoFactor::to_json_list).collect();
- Ok(Json(json!({
- "Data": twofactors_json,
- "Object": "list",
- "ContinuationToken": null,
- })))
- }
- #[post("/two-factor/get-recover", data = "<data>")]
- fn get_recover(data: JsonUpcase<PasswordData>, headers: Headers) -> JsonResult {
- let data: PasswordData = data.into_inner().data;
- let user = headers.user;
- if !user.check_valid_password(&data.MasterPasswordHash) {
- err!("Invalid password");
- }
- Ok(Json(json!({
- "Code": user.totp_recover,
- "Object": "twoFactorRecover"
- })))
- }
- #[derive(Deserialize)]
- #[allow(non_snake_case)]
- struct RecoverTwoFactor {
- MasterPasswordHash: String,
- Email: String,
- RecoveryCode: String,
- }
- #[post("/two-factor/recover", data = "<data>")]
- fn recover(data: JsonUpcase<RecoverTwoFactor>, conn: DbConn) -> JsonResult {
- let data: RecoverTwoFactor = data.into_inner().data;
- use crate::db::models::User;
- // Get the user
- let mut user = match User::find_by_mail(&data.Email, &conn) {
- Some(user) => user,
- None => err!("Username or password is incorrect. Try again."),
- };
- // Check password
- if !user.check_valid_password(&data.MasterPasswordHash) {
- err!("Username or password is incorrect. Try again.")
- }
- // Check if recovery code is correct
- if !user.check_valid_recovery_code(&data.RecoveryCode) {
- err!("Recovery code is incorrect. Try again.")
- }
- // Remove all twofactors from the user
- TwoFactor::delete_all_by_user(&user.uuid, &conn)?;
- // Remove the recovery code, not needed without twofactors
- user.totp_recover = None;
- user.save(&conn)?;
- Ok(Json(json!({})))
- }
- fn _generate_recover_code(user: &mut User, conn: &DbConn) {
- if user.totp_recover.is_none() {
- let totp_recover = BASE32.encode(&crypto::get_random(vec![0u8; 20]));
- user.totp_recover = Some(totp_recover);
- user.save(conn).ok();
- }
- }
- #[derive(Deserialize)]
- #[allow(non_snake_case)]
- struct DisableTwoFactorData {
- MasterPasswordHash: String,
- Type: NumberOrString,
- }
- #[post("/two-factor/disable", data = "<data>")]
- fn disable_twofactor(data: JsonUpcase<DisableTwoFactorData>, headers: Headers, conn: DbConn) -> JsonResult {
- let data: DisableTwoFactorData = data.into_inner().data;
- let password_hash = data.MasterPasswordHash;
- let user = headers.user;
- if !user.check_valid_password(&password_hash) {
- err!("Invalid password");
- }
- let type_ = data.Type.into_i32()?;
- if let Some(twofactor) = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn) {
- twofactor.delete(&conn)?;
- }
- Ok(Json(json!({
- "Enabled": false,
- "Type": type_,
- "Object": "twoFactorProvider"
- })))
- }
- #[put("/two-factor/disable", data = "<data>")]
- fn disable_twofactor_put(data: JsonUpcase<DisableTwoFactorData>, headers: Headers, conn: DbConn) -> JsonResult {
- disable_twofactor(data, headers, conn)
- }
- #[post("/two-factor/get-authenticator", data = "<data>")]
- fn generate_authenticator(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
- let data: PasswordData = data.into_inner().data;
- let user = headers.user;
- if !user.check_valid_password(&data.MasterPasswordHash) {
- err!("Invalid password");
- }
- let type_ = TwoFactorType::Authenticator as i32;
- let twofactor = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn);
- let (enabled, key) = match twofactor {
- Some(tf) => (true, tf.data),
- _ => (false, BASE32.encode(&crypto::get_random(vec![0u8; 20]))),
- };
- Ok(Json(json!({
- "Enabled": enabled,
- "Key": key,
- "Object": "twoFactorAuthenticator"
- })))
- }
- #[derive(Deserialize, Debug)]
- #[allow(non_snake_case)]
- struct EnableAuthenticatorData {
- MasterPasswordHash: String,
- Key: String,
- Token: NumberOrString,
- }
- #[post("/two-factor/authenticator", data = "<data>")]
- fn activate_authenticator(data: JsonUpcase<EnableAuthenticatorData>, headers: Headers, conn: DbConn) -> JsonResult {
- let data: EnableAuthenticatorData = data.into_inner().data;
- let password_hash = data.MasterPasswordHash;
- let key = data.Key;
- let token = data.Token.into_i32()? as u64;
- let mut user = headers.user;
- if !user.check_valid_password(&password_hash) {
- err!("Invalid password");
- }
- // Validate key as base32 and 20 bytes length
- let decoded_key: Vec<u8> = match BASE32.decode(key.as_bytes()) {
- Ok(decoded) => decoded,
- _ => err!("Invalid totp secret"),
- };
- if decoded_key.len() != 20 {
- err!("Invalid key length")
- }
- let type_ = TwoFactorType::Authenticator;
- let twofactor = TwoFactor::new(user.uuid.clone(), type_, key.to_uppercase());
- // Validate the token provided with the key
- validate_totp_code(token, &twofactor.data)?;
- _generate_recover_code(&mut user, &conn);
- twofactor.save(&conn)?;
- Ok(Json(json!({
- "Enabled": true,
- "Key": key,
- "Object": "twoFactorAuthenticator"
- })))
- }
- #[put("/two-factor/authenticator", data = "<data>")]
- fn activate_authenticator_put(data: JsonUpcase<EnableAuthenticatorData>, headers: Headers, conn: DbConn) -> JsonResult {
- activate_authenticator(data, headers, conn)
- }
- pub fn validate_totp_code_str(totp_code: &str, secret: &str) -> EmptyResult {
- let totp_code: u64 = match totp_code.parse() {
- Ok(code) => code,
- _ => err!("TOTP code is not a number"),
- };
- validate_totp_code(totp_code, secret)
- }
- pub fn validate_totp_code(totp_code: u64, secret: &str) -> EmptyResult {
- use oath::{totp_raw_now, HashType};
- let decoded_secret = match BASE32.decode(secret.as_bytes()) {
- Ok(s) => s,
- Err(_) => err!("Invalid TOTP secret"),
- };
- let generated = totp_raw_now(&decoded_secret, 6, 0, 30, &HashType::SHA1);
- if generated != totp_code {
- err!("Invalid TOTP code");
- }
- Ok(())
- }
- use u2f::messages::{RegisterResponse, SignResponse, U2fSignRequest};
- use u2f::protocol::{Challenge, U2f};
- use u2f::register::Registration;
- use crate::CONFIG;
- const U2F_VERSION: &str = "U2F_V2";
- lazy_static! {
- static ref APP_ID: String = format!("{}/app-id.json", &CONFIG.domain());
- static ref U2F: U2f = U2f::new(APP_ID.clone());
- }
- #[post("/two-factor/get-u2f", data = "<data>")]
- 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)?;
- 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>")]
- 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).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>>,
- }
- #[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
- // 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 Into<RegisterResponse> for RegisterResponseCopy {
- fn into(self) -> RegisterResponse {
- RegisterResponse {
- registration_data: self.registration_data,
- version: self.version,
- client_data: self.client_data,
- }
- }
- }
- #[post("/two-factor/u2f", data = "<data>")]
- 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) {
- Some(c) => c,
- None => err!("Can't recover challenge"),
- };
- let challenge: Challenge = serde_json::from_str(&tf_challenge.data)?;
- tf_challenge.delete(&conn)?;
- 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.clone(), response.into())?;
- let full_registration = U2FRegistration {
- id: data.Id.into_i32()?,
- name: data.Name,
- reg: registration,
- compromised: false,
- counter: 0,
- };
- let mut regs = get_u2f_registrations(&user.uuid, &conn)?.1;
- // TODO: Check that there is no repeat Id
- regs.push(full_registration);
- save_u2f_registrations(&user.uuid, ®s, &conn)?;
- _generate_recover_code(&mut user, &conn);
- 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>")]
- fn activate_u2f_put(data: JsonUpcase<EnableU2FData>, headers: Headers, conn: DbConn) -> JsonResult {
- activate_u2f(data, headers, conn)
- }
- 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)
- .expect("Error saving challenge");
- challenge
- }
- 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)
- }
- 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) {
- 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,
- }];
- // Save new format
- save_u2f_registrations(user_uuid, &new_regs, &conn)?;
- 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 fn generate_u2f_login(user_uuid: &str, conn: &DbConn) -> ApiResult<U2fSignRequest> {
- let challenge = _create_u2f_challenge(user_uuid, TwoFactorType::U2fLoginChallenge, conn);
- let registrations: Vec<_> = get_u2f_registrations(user_uuid, conn)?
- .1
- .into_iter()
- .map(|r| r.reg)
- .collect();
- if registrations.is_empty() {
- err!("No U2F devices registered")
- }
- Ok(U2F.sign_request(challenge, registrations))
- }
- pub 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);
- let challenge = match tf_challenge {
- Some(tf_challenge) => {
- let challenge: Challenge = serde_json::from_str(&tf_challenge.data)?;
- tf_challenge.delete(&conn)?;
- 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)?.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)?;
- 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) => {
- warn!("E {:#}", e);
- // break;
- }
- }
- }
- err!("error verifying response")
- }
- #[derive(Deserialize, Debug)]
- #[allow(non_snake_case)]
- struct EnableYubikeyData {
- MasterPasswordHash: String,
- Key1: Option<String>,
- Key2: Option<String>,
- Key3: Option<String>,
- Key4: Option<String>,
- Key5: Option<String>,
- Nfc: bool,
- }
- #[derive(Deserialize, Serialize, Debug)]
- #[allow(non_snake_case)]
- pub struct YubikeyMetadata {
- Keys: Vec<String>,
- pub Nfc: bool,
- }
- use yubico::config::Config;
- use yubico::verify;
- fn parse_yubikeys(data: &EnableYubikeyData) -> Vec<String> {
- let data_keys = [&data.Key1, &data.Key2, &data.Key3, &data.Key4, &data.Key5];
- data_keys.iter().filter_map(|e| e.as_ref().cloned()).collect()
- }
- fn jsonify_yubikeys(yubikeys: Vec<String>) -> serde_json::Value {
- let mut result = json!({});
- for (i, key) in yubikeys.into_iter().enumerate() {
- result[format!("Key{}", i + 1)] = Value::String(key);
- }
- result
- }
- fn get_yubico_credentials() -> Result<(String, String), Error> {
- match (CONFIG.yubico_client_id(), CONFIG.yubico_secret_key()) {
- (Some(id), Some(secret)) => Ok((id, secret)),
- _ => err!("`YUBICO_CLIENT_ID` or `YUBICO_SECRET_KEY` environment variable is not set. Yubikey OTP Disabled"),
- }
- }
- fn verify_yubikey_otp(otp: String) -> EmptyResult {
- let (yubico_id, yubico_secret) = get_yubico_credentials()?;
- let config = Config::default().set_client_id(yubico_id).set_key(yubico_secret);
- match CONFIG.yubico_server() {
- Some(server) => verify(otp, config.set_api_hosts(vec![server])),
- None => verify(otp, config),
- }
- .map_res("Failed to verify OTP")
- .and(Ok(()))
- }
- #[post("/two-factor/get-yubikey", data = "<data>")]
- fn generate_yubikey(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
- // Make sure the credentials are set
- get_yubico_credentials()?;
- let data: PasswordData = data.into_inner().data;
- let user = headers.user;
- if !user.check_valid_password(&data.MasterPasswordHash) {
- err!("Invalid password");
- }
- let user_uuid = &user.uuid;
- let yubikey_type = TwoFactorType::YubiKey as i32;
- let r = TwoFactor::find_by_user_and_type(user_uuid, yubikey_type, &conn);
- if let Some(r) = r {
- let yubikey_metadata: YubikeyMetadata = serde_json::from_str(&r.data)?;
- let mut result = jsonify_yubikeys(yubikey_metadata.Keys);
- result["Enabled"] = Value::Bool(true);
- result["Nfc"] = Value::Bool(yubikey_metadata.Nfc);
- result["Object"] = Value::String("twoFactorU2f".to_owned());
- Ok(Json(result))
- } else {
- Ok(Json(json!({
- "Enabled": false,
- "Object": "twoFactorU2f",
- })))
- }
- }
- #[post("/two-factor/yubikey", data = "<data>")]
- fn activate_yubikey(data: JsonUpcase<EnableYubikeyData>, headers: Headers, conn: DbConn) -> JsonResult {
- let data: EnableYubikeyData = data.into_inner().data;
- let mut user = headers.user;
- if !user.check_valid_password(&data.MasterPasswordHash) {
- err!("Invalid password");
- }
- // Check if we already have some data
- let mut yubikey_data = match TwoFactor::find_by_user_and_type(&user.uuid, TwoFactorType::YubiKey as i32, &conn) {
- Some(data) => data,
- None => TwoFactor::new(user.uuid.clone(), TwoFactorType::YubiKey, String::new()),
- };
- let yubikeys = parse_yubikeys(&data);
- if yubikeys.is_empty() {
- return Ok(Json(json!({
- "Enabled": false,
- "Object": "twoFactorU2f",
- })));
- }
- // Ensure they are valid OTPs
- for yubikey in &yubikeys {
- if yubikey.len() == 12 {
- // YubiKey ID
- continue;
- }
- verify_yubikey_otp(yubikey.to_owned()).map_res("Invalid Yubikey OTP provided")?;
- }
- let yubikey_ids: Vec<String> = yubikeys.into_iter().map(|x| (&x[..12]).to_owned()).collect();
- let yubikey_metadata = YubikeyMetadata {
- Keys: yubikey_ids,
- Nfc: data.Nfc,
- };
- yubikey_data.data = serde_json::to_string(&yubikey_metadata).unwrap();
- yubikey_data.save(&conn)?;
- _generate_recover_code(&mut user, &conn);
- let mut result = jsonify_yubikeys(yubikey_metadata.Keys);
- result["Enabled"] = Value::Bool(true);
- result["Nfc"] = Value::Bool(yubikey_metadata.Nfc);
- result["Object"] = Value::String("twoFactorU2f".to_owned());
- Ok(Json(result))
- }
- #[put("/two-factor/yubikey", data = "<data>")]
- fn activate_yubikey_put(data: JsonUpcase<EnableYubikeyData>, headers: Headers, conn: DbConn) -> JsonResult {
- activate_yubikey(data, headers, conn)
- }
- pub fn validate_yubikey_login(response: &str, twofactor_data: &str) -> EmptyResult {
- if response.len() != 44 {
- err!("Invalid Yubikey OTP length");
- }
- let yubikey_metadata: YubikeyMetadata = serde_json::from_str(twofactor_data).expect("Can't parse Yubikey Metadata");
- let response_id = &response[..12];
- if !yubikey_metadata.Keys.contains(&response_id.to_owned()) {
- err!("Given Yubikey is not registered");
- }
- let result = verify_yubikey_otp(response.to_owned());
- match result {
- Ok(_answer) => Ok(()),
- Err(_e) => err!("Failed to verify Yubikey against OTP server"),
- }
- }
- #[derive(Serialize, Deserialize)]
- struct DuoData {
- host: String,
- ik: String,
- sk: String,
- }
- impl DuoData {
- fn global() -> Option<Self> {
- match CONFIG.duo_host() {
- Some(host) => Some(Self {
- host,
- ik: CONFIG.duo_ikey().unwrap(),
- sk: CONFIG.duo_skey().unwrap(),
- }),
- None => None,
- }
- }
- fn msg(s: &str) -> Self {
- Self {
- host: s.into(),
- ik: s.into(),
- sk: s.into(),
- }
- }
- fn secret() -> Self {
- Self::msg("<global_secret>")
- }
- fn obscure(self) -> Self {
- let mut host = self.host;
- let mut ik = self.ik;
- let mut sk = self.sk;
- let digits = 4;
- let replaced = "************";
- host.replace_range(digits.., replaced);
- ik.replace_range(digits.., replaced);
- sk.replace_range(digits.., replaced);
- Self { host, ik, sk }
- }
- }
- enum DuoStatus {
- Global(DuoData), // Using the global duo config
- User(DuoData), // Using the user's config
- Disabled(bool), // True if there is a global setting
- }
- impl DuoStatus {
- fn data(self) -> Option<DuoData> {
- match self {
- DuoStatus::Global(data) => Some(data),
- DuoStatus::User(data) => Some(data),
- DuoStatus::Disabled(_) => None,
- }
- }
- }
- const DISABLED_MESSAGE_DEFAULT: &str = "<To use the global Duo keys, please leave these fields untouched>";
- #[post("/two-factor/get-duo", data = "<data>")]
- fn get_duo(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 data = get_user_duo_data(&headers.user.uuid, &conn);
- let (enabled, data) = match data {
- DuoStatus::Global(_) => (true, Some(DuoData::secret())),
- DuoStatus::User(data) => (true, Some(data.obscure())),
- DuoStatus::Disabled(true) => (false, Some(DuoData::msg(DISABLED_MESSAGE_DEFAULT))),
- DuoStatus::Disabled(false) => (false, None),
- };
- let json = if let Some(data) = data {
- json!({
- "Enabled": enabled,
- "Host": data.host,
- "SecretKey": data.sk,
- "IntegrationKey": data.ik,
- "Object": "twoFactorDuo"
- })
- } else {
- json!({
- "Enabled": enabled,
- "Object": "twoFactorDuo"
- })
- };
- Ok(Json(json))
- }
- #[derive(Deserialize)]
- #[allow(non_snake_case, dead_code)]
- struct EnableDuoData {
- MasterPasswordHash: String,
- Host: String,
- SecretKey: String,
- IntegrationKey: String,
- }
- impl From<EnableDuoData> for DuoData {
- fn from(d: EnableDuoData) -> Self {
- Self {
- host: d.Host,
- ik: d.IntegrationKey,
- sk: d.SecretKey,
- }
- }
- }
- fn check_duo_fields_custom(data: &EnableDuoData) -> bool {
- fn empty_or_default(s: &str) -> bool {
- let st = s.trim();
- st.is_empty() || s == DISABLED_MESSAGE_DEFAULT
- }
- !empty_or_default(&data.Host) && !empty_or_default(&data.SecretKey) && !empty_or_default(&data.IntegrationKey)
- }
- #[post("/two-factor/duo", data = "<data>")]
- fn activate_duo(data: JsonUpcase<EnableDuoData>, headers: Headers, conn: DbConn) -> JsonResult {
- let data: EnableDuoData = data.into_inner().data;
- if !headers.user.check_valid_password(&data.MasterPasswordHash) {
- err!("Invalid password");
- }
- let (data, data_str) = if check_duo_fields_custom(&data) {
- let data_req: DuoData = data.into();
- let data_str = serde_json::to_string(&data_req)?;
- duo_api_request("GET", "/auth/v2/check", "", &data_req).map_res("Failed to validate Duo credentials")?;
- (data_req.obscure(), data_str)
- } else {
- (DuoData::secret(), String::new())
- };
- let type_ = TwoFactorType::Duo;
- let twofactor = TwoFactor::new(headers.user.uuid.clone(), type_, data_str);
- twofactor.save(&conn)?;
- Ok(Json(json!({
- "Enabled": true,
- "Host": data.host,
- "SecretKey": data.sk,
- "IntegrationKey": data.ik,
- "Object": "twoFactorDuo"
- })))
- }
- #[put("/two-factor/duo", data = "<data>")]
- fn activate_duo_put(data: JsonUpcase<EnableDuoData>, headers: Headers, conn: DbConn) -> JsonResult {
- activate_duo(data, headers, conn)
- }
- fn duo_api_request(method: &str, path: &str, params: &str, data: &DuoData) -> EmptyResult {
- const AGENT: &str = "bitwarden_rs:Duo/1.0 (Rust)";
- use reqwest::{header::*, Client, Method};
- use std::str::FromStr;
- let url = format!("https://{}{}", &data.host, path);
- let date = Utc::now().to_rfc2822();
- let username = &data.ik;
- let fields = [&date, method, &data.host, path, params];
- let password = crypto::hmac_sign(&data.sk, &fields.join("\n"));
- let m = Method::from_str(method).unwrap_or_default();
- Client::new()
- .request(m, &url)
- .basic_auth(username, Some(password))
- .header(USER_AGENT, AGENT)
- .header(DATE, date)
- .send()?
- .error_for_status()?;
- Ok(())
- }
- const DUO_EXPIRE: i64 = 300;
- const APP_EXPIRE: i64 = 3600;
- const AUTH_PREFIX: &str = "AUTH";
- const DUO_PREFIX: &str = "TX";
- const APP_PREFIX: &str = "APP";
- use chrono::Utc;
- fn get_user_duo_data(uuid: &str, conn: &DbConn) -> DuoStatus {
- let type_ = TwoFactorType::Duo as i32;
- // If the user doesn't have an entry, disabled
- let twofactor = match TwoFactor::find_by_user_and_type(uuid, type_, &conn) {
- Some(t) => t,
- None => return DuoStatus::Disabled(DuoData::global().is_some()),
- };
- // If the user has the required values, we use those
- if let Ok(data) = serde_json::from_str(&twofactor.data) {
- return DuoStatus::User(data);
- }
- // Otherwise, we try to use the globals
- if let Some(global) = DuoData::global() {
- return DuoStatus::Global(global);
- }
- // If there are no globals configured, just disable it
- DuoStatus::Disabled(false)
- }
- // let (ik, sk, ak, host) = get_duo_keys();
- fn get_duo_keys_email(email: &str, conn: &DbConn) -> ApiResult<(String, String, String, String)> {
- let data = User::find_by_mail(email, &conn)
- .and_then(|u| get_user_duo_data(&u.uuid, &conn).data())
- .or_else(DuoData::global)
- .map_res("Can't fetch Duo keys")?;
- Ok((data.ik, data.sk, CONFIG.get_duo_akey(), data.host))
- }
- pub fn generate_duo_signature(email: &str, conn: &DbConn) -> ApiResult<(String, String)> {
- let now = Utc::now().timestamp();
- let (ik, sk, ak, host) = get_duo_keys_email(email, conn)?;
- let duo_sign = sign_duo_values(&sk, email, &ik, DUO_PREFIX, now + DUO_EXPIRE);
- let app_sign = sign_duo_values(&ak, email, &ik, APP_PREFIX, now + APP_EXPIRE);
- Ok((format!("{}:{}", duo_sign, app_sign), host))
- }
- fn sign_duo_values(key: &str, email: &str, ikey: &str, prefix: &str, expire: i64) -> String {
- let val = format!("{}|{}|{}", email, ikey, expire);
- let cookie = format!("{}|{}", prefix, BASE64.encode(val.as_bytes()));
- format!("{}|{}", cookie, crypto::hmac_sign(key, &cookie))
- }
- pub fn validate_duo_login(email: &str, response: &str, conn: &DbConn) -> EmptyResult {
- let split: Vec<&str> = response.split(':').collect();
- if split.len() != 2 {
- err!("Invalid response length");
- }
- let auth_sig = split[0];
- let app_sig = split[1];
- let now = Utc::now().timestamp();
- let (ik, sk, ak, _host) = get_duo_keys_email(email, conn)?;
- let auth_user = parse_duo_values(&sk, auth_sig, &ik, AUTH_PREFIX, now)?;
- let app_user = parse_duo_values(&ak, app_sig, &ik, APP_PREFIX, now)?;
- if !crypto::ct_eq(&auth_user, app_user) || !crypto::ct_eq(&auth_user, email) {
- err!("Error validating duo authentication")
- }
- Ok(())
- }
- fn parse_duo_values(key: &str, val: &str, ikey: &str, prefix: &str, time: i64) -> ApiResult<String> {
- let split: Vec<&str> = val.split('|').collect();
- if split.len() != 3 {
- err!("Invalid value length")
- }
- let u_prefix = split[0];
- let u_b64 = split[1];
- let u_sig = split[2];
- let sig = crypto::hmac_sign(key, &format!("{}|{}", u_prefix, u_b64));
- if !crypto::ct_eq(crypto::hmac_sign(key, &sig), crypto::hmac_sign(key, u_sig)) {
- err!("Duo signatures don't match")
- }
- if u_prefix != prefix {
- err!("Prefixes don't match")
- }
- let cookie_vec = match BASE64.decode(u_b64.as_bytes()) {
- Ok(c) => c,
- Err(_) => err!("Invalid Duo cookie encoding"),
- };
- let cookie = match String::from_utf8(cookie_vec) {
- Ok(c) => c,
- Err(_) => err!("Invalid Duo cookie encoding"),
- };
- let cookie_split: Vec<&str> = cookie.split('|').collect();
- if cookie_split.len() != 3 {
- err!("Invalid cookie length")
- }
- let username = cookie_split[0];
- let u_ikey = cookie_split[1];
- let expire = cookie_split[2];
- if !crypto::ct_eq(ikey, u_ikey) {
- err!("Invalid ikey")
- }
- let expire = match expire.parse() {
- Ok(e) => e,
- Err(_) => err!("Invalid expire time"),
- };
- if time >= expire {
- err!("Expired authorization")
- }
- Ok(username.into())
- }
|