From ab9042567f29ac05b0e809a60d4a3130a47e3ca0 Mon Sep 17 00:00:00 2001 From: David Perez Date: Fri, 4 Sep 2026 16:14:14 -0500 Subject: [PATCH] PM-43250: chore: Change email param on 'hashPassword' to salt --- .../data/auth/datasource/sdk/AuthSdkSource.kt | 4 +- .../auth/datasource/sdk/AuthSdkSourceImpl.kt | 5 +- .../auth/repository/AuthRepositoryImpl.kt | 8 +- .../vault/manager/VaultLockManagerImpl.kt | 2 +- .../auth/datasource/sdk/AuthSdkSourceTest.kt | 8 +- .../auth/repository/AuthRepositoryTest.kt | 664 +++++++++--------- .../vault/manager/VaultLockManagerTest.kt | 2 +- 7 files changed, 340 insertions(+), 353 deletions(-) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/datasource/sdk/AuthSdkSource.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/datasource/sdk/AuthSdkSource.kt index 6e6a9213857..11f9924a9cf 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/datasource/sdk/AuthSdkSource.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/datasource/sdk/AuthSdkSource.kt @@ -84,10 +84,10 @@ interface AuthSdkSource { ): Result /** - * Creates a hashed password provided the given [email], [password], [kdf], and [purpose]. + * Creates a hashed password provided the given [salt], [password], [kdf], and [purpose]. */ suspend fun hashPassword( - email: String, + salt: String, password: String, kdf: Kdf, purpose: HashPurpose, diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/datasource/sdk/AuthSdkSourceImpl.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/datasource/sdk/AuthSdkSourceImpl.kt index c706d9a4ef8..8d9bd776d07 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/datasource/sdk/AuthSdkSourceImpl.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/datasource/sdk/AuthSdkSourceImpl.kt @@ -152,14 +152,15 @@ class AuthSdkSourceImpl( } override suspend fun hashPassword( - email: String, + salt: String, password: String, kdf: Kdf, purpose: HashPurpose, ): Result = runCatchingWithLogs { useClient { + // Under the hood, the "email" parameter is the salt. auth().hashPassword( - email = email, + email = salt, password = password, kdfParams = kdf, purpose = purpose, 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 bd8df3cc04f..3675f59fad8 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 @@ -314,7 +314,7 @@ internal class AuthRepositoryImpl( userStateManager.hasPendingAccountDeletion = true return authSdkSource .hashPassword( - email = profile.email, + salt = profile.email, password = masterPassword, kdf = profile.toSdkParams(), purpose = HashPurpose.SERVER_AUTHORIZATION, @@ -524,7 +524,7 @@ internal class AuthRepositoryImpl( .preLogin(email = email) .flatMap { authSdkSource.hashPassword( - email = email, + salt = email, password = password, kdf = it.kdfParams.toSdkParams(), purpose = HashPurpose.SERVER_AUTHORIZATION, @@ -981,7 +981,7 @@ internal class AuthRepositoryImpl( val currentPasswordHash = currentPassword?.let { password -> authSdkSource .hashPassword( - email = profile.email, + salt = profile.email, password = password, kdf = profile.toSdkParams(), purpose = HashPurpose.SERVER_AUTHORIZATION, @@ -1705,7 +1705,7 @@ internal class AuthRepositoryImpl( // Save the master password hash. authSdkSource .hashPassword( - email = email, + salt = email, password = it, kdf = profile.toSdkParams(), purpose = HashPurpose.LOCAL_AUTHORIZATION, diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultLockManagerImpl.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultLockManagerImpl.kt index de62619dfca..b588a87dd87 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultLockManagerImpl.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultLockManagerImpl.kt @@ -273,7 +273,7 @@ internal class VaultLockManagerImpl( // Save the master password hash. authSdkSource .hashPassword( - email = email, + salt = email, password = password, kdf = kdf, purpose = HashPurpose.LOCAL_AUTHORIZATION, diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/datasource/sdk/AuthSdkSourceTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/datasource/sdk/AuthSdkSourceTest.kt index 39808b594ee..a1b8e8f2bc5 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/datasource/sdk/AuthSdkSourceTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/datasource/sdk/AuthSdkSourceTest.kt @@ -337,14 +337,14 @@ class AuthSdkSourceTest { coEvery { sdkClientManager.singleUseClient(block = capture(slot)) } coAnswers { slot.captured(client) } - val email = "email" + val salt = "salt" val password = "password" val kdf = mockk() val purpose = mockk() val expectedResult = "hashedPassword" coEvery { clientAuth.hashPassword( - email = email, + email = salt, password = password, kdfParams = kdf, purpose = purpose, @@ -352,7 +352,7 @@ class AuthSdkSourceTest { } returns expectedResult val result = authSkdSource.hashPassword( - email = email, + salt = salt, password = password, kdf = kdf, purpose = purpose, @@ -363,7 +363,7 @@ class AuthSdkSourceTest { ) coVerify { clientAuth.hashPassword( - email = email, + email = salt, password = password, kdfParams = kdf, purpose = purpose, 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 96054964d2f..cbccf5bf5f6 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 @@ -93,7 +93,6 @@ import com.x8bit.bitwarden.data.auth.manager.UserStateManager import com.x8bit.bitwarden.data.auth.manager.model.AuthRequest import com.x8bit.bitwarden.data.auth.manager.model.MigrateExistingUserToKeyConnectorResult import com.x8bit.bitwarden.data.auth.manager.model.MigrateNewUserToKeyConnectorResult -import com.x8bit.bitwarden.data.auth.repository.AuthRepositoryTest.Companion.MASTER_PASSWORD_POLICY_OPTIONS import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult import com.x8bit.bitwarden.data.auth.repository.model.DeleteAccountResult import com.x8bit.bitwarden.data.auth.repository.model.DeviceInfo @@ -203,7 +202,7 @@ class AuthRepositoryTest { coEvery { getNewAuthRequest(email = EMAIL) } returns AUTH_REQUEST_RESPONSE.asSuccess() coEvery { hashPassword( - email = EMAIL, + salt = EMAIL, password = PASSWORD, kdf = PRE_LOGIN_SUCCESS.kdfParams.toSdkParams(), purpose = HashPurpose.SERVER_AUTHORIZATION, @@ -211,7 +210,7 @@ class AuthRepositoryTest { } returns PASSWORD_HASH.asSuccess() coEvery { hashPassword( - email = EMAIL, + salt = EMAIL, password = PASSWORD, kdf = ACCOUNT_1.profile.toSdkParams(), purpose = HashPurpose.LOCAL_AUTHORIZATION, @@ -5088,7 +5087,7 @@ class AuthRepositoryTest { fakeAuthDiskSource.userState = SINGLE_USER_STATE_1 coEvery { authSdkSource.hashPassword( - email = email, + salt = email, password = currentPassword, kdf = ACCOUNT_1.profile.toSdkParams(), purpose = HashPurpose.SERVER_AUTHORIZATION, @@ -5118,7 +5117,7 @@ class AuthRepositoryTest { } returns Unit.asSuccess() coEvery { authSdkSource.hashPassword( - email = email, + salt = email, password = newPassword, kdf = ACCOUNT_1.profile.toSdkParams(), purpose = HashPurpose.LOCAL_AUTHORIZATION, @@ -5137,7 +5136,7 @@ class AuthRepositoryTest { ) coVerify { authSdkSource.hashPassword( - email = email, + salt = email, password = currentPassword, kdf = ACCOUNT_1.profile.toSdkParams(), purpose = HashPurpose.SERVER_AUTHORIZATION, @@ -5175,7 +5174,7 @@ class AuthRepositoryTest { fakeAuthDiskSource.userState = SINGLE_USER_STATE_1 coEvery { authSdkSource.hashPassword( - email = ACCOUNT_1.profile.email, + salt = EMAIL, password = currentPassword, kdf = ACCOUNT_1.profile.toSdkParams(), purpose = HashPurpose.SERVER_AUTHORIZATION, @@ -5200,7 +5199,7 @@ class AuthRepositoryTest { ) coVerify { authSdkSource.hashPassword( - email = ACCOUNT_1.profile.email, + salt = EMAIL, password = currentPassword, kdf = ACCOUNT_1.profile.toSdkParams(), purpose = HashPurpose.SERVER_AUTHORIZATION, @@ -7185,340 +7184,327 @@ class AuthRepositoryTest { continueResult, ) } +} - companion object { - private val FIXED_CLOCK: Clock = Clock.fixed( - Instant.parse("2023-10-27T12:00:00Z"), - ZoneOffset.UTC, - ) - private const val DEEPLINK_SCHEME = "https" - private const val UNIQUE_APP_ID = "testUniqueAppId" - private const val NAME = "Example Name" - private const val EMAIL = "test@bitwarden.com" - private const val EMAIL_2 = "test2@bitwarden.com" - private const val EMAIL_VERIFICATION_TOKEN = "thisisanawesometoken" - private const val PASSWORD = "password" - private const val PASSWORD_HASH = "passwordHash" - private const val ACCESS_TOKEN = "accessToken" - private const val ACCESS_TOKEN_2 = "accessToken2" - private const val REFRESH_TOKEN = "refreshToken" - private const val REFRESH_TOKEN_2 = "refreshToken2" - private const val ACCESS_TOKEN_2_EXPIRES_IN = 3600 - private const val TWO_FACTOR_CODE = "123456" - private val TWO_FACTOR_METHOD = TwoFactorAuthMethod.EMAIL - private const val TWO_FACTOR_REMEMBER = true - private val TWO_FACTOR_DATA = TwoFactorDataModel( - code = TWO_FACTOR_CODE, - method = TWO_FACTOR_METHOD.value.toString(), - remember = TWO_FACTOR_REMEMBER, - ) - private const val SSO_CODE = "ssoCode" - private const val SSO_CODE_VERIFIER = "ssoCodeVerifier" - private const val SSO_REDIRECT_URI = "bitwarden://sso-test" - private const val DEVICE_ACCESS_CODE = "accessCode" - private const val DEVICE_REQUEST_ID = "authRequestId" - private const val DEVICE_ASYMMETRICAL_KEY = "asymmetricalKey" - private const val DEVICE_REQUEST_PRIVATE_KEY = "requestPrivateKey" - - private const val DEFAULT_KDF_ITERATIONS = 600000 - private const val ENCRYPTED_USER_KEY = "encryptedUserKey" - private const val PUBLIC_KEY = "PublicKey" - private const val PRIVATE_KEY = "privateKey" - private const val USER_ID_1 = "2a135b23-e1fb-42c9-bec3-573857bc8181" - private const val USER_ID_2 = "b9d32ec0-6497-4582-9798-b350f53bfa02" - private const val ORGANIZATION_IDENTIFIER = "organizationIdentifier" - private val ACCOUNT_KEYS = createMockAccountKeysJson(number = 1) - private val ACCOUNT_KEYS_WITH_NULL_FIELDS = - createMockAccountKeysJsonWithNullFields(number = 1) - private val ACCOUNT_CRYPTOGRAPHIC_STATE_V2 = - createMockWrappedAccountCryptographicState(number = 1) - private val ACCOUNT_CRYPTOGRAPHIC_STATE_V1 = - WrappedAccountCryptographicState.V1(privateKey = "privateKey") - private val TWO_FACTOR_AUTH_METHODS_DATA = mapOf( - TwoFactorAuthMethod.EMAIL to JsonObject( - mapOf("Email" to JsonPrimitive("ex***@email.com")), - ), - TwoFactorAuthMethod.AUTHENTICATOR_APP to JsonObject(mapOf("Email" to JsonNull)), - ) - private val PRE_LOGIN_SUCCESS = PreLoginResponseJson( - kdfParams = PreLoginResponseJson.KdfParams.Pbkdf2(iterations = 1u), - ) - private val AUTH_REQUEST_RESPONSE = AuthRequestResponse( - privateKey = PRIVATE_KEY, - publicKey = PUBLIC_KEY, - accessCode = "accessCode", - fingerprint = "fingerprint", - ) - private val REFRESH_TOKEN_RESPONSE_JSON = RefreshTokenResponseJson.Success( - accessToken = ACCESS_TOKEN_2, - expiresIn = ACCESS_TOKEN_2_EXPIRES_IN, - refreshToken = REFRESH_TOKEN_2, - tokenType = "Bearer", - ) - private val TRUSTED_DEVICE_DECRYPTION_OPTIONS = TrustedDeviceUserDecryptionOptionsJson( - encryptedPrivateKey = null, - encryptedUserKey = null, - hasAdminApproval = false, - hasLoginApprovingDevice = false, - hasManageResetPasswordPermission = false, - ) - private val USER_DECRYPTION_OPTIONS = UserDecryptionOptionsJson( - hasMasterPassword = false, - trustedDeviceUserDecryptionOptions = TRUSTED_DEVICE_DECRYPTION_OPTIONS, - keyConnectorUserDecryptionOptions = null, - masterPasswordUnlock = null, - ) - - private val MASTER_PASSWORD_POLICY_OPTIONS = MasterPasswordPolicyOptionsJson( - minimumComplexity = 3, - minimumLength = 12, - shouldRequireUppercase = true, - shouldRequireLowercase = true, - shouldRequireNumbers = true, - shouldRequireSpecialCharacters = true, - shouldEnforceOnLogin = true, - ) - - /** - * The [PolicyInformation.MasterPassword] that [MASTER_PASSWORD_POLICY_OPTIONS] maps to via - * `toPolicyInformation`. - */ - private val MASTER_PASSWORD_POLICY_INFORMATION = PolicyInformation.MasterPassword( - minLength = 12, - minComplexity = 3, - requireUpper = true, - requireLower = true, - requireNumbers = true, - requireSpecial = true, - enforceOnLogin = true, - ) - - @Deprecated( - message = "Use GET_TOKEN_WITH_ACCOUNT_KEYS_RESPONSE_SUCCESS instead", - replaceWith = ReplaceWith( - expression = "GET_TOKEN_WITH_ACCOUNT_KEYS_RESPONSE_SUCCESS", - ), - ) - private val GET_TOKEN_RESPONSE_SUCCESS = GetTokenResponseJson.Success( - accessToken = ACCESS_TOKEN, - refreshToken = "refreshToken", - tokenType = "Bearer", - expiresInSeconds = 3600, - key = "key", - kdfType = KdfTypeJson.ARGON2_ID, - kdfIterations = 600000, - kdfMemory = 16, - kdfParallelism = 4, - privateKey = "privateKey", - accountKeys = null, - shouldForcePasswordReset = true, - twoFactorToken = null, - masterPasswordPolicyOptions = null, - userDecryptionOptions = null, - keyConnectorUrl = null, - ) - private val GET_TOKEN_WITH_ACCOUNT_KEYS_RESPONSE_SUCCESS = GetTokenResponseJson.Success( - accessToken = ACCESS_TOKEN, - refreshToken = "refreshToken", - tokenType = "Bearer", - expiresInSeconds = 3600, - key = "key", - kdfType = KdfTypeJson.ARGON2_ID, - kdfIterations = 600000, - kdfMemory = 16, - kdfParallelism = 4, - privateKey = "privateKey", - accountKeys = ACCOUNT_KEYS, - shouldForcePasswordReset = true, - twoFactorToken = null, - masterPasswordPolicyOptions = null, - userDecryptionOptions = UserDecryptionOptionsJson( - hasMasterPassword = true, - trustedDeviceUserDecryptionOptions = null, - keyConnectorUserDecryptionOptions = null, - masterPasswordUnlock = MasterPasswordUnlockDataJson( - kdf = KdfJson( - kdfType = KdfTypeJson.ARGON2_ID, - iterations = 600000, - memory = 16, - parallelism = 4, - ), - masterKeyWrappedUserKey = "key", - salt = "mockSalt", - ), - ), - keyConnectorUrl = null, - ) - private val BASE_PROFILE_1 = AccountJson.Profile( - userId = USER_ID_1, - email = EMAIL, - isEmailVerified = true, - name = "Bitwarden Tester", - hasPremiumPersonally = false, - hasPremiumFromOrganization = null, - stamp = null, - organizationId = null, - avatarColorHex = null, - forcePasswordResetReason = null, - kdfType = KdfTypeJson.ARGON2_ID, - kdfIterations = 600000, - kdfMemory = 16, - kdfParallelism = 4, - userDecryptionOptions = null, - isTwoFactorEnabled = false, - creationDate = Instant.parse("2024-09-13T01:00:00.00Z"), - ) - - private val PROFILE_1 = BASE_PROFILE_1.copy( - userDecryptionOptions = UserDecryptionOptionsJson( - hasMasterPassword = true, - trustedDeviceUserDecryptionOptions = null, - keyConnectorUserDecryptionOptions = null, - masterPasswordUnlock = MasterPasswordUnlockDataJson( - kdf = BASE_PROFILE_1.toSdkParams().toKdfRequestModel(), - masterKeyWrappedUserKey = ENCRYPTED_USER_KEY, - salt = EMAIL, - ), - ), - ) - private val ACCOUNT_1 = AccountJson( - profile = PROFILE_1, - settings = AccountJson.Settings( - environmentUrlData = EnvironmentUrlDataJson.DEFAULT_US, - ), - ) - private val ACCOUNT_2 = AccountJson( - profile = AccountJson.Profile( - userId = USER_ID_2, - email = EMAIL_2, - isEmailVerified = true, - name = "Bitwarden Tester 2", - hasPremiumPersonally = false, - hasPremiumFromOrganization = null, - stamp = null, - organizationId = null, - avatarColorHex = null, - forcePasswordResetReason = null, - kdfType = KdfTypeJson.PBKDF2_SHA256, - kdfIterations = 400000, - kdfMemory = null, - kdfParallelism = null, - userDecryptionOptions = null, - isTwoFactorEnabled = true, - creationDate = Instant.parse("2024-09-13T01:00:00.00Z"), +private val FIXED_CLOCK: Clock = Clock.fixed( + Instant.parse("2023-10-27T12:00:00Z"), + ZoneOffset.UTC, +) +private const val DEEPLINK_SCHEME = "https" +private const val UNIQUE_APP_ID = "testUniqueAppId" +private const val NAME = "Example Name" +private const val EMAIL = "test@bitwarden.com" +private const val EMAIL_2 = "test2@bitwarden.com" +private const val EMAIL_VERIFICATION_TOKEN = "thisisanawesometoken" +private const val SALT = "salt" +private const val PASSWORD = "password" +private const val PASSWORD_HASH = "passwordHash" +private const val ACCESS_TOKEN = "accessToken" +private const val ACCESS_TOKEN_2 = "accessToken2" +private const val REFRESH_TOKEN = "refreshToken" +private const val REFRESH_TOKEN_2 = "refreshToken2" +private const val ACCESS_TOKEN_2_EXPIRES_IN = 3600 +private const val TWO_FACTOR_CODE = "123456" +private val TWO_FACTOR_METHOD = TwoFactorAuthMethod.EMAIL +private const val TWO_FACTOR_REMEMBER = true +private val TWO_FACTOR_DATA = TwoFactorDataModel( + code = TWO_FACTOR_CODE, + method = TWO_FACTOR_METHOD.value.toString(), + remember = TWO_FACTOR_REMEMBER, +) +private const val SSO_CODE = "ssoCode" +private const val SSO_CODE_VERIFIER = "ssoCodeVerifier" +private const val SSO_REDIRECT_URI = "bitwarden://sso-test" +private const val DEVICE_ACCESS_CODE = "accessCode" +private const val DEVICE_REQUEST_ID = "authRequestId" +private const val DEVICE_ASYMMETRICAL_KEY = "asymmetricalKey" +private const val DEVICE_REQUEST_PRIVATE_KEY = "requestPrivateKey" + +private const val DEFAULT_KDF_ITERATIONS = 600000 +private const val ENCRYPTED_USER_KEY = "encryptedUserKey" +private const val PUBLIC_KEY = "PublicKey" +private const val PRIVATE_KEY = "privateKey" +private const val USER_ID_1 = "2a135b23-e1fb-42c9-bec3-573857bc8181" +private const val USER_ID_2 = "b9d32ec0-6497-4582-9798-b350f53bfa02" +private const val ORGANIZATION_IDENTIFIER = "organizationIdentifier" +private val ACCOUNT_KEYS = createMockAccountKeysJson(number = 1) +private val ACCOUNT_KEYS_WITH_NULL_FIELDS = createMockAccountKeysJsonWithNullFields(number = 1) +private val ACCOUNT_CRYPTOGRAPHIC_STATE_V2 = createMockWrappedAccountCryptographicState(number = 1) +private val ACCOUNT_CRYPTOGRAPHIC_STATE_V1 = WrappedAccountCryptographicState.V1( + privateKey = "privateKey", +) +private val TWO_FACTOR_AUTH_METHODS_DATA = mapOf( + TwoFactorAuthMethod.EMAIL to JsonObject(mapOf("Email" to JsonPrimitive("ex***@email.com"))), + TwoFactorAuthMethod.AUTHENTICATOR_APP to JsonObject(mapOf("Email" to JsonNull)), +) +private val PRE_LOGIN_SUCCESS = PreLoginResponseJson( + kdfParams = PreLoginResponseJson.KdfParams.Pbkdf2(iterations = 1u), +) +private val AUTH_REQUEST_RESPONSE = AuthRequestResponse( + privateKey = PRIVATE_KEY, + publicKey = PUBLIC_KEY, + accessCode = "accessCode", + fingerprint = "fingerprint", +) +private val REFRESH_TOKEN_RESPONSE_JSON = RefreshTokenResponseJson.Success( + accessToken = ACCESS_TOKEN_2, + expiresIn = ACCESS_TOKEN_2_EXPIRES_IN, + refreshToken = REFRESH_TOKEN_2, + tokenType = "Bearer", +) +private val TRUSTED_DEVICE_DECRYPTION_OPTIONS = TrustedDeviceUserDecryptionOptionsJson( + encryptedPrivateKey = null, + encryptedUserKey = null, + hasAdminApproval = false, + hasLoginApprovingDevice = false, + hasManageResetPasswordPermission = false, +) +private val USER_DECRYPTION_OPTIONS = UserDecryptionOptionsJson( + hasMasterPassword = false, + trustedDeviceUserDecryptionOptions = TRUSTED_DEVICE_DECRYPTION_OPTIONS, + keyConnectorUserDecryptionOptions = null, + masterPasswordUnlock = null, +) + +private val MASTER_PASSWORD_POLICY_OPTIONS = MasterPasswordPolicyOptionsJson( + minimumComplexity = 3, + minimumLength = 12, + shouldRequireUppercase = true, + shouldRequireLowercase = true, + shouldRequireNumbers = true, + shouldRequireSpecialCharacters = true, + shouldEnforceOnLogin = true, +) + +/** + * The [PolicyInformation.MasterPassword] that [MASTER_PASSWORD_POLICY_OPTIONS] maps to via + * `toPolicyInformation`. + */ +private val MASTER_PASSWORD_POLICY_INFORMATION = PolicyInformation.MasterPassword( + minLength = 12, + minComplexity = 3, + requireUpper = true, + requireLower = true, + requireNumbers = true, + requireSpecial = true, + enforceOnLogin = true, +) + +@Deprecated( + message = "Use GET_TOKEN_WITH_ACCOUNT_KEYS_RESPONSE_SUCCESS instead", + replaceWith = ReplaceWith(expression = "GET_TOKEN_WITH_ACCOUNT_KEYS_RESPONSE_SUCCESS"), +) +private val GET_TOKEN_RESPONSE_SUCCESS = GetTokenResponseJson.Success( + accessToken = ACCESS_TOKEN, + refreshToken = "refreshToken", + tokenType = "Bearer", + expiresInSeconds = 3600, + key = "key", + kdfType = KdfTypeJson.ARGON2_ID, + kdfIterations = 600000, + kdfMemory = 16, + kdfParallelism = 4, + privateKey = "privateKey", + accountKeys = null, + shouldForcePasswordReset = true, + twoFactorToken = null, + masterPasswordPolicyOptions = null, + userDecryptionOptions = null, + keyConnectorUrl = null, +) +private val GET_TOKEN_WITH_ACCOUNT_KEYS_RESPONSE_SUCCESS = GetTokenResponseJson.Success( + accessToken = ACCESS_TOKEN, + refreshToken = "refreshToken", + tokenType = "Bearer", + expiresInSeconds = 3600, + key = "key", + kdfType = KdfTypeJson.ARGON2_ID, + kdfIterations = 600000, + kdfMemory = 16, + kdfParallelism = 4, + privateKey = "privateKey", + accountKeys = ACCOUNT_KEYS, + shouldForcePasswordReset = true, + twoFactorToken = null, + masterPasswordPolicyOptions = null, + userDecryptionOptions = UserDecryptionOptionsJson( + hasMasterPassword = true, + trustedDeviceUserDecryptionOptions = null, + keyConnectorUserDecryptionOptions = null, + masterPasswordUnlock = MasterPasswordUnlockDataJson( + kdf = KdfJson( + kdfType = KdfTypeJson.ARGON2_ID, + iterations = 600000, + memory = 16, + parallelism = 4, ), - settings = AccountJson.Settings( - environmentUrlData = EnvironmentUrlDataJson.DEFAULT_EU, - ), - ) - private val SINGLE_USER_STATE_1 = UserStateJson( - activeUserId = USER_ID_1, - accounts = mapOf( - USER_ID_1 to ACCOUNT_1, - ), - ) - private val SINGLE_USER_STATE_1_WITH_PASS = UserStateJson( - activeUserId = USER_ID_1, - accounts = mapOf( - USER_ID_1 to ACCOUNT_1.copy( - profile = ACCOUNT_1.profile.copy( - userDecryptionOptions = UserDecryptionOptionsJson( - hasMasterPassword = true, - keyConnectorUserDecryptionOptions = null, - trustedDeviceUserDecryptionOptions = null, - masterPasswordUnlock = MasterPasswordUnlockDataJson( - kdf = BASE_PROFILE_1.toSdkParams().toKdfRequestModel(), - masterKeyWrappedUserKey = ENCRYPTED_USER_KEY, - salt = EMAIL, - ), - ), - ), - ), - ), - ) - - private val MOCK_MASTER_PASSWORD_UNLOCK = MasterPasswordUnlockData( - kdf = ACCOUNT_1.profile.toSdkParams(), masterKeyWrappedUserKey = "key", - salt = "mockSalt", - ) - - private val SINGLE_USER_STATE_2 = UserStateJson( - activeUserId = USER_ID_2, - accounts = mapOf( - USER_ID_2 to ACCOUNT_2, - ), - ) - private val MULTI_USER_STATE = UserStateJson( - activeUserId = USER_ID_1, - accounts = mapOf( - USER_ID_1 to ACCOUNT_1, - USER_ID_2 to ACCOUNT_2, - ), - ) - private val ACCOUNT_TOKENS_1: AccountTokensJson = AccountTokensJson( - accessToken = ACCESS_TOKEN, - refreshToken = REFRESH_TOKEN, - ) - private val ACCOUNT_TOKENS_2: AccountTokensJson = AccountTokensJson( - accessToken = ACCESS_TOKEN_2, - refreshToken = "refreshToken", - ) - private val VAULT_UNLOCK_DATA = listOf( - VaultUnlockData( - userId = USER_ID_1, - status = VaultUnlockData.Status.UNLOCKED, - ), - ) - - private val SERVER_CONFIG_DEFAULT = ServerConfig( - lastSync = 0L, - serverData = ConfigResponseJson( - type = "mockType", - version = "mockVersion", - gitHash = "mockGitHash", - server = null, - environment = ConfigResponseJson.EnvironmentJson( - cloudRegion = "mockCloudRegion", - vaultUrl = "mockVaultUrl", - apiUrl = "mockApiUrl", - identityUrl = "mockIdentityUrl", - notificationsUrl = "mockNotificationsUrl", - ssoUrl = "mockSsoUrl", - fillAssistRulesUrl = null, - ), - featureStates = emptyMap(), - communication = null, - settings = null, - ), - ) - - private val SERVER_CONFIG_UNOFFICIAL = SERVER_CONFIG_DEFAULT - .copy( - serverData = SERVER_CONFIG_DEFAULT.serverData.copy( - server = ConfigResponseJson.ServerJson( - name = "mockUnofficialServerName", - url = "mockUnofficialServerUrl", + salt = SALT, + ), + ), + keyConnectorUrl = null, +) +private val BASE_PROFILE_1 = AccountJson.Profile( + userId = USER_ID_1, + email = EMAIL, + isEmailVerified = true, + name = "Bitwarden Tester", + hasPremiumPersonally = false, + hasPremiumFromOrganization = null, + stamp = null, + organizationId = null, + avatarColorHex = null, + forcePasswordResetReason = null, + kdfType = KdfTypeJson.ARGON2_ID, + kdfIterations = 600000, + kdfMemory = 16, + kdfParallelism = 4, + userDecryptionOptions = null, + isTwoFactorEnabled = false, + creationDate = Instant.parse("2024-09-13T01:00:00.00Z"), +) + +private val PROFILE_1 = BASE_PROFILE_1.copy( + userDecryptionOptions = UserDecryptionOptionsJson( + hasMasterPassword = true, + trustedDeviceUserDecryptionOptions = null, + keyConnectorUserDecryptionOptions = null, + masterPasswordUnlock = MasterPasswordUnlockDataJson( + kdf = BASE_PROFILE_1.toSdkParams().toKdfRequestModel(), + masterKeyWrappedUserKey = ENCRYPTED_USER_KEY, + salt = EMAIL, + ), + ), +) +private val ACCOUNT_1 = AccountJson( + profile = PROFILE_1, + settings = AccountJson.Settings(environmentUrlData = EnvironmentUrlDataJson.DEFAULT_US), +) +private val ACCOUNT_2 = AccountJson( + profile = AccountJson.Profile( + userId = USER_ID_2, + email = EMAIL_2, + isEmailVerified = true, + name = "Bitwarden Tester 2", + hasPremiumPersonally = false, + hasPremiumFromOrganization = null, + stamp = null, + organizationId = null, + avatarColorHex = null, + forcePasswordResetReason = null, + kdfType = KdfTypeJson.PBKDF2_SHA256, + kdfIterations = 400000, + kdfMemory = null, + kdfParallelism = null, + userDecryptionOptions = null, + isTwoFactorEnabled = true, + creationDate = Instant.parse("2024-09-13T01:00:00.00Z"), + ), + settings = AccountJson.Settings( + environmentUrlData = EnvironmentUrlDataJson.DEFAULT_EU, + ), +) +private val SINGLE_USER_STATE_1 = UserStateJson( + activeUserId = USER_ID_1, + accounts = mapOf(USER_ID_1 to ACCOUNT_1), +) +private val SINGLE_USER_STATE_1_WITH_PASS = UserStateJson( + activeUserId = USER_ID_1, + accounts = mapOf( + USER_ID_1 to ACCOUNT_1.copy( + profile = ACCOUNT_1.profile.copy( + userDecryptionOptions = UserDecryptionOptionsJson( + hasMasterPassword = true, + keyConnectorUserDecryptionOptions = null, + trustedDeviceUserDecryptionOptions = null, + masterPasswordUnlock = MasterPasswordUnlockDataJson( + kdf = BASE_PROFILE_1.toSdkParams().toKdfRequestModel(), + masterKeyWrappedUserKey = ENCRYPTED_USER_KEY, + salt = EMAIL, ), ), - ) - - private val UPDATE_KDF_RESPONSE = UpdateKdfResponse( - masterPasswordAuthenticationData = MasterPasswordAuthenticationData( - kdf = mockk(relaxed = true), - salt = "mockSalt", - masterPasswordAuthenticationHash = "mockHash", ), - masterPasswordUnlockData = MasterPasswordUnlockData( - kdf = mockk(relaxed = true), - masterKeyWrappedUserKey = "mockKey", - salt = "mockSalt", - ), - oldMasterPasswordAuthenticationData = MasterPasswordAuthenticationData( - kdf = mockk(relaxed = true), - salt = "mockSalt", - masterPasswordAuthenticationHash = "mockHash", - ), - ) - } -} + ), + ), +) + +private val MOCK_MASTER_PASSWORD_UNLOCK = MasterPasswordUnlockData( + kdf = ACCOUNT_1.profile.toSdkParams(), + masterKeyWrappedUserKey = "key", + salt = SALT, +) + +private val SINGLE_USER_STATE_2 = UserStateJson( + activeUserId = USER_ID_2, + accounts = mapOf(USER_ID_2 to ACCOUNT_2), +) +private val MULTI_USER_STATE = UserStateJson( + activeUserId = USER_ID_1, + accounts = mapOf( + USER_ID_1 to ACCOUNT_1, + USER_ID_2 to ACCOUNT_2, + ), +) +private val ACCOUNT_TOKENS_1: AccountTokensJson = AccountTokensJson( + accessToken = ACCESS_TOKEN, + refreshToken = REFRESH_TOKEN, +) +private val ACCOUNT_TOKENS_2: AccountTokensJson = AccountTokensJson( + accessToken = ACCESS_TOKEN_2, + refreshToken = "refreshToken", +) +private val VAULT_UNLOCK_DATA = listOf( + VaultUnlockData( + userId = USER_ID_1, + status = VaultUnlockData.Status.UNLOCKED, + ), +) + +private val SERVER_CONFIG_DEFAULT = ServerConfig( + lastSync = 0L, + serverData = ConfigResponseJson( + type = "mockType", + version = "mockVersion", + gitHash = "mockGitHash", + server = null, + environment = ConfigResponseJson.EnvironmentJson( + cloudRegion = "mockCloudRegion", + vaultUrl = "mockVaultUrl", + apiUrl = "mockApiUrl", + identityUrl = "mockIdentityUrl", + notificationsUrl = "mockNotificationsUrl", + ssoUrl = "mockSsoUrl", + fillAssistRulesUrl = null, + ), + featureStates = emptyMap(), + communication = null, + settings = null, + ), +) + +private val SERVER_CONFIG_UNOFFICIAL = SERVER_CONFIG_DEFAULT.copy( + serverData = SERVER_CONFIG_DEFAULT.serverData.copy( + server = ConfigResponseJson.ServerJson( + name = "mockUnofficialServerName", + url = "mockUnofficialServerUrl", + ), + ), +) + +private val UPDATE_KDF_RESPONSE = UpdateKdfResponse( + masterPasswordAuthenticationData = MasterPasswordAuthenticationData( + kdf = mockk(relaxed = true), + salt = SALT, + masterPasswordAuthenticationHash = "mockHash", + ), + masterPasswordUnlockData = MasterPasswordUnlockData( + kdf = mockk(relaxed = true), + masterKeyWrappedUserKey = "mockKey", + salt = SALT, + ), + oldMasterPasswordAuthenticationData = MasterPasswordAuthenticationData( + kdf = mockk(relaxed = true), + salt = SALT, + masterPasswordAuthenticationHash = "mockHash", + ), +) diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultLockManagerTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultLockManagerTest.kt index fe3871e9159..87cdc371af2 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultLockManagerTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultLockManagerTest.kt @@ -82,7 +82,7 @@ class VaultLockManagerTest { private val authSdkSource: AuthSdkSource = mockk { coEvery { hashPassword( - email = MOCK_PROFILE.email, + salt = MOCK_PROFILE.email, password = "mockValue", kdf = MOCK_PROFILE.toSdkParams(), purpose = HashPurpose.LOCAL_AUTHORIZATION,