Fix Key Rotation during password change

When ticking the 'Also rotate my account's encryption key' box, the key
rotated ciphers are posted after the change of password.

During the password change the security stamp was reseted which made
the posted key's return an invalid auth. This reset is needed to prevent other clients from still being able to read/write.

This fixes this by adding a new database column which stores a stamp exception which includes the allowed route and the current security stamp before it gets reseted.
When the security stamp check fails it will check if there is a stamp exception and tries to match the route and security stamp.

Currently it only allows for one exception. But if needed we could expand it by using a Vec<UserStampException> and change the functions accordingly.

fixes #1240
This commit is contained in:
BlackDex 2020-12-14 19:58:23 +01:00
parent e38771bbbd
commit de86aa671e
13 changed files with 83 additions and 18 deletions

View File

@ -0,0 +1 @@
ALTER TABLE users ADD COLUMN stamp_exception TEXT DEFAULT NULL;

View File

@ -0,0 +1 @@
ALTER TABLE users ADD COLUMN stamp_exception TEXT DEFAULT NULL;

View File

@ -0,0 +1 @@
ALTER TABLE users ADD COLUMN stamp_exception TEXT DEFAULT NULL;

View File

@ -115,7 +115,7 @@ fn register(data: JsonUpcase<RegisterData>, conn: DbConn) -> EmptyResult {
user.client_kdf_type = client_kdf_type; user.client_kdf_type = client_kdf_type;
} }
user.set_password(&data.MasterPasswordHash); user.set_password(&data.MasterPasswordHash, None);
user.akey = data.Key; user.akey = data.Key;
// Add extra fields if present // Add extra fields if present
@ -232,7 +232,7 @@ fn post_password(data: JsonUpcase<ChangePassData>, headers: Headers, conn: DbCon
err!("Invalid password") err!("Invalid password")
} }
user.set_password(&data.NewMasterPasswordHash); user.set_password(&data.NewMasterPasswordHash, Some("post_rotatekey"));
user.akey = data.Key; user.akey = data.Key;
user.save(&conn) user.save(&conn)
} }
@ -259,7 +259,7 @@ fn post_kdf(data: JsonUpcase<ChangeKdfData>, headers: Headers, conn: DbConn) ->
user.client_kdf_iter = data.KdfIterations; user.client_kdf_iter = data.KdfIterations;
user.client_kdf_type = data.Kdf; user.client_kdf_type = data.Kdf;
user.set_password(&data.NewMasterPasswordHash); user.set_password(&data.NewMasterPasswordHash, None);
user.akey = data.Key; user.akey = data.Key;
user.save(&conn) user.save(&conn)
} }
@ -338,6 +338,7 @@ fn post_rotatekey(data: JsonUpcase<KeyData>, headers: Headers, conn: DbConn, nt:
user.akey = data.Key; user.akey = data.Key;
user.private_key = Some(data.PrivateKey); user.private_key = Some(data.PrivateKey);
user.reset_security_stamp(); user.reset_security_stamp();
user.reset_stamp_exception();
user.save(&conn) user.save(&conn)
} }
@ -445,7 +446,7 @@ fn post_email(data: JsonUpcase<ChangeEmailData>, headers: Headers, conn: DbConn)
user.email_new = None; user.email_new = None;
user.email_new_token = None; user.email_new_token = None;
user.set_password(&data.NewMasterPasswordHash); user.set_password(&data.NewMasterPasswordHash, None);
user.akey = data.Key; user.akey = data.Key;
user.save(&conn) user.save(&conn)

View File

