[PM-23290] Migrate PIN unlock keys to PinProtectedUserKeyEnvelope (#6024)

This commit is contained in:
André Bispo 2025-10-20 18:31:12 +01:00 committed by GitHub
parent d5912a5dc3
commit afeeb494da
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 645 additions and 137 deletions

View File

@ -216,25 +216,59 @@ interface AuthDiskSource : AppIdProvider {
/**
* Retrieves a pin-protected user key for the given [userId].
*/
@Deprecated(
message = "Use getPinProtectedUserKeyEnvelope instead.",
replaceWith = ReplaceWith("getPinProtectedUserKeyEnvelope"),
)
fun getPinProtectedUserKey(userId: String): String?
/**
* Retrieves a pin-protected user key envelope for the given [userId].
*/
fun getPinProtectedUserKeyEnvelope(userId: String): String?
/**
* Stores a pin-protected user key for the given [userId].
*
* When [inMemoryOnly] is `true`, the value will only be available via a call to
* [getPinProtectedUserKey] during the current app session.
*/
@Deprecated(
message = "Use storePinProtectedUserKeyEnvelope instead.",
replaceWith = ReplaceWith("storePinProtectedUserKeyEnvelope"),
)
fun storePinProtectedUserKey(
userId: String,
pinProtectedUserKey: String?,
inMemoryOnly: Boolean = false,
)
/**
* Stores a pin-protected user key envelope for the given [userId].
*
* When [inMemoryOnly] is `true`, the value will only be available via a call to
* [getPinProtectedUserKeyEnvelope] during the current app session.
*/
fun storePinProtectedUserKeyEnvelope(
userId: String,
pinProtectedUserKeyEnvelope: String?,
inMemoryOnly: Boolean = false,
)
/**
* Retrieves a flow for the pin-protected user key for the given [userId].
*/
@Deprecated(
message = "Use getPinProtectedUserKeyEnvelopeFlow instead.",
replaceWith = ReplaceWith("getPinProtectedUserKeyEnvelopeFlow"),
)
fun getPinProtectedUserKeyFlow(userId: String): Flow<String?>
/**
* Retrieves a flow for the pin-protected user key envelope for the given [userId].
*/
fun getPinProtectedUserKeyEnvelopeFlow(userId: String): Flow<String?>
/**
* Gets a two-factor auth token using a user's [email].
*/

View File

@ -37,6 +37,7 @@ private const val INVALID_UNLOCK_ATTEMPTS_KEY = "invalidUnlockAttempts"
private const val MASTER_KEY_ENCRYPTION_USER_KEY = "masterKeyEncryptedUserKey"
private const val MASTER_KEY_ENCRYPTION_PRIVATE_KEY = "encPrivateKey"
private const val PIN_PROTECTED_USER_KEY_KEY = "pinKeyEncryptedUserKey"
private const val PIN_PROTECTED_USER_KEY_KEY_ENVELOPE = "pinKeyEncryptedUserKeyEnvelope"
private const val ENCRYPTED_PIN_KEY = "protectedPin"
private const val ORGANIZATIONS_KEY = "organizations"
private const val ORGANIZATION_KEYS_KEY = "encOrgKeys"
@ -67,6 +68,7 @@ class AuthDiskSourceImpl(
AuthDiskSource {
private val inMemoryPinProtectedUserKeys = mutableMapOf<String, String?>()
private val inMemoryPinProtectedUserKeyEnvelopes = mutableMapOf<String, String?>()
private val mutableShouldUseKeyConnectorFlowMap =
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
private val mutableOrganizationsFlowMap =
@ -82,6 +84,8 @@ class AuthDiskSourceImpl(
mutableMapOf<String, MutableSharedFlow<String?>>()
private val mutablePinProtectedUserKeyFlowMap =
mutableMapOf<String, MutableSharedFlow<String?>>()
private val mutablePinProtectedUserKeyEnvelopeFlowMap =
mutableMapOf<String, MutableSharedFlow<String?>>()
private val mutableUserStateFlow = bufferedMutableSharedFlow<UserStateJson?>(replay = 1)
override var userState: UserStateJson?
@ -142,6 +146,7 @@ class AuthDiskSourceImpl(
storeUserKey(userId = userId, userKey = null)
storeUserAutoUnlockKey(userId = userId, userAutoUnlockKey = null)
storePinProtectedUserKey(userId = userId, pinProtectedUserKey = null)
storePinProtectedUserKeyEnvelope(userId = userId, pinProtectedUserKeyEnvelope = null)
storeEncryptedPin(userId = userId, encryptedPin = null)
storePrivateKey(userId = userId, privateKey = null)
storeAccountKeys(userId = userId, accountKeys = null)
@ -329,10 +334,24 @@ class AuthDiskSourceImpl(
getMutableBiometricUnlockKeyFlow(userId)
.onSubscription { emit(getUserBiometricUnlockKey(userId = userId)) }
@Deprecated(
"Use getPinProtectedUserKeyEnvelope instead.",
replaceWith = ReplaceWith("getPinProtectedUserKeyEnvelope"),
)
override fun getPinProtectedUserKey(userId: String): String? =
inMemoryPinProtectedUserKeys[userId]
?: getString(key = PIN_PROTECTED_USER_KEY_KEY.appendIdentifier(userId))
override fun getPinProtectedUserKeyEnvelope(userId: String): String? =
inMemoryPinProtectedUserKeyEnvelopes[userId]
?: getString(
key = PIN_PROTECTED_USER_KEY_KEY_ENVELOPE.appendIdentifier(userId),
)
@Deprecated(
"Use storePinProtectedUserKeyEnvelope instead.",
replaceWith = ReplaceWith("storePinProtectedUserKeyEnvelope"),
)
override fun storePinProtectedUserKey(
userId: String,
pinProtectedUserKey: String?,
@ -347,10 +366,32 @@ class AuthDiskSourceImpl(
getMutablePinProtectedUserKeyFlow(userId).tryEmit(pinProtectedUserKey)
}
override fun storePinProtectedUserKeyEnvelope(
userId: String,
pinProtectedUserKeyEnvelope: String?,
inMemoryOnly: Boolean,
) {
inMemoryPinProtectedUserKeyEnvelopes[userId] = pinProtectedUserKeyEnvelope
if (inMemoryOnly) return
putString(
key = PIN_PROTECTED_USER_KEY_KEY_ENVELOPE.appendIdentifier(userId),
value = pinProtectedUserKeyEnvelope,
)
getMutablePinProtectedUserKeyEnvelopeFlow(userId).tryEmit(pinProtectedUserKeyEnvelope)
}
@Deprecated(
"Use getPinProtectedUserKeyEnvelopeFlow instead.",
replaceWith = ReplaceWith("getPinProtectedUserKeyEnvelopeFlow"),
)
override fun getPinProtectedUserKeyFlow(userId: String): Flow<String?> =
getMutablePinProtectedUserKeyFlow(userId)
.onSubscription { emit(getPinProtectedUserKey(userId = userId)) }
override fun getPinProtectedUserKeyEnvelopeFlow(userId: String): Flow<String?> =
getMutablePinProtectedUserKeyEnvelopeFlow(userId)
.onSubscription { emit(getPinProtectedUserKeyEnvelope(userId = userId)) }
override fun getTwoFactorToken(email: String): String? =
getString(key = TWO_FACTOR_TOKEN_KEY.appendIdentifier(email))
@ -579,6 +620,12 @@ class AuthDiskSourceImpl(
bufferedMutableSharedFlow(replay = 1)
}
private fun getMutablePinProtectedUserKeyEnvelopeFlow(
userId: String,
): MutableSharedFlow<String?> = mutablePinProtectedUserKeyEnvelopeFlowMap.getOrPut(userId) {
bufferedMutableSharedFlow(replay = 1)
}
private fun migrateAccountTokens() {
userState
?.accounts

View File

@ -85,7 +85,8 @@ class UserLogoutManagerImpl(
// Save any data that will still need to be retained after otherwise clearing all dat
val vaultTimeoutInMinutes = settingsDiskSource.getVaultTimeoutInMinutes(userId = userId)
val vaultTimeoutAction = settingsDiskSource.getVaultTimeoutAction(userId = userId)
val pinProtectedUserKey = authDiskSource.getPinProtectedUserKey(userId = userId)
val pinProtectedUserKeyEnvelope = authDiskSource
.getPinProtectedUserKeyEnvelope(userId = userId)
switchUserIfAvailable(
currentUserId = userId,
@ -107,9 +108,9 @@ class UserLogoutManagerImpl(
vaultTimeoutAction = vaultTimeoutAction,
)
}
authDiskSource.storePinProtectedUserKey(
authDiskSource.storePinProtectedUserKeyEnvelope(
userId = userId,
pinProtectedUserKey = pinProtectedUserKey,
pinProtectedUserKeyEnvelope = pinProtectedUserKeyEnvelope,
)
}

View File

@ -156,7 +156,7 @@ class UserStateManagerImpl(
private fun getVaultUnlockType(
userId: String,
): VaultUnlockType = authDiskSource
.getPinProtectedUserKey(userId = userId)
.getPinProtectedUserKeyEnvelope(userId = userId)
?.let { VaultUnlockType.PIN }
?: VaultUnlockType.MASTER_PASSWORD
}

View File

@ -1295,8 +1295,8 @@ class AuthRepositoryImpl(
?.activeAccount
?.profile
?: return ValidatePinResult.Error(error = NoActiveUserException())
val pinProtectedUserKey = authDiskSource
.getPinProtectedUserKey(userId = activeAccount.userId)
val pinProtectedUserKeyEnvelope = authDiskSource
.getPinProtectedUserKeyEnvelope(userId = activeAccount.userId)
?: return ValidatePinResult.Error(
error = MissingPropertyException("Pin Protected User Key"),
)
@ -1304,7 +1304,7 @@ class AuthRepositoryImpl(
.validatePin(
userId = activeAccount.userId,
pin = pin,
pinProtectedUserKey = pinProtectedUserKey,
pinProtectedUserKey = pinProtectedUserKeyEnvelope,
)
.fold(
onSuccess = { ValidatePinResult.Success(isValid = it) },

View File

@ -29,6 +29,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
@ -293,7 +294,11 @@ class SettingsRepositoryImpl(
?.let { userId ->
authDiskSource
.getPinProtectedUserKeyFlow(userId)
.map { it != null }
.combine(
authDiskSource.getPinProtectedUserKeyEnvelopeFlow(userId),
) { pinProtectedUserKey, pinProtectedUserKeyEnvelope ->
pinProtectedUserKey != null || pinProtectedUserKeyEnvelope != null
}
}
?: flowOf(false)
@ -403,7 +408,7 @@ class SettingsRepositoryImpl(
?.userDecryptionOptions
?.hasMasterPassword != false
val timeoutAction = settingsDiskSource.getVaultTimeoutAction(userId = userId)
val hasPin = authDiskSource.getPinProtectedUserKey(userId = userId) != null
val hasPin = authDiskSource.getPinProtectedUserKeyEnvelope(userId = userId) != null
val hasBiometrics = authDiskSource.getUserBiometricUnlockKey(userId = userId) != null
// The timeout action cannot be "lock" if you do not have master password, pin, or
// biometrics unlock enabled.
@ -527,21 +532,27 @@ class SettingsRepositoryImpl(
val userId = activeUserId ?: return
unconfinedScope.launch {
vaultSdkSource
.derivePinKey(
.enrollPin(
userId = userId,
pin = pin,
)
.fold(
onSuccess = { derivePinKeyResponse ->
onSuccess = { enrollPinResponse ->
authDiskSource.apply {
storeEncryptedPin(
userId = userId,
encryptedPin = derivePinKeyResponse.encryptedPin,
encryptedPin = enrollPinResponse.userKeyEncryptedPin,
)
storePinProtectedUserKeyEnvelope(
userId = userId,
pinProtectedUserKeyEnvelope =
enrollPinResponse.pinProtectedUserKeyEnvelope,
inMemoryOnly = shouldRequireMasterPasswordOnRestart,
)
// Remove any legacy pin protected user keys.
storePinProtectedUserKey(
userId = userId,
pinProtectedUserKey = derivePinKeyResponse.pinProtectedUserKey,
inMemoryOnly = shouldRequireMasterPasswordOnRestart,
pinProtectedUserKey = null,
)
}
},
@ -561,6 +572,10 @@ class SettingsRepositoryImpl(
userId = userId,
encryptedPin = null,
)
authDiskSource.storePinProtectedUserKeyEnvelope(
userId = userId,
pinProtectedUserKeyEnvelope = null,
)
authDiskSource.storePinProtectedUserKey(
userId = userId,
pinProtectedUserKey = null,

View File

@ -2,7 +2,7 @@ package com.x8bit.bitwarden.data.vault.datasource.sdk
import com.bitwarden.collections.Collection
import com.bitwarden.collections.CollectionView
import com.bitwarden.core.DerivePinKeyResponse
import com.bitwarden.core.EnrollPinResponse
import com.bitwarden.core.InitOrgCryptoRequest
import com.bitwarden.core.InitUserCryptoMethod
import com.bitwarden.core.InitUserCryptoRequest
@ -75,30 +75,26 @@ interface VaultSdkSource {
): Result<DeriveKeyConnectorResult>
/**
* Derives a "pin key" from the given [pin] for the given [userId]. This can be used to later
* unlock their vault via a call to [initializeCrypto] with [InitUserCryptoMethod.Pin].
* Protects the current user key with the provided PIN. This can be used to later unlock
* their vault via a call to [initializeCrypto] with [InitUserCryptoMethod.PinEnvelope].
*
* This should only be called after a successful call to [initializeCrypto] for the associated
* user.
*/
suspend fun derivePinKey(
suspend fun enrollPin(
userId: String,
pin: String,
): Result<DerivePinKeyResponse>
): Result<EnrollPinResponse>
/**
* Derives a pin-protected user key from the given [encryptedPin] for the given [userId]. This
* value must be derived from a previous call to [derivePinKey] with a plaintext PIN. This can
* be used to later unlock their vault via a call to [initializeCrypto] with
* [InitUserCryptoMethod.Pin].
*
* This should only be called after a successful call to [initializeCrypto] for the associated
* user.
* Protects the current user key with the provided PIN. The result can be stored and later
* used to initialize another client instance by using the PIN and the PIN key with
* [initializeCrypto]. The provided pin is encrypted with the user key.
*/
suspend fun derivePinProtectedUserKey(
suspend fun enrollPinWithEncryptedPin(
userId: String,
encryptedPin: String,
): Result<String>
): Result<EnrollPinResponse>
/**
* Validate the user pin using the [pinProtectedUserKey].

View File

@ -4,7 +4,7 @@ import com.bitwarden.collections.Collection
import com.bitwarden.collections.CollectionView
import com.bitwarden.core.DeriveKeyConnectorException
import com.bitwarden.core.DeriveKeyConnectorRequest
import com.bitwarden.core.DerivePinKeyResponse
import com.bitwarden.core.EnrollPinResponse
import com.bitwarden.core.InitOrgCryptoRequest
import com.bitwarden.core.InitUserCryptoRequest
import com.bitwarden.core.UpdateKdfResponse
@ -109,24 +109,24 @@ class VaultSdkSourceImpl(
}
}
override suspend fun derivePinKey(
override suspend fun enrollPin(
userId: String,
pin: String,
): Result<DerivePinKeyResponse> =
): Result<EnrollPinResponse> =
runCatchingWithLogs {
getClient(userId = userId)
.crypto()
.derivePinKey(pin = pin)
.enrollPin(pin = pin)
}
override suspend fun derivePinProtectedUserKey(
override suspend fun enrollPinWithEncryptedPin(
userId: String,
encryptedPin: String,
): Result<String> =
): Result<EnrollPinResponse> =
runCatchingWithLogs {
getClient(userId = userId)
.crypto()
.derivePinUserKey(encryptedPin = encryptedPin)
.enrollPinWithEncryptedPin(encryptedPin = encryptedPin)
}
override suspend fun validatePin(

View File

@ -239,6 +239,7 @@ class VaultLockManagerImpl(
trustedDeviceManager
.trustThisDeviceIfNecessary(userId = userId)
updateKdfIfNeeded(initUserCryptoMethod)
migratePinProtectedUserKeyIfNeeded(userId = userId)
setVaultToUnlocked(userId = userId)
} else {
incrementInvalidUnlockCount(userId = userId)
@ -275,6 +276,39 @@ class VaultLockManagerImpl(
)
}
/**
* Migrates the PIN-protected user key for the given user if needed.
*
* If an encrypted PIN exists and no PIN-protected user key envelope is present,
* enrolls the PIN with the encrypted PIN and stores the resulting envelope.
* Optionally marks the envelope as in-memory only if the PIN-protected user key is not present.
*
* @param userId The ID of the user for whom to migrate the PIN-protected user key.
*/
private suspend fun migratePinProtectedUserKeyIfNeeded(userId: String) {
val encryptedPin = authDiskSource.getEncryptedPin(userId) ?: return
if (authDiskSource.getPinProtectedUserKeyEnvelope(userId) != null) return
val inMemoryOnly = authDiskSource.getPinProtectedUserKey(userId) == null
vaultSdkSource.enrollPinWithEncryptedPin(userId, encryptedPin)
.onSuccess { enrollPinResponse ->
authDiskSource.storeEncryptedPin(
userId = userId,
encryptedPin = enrollPinResponse.userKeyEncryptedPin,
)
authDiskSource.storePinProtectedUserKeyEnvelope(
userId = userId,
pinProtectedUserKeyEnvelope = enrollPinResponse.pinProtectedUserKeyEnvelope,
inMemoryOnly = inMemoryOnly,
)
authDiskSource.storePinProtectedUserKey(
userId = userId,
pinProtectedUserKey = null,
inMemoryOnly = inMemoryOnly,
)
}
}
/**
* Increments the stored invalid unlock count for the given [userId] and automatically logs out
* if this new value is greater than [MAXIMUM_INVALID_UNLOCK_ATTEMPTS].

View File

@ -360,17 +360,34 @@ class VaultRepositoryImpl(
): VaultUnlockResult {
val userId = activeUserId
?: return VaultUnlockResult.InvalidStateError(error = NoActiveUserException())
val pinProtectedUserKey = authDiskSource.getPinProtectedUserKey(userId = userId)
?: return VaultUnlockResult.InvalidStateError(
error = MissingPropertyException("Pin protected key"),
)
return this.unlockVaultForUser(
userId = userId,
initUserCryptoMethod = InitUserCryptoMethod.Pin(
pin = pin,
pinProtectedUserKey = pinProtectedUserKey,
),
)
return authDiskSource.getPinProtectedUserKeyEnvelope(userId = userId)
?.let { pinProtectedUserKeyEnvelope ->
this.unlockVaultForUser(
userId = userId,
initUserCryptoMethod = InitUserCryptoMethod.PinEnvelope(
pin = pin,
pinProtectedUserKeyEnvelope = pinProtectedUserKeyEnvelope,
),
)
}
?: run {
// This is needed to support unlocking with a legacy pin protected user key.
// Once the vault is unlocked, the user's pin protected user key is migrated to
// a pin protected user key envelope.
val pinProtectedUserKey = authDiskSource.getPinProtectedUserKey(userId = userId)
?: return VaultUnlockResult.InvalidStateError(
error = MissingPropertyException("Pin protected key"),
)
this.unlockVaultForUser(
userId = userId,
initUserCryptoMethod = InitUserCryptoMethod.Pin(
pin = pin,
pinProtectedUserKey = pinProtectedUserKey,
),
)
}
}
override suspend fun generateTotp(
@ -502,17 +519,29 @@ class VaultRepositoryImpl(
*/
private suspend fun deriveTemporaryPinProtectedUserKeyIfNecessary(userId: String) {
val encryptedPin = authDiskSource.getEncryptedPin(userId = userId) ?: return
val existingPinProtectedUserKey = authDiskSource.getPinProtectedUserKey(userId = userId)
if (existingPinProtectedUserKey != null) return
val existingPinProtectedUserKeyEnvelope = authDiskSource
.getPinProtectedUserKeyEnvelope(
userId = userId,
)
if (existingPinProtectedUserKeyEnvelope != null) return
vaultSdkSource
.derivePinProtectedUserKey(
.enrollPinWithEncryptedPin(
userId = userId,
encryptedPin = encryptedPin,
)
.onSuccess { pinProtectedUserKey ->
.onSuccess { enrollPinResponse ->
authDiskSource.storeEncryptedPin(
userId = userId,
encryptedPin = enrollPinResponse.userKeyEncryptedPin,
)
authDiskSource.storePinProtectedUserKeyEnvelope(
userId = userId,
pinProtectedUserKeyEnvelope = enrollPinResponse.pinProtectedUserKeyEnvelope,
inMemoryOnly = true,
)
authDiskSource.storePinProtectedUserKey(
userId = userId,
pinProtectedUserKey = pinProtectedUserKey,
pinProtectedUserKey = null,
inMemoryOnly = true,
)
}

View File

@ -331,6 +331,7 @@ class AuthDiskSourceTest {
assertNull(authDiskSource.getUserBiometricInitVector(userId = userId))
assertNull(authDiskSource.getUserBiometricUnlockKey(userId = userId))
assertNull(authDiskSource.getPinProtectedUserKey(userId = userId))
assertNull(authDiskSource.getPinProtectedUserKeyEnvelope(userId = userId))
assertNull(authDiskSource.getInvalidUnlockAttempts(userId = userId))
assertNull(authDiskSource.getUserKey(userId = userId))
assertNull(authDiskSource.getUserAutoUnlockKey(userId = userId))
@ -825,6 +826,63 @@ class AuthDiskSourceTest {
)
}
@Test
@Suppress("MaxLineLength")
fun `storePinProtectedUserKeyEnvelope should update result flow from getPinProtectedUserKeyEnvelopeFlow`() =
runTest {
val topSecretKey = "topsecret"
val mockUserId = "mockUserId"
authDiskSource.getPinProtectedUserKeyEnvelopeFlow(mockUserId).test {
assertNull(awaitItem())
authDiskSource.storePinProtectedUserKeyEnvelope(
userId = mockUserId,
pinProtectedUserKeyEnvelope = topSecretKey,
)
assertEquals(topSecretKey, awaitItem())
}
}
@Test
fun `getPinProtectedUserKeyEnvelope should pull from SharedPreferences`() {
val pinProtectedUserKeyEnvelopeBaseKey =
"bwPreferencesStorage:pinKeyEncryptedUserKeyEnvelope"
val mockUserId = "mockUserId"
val mockPinProtectedUserKeyEnvelope = "mockPinProtectedUserKeyEnvelope"
fakeSharedPreferences
.edit {
putString(
"${pinProtectedUserKeyEnvelopeBaseKey}_$mockUserId",
mockPinProtectedUserKeyEnvelope,
)
}
val actual = authDiskSource.getPinProtectedUserKeyEnvelope(userId = mockUserId)
assertEquals(
mockPinProtectedUserKeyEnvelope,
actual,
)
}
@Test
fun `storePinProtectedUserKeyEnvelope should pull from SharedPreferences`() {
val pinProtectedUserKeyEnvelopeBaseKey =
"bwPreferencesStorage:pinKeyEncryptedUserKeyEnvelope"
val mockUserId = "mockUserId"
val mockPinProtectedUserKeyEnvelope = "mockPinProtectedUserKeyEnvelope"
authDiskSource.storePinProtectedUserKeyEnvelope(
userId = mockUserId,
pinProtectedUserKeyEnvelope = mockPinProtectedUserKeyEnvelope,
)
val actual = fakeSharedPreferences
.getString(
"${pinProtectedUserKeyEnvelopeBaseKey}_$mockUserId",
null,
)
assertEquals(
mockPinProtectedUserKeyEnvelope,
actual,
)
}
@Test
fun `storePinProtectedUserKey should update result flow from getPinProtectedUserKeyFlow`() =
runTest {

View File

@ -65,6 +65,9 @@ class FakeAuthDiskSource : AuthDiskSource {
private val storedShowImportLogins = mutableMapOf<String, Boolean?>()
private val storedLastLockTimestampState = mutableMapOf<String, Instant?>()
private val storedAccountKeys = mutableMapOf<String, AccountKeysJson?>()
private val storedPinProtectedUserKeyEnvelopes = mutableMapOf<String, Pair<String?, Boolean>>()
private val mutablePinProtectedUserKeyEnvelopesFlowMap =
mutableMapOf<String, MutableSharedFlow<String?>>()
override var userState: UserStateJson? = null
set(value) {
@ -89,11 +92,19 @@ class FakeAuthDiskSource : AuthDiskSource {
storedBiometricInitVectors.remove(userId)
storedBiometricKeys.remove(userId)
storedOrganizationKeys.remove(userId)
storedPinProtectedUserKeyEnvelopes.remove(userId)
mutableShouldUseKeyConnectorFlowMap.remove(userId)
mutableOrganizationsFlowMap.remove(userId)
mutablePoliciesFlowMap.remove(userId)
mutableAccountTokensFlowMap.remove(userId)
mutablePinProtectedUserKeyEnvelopesFlowMap.remove(userId)
}
private fun getMutablePinProtectedUserKeyEnvelopeFlow(
userId: String,
): MutableSharedFlow<String?> = mutablePinProtectedUserKeyEnvelopesFlowMap.getOrPut(userId) {
bufferedMutableSharedFlow(replay = 1)
}
override fun getShouldUseKeyConnectorFlow(
@ -170,9 +181,17 @@ class FakeAuthDiskSource : AuthDiskSource {
storedUserAutoUnlockKeys[userId] = userAutoUnlockKey
}
@Deprecated(
"Use getPinProtectedUserKeyEnvelope instead.",
replaceWith = ReplaceWith("getPinProtectedUserKeyEnvelope"),
)
override fun getPinProtectedUserKey(userId: String): String? =
storedPinProtectedUserKeys[userId]?.first
@Deprecated(
"Use storePinProtectedUserKeyEnvelope instead.",
replaceWith = ReplaceWith("storePinProtectedUserKeyEnvelope"),
)
override fun storePinProtectedUserKey(
userId: String,
pinProtectedUserKey: String?,
@ -182,6 +201,10 @@ class FakeAuthDiskSource : AuthDiskSource {
getMutablePinProtectedUserKeyFlow(userId).tryEmit(pinProtectedUserKey)
}
@Deprecated(
"Use getPinProtectedUserKeyEnvelopeFlow instead.",
replaceWith = ReplaceWith("getPinProtectedUserKeyEnvelopeFlow"),
)
override fun getPinProtectedUserKeyFlow(userId: String): Flow<String?> =
getMutablePinProtectedUserKeyFlow(userId)
.onSubscription {
@ -331,6 +354,24 @@ class FakeAuthDiskSource : AuthDiskSource {
storedLastLockTimestampState[userId] = lastLockTimestamp
}
override fun getPinProtectedUserKeyEnvelope(userId: String): String? =
storedPinProtectedUserKeyEnvelopes[userId]?.first
override fun storePinProtectedUserKeyEnvelope(
userId: String,
pinProtectedUserKeyEnvelope: String?,
inMemoryOnly: Boolean,
) {
storedPinProtectedUserKeyEnvelopes[userId] = pinProtectedUserKeyEnvelope to inMemoryOnly
getMutablePinProtectedUserKeyEnvelopeFlow(userId).tryEmit(pinProtectedUserKeyEnvelope)
}
override fun getPinProtectedUserKeyEnvelopeFlow(userId: String): Flow<String?> =
getMutablePinProtectedUserKeyEnvelopeFlow(userId)
.onSubscription {
emit(getPinProtectedUserKeyEnvelope(userId))
}
/**
* Assert the the [isTdeLoginComplete] was stored successfully using the [userId].
*/
@ -427,6 +468,20 @@ class FakeAuthDiskSource : AuthDiskSource {
assertEquals(pinProtectedUserKey to inMemoryOnly, storedPinProtectedUserKeys[userId])
}
/**
* Assert that the [pinProtectedUserKeyEnvelope] was stored successfully using the [userId].
*/
fun assertPinProtectedUserKeyEnvelope(
userId: String,
pinProtectedUserKeyEnvelope: String?,
inMemoryOnly: Boolean = false,
) {
assertEquals(
pinProtectedUserKeyEnvelope to inMemoryOnly,
storedPinProtectedUserKeyEnvelopes[userId],
)
}
/**
* Assert the the [organizationKeys] was stored successfully using the [userId].
*/

View File

@ -34,7 +34,12 @@ import java.time.ZonedDateTime
class UserLogoutManagerTest {
private val authDiskSource: AuthDiskSource = mockk {
every { storeAccountTokens(userId = any(), accountTokens = null) } just runs
every { storePinProtectedUserKey(userId = any(), pinProtectedUserKey = any()) } just runs
every {
storePinProtectedUserKeyEnvelope(
userId = any(),
pinProtectedUserKeyEnvelope = any(),
)
} just runs
every { userState = any() } just runs
every { clearData(any()) } just runs
}
@ -137,8 +142,9 @@ class UserLogoutManagerTest {
every {
settingsDiskSource.getVaultTimeoutAction(userId = userId)
} returns vaultTimeoutAction
every {
authDiskSource.getPinProtectedUserKey(userId = userId)
authDiskSource.getPinProtectedUserKeyEnvelope(userId = userId)
} returns pinProtectedUserKey
userLogoutManager.softLogout(userId = userId, reason = LogoutReason.Timeout)
@ -164,9 +170,9 @@ class UserLogoutManagerTest {
userId = userId,
vaultTimeoutAction = vaultTimeoutAction,
)
authDiskSource.storePinProtectedUserKey(
authDiskSource.storePinProtectedUserKeyEnvelope(
userId = userId,
pinProtectedUserKey = pinProtectedUserKey,
pinProtectedUserKeyEnvelope = pinProtectedUserKey,
)
}
}
@ -186,7 +192,7 @@ class UserLogoutManagerTest {
settingsDiskSource.getVaultTimeoutAction(userId = userId)
} returns vaultTimeoutAction
every {
authDiskSource.getPinProtectedUserKey(userId = userId)
authDiskSource.getPinProtectedUserKeyEnvelope(userId = userId)
} returns pinProtectedUserKey
userLogoutManager.softLogout(userId = userId, reason = LogoutReason.Timeout)
@ -206,9 +212,9 @@ class UserLogoutManagerTest {
userId = userId,
vaultTimeoutAction = vaultTimeoutAction,
)
authDiskSource.storePinProtectedUserKey(
authDiskSource.storePinProtectedUserKeyEnvelope(
userId = userId,
pinProtectedUserKey = pinProtectedUserKey,
pinProtectedUserKeyEnvelope = pinProtectedUserKey,
)
}
}
@ -222,15 +228,15 @@ class UserLogoutManagerTest {
val pinProtectedUserKey = "pinProtectedUserKey"
every { authDiskSource.userState } returns MULTI_USER_STATE
every {
authDiskSource.getPinProtectedUserKeyEnvelope(userId)
} returns pinProtectedUserKey
every {
settingsDiskSource.getVaultTimeoutInMinutes(userId = userId)
} returns vaultTimeoutInMinutes
every {
settingsDiskSource.getVaultTimeoutAction(userId = userId)
} returns vaultTimeoutAction
every {
authDiskSource.getPinProtectedUserKey(userId = userId)
} returns pinProtectedUserKey
userLogoutManager.softLogout(userId = userId, reason = LogoutReason.SecurityStamp)
@ -249,9 +255,9 @@ class UserLogoutManagerTest {
userId = userId,
vaultTimeoutAction = vaultTimeoutAction,
)
authDiskSource.storePinProtectedUserKey(
authDiskSource.storePinProtectedUserKeyEnvelope(
userId = userId,
pinProtectedUserKey = pinProtectedUserKey,
pinProtectedUserKeyEnvelope = pinProtectedUserKey,
)
}
}

View File

@ -92,13 +92,13 @@ class UserStateManagerTest {
)
fakeAuthDiskSource.apply {
storePinProtectedUserKey(
storePinProtectedUserKeyEnvelope(
userId = USER_ID_1,
pinProtectedUserKey = "pinProtectedUseKey",
pinProtectedUserKeyEnvelope = "pinProtectedUseKey",
)
storePinProtectedUserKey(
storePinProtectedUserKeyEnvelope(
userId = USER_ID_2,
pinProtectedUserKey = "pinProtectedUseKey",
pinProtectedUserKeyEnvelope = "pinProtectedUseKey",
)
userState = MULTI_USER_STATE
}
@ -137,13 +137,13 @@ class UserStateManagerTest {
)
fakeAuthDiskSource.apply {
storePinProtectedUserKey(
storePinProtectedUserKeyEnvelope(
userId = USER_ID_1,
pinProtectedUserKey = null,
pinProtectedUserKeyEnvelope = null,
)
storePinProtectedUserKey(
storePinProtectedUserKeyEnvelope(
userId = USER_ID_2,
pinProtectedUserKey = null,
pinProtectedUserKeyEnvelope = null,
)
storeOrganizations(
userId = USER_ID_1,

View File

@ -6353,9 +6353,9 @@ class AuthRepositoryTest {
val pinProtectedUserKey = "pinProtectedUserKey"
val error = Throwable("Fail!")
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
fakeAuthDiskSource.storePinProtectedUserKey(
fakeAuthDiskSource.storePinProtectedUserKeyEnvelope(
userId = SINGLE_USER_STATE_1.activeUserId,
pinProtectedUserKey = pinProtectedUserKey,
pinProtectedUserKeyEnvelope = pinProtectedUserKey,
)
coEvery {
vaultSdkSource.validatePin(
@ -6387,9 +6387,9 @@ class AuthRepositoryTest {
val pin = "PIN"
val pinProtectedUserKey = "pinProtectedUserKey"
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
fakeAuthDiskSource.storePinProtectedUserKey(
fakeAuthDiskSource.storePinProtectedUserKeyEnvelope(
userId = SINGLE_USER_STATE_1.activeUserId,
pinProtectedUserKey = pinProtectedUserKey,
pinProtectedUserKeyEnvelope = pinProtectedUserKey,
)
coEvery {
vaultSdkSource.validatePin(
@ -6421,9 +6421,9 @@ class AuthRepositoryTest {
val pin = "PIN"
val pinProtectedUserKey = "pinProtectedUserKey"
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
fakeAuthDiskSource.storePinProtectedUserKey(
fakeAuthDiskSource.storePinProtectedUserKeyEnvelope(
userId = SINGLE_USER_STATE_1.activeUserId,
pinProtectedUserKey = pinProtectedUserKey,
pinProtectedUserKeyEnvelope = pinProtectedUserKey,
)
coEvery {
vaultSdkSource.validatePin(

View File

@ -3,7 +3,7 @@ package com.x8bit.bitwarden.data.platform.repository
import android.view.autofill.AutofillManager
import app.cash.turbine.test
import com.bitwarden.authenticatorbridge.util.generateSecretKey
import com.bitwarden.core.DerivePinKeyResponse
import com.bitwarden.core.EnrollPinResponse
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
import com.bitwarden.core.data.util.asFailure
import com.bitwarden.core.data.util.asSuccess
@ -202,9 +202,9 @@ class SettingsRepositoryTest {
// Updating the Vault settings values and calling setDefaultsIfNecessary again has no
// effect on the currently stored values since we have a way to unlock the vault.
fakeAuthDiskSource.storePinProtectedUserKey(
fakeAuthDiskSource.storePinProtectedUserKeyEnvelope(
userId = USER_ID,
pinProtectedUserKey = "pinProtectedKey",
pinProtectedUserKeyEnvelope = "pinProtectedKey",
)
fakeSettingsDiskSource.apply {
storeVaultTimeoutInMinutes(
@ -924,19 +924,19 @@ class SettingsRepositoryTest {
@Test
fun `storeUnlockPin when the master password on restart is required should only save an encrypted PIN to disk`() {
val pin = "1234"
val encryptedPin = "encryptedPin"
val pinProtectedUserKey = "pinProtectedUserKey"
val derivePinKeyResponse = DerivePinKeyResponse(
pinProtectedUserKey = pinProtectedUserKey,
encryptedPin = encryptedPin,
val userKeyEncryptedPin = "encryptedPin"
val pinProtectedUserKeyEnvelope = "pinProtectedUserKeyEnvelope"
val enrollResponse = EnrollPinResponse(
pinProtectedUserKeyEnvelope = pinProtectedUserKeyEnvelope,
userKeyEncryptedPin = userKeyEncryptedPin,
)
fakeAuthDiskSource.userState = MOCK_USER_STATE
coEvery {
vaultSdkSource.derivePinKey(
vaultSdkSource.enrollPin(
userId = USER_ID,
pin = pin,
)
} returns derivePinKeyResponse.asSuccess()
} returns enrollResponse.asSuccess()
settingsRepository.storeUnlockPin(
pin = pin,
@ -946,16 +946,16 @@ class SettingsRepositoryTest {
fakeAuthDiskSource.apply {
assertEncryptedPin(
userId = USER_ID,
encryptedPin = encryptedPin,
encryptedPin = userKeyEncryptedPin,
)
assertPinProtectedUserKey(
assertPinProtectedUserKeyEnvelope(
userId = USER_ID,
pinProtectedUserKey = pinProtectedUserKey,
pinProtectedUserKeyEnvelope = pinProtectedUserKeyEnvelope,
inMemoryOnly = true,
)
}
coVerify {
vaultSdkSource.derivePinKey(
vaultSdkSource.enrollPin(
userId = USER_ID,
pin = pin,
)
@ -966,19 +966,19 @@ class SettingsRepositoryTest {
@Test
fun `storeUnlockPin when the master password on restart is not required should save all PIN data to disk`() {
val pin = "1234"
val encryptedPin = "encryptedPin"
val pinProtectedUserKey = "pinProtectedUserKey"
val derivePinKeyResponse = DerivePinKeyResponse(
pinProtectedUserKey = pinProtectedUserKey,
encryptedPin = encryptedPin,
val userKeyEncryptedPin = "encryptedPin"
val pinProtectedUserKeyEnvelope = "pinProtectedUserKeyEnvelope"
val enrollResponse = EnrollPinResponse(
pinProtectedUserKeyEnvelope = pinProtectedUserKeyEnvelope,
userKeyEncryptedPin = userKeyEncryptedPin,
)
fakeAuthDiskSource.userState = MOCK_USER_STATE
coEvery {
vaultSdkSource.derivePinKey(
vaultSdkSource.enrollPin(
userId = USER_ID,
pin = pin,
)
} returns derivePinKeyResponse.asSuccess()
} returns enrollResponse.asSuccess()
settingsRepository.storeUnlockPin(
pin = pin,
@ -988,16 +988,16 @@ class SettingsRepositoryTest {
fakeAuthDiskSource.apply {
assertEncryptedPin(
userId = USER_ID,
encryptedPin = encryptedPin,
encryptedPin = userKeyEncryptedPin,
)
assertPinProtectedUserKey(
assertPinProtectedUserKeyEnvelope(
userId = USER_ID,
pinProtectedUserKey = pinProtectedUserKey,
pinProtectedUserKeyEnvelope = pinProtectedUserKeyEnvelope,
inMemoryOnly = false,
)
}
coVerify {
vaultSdkSource.derivePinKey(
vaultSdkSource.enrollPin(
userId = USER_ID,
pin = pin,
)
@ -1013,9 +1013,9 @@ class SettingsRepositoryTest {
userId = USER_ID,
encryptedPin = "encryptedPin",
)
storePinProtectedUserKey(
storePinProtectedUserKeyEnvelope(
userId = USER_ID,
pinProtectedUserKey = "pinProtectedUserKey",
pinProtectedUserKeyEnvelope = "pinProtectedUserKeyEnvelope",
)
}
@ -1026,9 +1026,9 @@ class SettingsRepositoryTest {
userId = USER_ID,
encryptedPin = null,
)
assertPinProtectedUserKey(
assertPinProtectedUserKeyEnvelope(
userId = USER_ID,
pinProtectedUserKey = null,
pinProtectedUserKeyEnvelope = null,
)
}
}

View File

@ -4,7 +4,7 @@ import com.bitwarden.collections.Collection
import com.bitwarden.collections.CollectionView
import com.bitwarden.core.DeriveKeyConnectorException
import com.bitwarden.core.DeriveKeyConnectorRequest
import com.bitwarden.core.DerivePinKeyResponse
import com.bitwarden.core.EnrollPinResponse
import com.bitwarden.core.InitOrgCryptoRequest
import com.bitwarden.core.InitUserCryptoRequest
import com.bitwarden.core.MasterPasswordAuthenticationData
@ -339,14 +339,14 @@ class VaultSdkSourceTest {
}
@Test
fun `derivePinKey should call SDK and return a Result with the correct data`() = runBlocking {
fun `enrollPin should call SDK and return a Result with the correct data`() = runBlocking {
val userId = "userId"
val pin = "pin"
val expectedResult = mockk<DerivePinKeyResponse>()
val expectedResult = mockk<EnrollPinResponse>()
coEvery {
clientCrypto.derivePinKey(pin = pin)
clientCrypto.enrollPin(pin = pin)
} returns expectedResult
val result = vaultSdkSource.derivePinKey(
val result = vaultSdkSource.enrollPin(
userId = userId,
pin = pin,
)
@ -355,21 +355,21 @@ class VaultSdkSourceTest {
result,
)
coVerify {
clientCrypto.derivePinKey(pin)
clientCrypto.enrollPin(pin)
}
coVerify { sdkClientManager.getOrCreateClient(userId = userId) }
}
@Test
fun `derivePinProtectedUserKey should call SDK and return a Result with the correct data`() =
fun `enrollPinWithEncryptedPin should call SDK and return a Result with the correct data`() =
runBlocking {
val userId = "userId"
val encryptedPin = "encryptedPin"
val expectedResult = "pinProtectedUserKey"
val expectedResult = mockk<EnrollPinResponse>()
coEvery {
clientCrypto.derivePinUserKey(encryptedPin = encryptedPin)
clientCrypto.enrollPinWithEncryptedPin(encryptedPin = encryptedPin)
} returns expectedResult
val result = vaultSdkSource.derivePinProtectedUserKey(
val result = vaultSdkSource.enrollPinWithEncryptedPin(
userId = userId,
encryptedPin = encryptedPin,
)
@ -378,7 +378,7 @@ class VaultSdkSourceTest {
result,
)
coVerify {
clientCrypto.derivePinUserKey(encryptedPin = encryptedPin)
clientCrypto.enrollPinWithEncryptedPin(encryptedPin = encryptedPin)
}
coVerify { sdkClientManager.getOrCreateClient(userId = userId) }
}

View File

@ -5,6 +5,7 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import app.cash.turbine.test
import com.bitwarden.core.EnrollPinResponse
import com.bitwarden.core.InitOrgCryptoRequest
import com.bitwarden.core.InitUserCryptoMethod
import com.bitwarden.core.InitUserCryptoRequest
@ -1635,6 +1636,134 @@ class VaultLockManagerTest {
}
}
@Suppress("MaxLineLength")
@Test
fun `unlockVault with initializeCrypto success should migrate pinProtectedUserKey`() =
runTest {
val kdf = MOCK_PROFILE.toSdkParams()
val email = MOCK_PROFILE.email
val masterPassword = "drowssap"
val userKey = "12345"
val privateKey = "54321"
val organizationKeys = mapOf("orgId1" to "orgKey1")
val userKeyEncryptedPin = "encryptedPin"
val pinProtectedUserKeyEnvelope = "pinProtectedUserKeyEnvelope"
val enrollResponse = EnrollPinResponse(
pinProtectedUserKeyEnvelope = pinProtectedUserKeyEnvelope,
userKeyEncryptedPin = userKeyEncryptedPin,
)
coEvery {
vaultSdkSource.initializeCrypto(
userId = USER_ID,
request = InitUserCryptoRequest(
userId = USER_ID,
kdfParams = kdf,
email = email,
privateKey = privateKey,
method = InitUserCryptoMethod.Password(
password = masterPassword,
userKey = userKey,
),
signingKey = null,
securityState = null,
),
)
} returns InitializeCryptoResult.Success.asSuccess()
coEvery {
vaultSdkSource.initializeOrganizationCrypto(
userId = USER_ID,
request = InitOrgCryptoRequest(organizationKeys = organizationKeys),
)
} returns InitializeCryptoResult.Success.asSuccess()
coEvery {
vaultSdkSource.enrollPinWithEncryptedPin(
userId = USER_ID,
encryptedPin = userKeyEncryptedPin,
)
} returns enrollResponse.asSuccess()
coEvery {
trustedDeviceManager.trustThisDeviceIfNecessary(userId = USER_ID)
} returns false.asSuccess()
assertEquals(
emptyList<VaultUnlockData>(),
vaultLockManager.vaultUnlockDataStateFlow.value,
)
mutableVaultTimeoutStateFlow.value = VaultTimeout.ThirtyMinutes
fakeAuthDiskSource.storeUserAutoUnlockKey(
userId = USER_ID,
userAutoUnlockKey = null,
)
fakeAuthDiskSource.storeEncryptedPin(
userId = USER_ID,
encryptedPin = userKeyEncryptedPin,
)
fakeAuthDiskSource.storePinProtectedUserKey(
userId = USER_ID,
pinProtectedUserKey = userKeyEncryptedPin,
)
val result = vaultLockManager.unlockVault(
userId = USER_ID,
email = email,
kdf = kdf,
privateKey = privateKey,
signingKey = null,
securityState = null,
initUserCryptoMethod = InitUserCryptoMethod.Password(
password = masterPassword,
userKey = userKey,
),
organizationKeys = organizationKeys,
)
assertEquals(VaultUnlockResult.Success, result)
assertEquals(
listOf(
VaultUnlockData(
userId = USER_ID,
status = VaultUnlockData.Status.UNLOCKED,
),
),
vaultLockManager.vaultUnlockDataStateFlow.value,
)
fakeAuthDiskSource.assertUserAutoUnlockKey(
userId = USER_ID,
userAutoUnlockKey = null,
)
fakeAuthDiskSource.assertMasterPasswordHash(
userId = USER_ID,
passwordHash = "hashedPassword",
)
fakeAuthDiskSource.assertPinProtectedUserKeyEnvelope(
userId = USER_ID,
pinProtectedUserKeyEnvelope = pinProtectedUserKeyEnvelope,
inMemoryOnly = false,
)
coVerify(exactly = 1) {
vaultSdkSource.initializeCrypto(
userId = USER_ID,
request = InitUserCryptoRequest(
userId = USER_ID,
kdfParams = kdf,
email = email,
privateKey = privateKey,
method = InitUserCryptoMethod.Password(
password = masterPassword,
userKey = userKey,
),
signingKey = null,
securityState = null,
),
)
vaultSdkSource.initializeOrganizationCrypto(
userId = USER_ID,
request = InitOrgCryptoRequest(organizationKeys = organizationKeys),
)
trustedDeviceManager.trustThisDeviceIfNecessary(userId = USER_ID)
}
}
/**
* Resets the verification call count for the given [mock] while leaving all other mocked
* behavior in place.

View File

@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.vault.repository
import app.cash.turbine.test
import com.bitwarden.collections.CollectionView
import com.bitwarden.core.DateTime
import com.bitwarden.core.EnrollPinResponse
import com.bitwarden.core.InitUserCryptoMethod
import com.bitwarden.core.data.repository.model.DataState
import com.bitwarden.core.data.util.asFailure
@ -233,7 +234,7 @@ class VaultRepositoryTest {
)
}
coVerify(exactly = 0) {
vaultSdkSource.derivePinProtectedUserKey(any(), any())
vaultSdkSource.enrollPinWithEncryptedPin(any(), any())
}
fakeAuthDiskSource.apply {
assertBiometricsKey(
@ -271,7 +272,7 @@ class VaultRepositoryTest {
result,
)
coVerify(exactly = 0) {
vaultSdkSource.derivePinProtectedUserKey(any(), any())
vaultSdkSource.enrollPinWithEncryptedPin(any(), any())
}
}
@ -298,7 +299,7 @@ class VaultRepositoryTest {
result,
)
coVerify(exactly = 0) {
vaultSdkSource.derivePinProtectedUserKey(any(), any())
vaultSdkSource.enrollPinWithEncryptedPin(any(), any())
}
}
@ -353,7 +354,7 @@ class VaultRepositoryTest {
)
}
coVerify(exactly = 0) {
vaultSdkSource.derivePinProtectedUserKey(any(), any())
vaultSdkSource.enrollPinWithEncryptedPin(any(), any())
}
}
@ -364,7 +365,11 @@ class VaultRepositoryTest {
val userId = MOCK_USER_STATE.activeUserId
val encryptedPin = "encryptedPin"
val privateKey = "mockPrivateKey-1"
val pinProtectedUserKey = "pinProtectedUserkey"
val pinProtectedUserKeyEnvelope = "pinProtectedUserKeyEnvelope"
val enrollResponse = EnrollPinResponse(
pinProtectedUserKeyEnvelope = pinProtectedUserKeyEnvelope,
userKeyEncryptedPin = encryptedPin,
)
val biometricsKey = "asdf1234"
fakeAuthDiskSource.userState = MOCK_USER_STATE
val encryptedBytes = byteArrayOf(1, 1)
@ -374,11 +379,11 @@ class VaultRepositoryTest {
every { iv } returns initVector
}
coEvery {
vaultSdkSource.derivePinProtectedUserKey(
vaultSdkSource.enrollPinWithEncryptedPin(
userId = userId,
encryptedPin = encryptedPin,
)
} returns pinProtectedUserKey.asSuccess()
} returns enrollResponse.asSuccess()
coEvery {
vaultLockManager.unlockVault(
userId = userId,
@ -403,6 +408,11 @@ class VaultRepositoryTest {
pinProtectedUserKey = null,
inMemoryOnly = true,
)
storePinProtectedUserKeyEnvelope(
userId = userId,
pinProtectedUserKeyEnvelope = null,
inMemoryOnly = true,
)
}
val result = vaultRepository.unlockVaultWithBiometrics(cipher = cipher)
@ -410,7 +420,12 @@ class VaultRepositoryTest {
assertEquals(VaultUnlockResult.Success, result)
fakeAuthDiskSource.assertPinProtectedUserKey(
userId = userId,
pinProtectedUserKey = pinProtectedUserKey,
pinProtectedUserKey = null,
inMemoryOnly = true,
)
fakeAuthDiskSource.assertPinProtectedUserKeyEnvelope(
userId = userId,
pinProtectedUserKeyEnvelope = pinProtectedUserKeyEnvelope,
inMemoryOnly = true,
)
coVerify {
@ -428,7 +443,7 @@ class VaultRepositoryTest {
)
}
coEvery {
vaultSdkSource.derivePinProtectedUserKey(
vaultSdkSource.enrollPinWithEncryptedPin(
userId = userId,
encryptedPin = encryptedPin,
)
@ -603,9 +618,15 @@ class VaultRepositoryTest {
runTest {
val userId = "mockId-1"
val mockVaultUnlockResult = VaultUnlockResult.Success
val userKeyEncryptedPin = "encryptedPin"
val pinProtectedUserKeyEnvelope = "pinProtectedUserKeyEnvelope"
val enrollResponse = EnrollPinResponse(
pinProtectedUserKeyEnvelope = pinProtectedUserKeyEnvelope,
userKeyEncryptedPin = userKeyEncryptedPin,
)
coEvery {
vaultSdkSource.derivePinProtectedUserKey(any(), any())
} returns "pinProtectedUserKey".asSuccess()
vaultSdkSource.enrollPinWithEncryptedPin(any(), any())
} returns enrollResponse.asSuccess()
prepareStateForUnlocking(unlockResult = mockVaultUnlockResult)
fakeAuthDiskSource.apply {
storeEncryptedPin(
@ -617,6 +638,11 @@ class VaultRepositoryTest {
pinProtectedUserKey = null,
inMemoryOnly = true,
)
storePinProtectedUserKeyEnvelope(
userId = userId,
pinProtectedUserKeyEnvelope = null,
inMemoryOnly = true,
)
}
val result = vaultRepository.unlockVaultWithMasterPassword(
@ -642,7 +668,7 @@ class VaultRepositoryTest {
organizationKeys = createMockOrganizationKeys(number = 1),
)
}
coVerify(exactly = 0) { vaultSdkSource.derivePinProtectedUserKey(any(), any()) }
coVerify(exactly = 0) { vaultSdkSource.enrollPinWithEncryptedPin(any(), any()) }
}
@Suppress("MaxLineLength")
@ -650,15 +676,19 @@ class VaultRepositoryTest {
fun `unlockVaultWithMasterPassword with VaultLockManager Success and a stored encrypted pin should unlock for the current user, derive a new pin-protected key, and return Success`() =
runTest {
val userId = "mockId-1"
val encryptedPin = "encryptedPin"
val pinProtectedUserKey = "pinProtectedUserkey"
val pinProtectedUserKey = "pinProtectedUserkeyEnvelope"
val encryptedPin = "userKeyEncryptedPin"
val enrollResponse = EnrollPinResponse(
pinProtectedUserKeyEnvelope = pinProtectedUserKey,
userKeyEncryptedPin = encryptedPin,
)
val mockVaultUnlockResult = VaultUnlockResult.Success
coEvery {
vaultSdkSource.derivePinProtectedUserKey(
vaultSdkSource.enrollPinWithEncryptedPin(
userId = userId,
encryptedPin = encryptedPin,
)
} returns pinProtectedUserKey.asSuccess()
} returns enrollResponse.asSuccess()
prepareStateForUnlocking(unlockResult = mockVaultUnlockResult)
fakeAuthDiskSource.apply {
storeEncryptedPin(
@ -670,6 +700,11 @@ class VaultRepositoryTest {
pinProtectedUserKey = null,
inMemoryOnly = true,
)
storePinProtectedUserKeyEnvelope(
userId = userId,
pinProtectedUserKeyEnvelope = null,
inMemoryOnly = true,
)
}
val result = vaultRepository.unlockVaultWithMasterPassword(
@ -682,7 +717,12 @@ class VaultRepositoryTest {
)
fakeAuthDiskSource.assertPinProtectedUserKey(
userId = userId,
pinProtectedUserKey = pinProtectedUserKey,
pinProtectedUserKey = null,
inMemoryOnly = true,
)
fakeAuthDiskSource.assertPinProtectedUserKeyEnvelope(
userId = userId,
pinProtectedUserKeyEnvelope = pinProtectedUserKey,
inMemoryOnly = true,
)
coVerify {
@ -701,7 +741,7 @@ class VaultRepositoryTest {
)
}
coEvery {
vaultSdkSource.derivePinProtectedUserKey(
vaultSdkSource.enrollPinWithEncryptedPin(
userId = userId,
encryptedPin = encryptedPin,
)
@ -762,6 +802,10 @@ class VaultRepositoryTest {
userId = "mockId-1",
pinProtectedUserKey = null,
)
fakeAuthDiskSource.storePinProtectedUserKeyEnvelope(
userId = "mockId-1",
pinProtectedUserKeyEnvelope = null,
)
fakeAuthDiskSource.storePrivateKey(
userId = "mockId-1",
privateKey = "mockPrivateKey-1",
@ -780,6 +824,10 @@ class VaultRepositoryTest {
userId = "mockId-1",
pinProtectedUserKey = "mockKey-1",
)
fakeAuthDiskSource.storePinProtectedUserKeyEnvelope(
userId = "mockId-1",
pinProtectedUserKeyEnvelope = null,
)
fakeAuthDiskSource.storePrivateKey(
userId = "mockId-1",
privateKey = null,
@ -801,6 +849,41 @@ class VaultRepositoryTest {
val result = vaultRepository.unlockVaultWithPin(pin = "1234")
assertEquals(
mockVaultUnlockResult,
result,
)
coVerify {
vaultLockManager.unlockVault(
userId = userId,
email = "email",
kdf = MOCK_PROFILE.toSdkParams(),
privateKey = "mockPrivateKey-1",
signingKey = null,
securityState = null,
initUserCryptoMethod = InitUserCryptoMethod.PinEnvelope(
pin = "1234",
pinProtectedUserKeyEnvelope = "mockKey-1",
),
organizationKeys = createMockOrganizationKeys(number = 1),
)
}
}
@Suppress("MaxLineLength")
@Test
fun `unlockVaultWithPin with PinProtectedUserKeyEnvelope null and VaultLockManager Success should unlock with pin for the current user and return Success`() =
runTest {
val userId = "mockId-1"
val mockVaultUnlockResult = VaultUnlockResult.Success
prepareStateForUnlocking(unlockResult = mockVaultUnlockResult)
fakeAuthDiskSource.storePinProtectedUserKeyEnvelope(
userId = userId,
pinProtectedUserKeyEnvelope = null,
)
val result = vaultRepository.unlockVaultWithPin(pin = "1234")
assertEquals(
mockVaultUnlockResult,
result,
@ -844,9 +927,9 @@ class VaultRepositoryTest {
privateKey = "mockPrivateKey-1",
signingKey = null,
securityState = null,
initUserCryptoMethod = InitUserCryptoMethod.Pin(
initUserCryptoMethod = InitUserCryptoMethod.PinEnvelope(
pin = "1234",
pinProtectedUserKey = "mockKey-1",
pinProtectedUserKeyEnvelope = "mockKey-1",
),
organizationKeys = createMockOrganizationKeys(number = 1),
)
@ -1432,6 +1515,10 @@ class VaultRepositoryTest {
userId = userId,
pinProtectedUserKey = "mockKey-1",
)
fakeAuthDiskSource.storePinProtectedUserKeyEnvelope(
userId = userId,
pinProtectedUserKeyEnvelope = "mockKey-1",
)
fakeAuthDiskSource.storeOrganizationKeys(
userId = userId,
organizationKeys = createMockOrganizationKeys(number = 1),
@ -1471,6 +1558,23 @@ class VaultRepositoryTest {
organizationKeys = createMockOrganizationKeys(number = 1),
)
} returns unlockResult
// PIN ENVELOPE unlock
coEvery {
vaultLockManager.unlockVault(
userId = userId,
email = "email",
kdf = MOCK_PROFILE.toSdkParams(),
privateKey = "mockPrivateKey-1",
signingKey = null,
securityState = null,
initUserCryptoMethod = InitUserCryptoMethod.PinEnvelope(
pin = mockPin,
pinProtectedUserKeyEnvelope = "mockKey-1",
),
organizationKeys = createMockOrganizationKeys(number = 1),
)
} returns unlockResult
}
//endregion Helper functions
}