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 (15182B)


      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::{Cipher, Device, Favorite, Folder, TwoFactorType, UserOrgType, UserOrganization};
    252 use crate::api::EmptyResult;
    253 use crate::db::DbConn;
    254 use crate::error::MapResult;
    255 
    256 /// Database methods
    257 impl User {
    258     pub async fn to_json(&self, conn: &DbConn) -> Value {
    259         let mut orgs_json = Vec::new();
    260         for c in UserOrganization::find_confirmed_by_user(&self.uuid, conn).await {
    261             orgs_json.push(c.to_json(conn).await);
    262         }
    263         let twofactor_enabled = TwoFactorType::has_twofactor(&self.uuid, conn)
    264             .await
    265             .expect("unable to get two factor info");
    266         // TODO: Might want to save the status field in the DB
    267         let status = if self.password_hash.is_empty() {
    268             UserStatus::Invited
    269         } else {
    270             UserStatus::Enabled
    271         };
    272         json!({
    273             "_status": i32::from(status),
    274             "id": self.uuid,
    275             "name": self.name,
    276             "email": self.email,
    277             "emailVerified": true,
    278             "premium": true,
    279             "premiumFromOrganization": false,
    280             "masterPasswordHint": self.password_hint,
    281             "culture": "en-US",
    282             "twoFactorEnabled": twofactor_enabled,
    283             "key": self.akey,
    284             "privateKey": self.private_key,
    285             "securityStamp": self.security_stamp,
    286             "organizations": orgs_json,
    287             "providers": [],
    288             "providerOrganizations": [],
    289             "forcePasswordReset": false,
    290             "avatarColor": self.avatar_color,
    291             "usesKeyConnector": false,
    292             "creationDate": util::format_date(&self.created_at),
    293             "object": "profile",
    294         })
    295     }
    296 
    297     pub async fn save(&mut self, conn: &DbConn) -> EmptyResult {
    298         if self.email.trim().is_empty() {
    299             err!("User email can't be empty")
    300         }
    301         self.updated_at = Utc::now().naive_utc();
    302         db_run! {conn:
    303              {
    304                 match diesel::replace_into(users::table)
    305                     .values(UserDb::to_db(self))
    306                     .execute(conn)
    307                 {
    308                     Ok(_) => Ok(()),
    309                     // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first.
    310                     Err(result::Error::DatabaseError(DatabaseErrorKind::ForeignKeyViolation, _)) => {
    311                         diesel::update(users::table)
    312                             .filter(users::uuid.eq(&self.uuid))
    313                             .set(UserDb::to_db(self))
    314                             .execute(conn)
    315                             .map_res("Error saving user")
    316                     }
    317                     Err(e) => Err(e.into()),
    318                 }.map_res("Error saving user")
    319             }
    320         }
    321     }
    322 
    323     pub async fn delete(self, conn: &DbConn) -> EmptyResult {
    324         for user_org in UserOrganization::find_confirmed_by_user(&self.uuid, conn).await {
    325             if user_org.atype == UserOrgType::Owner
    326                 && UserOrganization::count_confirmed_by_org_and_type(
    327                     &user_org.org_uuid,
    328                     UserOrgType::Owner,
    329                     conn,
    330                 )
    331                 .await
    332                     <= 1
    333             {
    334                 err!("Can't delete last owner")
    335             }
    336         }
    337         UserOrganization::delete_all_by_user(&self.uuid, conn).await?;
    338         Cipher::delete_all_by_user(&self.uuid, conn).await?;
    339         Favorite::delete_all_by_user(&self.uuid, conn).await?;
    340         Folder::delete_all_by_user(&self.uuid, conn).await?;
    341         Device::delete_all_by_user(&self.uuid, conn).await?;
    342         TwoFactorType::delete_all_by_user(&self.uuid, conn).await?;
    343         db_run! {conn: {
    344             diesel::delete(users::table.filter(users::uuid.eq(self.uuid)))
    345                 .execute(conn)
    346                 .map_res("Error deleting user")
    347         }}
    348     }
    349 
    350     pub async fn update_uuid_revision(uuid: &str, conn: &DbConn) {
    351         if let Err(e) = Self::_update_revision(uuid, &Utc::now().naive_utc(), conn).await {
    352             warn!("Failed to update revision for {}: {:#?}", uuid, e);
    353         }
    354     }
    355 
    356     pub async fn update_all_revisions(conn: &DbConn) -> EmptyResult {
    357         let updated_at = Utc::now().naive_utc();
    358         db_run! {conn: {
    359             util::retry(|| {
    360                 diesel::update(users::table)
    361                     .set(users::updated_at.eq(updated_at))
    362                     .execute(conn)
    363             }, 10)
    364             .map_res("Error updating revision date for all users")
    365         }}
    366     }
    367 
    368     pub async fn update_revision(&mut self, conn: &DbConn) -> EmptyResult {
    369         self.updated_at = Utc::now().naive_utc();
    370         Self::_update_revision(&self.uuid, &self.updated_at, conn).await
    371     }
    372 
    373     async fn _update_revision(uuid: &str, date: &NaiveDateTime, conn: &DbConn) -> EmptyResult {
    374         db_run! {conn: {
    375             util::retry(|| {
    376                 diesel::update(users::table.filter(users::uuid.eq(uuid)))
    377                     .set(users::updated_at.eq(date))
    378                     .execute(conn)
    379             }, 10)
    380             .map_res("Error updating user revision")
    381         }}
    382     }
    383 
    384     pub async fn find_by_mail(mail: &str, conn: &DbConn) -> Option<Self> {
    385         let lower_mail = mail.to_lowercase();
    386         db_run! {conn: {
    387             users::table
    388                 .filter(users::email.eq(lower_mail))
    389                 .first::<UserDb>(conn)
    390                 .ok()
    391                 .from_db()
    392         }}
    393     }
    394 
    395     pub async fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> {
    396         db_run! {conn: {
    397             users::table.filter(users::uuid.eq(uuid)).first::<UserDb>(conn).ok().from_db()
    398         }}
    399     }
    400 
    401     pub async fn get_all(conn: &DbConn) -> Vec<Self> {
    402         db_run! {conn: {
    403             users::table.load::<UserDb>(conn).expect("Error loading users").from_db()
    404         }}
    405     }
    406 
    407     pub async fn last_active(&self, conn: &DbConn) -> Option<NaiveDateTime> {
    408         match Device::find_latest_active_by_user(&self.uuid, conn).await {
    409             Some(device) => Some(device.updated_at),
    410             None => None,
    411         }
    412     }
    413 }