| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346 |
- use chrono::Utc;
- use data_encoding::BASE64;
- use rocket::Route;
- use rocket_contrib::json::Json;
- use serde_json;
- use crate::api::{ApiResult, EmptyResult, JsonResult, JsonUpcase, PasswordData};
- use crate::auth::Headers;
- use crate::crypto;
- use crate::db::{
- models::{TwoFactor, TwoFactorType, User},
- DbConn,
- };
- use crate::error::MapResult;
- use crate::CONFIG;
- pub fn routes() -> Vec<Route> {
- routes![
- get_duo,
- activate_duo,
- activate_duo_put,
- ]
- }
- #[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";
- 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())
- }
|