mirror of
https://github.com/dani-garcia/vaultwarden
synced 2024-12-14 09:33:44 +01:00
Adds Yubikey OTP Support
This commit is contained in:
parent
e66436625c
commit
9e0e4b13c5
3 changed files with 209 additions and 0 deletions
|
@ -83,6 +83,9 @@ pub fn routes() -> Vec<Route> {
|
||||||
generate_u2f_challenge,
|
generate_u2f_challenge,
|
||||||
activate_u2f,
|
activate_u2f,
|
||||||
activate_u2f_put,
|
activate_u2f_put,
|
||||||
|
generate_yubikey,
|
||||||
|
activate_yubikey,
|
||||||
|
activate_yubikey_put,
|
||||||
|
|
||||||
get_organization,
|
get_organization,
|
||||||
create_organization,
|
create_organization,
|
||||||
|
|
|
@ -491,3 +491,203 @@ pub fn validate_u2f_login(user_uuid: &str, response: &str, conn: &DbConn) -> Api
|
||||||
}
|
}
|
||||||
err!("error verifying response")
|
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)]
|
||||||
|
struct YubikeyMetadata {
|
||||||
|
Keys: Vec<String>,
|
||||||
|
Nfc: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
use yubico::Yubico;
|
||||||
|
use yubico::config::Config;
|
||||||
|
|
||||||
|
fn parse_yubikeys(data: &EnableYubikeyData) -> Vec<String> {
|
||||||
|
let mut yubikeys: Vec<String> = Vec::new();
|
||||||
|
|
||||||
|
if data.Key1.is_some() {
|
||||||
|
yubikeys.push(data.Key1.as_ref().unwrap().to_owned());
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.Key2.is_some() {
|
||||||
|
yubikeys.push(data.Key2.as_ref().unwrap().to_owned());
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.Key3.is_some() {
|
||||||
|
yubikeys.push(data.Key3.as_ref().unwrap().to_owned());
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.Key4.is_some() {
|
||||||
|
yubikeys.push(data.Key4.as_ref().unwrap().to_owned());
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.Key5.is_some() {
|
||||||
|
yubikeys.push(data.Key5.as_ref().unwrap().to_owned());
|
||||||
|
}
|
||||||
|
|
||||||
|
yubikeys
|
||||||
|
}
|
||||||
|
|
||||||
|
fn jsonify_yubikeys(yubikeys: Vec<String>) -> serde_json::Value {
|
||||||
|
let mut result = json!({});
|
||||||
|
|
||||||
|
for i in 0..yubikeys.len() {
|
||||||
|
let ref key = &yubikeys[i];
|
||||||
|
result[format!("Key{}", i+1)] = Value::String(key.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn verify_yubikey_otp(otp: String) -> JsonResult {
|
||||||
|
if !CONFIG.yubico_cred_set {
|
||||||
|
err!("`YUBICO_CLIENT_ID` or `YUBICO_SECRET_KEY` environment variable is not set. \
|
||||||
|
Yubikey OTP Disabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
let yubico = Yubico::new();
|
||||||
|
let config = Config::default().set_client_id(CONFIG.yubico_client_id.to_owned()).set_key(CONFIG.yubico_secret_key.to_owned());
|
||||||
|
|
||||||
|
let result = yubico.verify(otp, config);
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(_answer) => Ok(Json(json!({}))),
|
||||||
|
Err(_e) => err!("Failed to verify OTP"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/two-factor/get-yubikey", data = "<data>")]
|
||||||
|
fn generate_yubikey(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 user_uuid = &headers.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).expect("Can't parse YubikeyMetadata 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;
|
||||||
|
|
||||||
|
if !headers.user.check_valid_password(&data.MasterPasswordHash) {
|
||||||
|
err!("Invalid password");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we already have some data
|
||||||
|
let yubikey_data = TwoFactor::find_by_user_and_type(
|
||||||
|
&headers.user.uuid,
|
||||||
|
TwoFactorType::YubiKey as i32,
|
||||||
|
&conn,
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(yubikey_data) = yubikey_data {
|
||||||
|
yubikey_data.delete(&conn).expect("Error deleting current Yubikeys");
|
||||||
|
}
|
||||||
|
|
||||||
|
let yubikeys = parse_yubikeys(&data);
|
||||||
|
|
||||||
|
// Ensure they are valid OTPs
|
||||||
|
for yubikey in &yubikeys {
|
||||||
|
if yubikey.len() == 12 {
|
||||||
|
// YubiKey ID
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = verify_yubikey_otp(yubikey.to_owned());
|
||||||
|
|
||||||
|
if let Err(_e) = result {
|
||||||
|
err!("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,
|
||||||
|
};
|
||||||
|
|
||||||
|
let yubikey_registration = TwoFactor::new(
|
||||||
|
headers.user.uuid.clone(),
|
||||||
|
TwoFactorType::YubiKey,
|
||||||
|
serde_json::to_string(&yubikey_metadata).unwrap(),
|
||||||
|
);
|
||||||
|
yubikey_registration
|
||||||
|
.save(&conn).expect("Failed to save Yubikey info");
|
||||||
|
|
||||||
|
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(user_uuid: &str, response: &str, conn: &DbConn) -> ApiResult<()> {
|
||||||
|
if response.len() != 44 {
|
||||||
|
err!("Invalid Yubikey OTP length");
|
||||||
|
}
|
||||||
|
|
||||||
|
let yubikey_type = TwoFactorType::YubiKey as i32;
|
||||||
|
|
||||||
|
let twofactor = match TwoFactor::find_by_user_and_type(user_uuid, yubikey_type, &conn) {
|
||||||
|
Some(tf) => tf,
|
||||||
|
None => err!("No YubiKey devices registered"),
|
||||||
|
};
|
||||||
|
|
||||||
|
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"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -209,6 +209,12 @@ fn twofactor_auth(
|
||||||
two_factor::validate_u2f_login(user_uuid, twofactor_code, conn)?;
|
two_factor::validate_u2f_login(user_uuid, twofactor_code, conn)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Some(TwoFactorType::YubiKey) => {
|
||||||
|
use api::core::two_factor;
|
||||||
|
|
||||||
|
two_factor::validate_yubikey_login(user_uuid, twofactor_code, conn)?;
|
||||||
|
}
|
||||||
|
|
||||||
_ => err!("Invalid two factor provider"),
|
_ => err!("Invalid two factor provider"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue