[PM-24721] Refactor AccountKeys to top-level common model (#5693)

This commit is contained in:
Patrick Honkonen 2025-08-14 14:12:03 -04:00 committed by GitHub
parent 3282992221
commit a68fd8b44f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 185 additions and 139 deletions

View File

@ -1,5 +1,6 @@
package com.x8bit.bitwarden.data.auth.datasource.disk
import com.bitwarden.network.model.AccountKeysJson
import com.bitwarden.network.model.SyncResponseJson
import com.bitwarden.network.provider.AppIdProvider
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson
@ -144,14 +145,14 @@ interface AuthDiskSource : AppIdProvider {
/**
* Returns the profile account keys for the given [userId].
*/
fun getAccountKeys(userId: String): SyncResponseJson.Profile.AccountKeys?
fun getAccountKeys(userId: String): AccountKeysJson?
/**
* Stores the profile account keys for the given [userId].
*/
fun storeAccountKeys(
userId: String,
accountKeys: SyncResponseJson.Profile.AccountKeys?,
accountKeys: AccountKeysJson?,
)
/**

View File

@ -4,6 +4,7 @@ import android.content.SharedPreferences
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
import com.bitwarden.core.data.util.decodeFromStringOrNull
import com.bitwarden.data.datasource.disk.BaseEncryptedDiskSource
import com.bitwarden.network.model.AccountKeysJson
import com.bitwarden.network.model.SyncResponseJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
@ -242,13 +243,13 @@ class AuthDiskSourceImpl(
)
}
override fun getAccountKeys(userId: String): SyncResponseJson.Profile.AccountKeys? =
override fun getAccountKeys(userId: String): AccountKeysJson? =
getEncryptedString(key = PROFILE_ACCOUNT_KEYS_KEY.appendIdentifier(userId))
?.let { json.decodeFromStringOrNull(it) }
override fun storeAccountKeys(
userId: String,
accountKeys: SyncResponseJson.Profile.AccountKeys?,
accountKeys: AccountKeysJson?,
) {
putEncryptedString(
key = PROFILE_ACCOUNT_KEYS_KEY.appendIdentifier(userId),

View File

@ -10,9 +10,9 @@ import com.bitwarden.network.model.KdfTypeJson
import com.bitwarden.network.model.KeyConnectorUserDecryptionOptionsJson
import com.bitwarden.network.model.TrustedDeviceUserDecryptionOptionsJson
import com.bitwarden.network.model.UserDecryptionOptionsJson
import com.bitwarden.network.model.createMockAccountKeysJson
import com.bitwarden.network.model.createMockOrganization
import com.bitwarden.network.model.createMockPolicy
import com.bitwarden.network.model.createMockPrivateKeys
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.ForcePasswordResetReason
@ -283,7 +283,7 @@ class AuthDiskSourceTest {
authDiskSource.storePrivateKey(userId = userId, privateKey = "privateKey")
authDiskSource.storeAccountKeys(
userId = userId,
accountKeys = createMockPrivateKeys(number = 1),
accountKeys = createMockAccountKeysJson(number = 1),
)
authDiskSource.storeOrganizationKeys(
userId = userId,
@ -486,7 +486,7 @@ class AuthDiskSourceTest {
fun `getAccountKeys should pull from SharedPreferences`() {
val accountKeysBaseKey = "bwSecureStorage:profileAccountKeys"
val mockUserId = "mockUserId"
val mockAccountKeys = createMockPrivateKeys(number = 1)
val mockAccountKeys = createMockAccountKeysJson(number = 1)
fakeEncryptedSharedPreferences.edit {
putString(
"${accountKeysBaseKey}_$mockUserId",
@ -504,7 +504,7 @@ class AuthDiskSourceTest {
fun `storeAccountKeys should update sharedPreferences`() {
val accountKeysBaseKey = "bwSecureStorage:profileAccountKeys"
val mockUserId = "mockUserId"
val mockAccountKeys = createMockPrivateKeys(number = 1)
val mockAccountKeys = createMockAccountKeysJson(number = 1)
authDiskSource.storeAccountKeys(
userId = mockUserId,
accountKeys = mockAccountKeys,

View File

@ -1,6 +1,7 @@
package com.x8bit.bitwarden.data.auth.datasource.disk.util
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
import com.bitwarden.network.model.AccountKeysJson
import com.bitwarden.network.model.SyncResponseJson
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson
@ -63,7 +64,7 @@ class FakeAuthDiskSource : AuthDiskSource {
private val storedOnboardingStatus = mutableMapOf<String, OnboardingStatus?>()
private val storedShowImportLogins = mutableMapOf<String, Boolean?>()
private val storedLastLockTimestampState = mutableMapOf<String, Instant?>()
private val storedAccountKeys = mutableMapOf<String, SyncResponseJson.Profile.AccountKeys?>()
private val storedAccountKeys = mutableMapOf<String, AccountKeysJson?>()
override var userState: UserStateJson? = null
set(value) {
@ -146,12 +147,12 @@ class FakeAuthDiskSource : AuthDiskSource {
storedPrivateKeys[userId] = privateKey
}
override fun getAccountKeys(userId: String): SyncResponseJson.Profile.AccountKeys? =
override fun getAccountKeys(userId: String): AccountKeysJson? =
storedAccountKeys[userId]
override fun storeAccountKeys(
userId: String,
accountKeys: SyncResponseJson.Profile.AccountKeys?,
accountKeys: AccountKeysJson?,
) {
storedAccountKeys[userId] = accountKeys
}

View File

@ -51,6 +51,7 @@ import com.bitwarden.network.model.UserDecryptionOptionsJson
import com.bitwarden.network.model.VerifiedOrganizationDomainSsoDetailsResponse
import com.bitwarden.network.model.VerifyEmailTokenRequestJson
import com.bitwarden.network.model.VerifyEmailTokenResponseJson
import com.bitwarden.network.model.createMockAccountKeysJson
import com.bitwarden.network.model.createMockOrganization
import com.bitwarden.network.model.createMockPolicy
import com.bitwarden.network.service.AccountsService
@ -6714,6 +6715,7 @@ class AuthRepositoryTest {
kdfMemory = 16,
kdfParallelism = 4,
privateKey = "privateKey",
accountKeys = createMockAccountKeysJson(number = 1),
shouldForcePasswordReset = true,
shouldResetMasterPassword = true,
twoFactorToken = null,

View File

@ -6,6 +6,7 @@ import com.bitwarden.network.model.JwtTokenDataJson
import com.bitwarden.network.model.KdfTypeJson
import com.bitwarden.network.model.TrustedDeviceUserDecryptionOptionsJson
import com.bitwarden.network.model.UserDecryptionOptionsJson
import com.bitwarden.network.model.createMockAccountKeysJson
import com.bitwarden.network.util.parseJwtTokenDataOrNull
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.ForcePasswordResetReason
@ -109,6 +110,7 @@ private val GET_TOKEN_RESPONSE_SUCCESS = GetTokenResponseJson.Success(
kdfMemory = 16,
kdfParallelism = 4,
privateKey = "privateKey",
accountKeys = createMockAccountKeysJson(number = 1),
shouldForcePasswordReset = false,
shouldResetMasterPassword = true,
twoFactorToken = null,

View File

@ -0,0 +1,75 @@
package com.bitwarden.network.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Represents private keys in the vault response.
*
* @property signatureKeyPair The signature key pair of the profile.
* @property publicKeyEncryptionKeyPair The public key encryption key pair of the profile.
* @property securityState The security state of the profile (nullable).
*/
@Serializable
data class AccountKeysJson(
@SerialName("signatureKeyPair")
val signatureKeyPair: SignatureKeyPair?,
@SerialName("publicKeyEncryptionKeyPair")
val publicKeyEncryptionKeyPair: PublicKeyEncryptionKeyPair,
@SerialName("securityState")
val securityState: SecurityState?,
) {
/**
* Represents a signature key pair in the vault response.
*
* @property wrappedSigningKey The wrapped signing key of the signature key pair.
* @property verifyingKey The verifying key of the signature key pair.
*/
@Serializable
data class SignatureKeyPair(
@SerialName("wrappedSigningKey")
val wrappedSigningKey: String,
@SerialName("verifyingKey")
val verifyingKey: String,
)
/**
* Represents a public key encryption key pair in the vault response.
*
* @property wrappedPrivateKey The wrapped private key of the public key encryption key
* pair.
* @property publicKey The public key of the public key encryption key pair.
* @property signedPublicKey The signed public key of the public key encryption key pair
* (nullable).
*/
@Serializable
data class PublicKeyEncryptionKeyPair(
@SerialName("wrappedPrivateKey")
val wrappedPrivateKey: String,
@SerialName("publicKey")
val publicKey: String,
@SerialName("signedPublicKey")
val signedPublicKey: String?,
)
/**
* Represents security state in the vault response.
*
* @property securityState The security state of the profile.
* @property securityVersion The security version of the profile.
*/
@Serializable
data class SecurityState(
@SerialName("securityState")
val securityState: String,
@SerialName("securityVersion")
val securityVersion: Int,
)
}

View File

@ -23,6 +23,9 @@ sealed class GetTokenResponseJson {
* @property kdfIterations The number of iterations when calculating a user's password.
* @property kdfMemory The amount of memory to use when calculating a password hash (MB).
* @property kdfParallelism The number of threads to use when calculating a password hash.
* @property accountKeys The user's account keys, which include the signature key pair and
* public key encryption key pair. This is temporarily nullable to support older accounts that
* have not been upgraded to use account keys instead of the deprecated `PrivateKey` field.
* @property shouldForcePasswordReset Whether or not the app must force a password reset.
* @property shouldResetMasterPassword Whether or not the user is required to reset their
* master password.
@ -49,6 +52,12 @@ sealed class GetTokenResponseJson {
@SerialName("Key")
val key: String?,
@Deprecated(
message = "Use `accountKeys` instead.",
replaceWith = ReplaceWith(
"loginResponse.accountKeys?.publicKeyEncryptionKeyPair?.wrappedPrivateKey",
),
)
@SerialName("PrivateKey")
val privateKey: String?,
@ -64,6 +73,9 @@ sealed class GetTokenResponseJson {
@SerialName("KdfParallelism")
val kdfParallelism: Int?,
@SerialName("AccountKeys")
val accountKeys: AccountKeysJson?,
@SerialName("ForcePasswordReset")
val shouldForcePasswordReset: Boolean,

View File

@ -177,12 +177,15 @@ data class SyncResponseJson(
@SerialName("twoFactorEnabled")
val isTwoFactorEnabled: Boolean,
@Deprecated("Use accountKeys instead", ReplaceWith("accountKeys"))
@Deprecated(
message = "Use `accountKeys` instead",
ReplaceWith("profile.accountKeys?.publicKeyEncryptionKeyPair?.wrappedPrivateKey"),
)
@SerialName("privateKey")
val privateKey: String?,
@SerialName("accountKeys")
val accountKeys: AccountKeys?,
val accountKeys: AccountKeysJson?,
@SerialName("premium")
val isPremium: Boolean,
@ -416,77 +419,6 @@ data class SyncResponseJson(
@SerialName("managePolicies")
val shouldManagePolicies: Boolean,
)
/**
* Represents private keys in the vault response.
*
* @property signatureKeyPair The signature key pair of the profile.
* @property publicKeyEncryptionKeyPair The public key encryption key pair of the profile.
* @property securityState The security state of the profile (nullable).
*/
@Serializable
data class AccountKeys(
@SerialName("signatureKeyPair")
val signatureKeyPair: SignatureKeyPair?,
@SerialName("publicKeyEncryptionKeyPair")
val publicKeyEncryptionKeyPair: PublicKeyEncryptionKeyPair,
@SerialName("securityState")
val securityState: SecurityState?,
) {
/**
* Represents a signature key pair in the vault response.
*
* @property wrappedSigningKey The wrapped signing key of the signature key pair.
* @property verifyingKey The verifying key of the signature key pair.
*/
@Serializable
data class SignatureKeyPair(
@SerialName("wrappedSigningKey")
val wrappedSigningKey: String,
@SerialName("verifyingKey")
val verifyingKey: String,
)
/**
* Represents a public key encryption key pair in the vault response.
*
* @property wrappedPrivateKey The wrapped private key of the public key encryption key
* pair.
* @property publicKey The public key of the public key encryption key pair.
* @property signedPublicKey The signed public key of the public key encryption key pair
* (nullable).
*/
@Serializable
data class PublicKeyEncryptionKeyPair(
@SerialName("wrappedPrivateKey")
val wrappedPrivateKey: String,
@SerialName("publicKey")
val publicKey: String,
@SerialName("signedPublicKey")
val signedPublicKey: String?,
)
/**
* Represents security state in the vault response.
*
* @property securityState The security state of the profile.
* @property securityVersion The security version of the profile.
*/
@Serializable
data class SecurityState(
@SerialName("securityState")
val securityState: String,
@SerialName("securityVersion")
val securityVersion: Int,
)
}
}
/**

View File

@ -21,6 +21,7 @@ import com.bitwarden.network.model.TwoFactorAuthMethod
import com.bitwarden.network.model.UserDecryptionOptionsJson
import com.bitwarden.network.model.VerifyEmailTokenRequestJson
import com.bitwarden.network.model.VerifyEmailTokenResponseJson
import com.bitwarden.network.model.createMockAccountKeysJson
import com.bitwarden.network.util.DeviceModelProvider
import io.mockk.every
import io.mockk.mockk
@ -539,6 +540,23 @@ private const val LOGIN_SUCCESS_JSON = """
"token_type": "Bearer",
"refresh_token": "refreshToken",
"PrivateKey": "privateKey",
"AccountKeys": {
"signatureKeyPair": {
"wrappedSigningKey": "mockWrappedSigningKey-1",
"verifyingKey": "mockVerifyingKey-1"
},
"publicKeyEncryptionKeyPair": {
"wrappedPrivateKey": "mockWrappedPrivateKey-1",
"publicKey": "mockPublicKey-1",
"signedPublicKey": "mockSignedPublicKey-1",
"object": "publicKeyEncryptionKeyPair"
},
"securityState": {
"securityState": "mockSecurityState-1",
"securityVersion": 1
},
"object": "privateKeys"
},
"Key": "key",
"MasterPasswordPolicy": {
"MinComplexity": 10,
@ -583,6 +601,7 @@ private val LOGIN_SUCCESS = GetTokenResponseJson.Success(
kdfMemory = 16,
kdfParallelism = 4,
privateKey = "privateKey",
accountKeys = createMockAccountKeysJson(number = 1),
shouldForcePasswordReset = true,
shouldResetMasterPassword = true,
twoFactorToken = null,

View File

@ -0,0 +1,55 @@
package com.bitwarden.network.model
/**
* Create a mock set of private keys with a given [number].
*/
fun createMockAccountKeysJson(
number: Int,
): AccountKeysJson =
AccountKeysJson(
signatureKeyPair = createMockSignatureKeyPair(number = number),
publicKeyEncryptionKeyPair = createMockPublicKeyEncryptionKeyPair(number = number),
securityState = createMockSecurityState(number = number),
)
/**
* Create a mock [AccountKeysJson.SecurityState] with a given [number].
*/
fun createMockSecurityState(
number: Int,
securityState: String = "mockSecurityState-$number",
securityVersion: Int = number,
): AccountKeysJson.SecurityState =
AccountKeysJson.SecurityState(
securityState = securityState,
securityVersion = securityVersion,
)
/**
* Create a mock [AccountKeysJson.PublicKeyEncryptionKeyPair] with a given
* number.
*/
fun createMockPublicKeyEncryptionKeyPair(
number: Int,
publicKey: String = "mockPublicKey-$number",
wrappedPrivateKey: String = "mockWrappedPrivateKey-$number",
signedPublicKey: String? = "mockSignedPublicKey-$number",
): AccountKeysJson.PublicKeyEncryptionKeyPair =
AccountKeysJson.PublicKeyEncryptionKeyPair(
publicKey = publicKey,
wrappedPrivateKey = wrappedPrivateKey,
signedPublicKey = signedPublicKey,
)
/**
* Create a mock [AccountKeysJson.SignatureKeyPair] with a given number.
*/
fun createMockSignatureKeyPair(
number: Int,
wrappedSigningKey: String = "mockWrappedSigningKey-$number",
verifyingKey: String = "mockVerifyingKey-$number",
): AccountKeysJson.SignatureKeyPair =
AccountKeysJson.SignatureKeyPair(
wrappedSigningKey = wrappedSigningKey,
verifyingKey = verifyingKey,
)

View File

@ -18,7 +18,7 @@ fun createMockProfile(
isEmailVerified: Boolean = false,
isTwoFactorEnabled: Boolean = false,
privateKey: String? = "mockPrivateKey-$number",
accountKeys: SyncResponseJson.Profile.AccountKeys? = createMockPrivateKeys(number = number),
accountKeys: AccountKeysJson? = createMockAccountKeysJson(number = number),
isPremium: Boolean = false,
culture: String? = "mockCulture-$number",
name: String? = "mockName-$number",
@ -59,60 +59,6 @@ fun createMockProfile(
creationDate = creationDate,
)
/**
* Create a mock set of private keys with a given [number].
*/
fun createMockPrivateKeys(
number: Int,
): SyncResponseJson.Profile.AccountKeys =
SyncResponseJson.Profile.AccountKeys(
signatureKeyPair = createMockSignatureKeyPair(number = number),
publicKeyEncryptionKeyPair = createMockPublicKeyEncryptionKeyPair(number = number),
securityState = createMockSecurityState(number = number),
)
/**
* Create a mock [SyncResponseJson.Profile.AccountKeys.SecurityState] with a given [number].
*/
fun createMockSecurityState(
number: Int,
securityState: String = "mockSecurityState-$number",
securityVersion: Int = number,
): SyncResponseJson.Profile.AccountKeys.SecurityState =
SyncResponseJson.Profile.AccountKeys.SecurityState(
securityState = securityState,
securityVersion = securityVersion,
)
/**
* Create a mock [SyncResponseJson.Profile.AccountKeys.PublicKeyEncryptionKeyPair] with a given
* number.
*/
fun createMockPublicKeyEncryptionKeyPair(
number: Int,
publicKey: String = "mockPublicKey-$number",
wrappedPrivateKey: String = "mockWrappedPrivateKey-$number",
signedPublicKey: String? = "mockSignedPublicKey-$number",
): SyncResponseJson.Profile.AccountKeys.PublicKeyEncryptionKeyPair =
SyncResponseJson.Profile.AccountKeys.PublicKeyEncryptionKeyPair(
publicKey = publicKey,
wrappedPrivateKey = wrappedPrivateKey,
signedPublicKey = signedPublicKey,
)
/**
* Create a mock [SyncResponseJson.Profile.AccountKeys.SignatureKeyPair] with a given number.
*/
fun createMockSignatureKeyPair(
number: Int,
wrappedSigningKey: String = "mockWrappedSigningKey-$number",
verifyingKey: String = "mockVerifyingKey-$number",
): SyncResponseJson.Profile.AccountKeys.SignatureKeyPair =
SyncResponseJson.Profile.AccountKeys.SignatureKeyPair(
wrappedSigningKey = wrappedSigningKey,
verifyingKey = verifyingKey,
)
/**
* Create a mock [SyncResponseJson.Profile.Organization] with a given [number].
*/