vaultwarden/src/db/models/two_factor.rs
Jeremy Lin c476e19796 Add email notifications for incomplete 2FA logins
An incomplete 2FA login is one where the correct master password was provided,
but the 2FA token or action required to complete the login was not provided
within the configured time limit. This potentially indicates that the user's
master password has been compromised, but the login was blocked by 2FA.

Be aware that the 2FA step can usually still be completed after the email
notification has already been sent out, which could be confusing. Therefore,
the incomplete 2FA time limit should be long enough that this situation would
be unlikely. This feature can also be disabled entirely if desired.
2021-10-28 00:19:43 -07:00

220 lines
7.4 KiB
Rust

use serde_json::Value;
use crate::{api::EmptyResult, db::DbConn, error::MapResult};
use super::User;
db_object! {
#[derive(Identifiable, Queryable, Insertable, Associations, AsChangeset)]
#[table_name = "twofactor"]
#[belongs_to(User, foreign_key = "user_uuid")]
#[primary_key(uuid)]
pub struct TwoFactor {
pub uuid: String,
pub user_uuid: String,
pub atype: i32,
pub enabled: bool,
pub data: String,
pub last_used: i32,
}
}
#[allow(dead_code)]
#[derive(num_derive::FromPrimitive)]
pub enum TwoFactorType {
Authenticator = 0,
Email = 1,
Duo = 2,
YubiKey = 3,
U2f = 4,
Remember = 5,
OrganizationDuo = 6,
Webauthn = 7,
// These are implementation details
U2fRegisterChallenge = 1000,
U2fLoginChallenge = 1001,
EmailVerificationChallenge = 1002,
WebauthnRegisterChallenge = 1003,
WebauthnLoginChallenge = 1004,
}
/// Local methods
impl TwoFactor {
pub fn new(user_uuid: String, atype: TwoFactorType, data: String) -> Self {
Self {
uuid: crate::util::get_uuid(),
user_uuid,
atype: atype as i32,
enabled: true,
data,
last_used: 0,
}
}
pub fn to_json(&self) -> Value {
json!({
"Enabled": self.enabled,
"Key": "", // This key and value vary
"Object": "twoFactorAuthenticator" // This value varies
})
}
pub fn to_json_provider(&self) -> Value {
json!({
"Enabled": self.enabled,
"Type": self.atype,
"Object": "twoFactorProvider"
})
}
}
/// Database methods
impl TwoFactor {
pub fn save(&self, conn: &DbConn) -> EmptyResult {
db_run! { conn:
sqlite, mysql {
match diesel::replace_into(twofactor::table)
.values(TwoFactorDb::to_db(self))
.execute(conn)
{
Ok(_) => Ok(()),
// Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first.
Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => {
diesel::update(twofactor::table)
.filter(twofactor::uuid.eq(&self.uuid))
.set(TwoFactorDb::to_db(self))
.execute(conn)
.map_res("Error saving twofactor")
}
Err(e) => Err(e.into()),
}.map_res("Error saving twofactor")
}
postgresql {
let value = TwoFactorDb::to_db(self);
// We need to make sure we're not going to violate the unique constraint on user_uuid and atype.
// This happens automatically on other DBMS backends due to replace_into(). PostgreSQL does
// not support multiple constraints on ON CONFLICT clauses.
diesel::delete(twofactor::table.filter(twofactor::user_uuid.eq(&self.user_uuid)).filter(twofactor::atype.eq(&self.atype)))
.execute(conn)
.map_res("Error deleting twofactor for insert")?;
diesel::insert_into(twofactor::table)
.values(&value)
.on_conflict(twofactor::uuid)
.do_update()
.set(&value)
.execute(conn)
.map_res("Error saving twofactor")
}
}
}
pub fn delete(self, conn: &DbConn) -> EmptyResult {
db_run! { conn: {
diesel::delete(twofactor::table.filter(twofactor::uuid.eq(self.uuid)))
.execute(conn)
.map_res("Error deleting twofactor")
}}
}
pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
db_run! { conn: {
twofactor::table
.filter(twofactor::user_uuid.eq(user_uuid))
.filter(twofactor::atype.lt(1000)) // Filter implementation types
.load::<TwoFactorDb>(conn)
.expect("Error loading twofactor")
.from_db()
}}
}
pub fn find_by_user_and_type(user_uuid: &str, atype: i32, conn: &DbConn) -> Option<Self> {
db_run! { conn: {
twofactor::table
.filter(twofactor::user_uuid.eq(user_uuid))
.filter(twofactor::atype.eq(atype))
.first::<TwoFactorDb>(conn)
.ok()
.from_db()
}}
}
pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> EmptyResult {
db_run! { conn: {
diesel::delete(twofactor::table.filter(twofactor::user_uuid.eq(user_uuid)))
.execute(conn)
.map_res("Error deleting twofactors")
}}
}
pub fn migrate_u2f_to_webauthn(conn: &DbConn) -> EmptyResult {
let u2f_factors = db_run! { conn: {
twofactor::table
.filter(twofactor::atype.eq(TwoFactorType::U2f as i32))
.load::<TwoFactorDb>(conn)
.expect("Error loading twofactor")
.from_db()
}};
use crate::api::core::two_factor::u2f::U2FRegistration;
use crate::api::core::two_factor::webauthn::{get_webauthn_registrations, WebauthnRegistration};
use std::convert::TryInto;
use webauthn_rs::proto::*;
for mut u2f in u2f_factors {
let mut regs: Vec<U2FRegistration> = serde_json::from_str(&u2f.data)?;
// If there are no registrations or they are migrated (we do the migration in batch so we can consider them all migrated when the first one is)
if regs.is_empty() || regs[0].migrated == Some(true) {
continue;
}
let (_, mut webauthn_regs) = get_webauthn_registrations(&u2f.user_uuid, conn)?;
// If the user already has webauthn registrations saved, don't overwrite them
if !webauthn_regs.is_empty() {
continue;
}
for reg in &mut regs {
let x: [u8; 32] = reg.reg.pub_key[1..33].try_into().unwrap();
let y: [u8; 32] = reg.reg.pub_key[33..65].try_into().unwrap();
let key = COSEKey {
type_: COSEAlgorithm::ES256,
key: COSEKeyType::EC_EC2(COSEEC2Key {
curve: ECDSACurve::SECP256R1,
x,
y,
}),
};
let new_reg = WebauthnRegistration {
id: reg.id,
migrated: true,
name: reg.name.clone(),
credential: Credential {
counter: reg.counter,
verified: false,
cred: key,
cred_id: reg.reg.key_handle.clone(),
registration_policy: UserVerificationPolicy::Discouraged,
},
};
webauthn_regs.push(new_reg);
reg.migrated = Some(true);
}
u2f.data = serde_json::to_string(&regs)?;
u2f.save(conn)?;
TwoFactor::new(u2f.user_uuid.clone(), TwoFactorType::Webauthn, serde_json::to_string(&webauthn_regs)?)
.save(conn)?;
}
Ok(())
}
}