mirror of
https://github.com/dani-garcia/vaultwarden
synced 2024-11-26 05:40:18 +00:00
Implement selection between global config and user settings for duo keys.
This commit is contained in:
parent
cad63f9761
commit
8d9827c55f
4 changed files with 179 additions and 48 deletions
|
@ -717,33 +717,102 @@ pub fn validate_yubikey_login(response: &str, twofactor_data: &str) -> EmptyResu
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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>")]
|
#[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) {
|
||||||
err!("Invalid password");
|
err!("Invalid password");
|
||||||
}
|
}
|
||||||
|
|
||||||
let type_ = TwoFactorType::Duo as i32;
|
let data = get_user_duo_data(&headers.user.uuid, &conn);
|
||||||
let twofactor = TwoFactor::find_by_user_and_type(&headers.user.uuid, type_, &conn);
|
|
||||||
|
|
||||||
let (enabled, msg) = match twofactor {
|
let (enabled, data) = match data {
|
||||||
Some(_) => (true, "<secret>"),
|
DuoStatus::Global(_) => (true, Some(DuoData::secret())),
|
||||||
_ => (false, "<Ignore this, click enable, then log out and log back in to activate>"),
|
DuoStatus::User(data) => (true, Some(data.obscure())),
|
||||||
|
DuoStatus::Disabled(true) => (false, Some(DuoData::msg(DISABLED_MESSAGE_DEFAULT))),
|
||||||
|
DuoStatus::Disabled(false) => (false, None),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Json(json!({
|
let json = if let Some(data) = data {
|
||||||
"Enabled": enabled,
|
json!({
|
||||||
"Host": msg,
|
"Enabled": enabled,
|
||||||
"SecretKey": msg,
|
"Host": data.host,
|
||||||
"IntegrationKey": msg,
|
"SecretKey": data.sk,
|
||||||
"Object": "twoFactorDuo"
|
"IntegrationKey": data.ik,
|
||||||
})))
|
"Object": "twoFactorDuo"
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
json!({
|
||||||
|
"Enabled": enabled,
|
||||||
|
"Object": "twoFactorDuo"
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Json(json))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
@ -755,6 +824,25 @@ struct EnableDuoData {
|
||||||
IntegrationKey: 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>")]
|
#[post("/two-factor/duo", data = "<data>")]
|
||||||
fn activate_duo(data: JsonUpcase<EnableDuoData>, headers: Headers, conn: DbConn) -> JsonResult {
|
fn activate_duo(data: JsonUpcase<EnableDuoData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||||
let data: EnableDuoData = data.into_inner().data;
|
let data: EnableDuoData = data.into_inner().data;
|
||||||
|
@ -763,15 +851,24 @@ fn activate_duo(data: JsonUpcase<EnableDuoData>, headers: Headers, conn: DbConn)
|
||||||
err!("Invalid password");
|
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 type_ = TwoFactorType::Duo;
|
||||||
let twofactor = TwoFactor::new(headers.user.uuid.clone(), type_, String::new());
|
let twofactor = TwoFactor::new(headers.user.uuid.clone(), type_, data_str);
|
||||||
twofactor.save(&conn)?;
|
twofactor.save(&conn)?;
|
||||||
|
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
"Enabled": true,
|
"Enabled": true,
|
||||||
"Host": "<secret>",
|
"Host": data.host,
|
||||||
"SecretKey": "<secret>",
|
"SecretKey": data.sk,
|
||||||
"IntegrationKey": "<secret>",
|
"IntegrationKey": data.ik,
|
||||||
"Object": "twoFactorDuo"
|
"Object": "twoFactorDuo"
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
@ -781,23 +878,17 @@ fn activate_duo_put(data: JsonUpcase<EnableDuoData>, headers: Headers, conn: DbC
|
||||||
activate_duo(data, headers, conn)
|
activate_duo(data, headers, conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
// duo_api_request("GET", "/auth/v2/check", "", &data)?;
|
fn duo_api_request(method: &str, path: &str, params: &str, data: &DuoData) -> EmptyResult {
|
||||||
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 reqwest::{header::*, Client, Method};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
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();
|
||||||
|
|
||||||
|
@ -821,24 +912,49 @@ const APP_PREFIX: &str = "APP";
|
||||||
|
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
|
|
||||||
// let (ik, sk, ak) = get_duo_keys();
|
|
||||||
fn get_duo_keys() -> (String, String, String) {
|
fn get_user_duo_data(uuid: &str, conn: &DbConn) -> DuoStatus {
|
||||||
(
|
let type_ = TwoFactorType::Duo as i32;
|
||||||
CONFIG.duo_ikey().unwrap(),
|
|
||||||
CONFIG.duo_skey().unwrap(),
|
// If the user doesn't have an entry, disabled
|
||||||
CONFIG.get_duo_akey(),
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_duo_signature(email: &str) -> String {
|
// let (ik, sk, ak) = get_duo_keys();
|
||||||
|
fn get_duo_keys_email(email: &str, conn: &DbConn) -> ApiResult<(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()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate_duo_signature(email: &str, conn: &DbConn) -> ApiResult<String> {
|
||||||
let now = Utc::now().timestamp();
|
let now = Utc::now().timestamp();
|
||||||
|
|
||||||
let (ik, sk, ak) = get_duo_keys();
|
let (ik, sk, ak) = get_duo_keys_email(email, conn)?;
|
||||||
|
|
||||||
let duo_sign = sign_duo_values(&sk, email, &ik, DUO_PREFIX, now + DUO_EXPIRE);
|
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);
|
let app_sign = sign_duo_values(&ak, email, &ik, APP_PREFIX, now + APP_EXPIRE);
|
||||||
|
|
||||||
format!("{}:{}", duo_sign, app_sign)
|
Ok(format!("{}:{}", duo_sign, app_sign))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sign_duo_values(key: &str, email: &str, ikey: &str, prefix: &str, expire: i64) -> String {
|
fn sign_duo_values(key: &str, email: &str, ikey: &str, prefix: &str, expire: i64) -> String {
|
||||||
|
@ -848,7 +964,7 @@ 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(email: &str, response: &str) -> EmptyResult {
|
pub fn validate_duo_login(email: &str, response: &str, conn: &DbConn) -> EmptyResult {
|
||||||
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");
|
||||||
|
@ -859,7 +975,7 @@ pub fn validate_duo_login(email: &str, response: &str) -> EmptyResult {
|
||||||
|
|
||||||
let now = Utc::now().timestamp();
|
let now = Utc::now().timestamp();
|
||||||
|
|
||||||
let (ik, sk, ak) = get_duo_keys();
|
let (ik, sk, ak) = get_duo_keys_email(email, conn)?;
|
||||||
|
|
||||||
let auth_user = parse_duo_values(&sk, auth_sig, &ik, AUTH_PREFIX, now)?;
|
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)?;
|
let app_user = parse_duo_values(&ak, app_sig, &ik, APP_PREFIX, now)?;
|
||||||
|
|
|
@ -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(data.username.as_ref().unwrap(), twofactor_code)?,
|
Some(TwoFactorType::Duo) => _tf::validate_duo_login(data.username.as_ref().unwrap(), twofactor_code, conn)?,
|
||||||
|
|
||||||
Some(TwoFactorType::Remember) => {
|
Some(TwoFactorType::Remember) => {
|
||||||
match device.twofactor_remember {
|
match device.twofactor_remember {
|
||||||
|
@ -248,7 +248,7 @@ fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &DbConn) -> Api
|
||||||
None => err!("User does not exist"),
|
None => err!("User does not exist"),
|
||||||
};
|
};
|
||||||
|
|
||||||
let signature = two_factor::generate_duo_signature(&email);
|
let signature = two_factor::generate_duo_signature(&email, conn)?;
|
||||||
|
|
||||||
result["TwoFactorProviders2"][provider.to_string()] = json!({
|
result["TwoFactorProviders2"][provider.to_string()] = json!({
|
||||||
"Host": CONFIG.duo_host(),
|
"Host": CONFIG.duo_host(),
|
||||||
|
|
|
@ -305,10 +305,10 @@ make_config! {
|
||||||
yubico_server: String, true, option;
|
yubico_server: String, true, option;
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Duo settings
|
/// Global Duo settings (Note that users can override them)
|
||||||
duo: _enable_duo {
|
duo: _enable_duo {
|
||||||
/// Enabled
|
/// Enabled
|
||||||
_enable_duo: bool, true, def, true;
|
_enable_duo: bool, true, def, false;
|
||||||
/// Integration Key
|
/// Integration Key
|
||||||
duo_ikey: String, true, option;
|
duo_ikey: String, true, option;
|
||||||
/// Secret Key
|
/// Secret Key
|
||||||
|
|
15
src/error.rs
15
src/error.rs
|
@ -41,6 +41,8 @@ use regex::Error as RegexErr;
|
||||||
use reqwest::Error as ReqErr;
|
use reqwest::Error as ReqErr;
|
||||||
use serde_json::{Error as SerdeErr, Value};
|
use serde_json::{Error as SerdeErr, Value};
|
||||||
use std::io::Error as IOErr;
|
use std::io::Error as IOErr;
|
||||||
|
|
||||||
|
use std::option::NoneError as NoneErr;
|
||||||
use std::time::SystemTimeError as TimeErr;
|
use std::time::SystemTimeError as TimeErr;
|
||||||
use u2f::u2ferror::U2fError as U2fErr;
|
use u2f::u2ferror::U2fError as U2fErr;
|
||||||
use yubico::yubicoerror::YubicoError as YubiErr;
|
use yubico::yubicoerror::YubicoError as YubiErr;
|
||||||
|
@ -73,6 +75,13 @@ make_error! {
|
||||||
YubiError(YubiErr): _has_source, _api_error,
|
YubiError(YubiErr): _has_source, _api_error,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This is implemented by hand because NoneError doesn't implement neither Display nor Error
|
||||||
|
impl From<NoneErr> for Error {
|
||||||
|
fn from(_: NoneErr) -> Self {
|
||||||
|
Error::from(("NoneError", String::new()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for Error {
|
impl std::fmt::Debug for Error {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
match self.source() {
|
match self.source() {
|
||||||
|
@ -118,6 +127,12 @@ impl<E: Into<Error>> MapResult<()> for Result<usize, E> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<S> MapResult<S> for Option<S> {
|
||||||
|
fn map_res(self, msg: &str) -> Result<S, Error> {
|
||||||
|
self.ok_or_else(|| Error::new(msg, ""))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn _has_source<T>(e: T) -> Option<T> {
|
fn _has_source<T>(e: T) -> Option<T> {
|
||||||
Some(e)
|
Some(e)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue