Spiegel von
https://github.com/dani-garcia/vaultwarden.git
synchronisiert 2024-11-22 05:10:29 +01:00
Add global duo config and document options in .env template
Dieser Commit ist enthalten in:
Ursprung
cfbeb56371
Commit
754087b990
4 geänderte Dateien mit 99 neuen und 83 gelöschten Zeilen
|
@ -112,6 +112,21 @@
|
||||||
# YUBICO_SECRET_KEY=AAAAAAAAAAAAAAAAAAAAAAAA
|
# YUBICO_SECRET_KEY=AAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
# YUBICO_SERVER=http://yourdomain.com/wsapi/2.0/verify
|
# YUBICO_SERVER=http://yourdomain.com/wsapi/2.0/verify
|
||||||
|
|
||||||
|
## Duo Settings
|
||||||
|
## You need to configure all options to enable Duo support
|
||||||
|
## Create an account and protect an application as mentioned in this link (only the first step, not the rest):
|
||||||
|
## https://help.bitwarden.com/article/setup-two-step-login-duo/#create-a-duo-security-account
|
||||||
|
## Then set the following options, based on the values obtained from the last step:
|
||||||
|
# DUO_IKEY=<Integration Key>
|
||||||
|
# DUO_SKEY=<Secret Key>
|
||||||
|
# DUO_HOST=<API Hostname>
|
||||||
|
## The Aplication Key needs to be randomly generated. Recommended at least 40 characters in base64.
|
||||||
|
## Example command to generate it: 'openssl rand -base64 48'
|
||||||
|
## Note that this shouldn't change between runs.
|
||||||
|
# DUO_AKEY=<Application Key>
|
||||||
|
## After that, you should be able to follow the rest of the guide linked above,
|
||||||
|
## ignoring the fields that ask for the values that you already configured beforehand.
|
||||||
|
|
||||||
## Rocket specific settings, check Rocket documentation to learn more
|
## Rocket specific settings, check Rocket documentation to learn more
|
||||||
# ROCKET_ENV=staging
|
# ROCKET_ENV=staging
|
||||||
# ROCKET_ADDRESS=0.0.0.0 # Enable this to test mobile app
|
# ROCKET_ADDRESS=0.0.0.0 # Enable this to test mobile app
|
||||||
|
|
|
@ -717,16 +717,12 @@ pub fn validate_yubikey_login(response: &str, twofactor_data: &str) -> EmptyResu
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Default)]
|
|
||||||
pub struct DuoData {
|
|
||||||
pub host: String,
|
|
||||||
sk: String,
|
|
||||||
ik: String,
|
|
||||||
ak: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/two-factor/get-duo", data = "<data>")]
|
#[post("/two-factor/get-duo", data = "<data>")]
|
||||||
fn get_duo(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
|
fn get_duo(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||||
|
if CONFIG.duo_host().is_none() {
|
||||||
|
err!("Duo is disabled. Refer to the Wiki for instructions in how to enable it")
|
||||||
|
}
|
||||||
|
|
||||||
let data: PasswordData = data.into_inner().data;
|
let data: PasswordData = data.into_inner().data;
|
||||||
|
|
||||||
if !headers.user.check_valid_password(&data.MasterPasswordHash) {
|
if !headers.user.check_valid_password(&data.MasterPasswordHash) {
|
||||||
|
@ -736,24 +732,22 @@ fn get_duo(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> Js
|
||||||
let type_ = TwoFactorType::Duo as i32;
|
let type_ = TwoFactorType::Duo as i32;
|
||||||
let twofactor = TwoFactor::find_by_user_and_type(&headers.user.uuid, type_, &conn);
|
let twofactor = TwoFactor::find_by_user_and_type(&headers.user.uuid, type_, &conn);
|
||||||
|
|
||||||
let (enabled, data) = match twofactor {
|
let (enabled, msg) = match twofactor {
|
||||||
Some(tf) => (true, serde_json::from_str(&tf.data)?),
|
Some(_) => (true, "<secret>"),
|
||||||
_ => (false, DuoData::default()),
|
_ => (false, "<Ignore this, click enable, then log out and log back in to activate>"),
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: It's probably not the best idea to return the keys here
|
|
||||||
|
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
"Enabled": enabled,
|
"Enabled": enabled,
|
||||||
"Host": data.host,
|
"Host": msg,
|
||||||
"SecretKey": data.sk,
|
"SecretKey": msg,
|
||||||
"IntegrationKey": data.ik,
|
"IntegrationKey": msg,
|
||||||
"Object": "twoFactorDuo"
|
"Object": "twoFactorDuo"
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case, dead_code)]
|
||||||
struct EnableDuoData {
|
struct EnableDuoData {
|
||||||
MasterPasswordHash: String,
|
MasterPasswordHash: String,
|
||||||
Host: String,
|
Host: String,
|
||||||
|
@ -769,27 +763,15 @@ fn activate_duo(data: JsonUpcase<EnableDuoData>, headers: Headers, conn: DbConn)
|
||||||
err!("Invalid password");
|
err!("Invalid password");
|
||||||
}
|
}
|
||||||
|
|
||||||
let data = DuoData {
|
|
||||||
host: data.Host,
|
|
||||||
sk: data.SecretKey,
|
|
||||||
ik: data.IntegrationKey,
|
|
||||||
ak: BASE64.encode(&crypto::get_random_64()),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Validate parameters with the server
|
|
||||||
duo_api_request("GET", "/auth/v2/check", "", &data)?;
|
|
||||||
|
|
||||||
let data_json = serde_json::to_string(&data)?;
|
|
||||||
let type_ = TwoFactorType::Duo;
|
let type_ = TwoFactorType::Duo;
|
||||||
let twofactor = TwoFactor::new(headers.user.uuid.clone(), type_, data_json);
|
let twofactor = TwoFactor::new(headers.user.uuid.clone(), type_, String::new());
|
||||||
twofactor.save(&conn)?;
|
twofactor.save(&conn)?;
|
||||||
|
|
||||||
// TODO: It's probably not the best idea to return the keys here
|
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
"Enabled": true,
|
"Enabled": true,
|
||||||
"Host": data.host,
|
"Host": "<secret>",
|
||||||
"SecretKey": data.sk,
|
"SecretKey": "<secret>",
|
||||||
"IntegrationKey": data.ik,
|
"IntegrationKey": "<secret>",
|
||||||
"Object": "twoFactorDuo"
|
"Object": "twoFactorDuo"
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
@ -799,7 +781,8 @@ fn activate_duo_put(data: JsonUpcase<EnableDuoData>, headers: Headers, conn: DbC
|
||||||
activate_duo(data, headers, conn)
|
activate_duo(data, headers, conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn duo_api_request(method: &str, path: &str, params: &str, data: &DuoData) -> EmptyResult {
|
// duo_api_request("GET", "/auth/v2/check", "", &data)?;
|
||||||
|
fn _duo_api_request(method: &str, path: &str, params: &str) -> EmptyResult {
|
||||||
const AGENT: &str = "bitwarden_rs:Duo/1.0 (Rust)";
|
const AGENT: &str = "bitwarden_rs:Duo/1.0 (Rust)";
|
||||||
|
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
@ -807,13 +790,15 @@ fn duo_api_request(method: &str, path: &str, params: &str, data: &DuoData) -> Em
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use reqwest::{header::*, Client, Method};
|
use reqwest::{header::*, Client, Method};
|
||||||
|
|
||||||
let url = format!("https://{}{}", data.host, path);
|
let ik = CONFIG.duo_ikey().unwrap();
|
||||||
|
let sk = CONFIG.duo_skey().unwrap();
|
||||||
|
let host = CONFIG.duo_host().unwrap();
|
||||||
|
|
||||||
|
let url = format!("https://{}{}", host, path);
|
||||||
let date = Utc::now().to_rfc2822();
|
let date = Utc::now().to_rfc2822();
|
||||||
|
let username = &ik;
|
||||||
let username = &data.ik;
|
let fields = [&date, method, &host, path, params];
|
||||||
let fields = [&date, method, &data.host, path, params];
|
let password = crypto::hmac_sign(&sk, &fields.join("\n"));
|
||||||
let password = crypto::hmac_sign(&data.sk, &fields.join("\n"));
|
|
||||||
|
|
||||||
let m = Method::from_str(method).unwrap_or_default();
|
let m = Method::from_str(method).unwrap_or_default();
|
||||||
|
|
||||||
|
@ -837,11 +822,15 @@ const APP_PREFIX: &str = "APP";
|
||||||
|
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
|
|
||||||
pub fn generate_duo_signature(data: &DuoData, email: &str) -> String {
|
pub fn generate_duo_signature(email: &str) -> String {
|
||||||
let now = Utc::now().timestamp();
|
let now = Utc::now().timestamp();
|
||||||
|
|
||||||
let duo_sign = sign_duo_values(&data.sk, email, &data.ik, DUO_PREFIX, now + DUO_EXPIRE);
|
let ik = CONFIG.duo_ikey().unwrap();
|
||||||
let app_sign = sign_duo_values(&data.ak, email, &data.ik, APP_PREFIX, now + APP_EXPIRE);
|
let sk = CONFIG.duo_skey().unwrap();
|
||||||
|
let ak = CONFIG.duo_akey().unwrap();
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
format!("{}:{}", duo_sign, app_sign)
|
format!("{}:{}", duo_sign, app_sign)
|
||||||
}
|
}
|
||||||
|
@ -853,10 +842,8 @@ fn sign_duo_values(key: &str, email: &str, ikey: &str, prefix: &str, expire: i64
|
||||||
format!("{}|{}", cookie, crypto::hmac_sign(key, &cookie))
|
format!("{}|{}", cookie, crypto::hmac_sign(key, &cookie))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn validate_duo_login(response: &str, twofactor_data: &str) -> EmptyResult {
|
pub fn validate_duo_login(email: &str, response: &str) -> EmptyResult {
|
||||||
let data: DuoData = serde_json::from_str(twofactor_data)?;
|
let split: Vec<&str> = response.split(':').collect();
|
||||||
|
|
||||||
let split: Vec<&str> = response.split(":").collect();
|
|
||||||
if split.len() != 2 {
|
if split.len() != 2 {
|
||||||
err!("Invalid response length");
|
err!("Invalid response length");
|
||||||
}
|
}
|
||||||
|
@ -866,10 +853,14 @@ pub fn validate_duo_login(response: &str, twofactor_data: &str) -> EmptyResult {
|
||||||
|
|
||||||
let now = Utc::now().timestamp();
|
let now = Utc::now().timestamp();
|
||||||
|
|
||||||
let auth_user = parse_duo_values(&data.sk, auth_sig, &data.ik, AUTH_PREFIX, now)?;
|
let ik = CONFIG.duo_ikey().unwrap();
|
||||||
let app_user = parse_duo_values(&data.ak, app_sig, &data.ik, APP_PREFIX, now)?;
|
let sk = CONFIG.duo_skey().unwrap();
|
||||||
|
let ak = CONFIG.duo_akey().unwrap();
|
||||||
|
|
||||||
if !crypto::ct_eq(auth_user, app_user) {
|
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")
|
err!("Error validating duo authentication")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -877,7 +868,7 @@ pub fn validate_duo_login(response: &str, twofactor_data: &str) -> EmptyResult {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_duo_values(key: &str, val: &str, ikey: &str, prefix: &str, time: i64) -> ApiResult<String> {
|
fn parse_duo_values(key: &str, val: &str, ikey: &str, prefix: &str, time: i64) -> ApiResult<String> {
|
||||||
let split: Vec<&str> = val.split("|").collect();
|
let split: Vec<&str> = val.split('|').collect();
|
||||||
if split.len() != 3 {
|
if split.len() != 3 {
|
||||||
err!("Invalid value length")
|
err!("Invalid value length")
|
||||||
}
|
}
|
||||||
|
@ -906,7 +897,7 @@ fn parse_duo_values(key: &str, val: &str, ikey: &str, prefix: &str, time: i64) -
|
||||||
Err(_) => err!("Invalid Duo cookie encoding"),
|
Err(_) => err!("Invalid Duo cookie encoding"),
|
||||||
};
|
};
|
||||||
|
|
||||||
let cookie_split: Vec<&str> = cookie.split("|").collect();
|
let cookie_split: Vec<&str> = cookie.split('|').collect();
|
||||||
if cookie_split.len() != 3 {
|
if cookie_split.len() != 3 {
|
||||||
err!("Invalid cookie length")
|
err!("Invalid cookie length")
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ use num_traits::FromPrimitive;
|
||||||
use crate::db::models::*;
|
use crate::db::models::*;
|
||||||
use crate::db::DbConn;
|
use crate::db::DbConn;
|
||||||
|
|
||||||
use crate::util::{self, JsonMap};
|
use crate::util;
|
||||||
|
|
||||||
use crate::api::{ApiResult, EmptyResult, JsonResult};
|
use crate::api::{ApiResult, EmptyResult, JsonResult};
|
||||||
|
|
||||||
|
@ -178,7 +178,7 @@ fn twofactor_auth(
|
||||||
Some(TwoFactorType::Authenticator) => _tf::validate_totp_code_str(twofactor_code, &selected_data?)?,
|
Some(TwoFactorType::Authenticator) => _tf::validate_totp_code_str(twofactor_code, &selected_data?)?,
|
||||||
Some(TwoFactorType::U2f) => _tf::validate_u2f_login(user_uuid, twofactor_code, conn)?,
|
Some(TwoFactorType::U2f) => _tf::validate_u2f_login(user_uuid, twofactor_code, conn)?,
|
||||||
Some(TwoFactorType::YubiKey) => _tf::validate_yubikey_login(twofactor_code, &selected_data?)?,
|
Some(TwoFactorType::YubiKey) => _tf::validate_yubikey_login(twofactor_code, &selected_data?)?,
|
||||||
Some(TwoFactorType::Duo) => _tf::validate_duo_login(twofactor_code, &selected_data?)?,
|
Some(TwoFactorType::Duo) => _tf::validate_duo_login(data.username.as_ref().unwrap(), twofactor_code)?,
|
||||||
|
|
||||||
Some(TwoFactorType::Remember) => {
|
Some(TwoFactorType::Remember) => {
|
||||||
match device.twofactor_remember {
|
match device.twofactor_remember {
|
||||||
|
@ -227,42 +227,33 @@ fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &DbConn) -> Api
|
||||||
let mut challenge_list = Vec::new();
|
let mut challenge_list = Vec::new();
|
||||||
|
|
||||||
for key in request.registered_keys {
|
for key in request.registered_keys {
|
||||||
let mut challenge_map = JsonMap::new();
|
challenge_list.push(json!({
|
||||||
|
"appId": request.app_id,
|
||||||
challenge_map.insert("appId".into(), Value::String(request.app_id.clone()));
|
"challenge": request.challenge,
|
||||||
challenge_map.insert("challenge".into(), Value::String(request.challenge.clone()));
|
"version": key.version,
|
||||||
challenge_map.insert("version".into(), Value::String(key.version));
|
"keyHandle": key.key_handle,
|
||||||
challenge_map.insert("keyHandle".into(), Value::String(key.key_handle.unwrap_or_default()));
|
}));
|
||||||
|
|
||||||
challenge_list.push(Value::Object(challenge_map));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut map = JsonMap::new();
|
|
||||||
let challenge_list_str = serde_json::to_string(&challenge_list).unwrap();
|
let challenge_list_str = serde_json::to_string(&challenge_list).unwrap();
|
||||||
|
|
||||||
map.insert("Challenges".into(), Value::String(challenge_list_str));
|
result["TwoFactorProviders2"][provider.to_string()] = json!({
|
||||||
result["TwoFactorProviders2"][provider.to_string()] = Value::Object(map);
|
"Challenges": challenge_list_str,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(tf_type @ TwoFactorType::Duo) => {
|
Some(TwoFactorType::Duo) => {
|
||||||
let twofactor = match TwoFactor::find_by_user_and_type(user_uuid, tf_type as i32, &conn) {
|
|
||||||
Some(tf) => tf,
|
|
||||||
None => err!("No Duo devices registered"),
|
|
||||||
};
|
|
||||||
|
|
||||||
let duo_data: two_factor::DuoData = serde_json::from_str(&twofactor.data)?;
|
|
||||||
|
|
||||||
let email = match User::find_by_uuid(user_uuid, &conn) {
|
let email = match User::find_by_uuid(user_uuid, &conn) {
|
||||||
Some(u) => u.email,
|
Some(u) => u.email,
|
||||||
None => err!("User does not exist")
|
None => err!("User does not exist"),
|
||||||
};
|
};
|
||||||
|
|
||||||
let signature = two_factor::generate_duo_signature(&duo_data, &email);
|
let signature = two_factor::generate_duo_signature(&email);
|
||||||
|
|
||||||
let mut map = JsonMap::new();
|
result["TwoFactorProviders2"][provider.to_string()] = json!({
|
||||||
map.insert("Host".into(), Value::String(duo_data.host));
|
"Host": CONFIG.duo_host(),
|
||||||
map.insert("Signature".into(), Value::String(signature));
|
"Signature": signature,
|
||||||
result["TwoFactorProviders2"][provider.to_string()] = Value::Object(map);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(tf_type @ TwoFactorType::YubiKey) => {
|
Some(tf_type @ TwoFactorType::YubiKey) => {
|
||||||
|
@ -271,12 +262,11 @@ fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &DbConn) -> Api
|
||||||
None => err!("No YubiKey devices registered"),
|
None => err!("No YubiKey devices registered"),
|
||||||
};
|
};
|
||||||
|
|
||||||
let yubikey_metadata: two_factor::YubikeyMetadata =
|
let yubikey_metadata: two_factor::YubikeyMetadata = serde_json::from_str(&twofactor.data)?;
|
||||||
serde_json::from_str(&twofactor.data)?;
|
|
||||||
|
|
||||||
let mut map = JsonMap::new();
|
result["TwoFactorProviders2"][provider.to_string()] = json!({
|
||||||
map.insert("Nfc".into(), Value::Bool(yubikey_metadata.Nfc));
|
"Nfc": yubikey_metadata.Nfc,
|
||||||
result["TwoFactorProviders2"][provider.to_string()] = Value::Object(map);
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
_ => {}
|
_ => {}
|
||||||
|
|
|
@ -302,6 +302,20 @@ make_config! {
|
||||||
yubico_server: String, true, option;
|
yubico_server: String, true, option;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Duo settings
|
||||||
|
duo: _enable_duo {
|
||||||
|
/// Enabled
|
||||||
|
_enable_duo: bool, true, def, true;
|
||||||
|
/// Integration Key
|
||||||
|
duo_ikey: String, true, option;
|
||||||
|
/// Secret Key
|
||||||
|
duo_skey: Pass, true, option;
|
||||||
|
/// Host
|
||||||
|
duo_host: String, true, option;
|
||||||
|
/// Application Key
|
||||||
|
duo_akey: Pass, true, option;
|
||||||
|
},
|
||||||
|
|
||||||
/// SMTP Email Settings
|
/// SMTP Email Settings
|
||||||
smtp: _enable_smtp {
|
smtp: _enable_smtp {
|
||||||
/// Enabled
|
/// Enabled
|
||||||
|
@ -332,6 +346,12 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (cfg.duo_host.is_some() || cfg.duo_ikey.is_some() || cfg.duo_skey.is_some() || cfg.duo_akey.is_some())
|
||||||
|
&& !(cfg.duo_host.is_some() && cfg.duo_ikey.is_some() && cfg.duo_skey.is_some() && cfg.duo_akey.is_some())
|
||||||
|
{
|
||||||
|
err!("All Duo options need to be set for Duo support")
|
||||||
|
}
|
||||||
|
|
||||||
if cfg.yubico_client_id.is_some() != cfg.yubico_secret_key.is_some() {
|
if cfg.yubico_client_id.is_some() != cfg.yubico_secret_key.is_some() {
|
||||||
err!("Both `YUBICO_CLIENT_ID` and `YUBICO_SECRET_KEY` need to be set for Yubikey OTP support")
|
err!("Both `YUBICO_CLIENT_ID` and `YUBICO_SECRET_KEY` need to be set for Yubikey OTP support")
|
||||||
}
|
}
|
||||||
|
|
Laden …
In neuem Issue referenzieren