diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepository.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepository.kt index 0d73c8f860..d9f199778b 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepository.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepository.kt @@ -17,6 +17,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.PrevalidateSsoResult import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult import com.x8bit.bitwarden.data.auth.repository.model.ResendEmailResult import com.x8bit.bitwarden.data.auth.repository.model.ResetPasswordResult +import com.x8bit.bitwarden.data.auth.repository.model.SetPasswordResult import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult import com.x8bit.bitwarden.data.auth.repository.model.UserState import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult @@ -208,6 +209,16 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager { passwordHint: String?, ): ResetPasswordResult + /** + * Sets the user's password to [password] for the user within the given [organizationId] with + * an optional [passwordHint]. + */ + suspend fun setPassword( + organizationId: String, + password: String, + passwordHint: String?, + ): SetPasswordResult + /** * Set the value of [captchaTokenResultFlow]. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt index e5e9840804..3918930276 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt @@ -20,6 +20,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJso import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendEmailRequestJson import com.x8bit.bitwarden.data.auth.datasource.network.model.ResetPasswordRequestJson +import com.x8bit.bitwarden.data.auth.datasource.network.model.SetPasswordRequestJson import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorAuthMethod import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorDataModel import com.x8bit.bitwarden.data.auth.datasource.network.service.AccountsService @@ -45,6 +46,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.PrevalidateSsoResult import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult import com.x8bit.bitwarden.data.auth.repository.model.ResendEmailResult import com.x8bit.bitwarden.data.auth.repository.model.ResetPasswordResult +import com.x8bit.bitwarden.data.auth.repository.model.SetPasswordResult import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult import com.x8bit.bitwarden.data.auth.repository.model.UserState import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult @@ -803,6 +805,62 @@ class AuthRepositoryImpl( ) } + override suspend fun setPassword( + organizationId: String, + password: String, + passwordHint: String?, + ): SetPasswordResult { + val activeAccount = authDiskSource + .userState + ?.activeAccount + ?: return SetPasswordResult.Error + + // Update the saved master password hash. + val passwordHash = authSdkSource + .hashPassword( + email = activeAccount.profile.email, + password = password, + kdf = activeAccount.profile.toSdkParams(), + purpose = HashPurpose.SERVER_AUTHORIZATION, + ) + .getOrElse { return@setPassword SetPasswordResult.Error } + + return authSdkSource + .makeRegisterKeys( + email = activeAccount.profile.email, + password = password, + kdf = activeAccount.profile.toSdkParams(), + ) + .flatMap { keyResponse -> + accountsService.setPassword( + body = SetPasswordRequestJson( + passwordHash = passwordHash, + passwordHint = passwordHint, + organizationIdentifier = organizationId, + kdfIterations = activeAccount.profile.kdfIterations, + kdfMemory = activeAccount.profile.kdfMemory, + kdfParallelism = activeAccount.profile.kdfParallelism, + kdfType = activeAccount.profile.kdfType, + key = keyResponse.encryptedUserKey, + keys = RegisterRequestJson.Keys( + publicKey = keyResponse.keys.public, + encryptedPrivateKey = keyResponse.keys.private, + ), + ), + ) + } + .onSuccess { + authDiskSource.storeMasterPasswordHash( + userId = activeAccount.profile.userId, + passwordHash = passwordHash, + ) + } + .fold( + onFailure = { SetPasswordResult.Error }, + onSuccess = { SetPasswordResult.Success }, + ) + } + override fun setCaptchaCallbackTokenResult(tokenResult: CaptchaCallbackTokenResult) { captchaTokenChannel.trySend(tokenResult) } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/model/SetPasswordResult.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/model/SetPasswordResult.kt new file mode 100644 index 0000000000..b7c2e88c95 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/model/SetPasswordResult.kt @@ -0,0 +1,16 @@ +package com.x8bit.bitwarden.data.auth.repository.model + +/** + * Models result of setting a user's password. + */ +sealed class SetPasswordResult { + /** + * The password was set successfully. + */ + data object Success : SetPasswordResult() + + /** + * There was an error setting the password. + */ + data object Error : SetPasswordResult() +} diff --git a/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt index 066d8f56f4..f25dcba63f 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt @@ -27,6 +27,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJso import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendEmailRequestJson import com.x8bit.bitwarden.data.auth.datasource.network.model.ResetPasswordRequestJson +import com.x8bit.bitwarden.data.auth.datasource.network.model.SetPasswordRequestJson import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorAuthMethod import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorDataModel import com.x8bit.bitwarden.data.auth.datasource.network.service.AccountsService @@ -54,6 +55,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.PrevalidateSsoResult import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult import com.x8bit.bitwarden.data.auth.repository.model.ResendEmailResult import com.x8bit.bitwarden.data.auth.repository.model.ResetPasswordResult +import com.x8bit.bitwarden.data.auth.repository.model.SetPasswordResult import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult import com.x8bit.bitwarden.data.auth.repository.model.UserOrganizations import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult @@ -2456,6 +2458,187 @@ class AuthRepositoryTest { } } + @Test + fun `setPassword without active account should return Error`() = runTest { + fakeAuthDiskSource.userState = null + + val result = repository.setPassword( + organizationId = "organizationId", + password = "password", + passwordHint = "passwordHint", + ) + + assertEquals(SetPasswordResult.Error, result) + fakeAuthDiskSource.assertMasterPasswordHash(userId = USER_ID_1, passwordHash = null) + } + + @Test + fun `setPassword with authSdkSource hashPassword failure should return Error`() = runTest { + val password = "password" + fakeAuthDiskSource.userState = SINGLE_USER_STATE_1 + coEvery { + authSdkSource.hashPassword( + email = EMAIL, + password = password, + kdf = SINGLE_USER_STATE_1.activeAccount.profile.toSdkParams(), + purpose = HashPurpose.SERVER_AUTHORIZATION, + ) + } returns Throwable("Fail").asFailure() + + val result = repository.setPassword( + organizationId = "organizationId", + password = password, + passwordHint = "passwordHint", + ) + + assertEquals(SetPasswordResult.Error, result) + fakeAuthDiskSource.assertMasterPasswordHash(userId = USER_ID_1, passwordHash = null) + } + + @Test + fun `setPassword with authSdkSource makeRegisterKeys failure should return Error`() = runTest { + val password = "password" + val passwordHash = "passwordHash" + val kdf = SINGLE_USER_STATE_1.activeAccount.profile.toSdkParams() + fakeAuthDiskSource.userState = SINGLE_USER_STATE_1 + coEvery { + authSdkSource.hashPassword( + email = EMAIL, + password = password, + kdf = kdf, + purpose = HashPurpose.SERVER_AUTHORIZATION, + ) + } returns passwordHash.asSuccess() + coEvery { + authSdkSource.makeRegisterKeys( + email = EMAIL, + password = password, + kdf = kdf, + ) + } returns Throwable("Fail").asFailure() + + val result = repository.setPassword( + organizationId = "organizationId", + password = password, + passwordHint = "passwordHint", + ) + + assertEquals(SetPasswordResult.Error, result) + fakeAuthDiskSource.assertMasterPasswordHash(userId = USER_ID_1, passwordHash = null) + } + + @Test + fun `setPassword with accountsService setPassword failure should return Error`() = runTest { + val password = "password" + val passwordHash = "passwordHash" + val passwordHint = "passwordHint" + val organizationId = "organizationIdentifier" + val encryptedUserKey = "encryptedUserKey" + val privateRsaKey = "privateRsaKey" + val publicRsaKey = "publicRsaKey" + val profile = SINGLE_USER_STATE_1.activeAccount.profile + val kdf = profile.toSdkParams() + val registerKeyResponse = RegisterKeyResponse( + masterPasswordHash = passwordHash, + encryptedUserKey = encryptedUserKey, + keys = RsaKeyPair(public = publicRsaKey, private = privateRsaKey), + ) + val setPasswordRequestJson = SetPasswordRequestJson( + passwordHash = passwordHash, + passwordHint = passwordHint, + organizationIdentifier = organizationId, + kdfIterations = profile.kdfIterations, + kdfMemory = profile.kdfMemory, + kdfParallelism = profile.kdfParallelism, + kdfType = profile.kdfType, + key = encryptedUserKey, + keys = RegisterRequestJson.Keys( + publicKey = publicRsaKey, + encryptedPrivateKey = privateRsaKey, + ), + ) + fakeAuthDiskSource.userState = SINGLE_USER_STATE_1 + coEvery { + authSdkSource.hashPassword( + email = EMAIL, + password = password, + kdf = kdf, + purpose = HashPurpose.SERVER_AUTHORIZATION, + ) + } returns passwordHash.asSuccess() + coEvery { + authSdkSource.makeRegisterKeys(email = EMAIL, password = password, kdf = kdf) + } returns registerKeyResponse.asSuccess() + coEvery { + accountsService.setPassword(body = setPasswordRequestJson) + } returns Throwable("Fail").asFailure() + + val result = repository.setPassword( + organizationId = organizationId, + password = password, + passwordHint = passwordHint, + ) + + assertEquals(SetPasswordResult.Error, result) + fakeAuthDiskSource.assertMasterPasswordHash(userId = USER_ID_1, passwordHash = null) + } + + @Test + fun `setPassword with accountsService setPassword success should return Success`() = runTest { + val password = "password" + val passwordHash = "passwordHash" + val passwordHint = "passwordHint" + val organizationId = "organizationIdentifier" + val encryptedUserKey = "encryptedUserKey" + val privateRsaKey = "privateRsaKey" + val publicRsaKey = "publicRsaKey" + val profile = SINGLE_USER_STATE_1.activeAccount.profile + val kdf = profile.toSdkParams() + val registerKeyResponse = RegisterKeyResponse( + masterPasswordHash = passwordHash, + encryptedUserKey = encryptedUserKey, + keys = RsaKeyPair(public = publicRsaKey, private = privateRsaKey), + ) + val setPasswordRequestJson = SetPasswordRequestJson( + passwordHash = passwordHash, + passwordHint = passwordHint, + organizationIdentifier = organizationId, + kdfIterations = profile.kdfIterations, + kdfMemory = profile.kdfMemory, + kdfParallelism = profile.kdfParallelism, + kdfType = profile.kdfType, + key = encryptedUserKey, + keys = RegisterRequestJson.Keys( + publicKey = publicRsaKey, + encryptedPrivateKey = privateRsaKey, + ), + ) + fakeAuthDiskSource.userState = SINGLE_USER_STATE_1 + coEvery { + authSdkSource.hashPassword( + email = EMAIL, + password = password, + kdf = kdf, + purpose = HashPurpose.SERVER_AUTHORIZATION, + ) + } returns passwordHash.asSuccess() + coEvery { + authSdkSource.makeRegisterKeys(email = EMAIL, password = password, kdf = kdf) + } returns registerKeyResponse.asSuccess() + coEvery { + accountsService.setPassword(body = setPasswordRequestJson) + } returns Unit.asSuccess() + + val result = repository.setPassword( + organizationId = organizationId, + password = password, + passwordHint = passwordHint, + ) + + assertEquals(SetPasswordResult.Success, result) + fakeAuthDiskSource.assertMasterPasswordHash(userId = USER_ID_1, passwordHash = passwordHash) + } + @Test fun `passwordHintRequest with valid email should return Success`() = runTest { val email = "valid@example.com"