diff --git a/src/blocking.rs b/src/blocking.rs index 972ea05..14cb506 100644 --- a/src/blocking.rs +++ b/src/blocking.rs @@ -269,6 +269,17 @@ impl BlockingIronOxide { self.runtime .block_on(self.ironoxide.user_rotate_private_key(password)) } + /// See [ironoxide::user::UserOps::user_change_password](trait.UserOps.html#tymethod.user_change_password) + pub fn user_change_password( + &self, + current_password: &str, + new_password: &str, + ) -> Result { + self.runtime.block_on( + self.ironoxide + .user_change_password(current_password, new_password), + ) + } } /// Creates a tokio runtime on the current thread diff --git a/src/internal.rs b/src/internal.rs index cd6ae50..f8250b3 100644 --- a/src/internal.rs +++ b/src/internal.rs @@ -51,6 +51,7 @@ lazy_static! { pub enum RequestErrorCode { UserVerify, UserCreate, + UserUpdate, UserDeviceAdd, UserDeviceDelete, UserDeviceList, @@ -99,6 +100,7 @@ pub enum SdkOperation { UserVerify, UserGetPublicKey, UserRotatePrivateKey, + UserChangePassword, GroupList, GroupCreate, GroupGetMetadata, diff --git a/src/internal/user_api.rs b/src/internal/user_api.rs index 6e37612..51aa7c1 100644 --- a/src/internal/user_api.rs +++ b/src/internal/user_api.rs @@ -119,6 +119,8 @@ pub struct UserCreateResult { needs_rotation: bool, } +pub type UserUpdateResult = UserCreateResult; + impl UserCreateResult { /// Public key for the user /// @@ -772,6 +774,41 @@ fn gen_device_add_signature( .into() } +/// Change the password for the user +pub async fn user_change_password( + password: Password, + new_password: Password, + auth: &RequestAuth, +) -> Result { + let requests::user_get::CurrentUserResponse { + user_private_key: encrypted_priv_key, + id: curr_user_id, + .. + } = requests::user_get::get_curr_user(auth).await?; + let new_encrypted_priv_key = { + let priv_key: PrivateKey = aes::decrypt_user_master_key( + &password.0, + &aes::EncryptedMasterKey::new_from_slice(&encrypted_priv_key.0)?, + )? + .into(); + + aes::encrypt_user_master_key( + &Mutex::new(OsRng::default()), + &new_password.0, + priv_key.as_bytes(), + )? + }; + Ok( + requests::user_update::user_update( + auth, + &curr_user_id, + Some(new_encrypted_priv_key.into()), + ) + .await? + .try_into()?, + ) +} + #[cfg(test)] pub(crate) mod tests { use super::*; diff --git a/src/internal/user_api/requests.rs b/src/internal/user_api/requests.rs index 206b529..265e71a 100644 --- a/src/internal/user_api/requests.rs +++ b/src/internal/user_api/requests.rs @@ -270,6 +270,58 @@ pub mod user_create { } } +pub mod user_update { + use crate::internal::{user_api::UserCreateResult, TryInto}; + + use super::*; + + #[derive(Debug, PartialEq, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct UserUpdateResponse { + id: String, + status: usize, + segment_id: usize, + pub user_private_key: EncryptedPrivateKey, + pub user_master_public_key: PublicKey, + needs_rotation: bool, + } + + #[derive(Debug, Serialize)] + #[serde(rename_all = "camelCase")] + struct UserUpdateReq { + user_private_key: Option, + } + + pub async fn user_update( + auth: &RequestAuth, + user_id: &UserId, + encrypted_user_private_key: Option, + ) -> Result { + let req_body = UserUpdateReq { + user_private_key: encrypted_user_private_key, + }; + auth.request + .put( + &format!("users/{}", rest::url_encode(user_id.id())), + &req_body, + RequestErrorCode::UserUpdate, + AuthV2Builder::new(auth, OffsetDateTime::now_utc()), + ) + .await + } + + impl TryFrom for UserCreateResult { + type Error = IronOxideErr; + + fn try_from(resp: UserUpdateResponse) -> Result { + Ok(UserCreateResult { + user_public_key: resp.user_master_public_key.try_into()?, + needs_rotation: resp.needs_rotation, + }) + } + } +} + pub mod user_key_list { use super::*; diff --git a/src/user.rs b/src/user.rs index afeb43b..f6953f0 100644 --- a/src/user.rs +++ b/src/user.rs @@ -5,7 +5,7 @@ pub use crate::internal::user_api::{ DeviceAddResult, DeviceId, DeviceName, EncryptedPrivateKey, Jwt, JwtClaims, KeyPair, UserCreateResult, UserDevice, UserDeviceListResult, UserId, UserResult, - UserUpdatePrivateKeyResult, + UserUpdatePrivateKeyResult, UserUpdateResult, }; use crate::{ common::{PublicKey, SdkOperation}, @@ -250,6 +250,31 @@ pub trait UserOps { /// # } /// ``` async fn user_delete_device(&self, device_id: Option<&DeviceId>) -> Result; + + /// Change the password for the user + /// + /// This will result in the password that is being used to encrypt the user private key to be changed. + /// + /// # Arguments + /// `current_password` - Password to unlock the current user's private key + /// `new_password` - New password to lock the current user's private key + /// + /// # Examples + /// ``` + /// # async fn run() -> Result<(), ironoxide::IronOxideErr> { + /// # use ironoxide::prelude::*; + /// # let sdk: IronOxide = unimplemented!(); + /// let password = "foobar"; + /// let new_password = "barbaz"; + /// let change_password_result = sdk.user_change_password(password, new_password).await?; + /// # Ok(()) + /// # } + /// ``` + async fn user_change_password( + &self, + current_password: &str, + new_password: &str, + ) -> Result; } #[async_trait] @@ -351,6 +376,23 @@ impl UserOps for IronOxide { ) .await? } + + async fn user_change_password( + &self, + current_password: &str, + new_password: &str, + ) -> Result { + add_optional_timeout( + user_api::user_change_password( + current_password.try_into()?, + new_password.try_into()?, + self.device.auth(), + ), + self.config.sdk_operation_timeout, + SdkOperation::UserChangePassword, + ) + .await? + } } #[cfg(test)] diff --git a/tests/user_ops.rs b/tests/user_ops.rs index aa342ae..ed2959c 100644 --- a/tests/user_ops.rs +++ b/tests/user_ops.rs @@ -93,6 +93,57 @@ async fn user_private_key_rotation() -> Result<(), IronOxideErr> { Ok(()) } +#[tokio::test] +async fn user_change_password() -> Result<(), IronOxideErr> { + let account_id: UserId = Uuid::new_v4().to_string().try_into()?; + let first_password = "foo"; + let new_password = "bar"; + let initial_result = IronOxide::user_create( + &gen_jwt(Some(account_id.id())).0, + first_password, + &Default::default(), + None, + ) + .await?; + let device: DeviceContext = IronOxide::generate_new_device( + &gen_jwt(Some(account_id.id())).0, + first_password, + &Default::default(), + None, + ) + .await? + .into(); + let sdk = ironoxide::initialize(&device, &Default::default()).await?; + let change_passcode_result = sdk + .user_change_password(first_password, new_password) + .await?; + + assert_eq!( + initial_result.user_public_key(), + change_passcode_result.user_public_key() + ); + + //Make sure we can't add a device with the old password. + assert!(IronOxide::generate_new_device( + &gen_jwt(Some(account_id.id())).0, + first_password, + &Default::default(), + None, + ) + .await + .is_err()); + + //Make sure we can add a new device with the new password + IronOxide::generate_new_device( + &gen_jwt(Some(account_id.id())).0, + new_password, + &Default::default(), + None, + ) + .await?; + Ok(()) +} + #[tokio::test] async fn sdk_init_with_private_key_rotation() -> Result<(), IronOxideErr> { use ironoxide::InitAndRotationCheck;