@ -215,12 +215,10 @@ pub fn generate_admin_claims() -> AdminJWTClaims {
// //
// Bearer token authentication // Bearer token authentication
// //
use rocket::{ use rocket::request::{FromRequest, Outcome, Request};
request::{FromRequest, Request, Outcome},
};
use crate::db::{ use crate::db::{
models::{Device, User, UserOrgStatus, UserOrgType, UserOrganization, CollectionUser}, models::{CollectionUser, Device, User, UserOrgStatus, UserOrgType, UserOrganization, UserStampException},
DbConn, DbConn,
}; };
@ -298,7 +296,25 @@ impl<'a, 'r> FromRequest<'a, 'r> for Headers {
}; };
if user.security_stamp != claims.sstamp { if user.security_stamp != claims.sstamp {
err_handler!("Invalid security stamp") if let Some(stamp_exception) = user
.stamp_exception
.as_deref()
.and_then(|s| serde_json::from_str::<UserStampException>(s).ok())
{
let current_route = match request.route().and_then(|r| r.name) {
Some(name) => name,
_ => err_handler!("Error getting current route for stamp exception"),
};
// Check if both match, if not this route is not allowed with the current security stamp.
if stamp_exception.route != current_route {
err_handler!("Invalid security stamp: Current route and exception route do not match")
} else if stamp_exception.security_stamp != claims.sstamp {
err_handler!("Invalid security stamp for matched stamp exception")
}
} else {
err_handler!("Invalid security stamp")
}
} }
Outcome::Success(Headers { host, device, user }) Outcome::Success(Headers { host, device, user })
@ -423,10 +439,6 @@ impl Into<Headers> for AdminHeaders {
} }
} }
// col_id is usually the forth param ("/organizations/<org_id>/collections/<col_id>") // col_id is usually the forth param ("/organizations/<org_id>/collections/<col_id>")
// But there cloud be cases where it is located in a query value. // But there cloud be cases where it is located in a query value.
// First check the param, if this is not a valid uuid, we will try the query value. // First check the param, if this is not a valid uuid, we will try the query value.
@ -478,7 +490,7 @@ impl<'a, 'r> FromRequest<'a, 'r> for ManagerHeaders {
None => err_handler!("The current user isn't a manager for this collection"), None => err_handler!("The current user isn't a manager for this collection"),
} }
} }
}, }
_ => err_handler!("Error getting the collection id"), _ => err_handler!("Error getting the collection id"),
} }

View File

@ -18,4 +18,4 @@ pub use self::folder::{Folder, FolderCipher};
pub use self::org_policy::{OrgPolicy, OrgPolicyType}; pub use self::org_policy::{OrgPolicy, OrgPolicyType};
pub use self::organization::{Organization, UserOrgStatus, UserOrgType, UserOrganization}; pub use self::organization::{Organization, UserOrgStatus, UserOrgType, UserOrganization};
pub use self::two_factor::{TwoFactor, TwoFactorType}; pub use self::two_factor::{TwoFactor, TwoFactorType};
pub use self::user::{Invitation, User}; pub use self::user::{Invitation, User, UserStampException};

View File

