From 878cd4a4d3a27c05e3306cf23d5a37920d313400 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa?= Date: Wed, 31 Jan 2024 13:49:07 +0100 Subject: [PATCH] Implement password update support (#566) ## Type of change ``` - [ ] Bug fix - [x] New feature development - [ ] Tech debt (refactoring, code cleanup, dependency upgrades, etc) - [ ] Build/deploy pipeline (DevOps) - [ ] Other ``` ## Objective Support re-encrypting the user key with the new password. --- crates/bitwarden-uniffi/src/crypto.rs | 15 ++- crates/bitwarden/src/mobile/client_crypto.rs | 11 +- crates/bitwarden/src/mobile/crypto.rs | 120 +++++++++++++++++++ languages/kotlin/doc.md | 12 ++ 4 files changed, 156 insertions(+), 2 deletions(-) diff --git a/crates/bitwarden-uniffi/src/crypto.rs b/crates/bitwarden-uniffi/src/crypto.rs index 92afcbb87..2d847b33d 100644 --- a/crates/bitwarden-uniffi/src/crypto.rs +++ b/crates/bitwarden-uniffi/src/crypto.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use bitwarden::mobile::crypto::{ - DerivePinKeyResponse, InitOrgCryptoRequest, InitUserCryptoRequest, + DerivePinKeyResponse, InitOrgCryptoRequest, InitUserCryptoRequest, UpdatePasswordResponse, }; use bitwarden_crypto::EncString; @@ -51,6 +51,19 @@ impl ClientCrypto { .await?) } + /// Update the user's password, which will re-encrypt the user's encryption key with the new + /// password. This returns the new encrypted user key and the new password hash. + pub async fn update_password(&self, new_password: String) -> Result { + Ok(self + .0 + .0 + .write() + .await + .crypto() + .update_password(new_password) + .await?) + } + /// Generates a PIN protected user key from the provided PIN. The result can be stored and later /// used to initialize another client instance by using the PIN and the PIN key with /// `initialize_user_crypto`. diff --git a/crates/bitwarden/src/mobile/client_crypto.rs b/crates/bitwarden/src/mobile/client_crypto.rs index 6f8887002..f6ea3346b 100644 --- a/crates/bitwarden/src/mobile/client_crypto.rs +++ b/crates/bitwarden/src/mobile/client_crypto.rs @@ -7,7 +7,8 @@ use crate::{ error::Result, mobile::crypto::{ derive_pin_key, derive_pin_user_key, get_user_encryption_key, initialize_org_crypto, - initialize_user_crypto, DerivePinKeyResponse, InitOrgCryptoRequest, InitUserCryptoRequest, + initialize_user_crypto, update_password, DerivePinKeyResponse, InitOrgCryptoRequest, + InitUserCryptoRequest, UpdatePasswordResponse, }, }; @@ -31,6 +32,14 @@ impl<'a> ClientCrypto<'a> { get_user_encryption_key(self.client).await } + #[cfg(feature = "internal")] + pub async fn update_password( + &mut self, + new_password: String, + ) -> Result { + update_password(self.client, new_password) + } + #[cfg(feature = "internal")] pub async fn derive_pin_key(&mut self, pin: String) -> Result { derive_pin_key(self.client, pin) diff --git a/crates/bitwarden/src/mobile/crypto.rs b/crates/bitwarden/src/mobile/crypto.rs index bd71af2be..9b431b4b4 100644 --- a/crates/bitwarden/src/mobile/crypto.rs +++ b/crates/bitwarden/src/mobile/crypto.rs @@ -125,6 +125,53 @@ pub async fn get_user_encryption_key(client: &mut Client) -> Result { Ok(user_key.to_base64()) } +#[cfg(feature = "internal")] +#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[cfg_attr(feature = "mobile", derive(uniffi::Record))] +pub struct UpdatePasswordResponse { + /// Hash of the new password + password_hash: String, + /// User key, encrypted with the new password + new_key: EncString, +} + +pub fn update_password( + client: &mut Client, + new_password: String, +) -> Result { + let user_key = client + .get_encryption_settings()? + .get_key(&None) + .ok_or(Error::VaultLocked)?; + + let login_method = client + .login_method + .as_ref() + .ok_or(Error::NotAuthenticated)?; + + // Derive a new master key from password + let new_master_key = match login_method { + LoginMethod::User( + UserLoginMethod::Username { email, kdf, .. } + | UserLoginMethod::ApiKey { email, kdf, .. }, + ) => MasterKey::derive(new_password.as_bytes(), email.as_bytes(), kdf)?, + _ => return Err(Error::NotAuthenticated), + }; + + let new_key = new_master_key.encrypt_user_key(user_key)?; + + let password_hash = new_master_key.derive_master_key_hash( + new_password.as_bytes(), + bitwarden_crypto::HashPurpose::ServerAuthorization, + )?; + + Ok(UpdatePasswordResponse { + password_hash, + new_key, + }) +} + #[cfg(feature = "internal")] #[derive(Serialize, Deserialize, Debug, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] @@ -194,6 +241,79 @@ mod tests { use super::*; use crate::{client::Kdf, Client}; + #[tokio::test] + async fn test_update_password() { + let mut client = Client::new(None); + + let priv_key = "2.kmLY8NJVuiKBFJtNd/ZFpA==|qOodlRXER+9ogCe3yOibRHmUcSNvjSKhdDuztLlucs10jLiNoVVVAc+9KfNErLSpx5wmUF1hBOJM8zwVPjgQTrmnNf/wuDpwiaCxNYb/0v4FygPy7ccAHK94xP1lfqq7U9+tv+/yiZSwgcT+xF0wFpoxQeNdNRFzPTuD9o4134n8bzacD9DV/WjcrXfRjbBCzzuUGj1e78+A7BWN7/5IWLz87KWk8G7O/W4+8PtEzlwkru6Wd1xO19GYU18oArCWCNoegSmcGn7w7NDEXlwD403oY8Oa7ylnbqGE28PVJx+HLPNIdSC6YKXeIOMnVs7Mctd/wXC93zGxAWD6ooTCzHSPVV50zKJmWIG2cVVUS7j35H3rGDtUHLI+ASXMEux9REZB8CdVOZMzp2wYeiOpggebJy6MKOZqPT1R3X0fqF2dHtRFPXrNsVr1Qt6bS9qTyO4ag1/BCvXF3P1uJEsI812BFAne3cYHy5bIOxuozPfipJrTb5WH35bxhElqwT3y/o/6JWOGg3HLDun31YmiZ2HScAsUAcEkA4hhoTNnqy4O2s3yVbCcR7jF7NLsbQc0MDTbnjxTdI4VnqUIn8s2c9hIJy/j80pmO9Bjxp+LQ9a2hUkfHgFhgHxZUVaeGVth8zG2kkgGdrp5VHhxMVFfvB26Ka6q6qE/UcS2lONSv+4T8niVRJz57qwctj8MNOkA3PTEfe/DP/LKMefke31YfT0xogHsLhDkx+mS8FCc01HReTjKLktk/Jh9mXwC5oKwueWWwlxI935ecn+3I2kAuOfMsgPLkoEBlwgiREC1pM7VVX1x8WmzIQVQTHd4iwnX96QewYckGRfNYWz/zwvWnjWlfcg8kRSe+68EHOGeRtC5r27fWLqRc0HNcjwpgHkI/b6czerCe8+07TWql4keJxJxhBYj3iOH7r9ZS8ck51XnOb8tGL1isimAJXodYGzakwktqHAD7MZhS+P02O+6jrg7d+yPC2ZCuS/3TOplYOCHQIhnZtR87PXTUwr83zfOwAwCyv6KP84JUQ45+DItrXLap7nOVZKQ5QxYIlbThAO6eima6Zu5XHfqGPMNWv0bLf5+vAjIa5np5DJrSwz9no/hj6CUh0iyI+SJq4RGI60lKtypMvF6MR3nHLEHOycRUQbZIyTHWl4QQLdHzuwN9lv10ouTEvNr6sFflAX2yb6w3hlCo7oBytH3rJekjb3IIOzBpeTPIejxzVlh0N9OT5MZdh4sNKYHUoWJ8mnfjdM+L4j5Q2Kgk/XiGDgEebkUxiEOQUdVpePF5uSCE+TPav/9FIRGXGiFn6NJMaU7aBsDTFBLloffFLYDpd8/bTwoSvifkj7buwLYM+h/qcnfdy5FWau1cKav+Blq/ZC0qBpo658RTC8ZtseAFDgXoQZuksM10hpP9bzD04Bx30xTGX81QbaSTNwSEEVrOtIhbDrj9OI43KH4O6zLzK+t30QxAv5zjk10RZ4+5SAdYndIlld9Y62opCfPDzRy3ubdve4ZEchpIKWTQvIxq3T5ogOhGaWBVYnkMtM2GVqvWV//46gET5SH/MdcwhACUcZ9kCpMnWH9CyyUwYvTT3UlNyV+DlS27LMPvaw7tx7qa+GfNCoCBd8S4esZpQYK/WReiS8=|pc7qpD42wxyXemdNPuwxbh8iIaryrBPu8f/DGwYdHTw="; + + let kdf = Kdf::PBKDF2 { + iterations: 100_000.try_into().unwrap(), + }; + + initialize_user_crypto( + &mut client, + InitUserCryptoRequest { + kdf_params: kdf.clone(), + email: "test@bitwarden.com".into(), + private_key: priv_key.to_owned(), + method: InitUserCryptoMethod::Password { + password: "asdfasdfasdf".into(), + user_key: "2.u2HDQ/nH2J7f5tYHctZx6Q==|NnUKODz8TPycWJA5svexe1wJIz2VexvLbZh2RDfhj5VI3wP8ZkR0Vicvdv7oJRyLI1GyaZDBCf9CTBunRTYUk39DbZl42Rb+Xmzds02EQhc=|rwuo5wgqvTJf3rgwOUfabUyzqhguMYb3sGBjOYqjevc=".into(), + }, + }, + ) + .await + .unwrap(); + + let new_password_response = update_password(&mut client, "123412341234".into()).unwrap(); + + let mut client2 = Client::new(None); + + initialize_user_crypto( + &mut client2, + InitUserCryptoRequest { + kdf_params: kdf.clone(), + email: "test@bitwarden.com".into(), + private_key: priv_key.to_owned(), + method: InitUserCryptoMethod::Password { + password: "123412341234".into(), + user_key: new_password_response.new_key.to_string(), + }, + }, + ) + .await + .unwrap(); + + let new_hash = client2 + .kdf() + .hash_password( + "test@bitwarden.com".into(), + "123412341234".into(), + kdf.clone(), + bitwarden_crypto::HashPurpose::ServerAuthorization, + ) + .await + .unwrap(); + + assert_eq!(new_hash, new_password_response.password_hash); + + assert_eq!( + client + .get_encryption_settings() + .unwrap() + .get_key(&None) + .unwrap() + .to_base64(), + client2 + .get_encryption_settings() + .unwrap() + .get_key(&None) + .unwrap() + .to_base64() + ); + } + #[tokio::test] async fn test_initialize_user_crypto_pin() { let mut client = Client::new(None); diff --git a/languages/kotlin/doc.md b/languages/kotlin/doc.md index fd98e463e..f85222738 100644 --- a/languages/kotlin/doc.md +++ b/languages/kotlin/doc.md @@ -311,6 +311,18 @@ as it can be used to decrypt all of the user's data **Output**: std::result::Result +### `update_password` + +Update the user's password, which will re-encrypt the user's encryption key with the new +password. This returns the new encrypted user key and the new password hash. + +**Arguments**: + +- self: +- new_password: String + +**Output**: std::result::Result + ### `derive_pin_key` Generates a PIN protected user key from the provided PIN. The result can be stored and later used to