PM-45875: chore: Update key connector API to use salt instead of email (#7499)

This commit is contained in:
David Perez
2026-10-28 17:10:13 +00:00
committed by GitHub
parent d47cf96aec
commit b51dbfed61
8 changed files with 118 additions and 63 deletions
@@ -18,7 +18,7 @@ interface KeyConnectorManager {
userId: String,
url: String,
userKeyEncrypted: String,
email: String,
salt: String,
masterPassword: String,
kdf: Kdf,
): Result<MigrateExistingUserToKeyConnectorResult>
@@ -30,7 +30,7 @@ class KeyConnectorManagerImpl(
userId: String,
url: String,
userKeyEncrypted: String,
email: String,
salt: String,
masterPassword: String,
kdf: Kdf,
): Result<MigrateExistingUserToKeyConnectorResult> =
@@ -38,7 +38,7 @@ class KeyConnectorManagerImpl(
.deriveKeyConnector(
userId = userId,
userKeyEncrypted = userKeyEncrypted,
email = email,
salt = salt,
password = masterPassword,
kdf = kdf,
)
@@ -934,11 +934,12 @@ internal class AuthRepositoryImpl(
?.profile
?: return RemovePasswordResult.Error(error = NoActiveUserException())
val userId = profile.userId
val userKey = profile
val masterPasswordUnlock = profile
.userDecryptionOptions
?.masterPasswordUnlock
?.masterKeyWrappedUserKey
?: return RemovePasswordResult.Error(error = MissingPropertyException("User Key"))
?: return RemovePasswordResult.Error(
error = MissingPropertyException("Master Password Unlock"),
)
val keyConnectorUrl = organizations
.find {
it.isKeyConnectorEnabled &&
@@ -953,10 +954,10 @@ internal class AuthRepositoryImpl(
.migrateExistingUserToKeyConnector(
userId = userId,
url = keyConnectorUrl,
userKeyEncrypted = userKey,
email = profile.email,
userKeyEncrypted = masterPasswordUnlock.masterKeyWrappedUserKey,
salt = masterPasswordUnlock.salt,
masterPassword = masterPassword,
kdf = profile.toSdkParams(),
kdf = masterPasswordUnlock.kdf.toKdf(),
)
.map { migrateResult: MigrateExistingUserToKeyConnectorResult ->
when (migrateResult) {
@@ -76,7 +76,7 @@ interface VaultSdkSource {
suspend fun deriveKeyConnector(
userId: String,
userKeyEncrypted: String,
email: String,
salt: String,
password: String,
kdf: Kdf,
): Result<DeriveKeyConnectorResult>
@@ -89,7 +89,7 @@ class VaultSdkSourceImpl(
override suspend fun deriveKeyConnector(
userId: String,
userKeyEncrypted: String,
email: String,
salt: String,
password: String,
kdf: Kdf,
): Result<DeriveKeyConnectorResult> =
@@ -102,7 +102,7 @@ class VaultSdkSourceImpl(
userKeyEncrypted = userKeyEncrypted,
password = password,
kdf = kdf,
email = email,
email = salt,
),
)
DeriveKeyConnectorResult.Success(key)
@@ -50,7 +50,7 @@ class KeyConnectorManagerTest {
vaultSdkSource.deriveKeyConnector(
userId = USER_ID,
userKeyEncrypted = ENCRYPTED_USER_KEY,
email = EMAIL,
salt = SALT,
password = MASTER_PASSWORD,
kdf = KDF,
)
@@ -60,7 +60,7 @@ class KeyConnectorManagerTest {
userId = USER_ID,
url = URL,
userKeyEncrypted = ENCRYPTED_USER_KEY,
email = EMAIL,
salt = SALT,
masterPassword = MASTER_PASSWORD,
kdf = KDF,
)
@@ -77,7 +77,7 @@ class KeyConnectorManagerTest {
vaultSdkSource.deriveKeyConnector(
userId = USER_ID,
userKeyEncrypted = ENCRYPTED_USER_KEY,
email = EMAIL,
salt = SALT,
password = MASTER_PASSWORD,
kdf = KDF,
)
@@ -90,7 +90,7 @@ class KeyConnectorManagerTest {
userId = USER_ID,
url = URL,
userKeyEncrypted = ENCRYPTED_USER_KEY,
email = EMAIL,
salt = SALT,
masterPassword = MASTER_PASSWORD,
kdf = KDF,
)
@@ -109,7 +109,7 @@ class KeyConnectorManagerTest {
vaultSdkSource.deriveKeyConnector(
userId = USER_ID,
userKeyEncrypted = ENCRYPTED_USER_KEY,
email = EMAIL,
salt = SALT,
password = MASTER_PASSWORD,
kdf = KDF,
)
@@ -119,7 +119,7 @@ class KeyConnectorManagerTest {
userId = USER_ID,
url = URL,
userKeyEncrypted = ENCRYPTED_USER_KEY,
email = EMAIL,
salt = SALT,
masterPassword = MASTER_PASSWORD,
kdf = KDF,
)
@@ -139,7 +139,7 @@ class KeyConnectorManagerTest {
vaultSdkSource.deriveKeyConnector(
userId = USER_ID,
userKeyEncrypted = ENCRYPTED_USER_KEY,
email = EMAIL,
salt = SALT,
password = MASTER_PASSWORD,
kdf = KDF,
)
@@ -153,7 +153,7 @@ class KeyConnectorManagerTest {
userId = USER_ID,
url = URL,
userKeyEncrypted = ENCRYPTED_USER_KEY,
email = EMAIL,
salt = SALT,
masterPassword = MASTER_PASSWORD,
kdf = KDF,
)
@@ -170,7 +170,7 @@ class KeyConnectorManagerTest {
vaultSdkSource.deriveKeyConnector(
userId = USER_ID,
userKeyEncrypted = ENCRYPTED_USER_KEY,
email = EMAIL,
salt = SALT,
password = MASTER_PASSWORD,
kdf = KDF,
)
@@ -184,7 +184,7 @@ class KeyConnectorManagerTest {
userId = USER_ID,
url = URL,
userKeyEncrypted = ENCRYPTED_USER_KEY,
email = EMAIL,
salt = SALT,
masterPassword = MASTER_PASSWORD,
kdf = KDF,
)
@@ -452,11 +452,11 @@ private const val USER_ID: String = "userId"
private const val URL: String = "www.example.com"
private const val USER_KEY: String = "userKey"
private const val ENCRYPTED_USER_KEY: String = "userKeyEncrypted"
private const val EMAIL: String = "email@email.com"
private const val MASTER_PASSWORD: String = "masterPassword"
private const val MASTER_KEY: String = "masterKey"
private const val PUBLIC_KEY: String = "publicKey"
private const val PRIVATE_KEY: String = "privateKey"
private const val SALT: String = "salt"
private const val SIGNED_PUBLIC_KEY: String = "mockSignedPublicKey-1"
private const val SIGNING_KEY: String = "mockWrappedSigningKey-1"
private const val SECURITY_STATE: String = "mockSecurityState-1"
@@ -82,6 +82,7 @@ import com.x8bit.bitwarden.data.auth.datasource.disk.model.PendingAuthRequestJso
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
import com.x8bit.bitwarden.data.auth.datasource.disk.util.FakeAuthDiskSource
import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toKdf
import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toKdfRequestModel
import com.x8bit.bitwarden.data.auth.manager.AuthRequestManager
import com.x8bit.bitwarden.data.auth.manager.AuthStateManager
@@ -449,7 +450,12 @@ class AuthRepositoryTest {
val kdf = SINGLE_USER_STATE_1.activeAccount.profile.toSdkParams()
val error = Throwable("Fail")
coEvery {
authSdkSource.hashPassword(EMAIL, masterPassword, kdf, HashPurpose.SERVER_AUTHORIZATION)
authSdkSource.hashPassword(
salt = SALT,
password = masterPassword,
kdf = kdf,
purpose = HashPurpose.SERVER_AUTHORIZATION,
)
} returns error.asFailure()
val result = repository.deleteAccountWithMasterPassword(masterPassword = masterPassword)
@@ -459,7 +465,12 @@ class AuthRepositoryTest {
userStateManager.hasPendingAccountDeletion = true
}
coVerify {
authSdkSource.hashPassword(EMAIL, masterPassword, kdf, HashPurpose.SERVER_AUTHORIZATION)
authSdkSource.hashPassword(
salt = SALT,
password = masterPassword,
kdf = kdf,
purpose = HashPurpose.SERVER_AUTHORIZATION,
)
}
}
@@ -471,7 +482,12 @@ class AuthRepositoryTest {
val kdf = SINGLE_USER_STATE_1.activeAccount.profile.toSdkParams()
val error = Throwable("Fail")
coEvery {
authSdkSource.hashPassword(EMAIL, masterPassword, kdf, HashPurpose.SERVER_AUTHORIZATION)
authSdkSource.hashPassword(
salt = SALT,
password = masterPassword,
kdf = kdf,
purpose = HashPurpose.SERVER_AUTHORIZATION,
)
} returns hashedMasterPassword.asSuccess()
coEvery {
accountsService.deleteAccount(
@@ -487,7 +503,12 @@ class AuthRepositoryTest {
userStateManager.hasPendingAccountDeletion = true
}
coVerify {
authSdkSource.hashPassword(EMAIL, masterPassword, kdf, HashPurpose.SERVER_AUTHORIZATION)
authSdkSource.hashPassword(
salt = SALT,
password = masterPassword,
kdf = kdf,
purpose = HashPurpose.SERVER_AUTHORIZATION,
)
accountsService.deleteAccount(
masterPasswordHash = hashedMasterPassword,
oneTimePassword = null,
@@ -502,7 +523,12 @@ class AuthRepositoryTest {
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
val kdf = SINGLE_USER_STATE_1.activeAccount.profile.toSdkParams()
coEvery {
authSdkSource.hashPassword(EMAIL, masterPassword, kdf, HashPurpose.SERVER_AUTHORIZATION)
authSdkSource.hashPassword(
salt = SALT,
password = masterPassword,
kdf = kdf,
purpose = HashPurpose.SERVER_AUTHORIZATION,
)
} returns hashedMasterPassword.asSuccess()
coEvery {
accountsService.deleteAccount(
@@ -521,7 +547,12 @@ class AuthRepositoryTest {
userStateManager.hasPendingAccountDeletion = true
}
coVerify {
authSdkSource.hashPassword(EMAIL, masterPassword, kdf, HashPurpose.SERVER_AUTHORIZATION)
authSdkSource.hashPassword(
salt = SALT,
password = masterPassword,
kdf = kdf,
purpose = HashPurpose.SERVER_AUTHORIZATION,
)
accountsService.deleteAccount(
masterPasswordHash = hashedMasterPassword,
oneTimePassword = null,
@@ -4992,7 +5023,7 @@ class AuthRepositoryTest {
}
@Test
fun `removePassword with no masterKeyWrappedUserKey should return error`() = runTest {
fun `removePassword with no userDecryptionOptions should return error`() = runTest {
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1.copy(
accounts = mapOf(
USER_ID_1 to ACCOUNT_1.copy(
@@ -5004,7 +5035,7 @@ class AuthRepositoryTest {
val result = repository.removePassword(masterPassword = PASSWORD)
assertEquals(
RemovePasswordResult.Error(error = MissingPropertyException("User Key")),
RemovePasswordResult.Error(error = MissingPropertyException("Master Password Unlock")),
result,
)
}
@@ -5044,15 +5075,16 @@ class AuthRepositoryTest {
keyConnectorUrl = url,
),
)
val masterPasswordUnlock = PROFILE_1.userDecryptionOptions?.masterPasswordUnlock!!
every { organizationManager.organizations } returns organizations
coEvery {
keyConnectorManager.migrateExistingUserToKeyConnector(
userId = USER_ID_1,
url = url,
userKeyEncrypted = ENCRYPTED_USER_KEY,
email = PROFILE_1.email,
userKeyEncrypted = masterPasswordUnlock.masterKeyWrappedUserKey,
salt = masterPasswordUnlock.salt,
masterPassword = PASSWORD,
kdf = PROFILE_1.toSdkParams(),
kdf = masterPasswordUnlock.kdf.toKdf(),
)
} returns error.asFailure()
@@ -5076,15 +5108,16 @@ class AuthRepositoryTest {
keyConnectorUrl = url,
),
)
val masterPasswordUnlock = PROFILE_1.userDecryptionOptions?.masterPasswordUnlock!!
every { organizationManager.organizations } returns organizations
coEvery {
keyConnectorManager.migrateExistingUserToKeyConnector(
userId = USER_ID_1,
url = url,
userKeyEncrypted = ENCRYPTED_USER_KEY,
email = PROFILE_1.email,
userKeyEncrypted = masterPasswordUnlock.masterKeyWrappedUserKey,
salt = masterPasswordUnlock.salt,
masterPassword = PASSWORD,
kdf = PROFILE_1.toSdkParams(),
kdf = masterPasswordUnlock.kdf.toKdf(),
)
} returns expectedResult.asSuccess()
@@ -5111,15 +5144,16 @@ class AuthRepositoryTest {
keyConnectorUrl = url,
),
)
val masterPasswordUnlock = PROFILE_1.userDecryptionOptions?.masterPasswordUnlock!!
every { organizationManager.organizations } returns organizations
coEvery {
keyConnectorManager.migrateExistingUserToKeyConnector(
userId = USER_ID_1,
url = url,
userKeyEncrypted = ENCRYPTED_USER_KEY,
email = PROFILE_1.email,
userKeyEncrypted = masterPasswordUnlock.masterKeyWrappedUserKey,
salt = masterPasswordUnlock.salt,
masterPassword = PASSWORD,
kdf = PROFILE_1.toSdkParams(),
kdf = masterPasswordUnlock.kdf.toKdf(),
)
} returns expectedResult.asSuccess()
@@ -5136,6 +5170,7 @@ class AuthRepositoryTest {
fun `removePassword with migrateExistingUserToKeyConnector success should sync and return success`() =
runTest {
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
val masterPasswordUnlock = PROFILE_1.userDecryptionOptions?.masterPasswordUnlock!!
val url = "www.example.com"
val organizations = listOf(
createMockOrganization(
@@ -5150,10 +5185,10 @@ class AuthRepositoryTest {
keyConnectorManager.migrateExistingUserToKeyConnector(
userId = USER_ID_1,
url = url,
userKeyEncrypted = ENCRYPTED_USER_KEY,
email = PROFILE_1.email,
userKeyEncrypted = masterPasswordUnlock.masterKeyWrappedUserKey,
salt = masterPasswordUnlock.salt,
masterPassword = PASSWORD,
kdf = PROFILE_1.toSdkParams(),
kdf = masterPasswordUnlock.kdf.toKdf(),
)
} returns MigrateExistingUserToKeyConnectorResult.Success.asSuccess()
every {
@@ -5294,7 +5329,7 @@ class AuthRepositoryTest {
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
coEvery {
authSdkSource.hashPassword(
salt = EMAIL,
salt = SALT,
password = currentPassword,
kdf = ACCOUNT_1.profile.toSdkParams(),
purpose = HashPurpose.SERVER_AUTHORIZATION,
@@ -5319,7 +5354,7 @@ class AuthRepositoryTest {
)
coVerify {
authSdkSource.hashPassword(
salt = EMAIL,
salt = SALT,
password = currentPassword,
kdf = ACCOUNT_1.profile.toSdkParams(),
purpose = HashPurpose.SERVER_AUTHORIZATION,
@@ -6037,7 +6072,7 @@ class AuthRepositoryTest {
organizationIdentifier = organizationIdentifier,
passwordHint = passwordHint,
kdf = profile.toKdfRequestModel(),
salt = profile.email,
salt = SALT,
masterPasswordAuthenticationHash = passwordHash,
masterKeyWrappedUserKey = encryptedUserKey,
keys = KeysJson(
@@ -6070,7 +6105,13 @@ class AuthRepositoryTest {
privateKey = privateRsaKey,
),
)
fakeAuthDiskSource.assertUserState(SINGLE_USER_STATE_1)
fakeAuthDiskSource.assertUserState(
SINGLE_USER_STATE_1.copy(
accounts = mapOf(
USER_ID_1 to ACCOUNT_1.copy(profile = PROFILE_1_WITH_EMAIL_SALT),
),
),
)
coVerify(exactly = 1) {
authSdkSource.makeRegisterKeys(email = EMAIL, password = password, kdf = kdf)
accountsService.setPassword(body = setPasswordRequestJson)
@@ -7523,6 +7564,19 @@ private val BASE_PROFILE_1 = AccountJson.Profile(
)
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 = SALT,
),
),
)
private val PROFILE_1_WITH_EMAIL_SALT = BASE_PROFILE_1.copy(
userDecryptionOptions = UserDecryptionOptionsJson(
hasMasterPassword = true,
trustedDeviceUserDecryptionOptions = null,
@@ -210,7 +210,7 @@ class VaultSdkSourceTest {
runBlocking {
val userId = "userId"
val userKeyEncrypted = "userKeyEncrypted"
val email = "email"
val salt = "salt"
val password = "password"
val expectedResult = "expectedResult"
val kdf = mockk<Kdf>()
@@ -218,7 +218,7 @@ class VaultSdkSourceTest {
clientCrypto.deriveKeyConnector(
request = DeriveKeyConnectorRequest(
userKeyEncrypted = userKeyEncrypted,
email = email,
email = salt,
password = password,
kdf = kdf,
),
@@ -227,7 +227,7 @@ class VaultSdkSourceTest {
val result = vaultSdkSource.deriveKeyConnector(
userId = userId,
userKeyEncrypted = userKeyEncrypted,
email = email,
salt = salt,
password = password,
kdf = kdf,
)
@@ -240,7 +240,7 @@ class VaultSdkSourceTest {
clientCrypto.deriveKeyConnector(
request = DeriveKeyConnectorRequest(
userKeyEncrypted = userKeyEncrypted,
email = email,
email = salt,
password = password,
kdf = kdf,
),
@@ -254,14 +254,14 @@ class VaultSdkSourceTest {
runBlocking {
val userId = "userId"
val userKeyEncrypted = "userKeyEncrypted"
val email = "email"
val salt = "salt"
val password = "password"
val kdf = mockk<Kdf>()
coEvery {
clientCrypto.deriveKeyConnector(
request = DeriveKeyConnectorRequest(
userKeyEncrypted = userKeyEncrypted,
email = email,
email = salt,
password = password,
kdf = kdf,
),
@@ -272,7 +272,7 @@ class VaultSdkSourceTest {
val result = vaultSdkSource.deriveKeyConnector(
userId = userId,
userKeyEncrypted = userKeyEncrypted,
email = email,
salt = salt,
password = password,
kdf = kdf,
)
@@ -285,7 +285,7 @@ class VaultSdkSourceTest {
clientCrypto.deriveKeyConnector(
request = DeriveKeyConnectorRequest(
userKeyEncrypted = userKeyEncrypted,
email = email,
email = salt,
password = password,
kdf = kdf,
),
@@ -299,7 +299,7 @@ class VaultSdkSourceTest {
runBlocking {
val userId = "userId"
val userKeyEncrypted = "userKeyEncrypted"
val email = "email"
val salt = "salt"
val password = "password"
val error = BitwardenException.DeriveKeyConnector(
v1 = DeriveKeyConnectorException.Crypto(message = "mock message"),
@@ -309,7 +309,7 @@ class VaultSdkSourceTest {
clientCrypto.deriveKeyConnector(
request = DeriveKeyConnectorRequest(
userKeyEncrypted = userKeyEncrypted,
email = email,
email = salt,
password = password,
kdf = kdf,
),
@@ -318,7 +318,7 @@ class VaultSdkSourceTest {
val result = vaultSdkSource.deriveKeyConnector(
userId = userId,
userKeyEncrypted = userKeyEncrypted,
email = email,
salt = salt,
password = password,
kdf = kdf,
)
@@ -331,7 +331,7 @@ class VaultSdkSourceTest {
clientCrypto.deriveKeyConnector(
request = DeriveKeyConnectorRequest(
userKeyEncrypted = userKeyEncrypted,
email = email,
email = salt,
password = password,
kdf = kdf,
),
@@ -344,7 +344,7 @@ class VaultSdkSourceTest {
runBlocking {
val userId = "userId"
val userKeyEncrypted = "userKeyEncrypted"
val email = "email"
val salt = "salt"
val password = "password"
val error = mockk<BitwardenException> {
every { message } returns "Other error"
@@ -354,7 +354,7 @@ class VaultSdkSourceTest {
clientCrypto.deriveKeyConnector(
request = DeriveKeyConnectorRequest(
userKeyEncrypted = userKeyEncrypted,
email = email,
email = salt,
password = password,
kdf = kdf,
),
@@ -363,7 +363,7 @@ class VaultSdkSourceTest {
val result = vaultSdkSource.deriveKeyConnector(
userId = userId,
userKeyEncrypted = userKeyEncrypted,
email = email,
salt = salt,
password = password,
kdf = kdf,
)
@@ -376,7 +376,7 @@ class VaultSdkSourceTest {
clientCrypto.deriveKeyConnector(
request = DeriveKeyConnectorRequest(
userKeyEncrypted = userKeyEncrypted,
email = email,
email = salt,
password = password,
kdf = kdf,
),