@ -37,6 +37,7 @@ db_object! {
pub totp_recover: Option<String>, pub totp_recover: Option<String>,
pub security_stamp: String, pub security_stamp: String,
pub stamp_exception: Option<String>,
pub equivalent_domains: String, pub equivalent_domains: String,
pub excluded_globals: String, pub excluded_globals: String,
@ -60,6 +61,12 @@ enum UserStatus {
_Disabled = 2, _Disabled = 2,
} }
#[derive(Serialize, Deserialize)]
pub struct UserStampException {
pub route: String,
pub security_stamp: String
}
/// Local methods /// Local methods
impl User { impl User {
pub const CLIENT_KDF_TYPE_DEFAULT: i32 = 0; // PBKDF2: 0 pub const CLIENT_KDF_TYPE_DEFAULT: i32 = 0; // PBKDF2: 0
@ -88,6 +95,7 @@ impl User {
password_iterations: CONFIG.password_iterations(), password_iterations: CONFIG.password_iterations(),
security_stamp: crate::util::get_uuid(), security_stamp: crate::util::get_uuid(),
stamp_exception: None,
password_hint: None, password_hint: None,
private_key: None, private_key: None,
@ -121,14 +129,52 @@ impl User {
} }
} }
pub fn set_password(&mut self, password: &str) { /// Set the password hash generated
/// And resets the security_stamp. Based upon the allow_next_route the security_stamp will be different.
///
/// # Arguments
///
/// * `password` - A str which contains a hashed version of the users master password.
/// * `allow_next_route` - A Option<&str> with the function name of the next allowed (rocket) route.
///
pub fn set_password(&mut self, password: &str, allow_next_route: Option<&str>) {
self.password_hash = crypto::hash_password(password.as_bytes(), &self.salt, self.password_iterations as u32); self.password_hash = crypto::hash_password(password.as_bytes(), &self.salt, self.password_iterations as u32);
self.reset_security_stamp();
if let Some(route) = allow_next_route {
self.set_stamp_exception(route);
}
self.reset_security_stamp()
} }
pub fn reset_security_stamp(&mut self) { pub fn reset_security_stamp(&mut self) {
self.security_stamp = crate::util::get_uuid(); self.security_stamp = crate::util::get_uuid();
} }
/// Set the stamp_exception to only allow a subsequent request matching a specific route using the current security-stamp.
///
/// # Arguments
/// * `route_exception` - A str with the function name of the next allowed (rocket) route.
///
/// ### Future
/// In the future it could be posible that we need more of these exception routes.
/// In that case we could use an Vec<UserStampException> and add multiple exceptions.
pub fn set_stamp_exception(&mut self, route_exception: &str) {
let stamp_exception = UserStampException {
route: route_exception.to_string(),
security_stamp: self.security_stamp.to_string()
};
self.stamp_exception = Some(serde_json::to_string(&stamp_exception).unwrap_or_default());
}
/// Resets the stamp_exception to prevent re-use of the previous security-stamp
///
/// ### Future
/// In the future it could be posible that we need more of these exception routes.
/// In that case we could use an Vec<UserStampException> and add multiple exceptions.
pub fn reset_stamp_exception(&mut self) {
self.stamp_exception = None;
}
} }
use super::{Cipher, Device, Favorite, Folder, TwoFactor, UserOrgType, UserOrganization}; use super::{Cipher, Device, Favorite, Folder, TwoFactor, UserOrgType, UserOrganization};
@ -295,7 +341,7 @@ impl User {
match Device::find_latest_active_by_user(&self.uuid, conn) { match Device::find_latest_active_by_user(&self.uuid, conn) {
Some(device) => Some(device.updated_at), Some(device) => Some(device.updated_at),
None => None None => None
} }
} }
} }

View File

@ -136,6 +136,7 @@ table! {
totp_secret -> Nullable<Text>, totp_secret -> Nullable<Text>,
totp_recover -> Nullable<Text>, totp_recover -> Nullable<Text>,
security_stamp -> Text, security_stamp -> Text,
stamp_exception -> Nullable<Text>,
equivalent_domains -> Text, equivalent_domains -> Text,
excluded_globals -> Text, excluded_globals -> Text,
client_kdf_type -> Integer, client_kdf_type -> Integer,

View File

@ -136,6 +136,7 @@ table! {
totp_secret -> Nullable<Text>, totp_secret -> Nullable<Text>,
totp_recover -> Nullable<Text>, totp_recover -> Nullable<Text>,
security_stamp -> Text, security_stamp -> Text,
stamp_exception -> Nullable<Text>,
equivalent_domains -> Text, equivalent_domains -> Text,
excluded_globals -> Text, excluded_globals -> Text,
client_kdf_type -> Integer, client_kdf_type -> Integer,

View File

@ -136,6 +136,7 @@ table! {
totp_secret -> Nullable<Text>, totp_secret -> Nullable<Text>,
totp_recover -> Nullable<Text>, totp_recover -> Nullable<Text>,
security_stamp -> Text, security_stamp -> Text,
stamp_exception -> Nullable<Text>,
equivalent_domains -> Text, equivalent_domains -> Text,
excluded_globals -> Text, excluded_globals -> Text,
client_kdf_type -> Integer, client_kdf_type -> Integer,