Add set-password function to auth repo (#1121)

This commit is contained in:
David Perez 2024-03-11 14:35:06 -05:00 committed by Álison Fernandes
parent 2237d58ed5
commit 53c241b4d7
4 changed files with 268 additions and 0 deletions

View file

@ -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].
*/

View file

@ -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)
}

View file

@ -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()
}

View file

@ -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"