vw_small

Hardened fork of Vaultwarden (https://github.com/dani-garcia/vaultwarden) with fewer features.
git clone https://git.philomathiclife.com/repos/vw_small
Log | Files | Refs | README

user.rs (15211B)


      1 use crate::config;
      2 use crate::crypto;
      3 use crate::util;
      4 use chrono::{NaiveDateTime, TimeDelta, Utc};
      5 use diesel::result::{self, DatabaseErrorKind};
      6 use serde_json::Value;
      7 
      8 db_object! {
      9     #[derive(Identifiable, Queryable, Insertable, AsChangeset)]
     10     #[diesel(table_name = users)]
     11     #[diesel(treat_none_as_null = true)]
     12     #[diesel(primary_key(uuid))]
     13     pub struct User {
     14         pub uuid: String,
     15         pub enabled: bool,
     16         pub created_at: NaiveDateTime,
     17         pub updated_at: NaiveDateTime,
     18         pub verified_at: Option<NaiveDateTime>,
     19         pub last_verifying_at: Option<NaiveDateTime>,
     20         login_verify_count: i32,
     21         pub email: String,
     22         pub email_new: Option<String>,
     23         pub email_new_token: Option<String>,
     24         pub name: String,
     25         pub password_hash: Vec<u8>,
     26         pub salt: Vec<u8>,
     27         password_iterations: i32,
     28         pub password_hint: Option<String>,
     29         pub akey: String,
     30         pub private_key: Option<String>,
     31         pub public_key: Option<String>,
     32         #[diesel(column_name = "totp_secret")] // Note, this is only added to the UserDb structs, not to User
     33         _totp_secret: Option<String>,
     34         pub totp_recover: Option<String>,
     35         pub security_stamp: String,
     36         pub stamp_exception: Option<String>,
     37         pub equivalent_domains: String,
     38         pub excluded_globals: String,
     39         pub client_kdf_type: i32,
     40         client_kdf_iter: i32,
     41         client_kdf_memory: Option<i32>,
     42         client_kdf_parallelism: Option<i32>,
     43         pub api_key: Option<String>,
     44         pub avatar_color: Option<String>,
     45         pub external_id: Option<String>, // Todo: Needs to be removed in the future, this is not used anymore.
     46     }
     47 }
     48 
     49 pub enum UserKdfType {
     50     Pbkdf2 = 0,
     51     Argon2id = 1,
     52 }
     53 impl From<UserKdfType> for i32 {
     54     fn from(value: UserKdfType) -> Self {
     55         match value {
     56             UserKdfType::Pbkdf2 => 0i32,
     57             UserKdfType::Argon2id => 1i32,
     58         }
     59     }
     60 }
     61 
     62 enum UserStatus {
     63     Enabled = 0,
     64     Invited = 1,
     65     _Disabled = 2,
     66 }
     67 impl From<UserStatus> for i32 {
     68     fn from(value: UserStatus) -> Self {
     69         match value {
     70             UserStatus::Enabled => 0i32,
     71             UserStatus::Invited => 1i32,
     72             UserStatus::_Disabled => 2i32,
     73         }
     74     }
     75 }
     76 
     77 #[derive(Serialize, Deserialize)]
     78 pub struct UserStampException {
     79     pub routes: Vec<String>,
     80     pub security_stamp: String,
     81     pub expire: u64,
     82 }
     83 
     84 /// Local methods
     85 impl User {
     86     pub fn client_kdf_type_default() -> i32 {
     87         i32::from(UserKdfType::Pbkdf2)
     88     }
     89     pub const CLIENT_KDF_ITER_DEFAULT: u32 = 600_000u32;
     90 
     91     pub fn new(email: &str) -> Self {
     92         let now = Utc::now().naive_utc();
     93         let email = email.to_lowercase();
     94         Self {
     95             uuid: util::get_uuid(),
     96             enabled: true,
     97             created_at: now,
     98             updated_at: now,
     99             verified_at: None,
    100             last_verifying_at: None,
    101             login_verify_count: 0,
    102             name: email.clone(),
    103             email,
    104             akey: String::new(),
    105             email_new: None,
    106             email_new_token: None,
    107             password_hash: Vec::new(),
    108             salt: crypto::get_random_bytes::<64>().to_vec(),
    109             password_iterations: i32::try_from(config::get_config().password_iterations)
    110                 .expect("overflow"),
    111             security_stamp: util::get_uuid(),
    112             stamp_exception: None,
    113             password_hint: None,
    114             private_key: None,
    115             public_key: None,
    116             _totp_secret: None,
    117             totp_recover: None,
    118             equivalent_domains: "[]".to_owned(),
    119             excluded_globals: "[]".to_owned(),
    120             client_kdf_type: Self::client_kdf_type_default(),
    121             client_kdf_iter: i32::try_from(Self::CLIENT_KDF_ITER_DEFAULT).expect("overflow"),
    122             client_kdf_memory: None,
    123             client_kdf_parallelism: None,
    124             api_key: None,
    125             avatar_color: None,
    126             external_id: None, // Todo: Needs to be removed in the future, this is not used anymore.
    127         }
    128     }
    129     pub fn login_verify_count(&self) -> u32 {
    130         u32::try_from(self.login_verify_count).expect("underflow")
    131     }
    132     pub fn set_login_verify_count(&mut self, count: u32) {
    133         self.login_verify_count = i32::try_from(count).expect("overflow");
    134     }
    135     pub fn password_iterations(&self) -> u32 {
    136         u32::try_from(self.password_iterations).expect("underflow")
    137     }
    138     pub fn set_password_iterations(&mut self, iter: u32) {
    139         self.password_iterations = i32::try_from(iter).expect("overflow");
    140     }
    141     pub fn client_kdf_iter(&self) -> u32 {
    142         u32::try_from(self.client_kdf_iter).expect("underflow")
    143     }
    144     pub fn set_client_kdf_iter(&mut self, iter: u32) {
    145         self.client_kdf_iter = i32::try_from(iter).expect("overflow");
    146     }
    147     pub fn client_kdf_memory(&self) -> Option<u32> {
    148         self.client_kdf_memory
    149             .map(|mem| u32::try_from(mem).expect("underflow"))
    150     }
    151     pub fn set_client_kdf_memory(&mut self, mem: Option<u32>) {
    152         self.client_kdf_memory = mem.map(|kdf| i32::try_from(kdf).expect("overflow"));
    153     }
    154     pub fn client_kdf_parallelism(&self) -> Option<u32> {
    155         self.client_kdf_parallelism
    156             .map(|mem| u32::try_from(mem).expect("underflow"))
    157     }
    158     pub fn set_client_kdf_parallelism(&mut self, par: Option<u32>) {
    159         self.client_kdf_parallelism = par.map(|pll| i32::try_from(pll).expect("overflow"));
    160     }
    161 
    162     pub fn check_valid_password(&self, password: &str) -> bool {
    163         crypto::verify_password_hash(
    164             password.as_bytes(),
    165             &self.salt,
    166             &self.password_hash,
    167             u32::try_from(self.password_iterations)
    168                 .expect("underflow converting password iterations into a u32"),
    169         )
    170     }
    171 
    172     pub fn check_valid_recovery_code(&self, recovery_code: &str) -> bool {
    173         self.totp_recover.as_ref().map_or(false, |totp_recover| {
    174             crypto::ct_eq(recovery_code, totp_recover.to_lowercase())
    175         })
    176     }
    177 
    178     pub fn check_valid_api_key(&self, key: &str) -> bool {
    179         matches!(self.api_key, Some(ref api_key) if crypto::ct_eq(api_key, key))
    180     }
    181 
    182     /// Set the password hash generated
    183     /// And resets the security_stamp. Based upon the allow_next_route the security_stamp will be different.
    184     ///
    185     /// # Arguments
    186     ///
    187     /// * `password` - A str which contains a hashed version of the users master password.
    188     /// * `new_key` - A String  which contains the new aKey value of the users master password.
    189     /// * `allow_next_route` - A Option<Vec<String>> with the function names of the next allowed (rocket) routes.
    190     ///                       These routes are able to use the previous stamp id for the next 2 minutes.
    191     ///                       After these 2 minutes this stamp will expire.
    192     ///
    193     pub fn set_password(
    194         &mut self,
    195         password: &str,
    196         new_key: Option<String>,
    197         reset_security_stamp: bool,
    198         allow_next_route: Option<Vec<String>>,
    199     ) {
    200         self.password_hash = crypto::hash_password(
    201             password.as_bytes(),
    202             &self.salt,
    203             u32::try_from(self.password_iterations)
    204                 .expect("underflow converting password iterations into a u32"),
    205         );
    206         if let Some(route) = allow_next_route {
    207             self.set_stamp_exception(route);
    208         }
    209         if let Some(new_key) = new_key {
    210             self.akey = new_key;
    211         }
    212         if reset_security_stamp {
    213             self.reset_security_stamp();
    214         }
    215     }
    216 
    217     pub fn reset_security_stamp(&mut self) {
    218         self.security_stamp = util::get_uuid();
    219     }
    220 
    221     /// Set the stamp_exception to only allow a subsequent request matching a specific route using the current security-stamp.
    222     ///
    223     /// # Arguments
    224     /// * `route_exception` - A Vec<String> with the function names of the next allowed (rocket) routes.
    225     ///                       These routes are able to use the previous stamp id for the next 2 minutes.
    226     ///                       After these 2 minutes this stamp will expire.
    227     ///
    228     pub fn set_stamp_exception(&mut self, route_exception: Vec<String>) {
    229         let stamp_exception = UserStampException {
    230             routes: route_exception,
    231             security_stamp: self.security_stamp.clone(),
    232             expire: u64::try_from(
    233                 Utc::now()
    234                     .checked_add_signed(
    235                         TimeDelta::try_minutes(2).expect("TimeDelta::try_minutes(2) to work"),
    236                     )
    237                     .expect("overflow")
    238                     .timestamp(),
    239             )
    240             .expect("underflow"),
    241         };
    242         self.stamp_exception = Some(serde_json::to_string(&stamp_exception).unwrap_or_default());
    243     }
    244 
    245     /// Resets the stamp_exception to prevent re-use of the previous security-stamp
    246     pub fn reset_stamp_exception(&mut self) {
    247         self.stamp_exception = None;
    248     }
    249 }
    250 
    251 use super::{
    252     Cipher, Device, Favorite, Folder, TwoFactorType, UserOrgType, UserOrganization,
    253     WebAuthnChallenge,
    254 };
    255 use crate::api::EmptyResult;
    256 use crate::db::DbConn;
    257 use crate::error::MapResult;
    258 
    259 /// Database methods
    260 impl User {
    261     pub async fn to_json(&self, conn: &DbConn) -> Value {
    262         let mut orgs_json = Vec::new();
    263         for c in UserOrganization::find_confirmed_by_user(&self.uuid, conn).await {
    264             orgs_json.push(c.to_json(conn).await);
    265         }
    266         let twofactor_enabled = TwoFactorType::has_twofactor(&self.uuid, conn)
    267             .await
    268             .expect("unable to get two factor info");
    269         // TODO: Might want to save the status field in the DB
    270         let status = if self.password_hash.is_empty() {
    271             UserStatus::Invited
    272         } else {
    273             UserStatus::Enabled
    274         };
    275         json!({
    276             "_status": i32::from(status),
    277             "id": self.uuid,
    278             "name": self.name,
    279             "email": self.email,
    280             "emailVerified": true,
    281             "premium": true,
    282             "premiumFromOrganization": false,
    283             "masterPasswordHint": self.password_hint,
    284             "culture": "en-US",
    285             "twoFactorEnabled": twofactor_enabled,
    286             "key": self.akey,
    287             "privateKey": self.private_key,
    288             "securityStamp": self.security_stamp,
    289             "organizations": orgs_json,
    290             "providers": [],
    291             "providerOrganizations": [],
    292             "forcePasswordReset": false,
    293             "avatarColor": self.avatar_color,
    294             "usesKeyConnector": false,
    295             "object": "profile",
    296         })
    297     }
    298 
    299     pub async fn save(&mut self, conn: &DbConn) -> EmptyResult {
    300         if self.email.trim().is_empty() {
    301             err!("User email can't be empty")
    302         }
    303         self.updated_at = Utc::now().naive_utc();
    304         db_run! {conn:
    305              {
    306                 match diesel::replace_into(users::table)
    307                     .values(UserDb::to_db(self))
    308                     .execute(conn)
    309                 {
    310                     Ok(_) => Ok(()),
    311                     // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first.
    312                     Err(result::Error::DatabaseError(DatabaseErrorKind::ForeignKeyViolation, _)) => {
    313                         diesel::update(users::table)
    314                             .filter(users::uuid.eq(&self.uuid))
    315                             .set(UserDb::to_db(self))
    316                             .execute(conn)
    317                             .map_res("Error saving user")
    318                     }
    319                     Err(e) => Err(e.into()),
    320                 }.map_res("Error saving user")
    321             }
    322         }
    323     }
    324 
    325     pub async fn delete(self, conn: &DbConn) -> EmptyResult {
    326         for user_org in UserOrganization::find_confirmed_by_user(&self.uuid, conn).await {
    327             if user_org.atype == UserOrgType::Owner
    328                 && UserOrganization::count_confirmed_by_org_and_type(
    329                     &user_org.org_uuid,
    330                     UserOrgType::Owner,
    331                     conn,
    332                 )
    333                 .await
    334                     <= 1
    335             {
    336                 err!("Can't delete last owner")
    337             }
    338         }
    339         UserOrganization::delete_all_by_user(&self.uuid, conn).await?;
    340         Cipher::delete_all_by_user(&self.uuid, conn).await?;
    341         Favorite::delete_all_by_user(&self.uuid, conn).await?;
    342         Folder::delete_all_by_user(&self.uuid, conn).await?;
    343         Device::delete_all_by_user(&self.uuid, conn).await?;
    344         TwoFactorType::delete_all_by_user(&self.uuid, conn).await?;
    345         WebAuthnChallenge::delete_all(&self.uuid, conn).await?;
    346         db_run! {conn: {
    347             diesel::delete(users::table.filter(users::uuid.eq(self.uuid)))
    348                 .execute(conn)
    349                 .map_res("Error deleting user")
    350         }}
    351     }
    352 
    353     pub async fn update_uuid_revision(uuid: &str, conn: &DbConn) {
    354         if let Err(e) = Self::_update_revision(uuid, &Utc::now().naive_utc(), conn).await {
    355             warn!("Failed to update revision for {}: {:#?}", uuid, e);
    356         }
    357     }
    358 
    359     pub async fn update_all_revisions(conn: &DbConn) -> EmptyResult {
    360         let updated_at = Utc::now().naive_utc();
    361         db_run! {conn: {
    362             util::retry(|| {
    363                 diesel::update(users::table)
    364                     .set(users::updated_at.eq(updated_at))
    365                     .execute(conn)
    366             }, 10)
    367             .map_res("Error updating revision date for all users")
    368         }}
    369     }
    370 
    371     pub async fn update_revision(&mut self, conn: &DbConn) -> EmptyResult {
    372         self.updated_at = Utc::now().naive_utc();
    373         Self::_update_revision(&self.uuid, &self.updated_at, conn).await
    374     }
    375 
    376     async fn _update_revision(uuid: &str, date: &NaiveDateTime, conn: &DbConn) -> EmptyResult {
    377         db_run! {conn: {
    378             util::retry(|| {
    379                 diesel::update(users::table.filter(users::uuid.eq(uuid)))
    380                     .set(users::updated_at.eq(date))
    381                     .execute(conn)
    382             }, 10)
    383             .map_res("Error updating user revision")
    384         }}
    385     }
    386 
    387     pub async fn find_by_mail(mail: &str, conn: &DbConn) -> Option<Self> {
    388         let lower_mail = mail.to_lowercase();
    389         db_run! {conn: {
    390             users::table
    391                 .filter(users::email.eq(lower_mail))
    392                 .first::<UserDb>(conn)
    393                 .ok()
    394                 .from_db()
    395         }}
    396     }
    397 
    398     pub async fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> {
    399         db_run! {conn: {
    400             users::table.filter(users::uuid.eq(uuid)).first::<UserDb>(conn).ok().from_db()
    401         }}
    402     }
    403 
    404     pub async fn get_all(conn: &DbConn) -> Vec<Self> {
    405         db_run! {conn: {
    406             users::table.load::<UserDb>(conn).expect("Error loading users").from_db()
    407         }}
    408     }
    409 
    410     pub async fn last_active(&self, conn: &DbConn) -> Option<NaiveDateTime> {
    411         match Device::find_latest_active_by_user(&self.uuid, conn).await {
    412             Some(device) => Some(device.updated_at),
    413             None => None,
    414         }
    415     }
    416 }