From 7cb1fceecf6fc5a1d90cf196d7af54336adcce76 Mon Sep 17 00:00:00 2001 From: David Perez Date: Wed, 24 Jun 2026 14:31:49 -0500 Subject: [PATCH] PM-39510: Use V1 Set and Reset password models --- .../auth/repository/AuthRepositoryImpl.kt | 22 ++-- .../auth/repository/AuthRepositoryTest.kt | 31 +++-- .../network/model/ResetPasswordRequestJson.kt | 109 +++++++++++------- .../network/service/AccountsServiceTest.kt | 60 +++++++--- 4 files changed, 141 insertions(+), 81 deletions(-) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt index 5abf9710d73..e74d30c592d 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt @@ -109,7 +109,6 @@ import com.x8bit.bitwarden.data.auth.repository.util.activeUserIdChangesFlow import com.x8bit.bitwarden.data.auth.repository.util.policyInformation import com.x8bit.bitwarden.data.auth.repository.util.toAccountCryptographicState import com.x8bit.bitwarden.data.auth.repository.util.toDeviceInfo -import com.x8bit.bitwarden.data.auth.repository.util.toKdfRequestModel import com.x8bit.bitwarden.data.auth.repository.util.toOrganizations import com.x8bit.bitwarden.data.auth.repository.util.toRemovedPasswordUserStateJson import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams @@ -1103,13 +1102,11 @@ class AuthRepositoryImpl( ) .flatMap { response -> accountsService.resetPassword( - body = ResetPasswordRequestJson( + body = ResetPasswordRequestJson.V1( currentPasswordHash = currentPasswordHash, + newPasswordHash = response.passwordHash, passwordHint = passwordHint, - kdf = profile.toKdfRequestModel(), - salt = profile.email, - masterPasswordAuthenticationHash = response.passwordHash, - masterKeyWrappedUserKey = response.newKey, + key = response.newKey, ), ) } @@ -1168,13 +1165,16 @@ class AuthRepositoryImpl( .flatMap { response -> accountsService .setPassword( - body = SetPasswordRequestJson.V2( + body = SetPasswordRequestJson.V1( passwordHint = passwordHint, organizationIdentifier = organizationIdentifier, - kdf = profile.toKdfRequestModel(), - salt = profile.email, - masterPasswordAuthenticationHash = response.passwordHash, - masterKeyWrappedUserKey = response.newKey, + kdfIterations = profile.kdfIterations, + kdfMemory = profile.kdfMemory, + kdfParallelism = profile.kdfParallelism, + kdfType = profile.kdfType, + key = response.newKey, + passwordHash = response.passwordHash, + keys = null, ), ) .map { response } diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt index 5f5f0065ce5..0a498b5771d 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt @@ -131,7 +131,6 @@ import com.x8bit.bitwarden.data.auth.repository.util.CookieCallbackResult import com.x8bit.bitwarden.data.auth.repository.util.DuoCallbackTokenResult import com.x8bit.bitwarden.data.auth.repository.util.SsoCallbackResult import com.x8bit.bitwarden.data.auth.repository.util.WebAuthResult -import com.x8bit.bitwarden.data.auth.repository.util.toKdfRequestModel import com.x8bit.bitwarden.data.auth.repository.util.toRemovedPasswordUserStateJson import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams import com.x8bit.bitwarden.data.auth.repository.util.toUserState @@ -5059,7 +5058,6 @@ class AuthRepositoryTest { val newPassword = "newPassword" val newPasswordHash = "newPasswordHash" val newKey = "newKey" - val kdf = ACCOUNT_1.profile.toKdfRequestModel() val email = ACCOUNT_1.profile.email fakeAuthDiskSource.userState = SINGLE_USER_STATE_1 coEvery { @@ -5082,13 +5080,11 @@ class AuthRepositoryTest { .asSuccess() coEvery { accountsService.resetPassword( - body = ResetPasswordRequestJson( + body = ResetPasswordRequestJson.V1( currentPasswordHash = currentPasswordHash, + newPasswordHash = newPasswordHash, passwordHint = null, - kdf = kdf, - salt = email, - masterPasswordAuthenticationHash = newPasswordHash, - masterKeyWrappedUserKey = newKey, + key = newKey, ), ) } returns Unit.asSuccess() @@ -5123,13 +5119,11 @@ class AuthRepositoryTest { newPassword = newPassword, ) accountsService.resetPassword( - body = ResetPasswordRequestJson( + body = ResetPasswordRequestJson.V1( currentPasswordHash = currentPasswordHash, + newPasswordHash = newPasswordHash, passwordHint = null, - kdf = kdf, - salt = email, - masterPasswordAuthenticationHash = newPasswordHash, - masterKeyWrappedUserKey = newKey, + key = newKey, ), ) } @@ -5752,13 +5746,16 @@ class AuthRepositoryTest { passwordHash = passwordHash, newKey = encryptedUserKey, ) - val setPasswordRequestJson = SetPasswordRequestJson.V2( + val setPasswordRequestJson = SetPasswordRequestJson.V1( passwordHint = passwordHint, organizationIdentifier = organizationIdentifier, - kdf = profile.toKdfRequestModel(), - salt = EMAIL, - masterPasswordAuthenticationHash = passwordHash, - masterKeyWrappedUserKey = encryptedUserKey, + kdfType = profile.kdfType, + kdfIterations = profile.kdfIterations, + kdfMemory = profile.kdfMemory, + kdfParallelism = profile.kdfParallelism, + passwordHash = passwordHash, + key = encryptedUserKey, + keys = null, ) fakeAuthDiskSource.userState = userState coEvery { diff --git a/network/src/main/kotlin/com/bitwarden/network/model/ResetPasswordRequestJson.kt b/network/src/main/kotlin/com/bitwarden/network/model/ResetPasswordRequestJson.kt index 95d5a15a7fb..b23f717887c 100644 --- a/network/src/main/kotlin/com/bitwarden/network/model/ResetPasswordRequestJson.kt +++ b/network/src/main/kotlin/com/bitwarden/network/model/ResetPasswordRequestJson.kt @@ -5,45 +5,76 @@ import kotlinx.serialization.Serializable /** * Request body for resetting the password. - * - * @property currentPasswordHash The hash of the user's current password. - * @property passwordHint The hint for the master password (nullable). - * @property authenticationData The data to authenticate with a master password. - * @property unlockData The data to unlock with a master password. */ @Serializable -data class ResetPasswordRequestJson( - @SerialName("masterPasswordHash") - val currentPasswordHash: String?, - - @SerialName("masterPasswordHint") - val passwordHint: String?, - - @SerialName("authenticationData") - val authenticationData: MasterPasswordAuthenticationDataJson, - - @SerialName("unlockData") - val unlockData: MasterPasswordUnlockDataJson, -) { - constructor( - currentPasswordHash: String?, - passwordHint: String?, - kdf: KdfJson, - salt: String, - masterPasswordAuthenticationHash: String, - masterKeyWrappedUserKey: String, - ) : this( - currentPasswordHash = currentPasswordHash, - passwordHint = passwordHint, - authenticationData = MasterPasswordAuthenticationDataJson( - kdf = kdf, - salt = salt, - masterPasswordAuthenticationHash = masterPasswordAuthenticationHash, - ), - unlockData = MasterPasswordUnlockDataJson( - kdf = kdf, - salt = salt, - masterKeyWrappedUserKey = masterKeyWrappedUserKey, - ), - ) +sealed class ResetPasswordRequestJson { + abstract val currentPasswordHash: String? + + /** + * Request body for resetting the password using the V1 flow. + * + * @param currentPasswordHash The hash of the user's current password. + * @param newPasswordHash The hash of the user's new password. + * @param passwordHint The hint for the master password (nullable). + * @param key The user key for the request (encrypted). + */ + @Serializable + data class V1( + @SerialName("masterPasswordHash") + override val currentPasswordHash: String?, + + @SerialName("newMasterPasswordHash") + val newPasswordHash: String, + + @SerialName("masterPasswordHint") + val passwordHint: String?, + + @SerialName("key") + val key: String, + ) : ResetPasswordRequestJson() + + /** + * Request body for resetting the password using the V2 flow. + * + * @property currentPasswordHash The hash of the user's current password. + * @property passwordHint The hint for the master password (nullable). + * @property authenticationData The data to authenticate with a master password. + * @property unlockData The data to unlock with a master password. + */ + @Serializable + data class V2( + @SerialName("masterPasswordHash") + override val currentPasswordHash: String?, + + @SerialName("masterPasswordHint") + val passwordHint: String?, + + @SerialName("authenticationData") + val authenticationData: MasterPasswordAuthenticationDataJson, + + @SerialName("unlockData") + val unlockData: MasterPasswordUnlockDataJson, + ) : ResetPasswordRequestJson() { + constructor( + currentPasswordHash: String?, + passwordHint: String?, + kdf: KdfJson, + salt: String, + masterPasswordAuthenticationHash: String, + masterKeyWrappedUserKey: String, + ) : this( + currentPasswordHash = currentPasswordHash, + passwordHint = passwordHint, + authenticationData = MasterPasswordAuthenticationDataJson( + kdf = kdf, + salt = salt, + masterPasswordAuthenticationHash = masterPasswordAuthenticationHash, + ), + unlockData = MasterPasswordUnlockDataJson( + kdf = kdf, + salt = salt, + masterKeyWrappedUserKey = masterKeyWrappedUserKey, + ), + ) + } } diff --git a/network/src/test/kotlin/com/bitwarden/network/service/AccountsServiceTest.kt b/network/src/test/kotlin/com/bitwarden/network/service/AccountsServiceTest.kt index 646bb69fd83..c0399ca3ab6 100644 --- a/network/src/test/kotlin/com/bitwarden/network/service/AccountsServiceTest.kt +++ b/network/src/test/kotlin/com/bitwarden/network/service/AccountsServiceTest.kt @@ -166,34 +166,27 @@ class AccountsServiceTest : BaseServiceTest() { } @Test - fun `resetPassword with empty response is success`() = runTest { + fun `resetPassword with v1 request and empty response is success`() = runTest { val response = MockResponse().setBody("") server.enqueue(response) val result = service.resetPassword( - body = ResetPasswordRequestJson( + body = ResetPasswordRequestJson.V1( currentPasswordHash = "", + newPasswordHash = "", passwordHint = null, - kdf = KdfJson( - iterations = 7, - memory = 1, - parallelism = 2, - kdfType = KdfTypeJson.ARGON2_ID, - ), - salt = "", - masterPasswordAuthenticationHash = "", - masterKeyWrappedUserKey = "", + key = "", ), ) assertTrue(result.isSuccess) } @Test - fun `resetPassword with empty response and null current password is success`() = runTest { + fun `resetPassword with v2 request and empty response is success`() = runTest { val response = MockResponse().setBody("") server.enqueue(response) val result = service.resetPassword( - body = ResetPasswordRequestJson( - currentPasswordHash = null, + body = ResetPasswordRequestJson.V2( + currentPasswordHash = "", passwordHint = null, kdf = KdfJson( iterations = 7, @@ -209,6 +202,45 @@ class AccountsServiceTest : BaseServiceTest() { assertTrue(result.isSuccess) } + @Test + fun `resetPassword with v1 request and empty response and null current password is success`() = + runTest { + val response = MockResponse().setBody("") + server.enqueue(response) + val result = service.resetPassword( + body = ResetPasswordRequestJson.V1( + currentPasswordHash = null, + newPasswordHash = "", + passwordHint = null, + key = "", + ), + ) + assertTrue(result.isSuccess) + } + + @Test + fun `resetPassword with v2 request and empty response and null current password is success`() = + runTest { + val response = MockResponse().setBody("") + server.enqueue(response) + val result = service.resetPassword( + body = ResetPasswordRequestJson.V2( + currentPasswordHash = null, + passwordHint = null, + kdf = KdfJson( + iterations = 7, + memory = 1, + parallelism = 2, + kdfType = KdfTypeJson.ARGON2_ID, + ), + salt = "", + masterPasswordAuthenticationHash = "", + masterKeyWrappedUserKey = "", + ), + ) + assertTrue(result.isSuccess) + } + @Test fun `setPassword with v1 request and empty response is success`() = runTest { val response = MockResponse().setBody("")