Update logic for handling the pin protected user key (#6169)

This commit is contained in:
David Perez 2025-11-18 09:32:39 -06:00 committed by GitHub
parent 169b21cfdb
commit 794b27a750
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 463 additions and 385 deletions

View File

@ -145,9 +145,6 @@ class AuthDiskSourceImpl(
storeInvalidUnlockAttempts(userId = userId, invalidUnlockAttempts = null) storeInvalidUnlockAttempts(userId = userId, invalidUnlockAttempts = null)
storeUserKey(userId = userId, userKey = null) storeUserKey(userId = userId, userKey = null)
storeUserAutoUnlockKey(userId = userId, userAutoUnlockKey = 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) storePrivateKey(userId = userId, privateKey = null)
storeAccountKeys(userId = userId, accountKeys = null) storeAccountKeys(userId = userId, accountKeys = null)
storeOrganizationKeys(userId = userId, organizationKeys = null) storeOrganizationKeys(userId = userId, organizationKeys = null)
@ -163,9 +160,13 @@ class AuthDiskSourceImpl(
storeShowImportLogins(userId = userId, showImportLogins = null) storeShowImportLogins(userId = userId, showImportLogins = null)
storeLastLockTimestamp(userId = userId, lastLockTimestamp = null) storeLastLockTimestamp(userId = userId, lastLockTimestamp = null)
// Do not remove the DeviceKey or PendingAuthRequest on logout, these are persisted // Certain values are never removed as required by the feature requirements:
// indefinitely unless the TDE flow explicitly removes them. // * EncryptedPin
// Do not remove OnboardingStatus we want to keep track of this even after logout. // * PinProtectedUserKey
// * PinProtectedUserKeyEnvelope
// * DeviceKey
// * PendingAuthRequest
// * OnboardingStatus
} }
override fun getAuthenticatorSyncUnlockKey(userId: String): String? = override fun getAuthenticatorSyncUnlockKey(userId: String): String? =

View File

@ -77,16 +77,10 @@ class UserLogoutManagerImpl(
if (isExpired) { if (isExpired) {
showToast(message = BitwardenString.login_expired) showToast(message = BitwardenString.login_expired)
} }
authDiskSource.storeAccountTokens(
userId = userId,
accountTokens = null,
)
// Save any data that will still need to be retained after otherwise clearing all dat // Save any data that will still need to be retained after otherwise clearing all dat
val vaultTimeoutInMinutes = settingsDiskSource.getVaultTimeoutInMinutes(userId = userId) val vaultTimeoutInMinutes = settingsDiskSource.getVaultTimeoutInMinutes(userId = userId)
val vaultTimeoutAction = settingsDiskSource.getVaultTimeoutAction(userId = userId) val vaultTimeoutAction = settingsDiskSource.getVaultTimeoutAction(userId = userId)
val pinProtectedUserKeyEnvelope = authDiskSource
.getPinProtectedUserKeyEnvelope(userId = userId)
switchUserIfAvailable( switchUserIfAvailable(
currentUserId = userId, currentUserId = userId,
@ -108,10 +102,6 @@ class UserLogoutManagerImpl(
vaultTimeoutAction = vaultTimeoutAction, vaultTimeoutAction = vaultTimeoutAction,
) )
} }
authDiskSource.storePinProtectedUserKeyEnvelope(
userId = userId,
pinProtectedUserKeyEnvelope = pinProtectedUserKeyEnvelope,
)
} }
private fun clearData(userId: String) { private fun clearData(userId: String) {

View File

@ -0,0 +1,30 @@
package com.x8bit.bitwarden.data.vault.manager
/**
* Manager class to manage the pin-protected user key.
*/
interface PinProtectedUserKeyManager {
/**
* Checks if the given [userId] has an associated encrypted PIN key but not a pin-protected
* user key. This indicates a scenario in which a user has requested PIN unlocking but requires
* master-password unlocking on app restart. This function may then be called after such an
* unlock to derive a pin-protected user key and store it in memory for use for any subsequent
* unlocks during this current app session.
*
* If the user's vault has not yet been unlocked, this call will do nothing.
*
* @param userId The ID of the user to check.
*/
suspend fun deriveTemporaryPinProtectedUserKeyIfNecessary(userId: String)
/**
* 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.
*/
suspend fun migratePinProtectedUserKeyIfNeeded(userId: String)
}

View File

@ -0,0 +1,95 @@
package com.x8bit.bitwarden.data.vault.manager
import com.bitwarden.core.EnrollPinResponse
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
import timber.log.Timber
/**
* The default implementation of the [PinProtectedUserKeyManager].
*/
internal class PinProtectedUserKeyManagerImpl(
private val authDiskSource: AuthDiskSource,
private val vaultSdkSource: VaultSdkSource,
) : PinProtectedUserKeyManager {
override suspend fun deriveTemporaryPinProtectedUserKeyIfNecessary(userId: String) {
val encryptedPin = authDiskSource.getEncryptedPin(userId = userId) ?: return
if (authDiskSource.getPinProtectedUserKeyEnvelope(userId = userId) != null) return
this
.enrollPinWithEncryptedPin(
userId = userId,
encryptedPin = encryptedPin,
inMemoryOnly = true,
)
.onSuccess {
Timber.d("[Auth] Set PIN-protected user key in memory")
}
}
override suspend fun migratePinProtectedUserKeyIfNeeded(userId: String) {
val encryptedPin = authDiskSource.getEncryptedPin(userId = userId) ?: return
if (authDiskSource.getPinProtectedUserKeyEnvelope(userId = userId) != null) return
val inMemoryOnly = authDiskSource.getPinProtectedUserKey(userId = userId) == null
this
.enrollPinWithEncryptedPin(
userId = userId,
encryptedPin = encryptedPin,
inMemoryOnly = inMemoryOnly,
)
.onSuccess {
if (inMemoryOnly) {
Timber.d("[Auth] Set PIN-protected user key in memory")
} else {
Timber.d("[Auth] Migrated from legacy PIN to PIN-protected user key envelope")
}
}
}
private suspend fun enrollPinWithEncryptedPin(
userId: String,
encryptedPin: String,
inMemoryOnly: Boolean,
): Result<EnrollPinResponse> =
vaultSdkSource
.enrollPinWithEncryptedPin(userId = userId, encryptedPin = encryptedPin)
.onSuccess { enrollPinResponse ->
storePinData(
userId = userId,
encryptedPin = enrollPinResponse.userKeyEncryptedPin,
pinProtectedUserKeyEnvelope = enrollPinResponse.pinProtectedUserKeyEnvelope,
inMemoryOnly = inMemoryOnly,
)
}
.onFailure {
storePinData(
userId = userId,
encryptedPin = null,
pinProtectedUserKeyEnvelope = null,
inMemoryOnly = false,
)
}
private fun storePinData(
userId: String,
encryptedPin: String?,
pinProtectedUserKeyEnvelope: String?,
inMemoryOnly: Boolean,
) {
authDiskSource.storeEncryptedPin(userId = userId, encryptedPin = encryptedPin)
authDiskSource.storePinProtectedUserKeyEnvelope(
userId = userId,
pinProtectedUserKeyEnvelope = pinProtectedUserKeyEnvelope,
inMemoryOnly = inMemoryOnly,
)
// This property is deprecated and we should be migrated to the PinProtectedUserKeyEnvelope.
// Because of this, we always clear this value and it should always be cleared at the disk
// level, not the in-memory level.
authDiskSource.storePinProtectedUserKey(
userId = userId,
pinProtectedUserKey = null,
inMemoryOnly = false,
)
}
}

View File

@ -91,6 +91,7 @@ class VaultLockManagerImpl(
private val userLogoutManager: UserLogoutManager, private val userLogoutManager: UserLogoutManager,
private val trustedDeviceManager: TrustedDeviceManager, private val trustedDeviceManager: TrustedDeviceManager,
private val kdfManager: KdfManager, private val kdfManager: KdfManager,
private val pinProtectedUserKeyManager: PinProtectedUserKeyManager,
dispatcherManager: DispatcherManager, dispatcherManager: DispatcherManager,
context: Context, context: Context,
) : VaultLockManager { ) : VaultLockManager {
@ -234,7 +235,8 @@ class VaultLockManagerImpl(
trustedDeviceManager trustedDeviceManager
.trustThisDeviceIfNecessary(userId = userId) .trustThisDeviceIfNecessary(userId = userId)
updateKdfIfNeeded(initUserCryptoMethod) updateKdfIfNeeded(initUserCryptoMethod)
migratePinProtectedUserKeyIfNeeded(userId = userId) pinProtectedUserKeyManager
.migratePinProtectedUserKeyIfNeeded(userId = userId)
setVaultToUnlocked(userId = userId) setVaultToUnlocked(userId = userId)
} else { } else {
incrementInvalidUnlockCount(userId = userId) incrementInvalidUnlockCount(userId = userId)
@ -308,47 +310,6 @@ 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,
)
if (inMemoryOnly) {
Timber.d("[Auth] Set PIN-protected user key in memory")
} else {
Timber.d("[Auth] Migrated from legacy PIN to PIN-protected user key envelope")
}
}
}
/** /**
* Increments the stored invalid unlock count for the given [userId] and automatically logs out * 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]. * if this new value is greater than [MAXIMUM_INVALID_UNLOCK_ATTEMPTS].

View File

@ -31,6 +31,8 @@ import com.x8bit.bitwarden.data.vault.manager.FileManager
import com.x8bit.bitwarden.data.vault.manager.FileManagerImpl import com.x8bit.bitwarden.data.vault.manager.FileManagerImpl
import com.x8bit.bitwarden.data.vault.manager.FolderManager import com.x8bit.bitwarden.data.vault.manager.FolderManager
import com.x8bit.bitwarden.data.vault.manager.FolderManagerImpl import com.x8bit.bitwarden.data.vault.manager.FolderManagerImpl
import com.x8bit.bitwarden.data.vault.manager.PinProtectedUserKeyManager
import com.x8bit.bitwarden.data.vault.manager.PinProtectedUserKeyManagerImpl
import com.x8bit.bitwarden.data.vault.manager.SendManager import com.x8bit.bitwarden.data.vault.manager.SendManager
import com.x8bit.bitwarden.data.vault.manager.SendManagerImpl import com.x8bit.bitwarden.data.vault.manager.SendManagerImpl
import com.x8bit.bitwarden.data.vault.manager.TotpCodeManager import com.x8bit.bitwarden.data.vault.manager.TotpCodeManager
@ -146,6 +148,7 @@ object VaultManagerModule {
dispatcherManager: DispatcherManager, dispatcherManager: DispatcherManager,
trustedDeviceManager: TrustedDeviceManager, trustedDeviceManager: TrustedDeviceManager,
kdfManager: KdfManager, kdfManager: KdfManager,
pinProtectedUserKeyManager: PinProtectedUserKeyManager,
): VaultLockManager = ): VaultLockManager =
VaultLockManagerImpl( VaultLockManagerImpl(
context = context, context = context,
@ -160,6 +163,18 @@ object VaultManagerModule {
dispatcherManager = dispatcherManager, dispatcherManager = dispatcherManager,
trustedDeviceManager = trustedDeviceManager, trustedDeviceManager = trustedDeviceManager,
kdfManager = kdfManager, kdfManager = kdfManager,
pinProtectedUserKeyManager = pinProtectedUserKeyManager,
)
@Provides
@Singleton
fun providePinProtectedUserKeyManager(
authDiskSource: AuthDiskSource,
vaultSdkSource: VaultSdkSource,
): PinProtectedUserKeyManager =
PinProtectedUserKeyManagerImpl(
authDiskSource = authDiskSource,
vaultSdkSource = vaultSdkSource,
) )
@Provides @Provides

View File

@ -28,6 +28,7 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
import com.x8bit.bitwarden.data.vault.manager.CipherManager import com.x8bit.bitwarden.data.vault.manager.CipherManager
import com.x8bit.bitwarden.data.vault.manager.CredentialExchangeImportManager import com.x8bit.bitwarden.data.vault.manager.CredentialExchangeImportManager
import com.x8bit.bitwarden.data.vault.manager.FolderManager import com.x8bit.bitwarden.data.vault.manager.FolderManager
import com.x8bit.bitwarden.data.vault.manager.PinProtectedUserKeyManager
import com.x8bit.bitwarden.data.vault.manager.SendManager import com.x8bit.bitwarden.data.vault.manager.SendManager
import com.x8bit.bitwarden.data.vault.manager.TotpCodeManager import com.x8bit.bitwarden.data.vault.manager.TotpCodeManager
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
@ -40,7 +41,6 @@ import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult
import com.x8bit.bitwarden.data.vault.repository.model.ImportCredentialsResult import com.x8bit.bitwarden.data.vault.repository.model.ImportCredentialsResult
import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
import com.x8bit.bitwarden.data.vault.repository.util.logTag
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCipher import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCipher
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkFolder import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkFolder
import com.x8bit.bitwarden.data.vault.repository.util.toSdkAccount import com.x8bit.bitwarden.data.vault.repository.util.toSdkAccount
@ -79,6 +79,7 @@ class VaultRepositoryImpl(
private val totpCodeManager: TotpCodeManager, private val totpCodeManager: TotpCodeManager,
private val vaultSyncManager: VaultSyncManager, private val vaultSyncManager: VaultSyncManager,
private val credentialExchangeImportManager: CredentialExchangeImportManager, private val credentialExchangeImportManager: CredentialExchangeImportManager,
private val pinProtectedUserKeyManager: PinProtectedUserKeyManager,
dispatcherManager: DispatcherManager, dispatcherManager: DispatcherManager,
) : VaultRepository, ) : VaultRepository,
CipherManager by cipherManager, CipherManager by cipherManager,
@ -328,11 +329,8 @@ class VaultRepositoryImpl(
) )
authDiskSource.storeUserBiometricInitVector(userId = userId, iv = cipher.iv) authDiskSource.storeUserBiometricInitVector(userId = userId, iv = cipher.iv)
} }
deriveTemporaryPinProtectedUserKeyIfNecessary( pinProtectedUserKeyManager.deriveTemporaryPinProtectedUserKeyIfNecessary(
userId = userId, userId = userId,
initUserCryptoMethod = InitUserCryptoMethod.DecryptedKey(
decryptedUserKey = decryptedUserKey,
),
) )
} }
} }
@ -369,9 +367,8 @@ class VaultRepositoryImpl(
) )
.also { .also {
if (it is VaultUnlockResult.Success) { if (it is VaultUnlockResult.Success) {
deriveTemporaryPinProtectedUserKeyIfNecessary( pinProtectedUserKeyManager.deriveTemporaryPinProtectedUserKeyIfNecessary(
userId = userId, userId = userId,
initUserCryptoMethod = initUserCryptoMethod,
) )
} }
} }
@ -530,54 +527,6 @@ class VaultRepositoryImpl(
) )
} }
/**
* Checks if the given [userId] has an associated encrypted PIN key but not a pin-protected user
* key. This indicates a scenario in which a user has requested PIN unlocking but requires
* master-password unlocking on app restart. This function may then be called after such an
* unlock to derive a pin-protected user key and store it in memory for use for any subsequent
* unlocks during this current app session.
*
* If the user's vault has not yet been unlocked, this call will do nothing.
*
* @param userId The ID of the user to check.
* @param initUserCryptoMethod The method used to initialize the user's crypto.
*/
private suspend fun deriveTemporaryPinProtectedUserKeyIfNecessary(
userId: String,
initUserCryptoMethod: InitUserCryptoMethod,
) {
Timber.d("[Auth] Vault unlocked, method: ${initUserCryptoMethod.logTag}")
val encryptedPin = authDiskSource.getEncryptedPin(userId = userId) ?: return
val existingPinProtectedUserKeyEnvelope = authDiskSource
.getPinProtectedUserKeyEnvelope(
userId = userId,
)
if (existingPinProtectedUserKeyEnvelope != null) return
vaultSdkSource
.enrollPinWithEncryptedPin(
userId = userId,
encryptedPin = encryptedPin,
)
.onSuccess { enrollPinResponse ->
authDiskSource.storeEncryptedPin(
userId = userId,
encryptedPin = enrollPinResponse.userKeyEncryptedPin,
)
authDiskSource.storePinProtectedUserKeyEnvelope(
userId = userId,
pinProtectedUserKeyEnvelope = enrollPinResponse.pinProtectedUserKeyEnvelope,
inMemoryOnly = true,
)
authDiskSource.storePinProtectedUserKey(
userId = userId,
pinProtectedUserKey = null,
inMemoryOnly = true,
)
Timber.d("[Auth] Set PIN-protected user key in memory")
}
}
private suspend fun unlockVaultForUser( private suspend fun unlockVaultForUser(
userId: String, userId: String,
initUserCryptoMethod: InitUserCryptoMethod, initUserCryptoMethod: InitUserCryptoMethod,

View File

@ -7,6 +7,7 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
import com.x8bit.bitwarden.data.vault.manager.CipherManager import com.x8bit.bitwarden.data.vault.manager.CipherManager
import com.x8bit.bitwarden.data.vault.manager.CredentialExchangeImportManager import com.x8bit.bitwarden.data.vault.manager.CredentialExchangeImportManager
import com.x8bit.bitwarden.data.vault.manager.FolderManager import com.x8bit.bitwarden.data.vault.manager.FolderManager
import com.x8bit.bitwarden.data.vault.manager.PinProtectedUserKeyManager
import com.x8bit.bitwarden.data.vault.manager.SendManager import com.x8bit.bitwarden.data.vault.manager.SendManager
import com.x8bit.bitwarden.data.vault.manager.TotpCodeManager import com.x8bit.bitwarden.data.vault.manager.TotpCodeManager
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
@ -40,6 +41,7 @@ object VaultRepositoryModule {
totpCodeManager: TotpCodeManager, totpCodeManager: TotpCodeManager,
vaultSyncManager: VaultSyncManager, vaultSyncManager: VaultSyncManager,
credentialExchangeImportManager: CredentialExchangeImportManager, credentialExchangeImportManager: CredentialExchangeImportManager,
pinProtectedUserKeyManager: PinProtectedUserKeyManager,
): VaultRepository = VaultRepositoryImpl( ): VaultRepository = VaultRepositoryImpl(
vaultDiskSource = vaultDiskSource, vaultDiskSource = vaultDiskSource,
vaultSdkSource = vaultSdkSource, vaultSdkSource = vaultSdkSource,
@ -52,5 +54,6 @@ object VaultRepositoryModule {
totpCodeManager = totpCodeManager, totpCodeManager = totpCodeManager,
vaultSyncManager = vaultSyncManager, vaultSyncManager = vaultSyncManager,
credentialExchangeImportManager = credentialExchangeImportManager, credentialExchangeImportManager = credentialExchangeImportManager,
pinProtectedUserKeyManager = pinProtectedUserKeyManager,
) )
} }

View File

@ -267,9 +267,15 @@ class AuthDiskSourceTest {
userId = userId, userId = userId,
biometricsKey = "1234-9876-0192", biometricsKey = "1234-9876-0192",
) )
val pinProtectedUserKey = "pinProtectedUserKey"
authDiskSource.storePinProtectedUserKey( authDiskSource.storePinProtectedUserKey(
userId = userId, userId = userId,
pinProtectedUserKey = "pinProtectedUserKey", pinProtectedUserKey = pinProtectedUserKey,
)
val pinProtectedUserKeyEnvelope = "pinProtectedUserKeyEnvelope"
authDiskSource.storePinProtectedUserKeyEnvelope(
userId = userId,
pinProtectedUserKeyEnvelope = pinProtectedUserKeyEnvelope,
) )
authDiskSource.storeInvalidUnlockAttempts( authDiskSource.storeInvalidUnlockAttempts(
userId = userId, userId = userId,
@ -304,7 +310,8 @@ class AuthDiskSourceTest {
refreshToken = "refreshToken", refreshToken = "refreshToken",
), ),
) )
authDiskSource.storeEncryptedPin(userId = userId, encryptedPin = "encryptedPin") val encryptedPin = "encryptedPin"
authDiskSource.storeEncryptedPin(userId = userId, encryptedPin = encryptedPin)
authDiskSource.storeMasterPasswordHash(userId = userId, passwordHash = "passwordHash") authDiskSource.storeMasterPasswordHash(userId = userId, passwordHash = "passwordHash")
authDiskSource.storeAuthenticatorSyncUnlockKey( authDiskSource.storeAuthenticatorSyncUnlockKey(
userId = userId, userId = userId,
@ -326,12 +333,16 @@ class AuthDiskSourceTest {
OnboardingStatus.AUTOFILL_SETUP, OnboardingStatus.AUTOFILL_SETUP,
authDiskSource.getOnboardingStatus(userId = userId), authDiskSource.getOnboardingStatus(userId = userId),
) )
assertEquals(encryptedPin, authDiskSource.getEncryptedPin(userId = userId))
assertEquals(pinProtectedUserKey, authDiskSource.getPinProtectedUserKey(userId = userId))
assertEquals(
pinProtectedUserKeyEnvelope,
authDiskSource.getPinProtectedUserKeyEnvelope(userId = userId),
)
// These should be cleared // These should be cleared
assertNull(authDiskSource.getUserBiometricInitVector(userId = userId)) assertNull(authDiskSource.getUserBiometricInitVector(userId = userId))
assertNull(authDiskSource.getUserBiometricUnlockKey(userId = userId)) assertNull(authDiskSource.getUserBiometricUnlockKey(userId = userId))
assertNull(authDiskSource.getPinProtectedUserKey(userId = userId))
assertNull(authDiskSource.getPinProtectedUserKeyEnvelope(userId = userId))
assertNull(authDiskSource.getInvalidUnlockAttempts(userId = userId)) assertNull(authDiskSource.getInvalidUnlockAttempts(userId = userId))
assertNull(authDiskSource.getUserKey(userId = userId)) assertNull(authDiskSource.getUserKey(userId = userId))
assertNull(authDiskSource.getUserAutoUnlockKey(userId = userId)) assertNull(authDiskSource.getUserAutoUnlockKey(userId = userId))
@ -341,7 +352,6 @@ class AuthDiskSourceTest {
assertNull(authDiskSource.getOrganizations(userId = userId)) assertNull(authDiskSource.getOrganizations(userId = userId))
assertNull(authDiskSource.getPolicies(userId = userId)) assertNull(authDiskSource.getPolicies(userId = userId))
assertNull(authDiskSource.getAccountTokens(userId = userId)) assertNull(authDiskSource.getAccountTokens(userId = userId))
assertNull(authDiskSource.getEncryptedPin(userId = userId))
assertNull(authDiskSource.getMasterPasswordHash(userId = userId)) assertNull(authDiskSource.getMasterPasswordHash(userId = userId))
assertNull(authDiskSource.getShouldUseKeyConnector(userId = userId)) assertNull(authDiskSource.getShouldUseKeyConnector(userId = userId))
assertNull(authDiskSource.getIsTdeLoginComplete(userId = userId)) assertNull(authDiskSource.getIsTdeLoginComplete(userId = userId))

View File

@ -84,8 +84,6 @@ class FakeAuthDiskSource : AuthDiskSource {
storedPrivateKeys.remove(userId) storedPrivateKeys.remove(userId)
storedTwoFactorTokens.clear() storedTwoFactorTokens.clear()
storedUserAutoUnlockKeys.remove(userId) storedUserAutoUnlockKeys.remove(userId)
storedPinProtectedUserKeys.remove(userId)
storedEncryptedPins.remove(userId)
storedOrganizations.remove(userId) storedOrganizations.remove(userId)
storedPolicies.remove(userId) storedPolicies.remove(userId)
storedAccountTokens.remove(userId) storedAccountTokens.remove(userId)
@ -98,7 +96,6 @@ class FakeAuthDiskSource : AuthDiskSource {
mutableOrganizationsFlowMap.remove(userId) mutableOrganizationsFlowMap.remove(userId)
mutablePoliciesFlowMap.remove(userId) mutablePoliciesFlowMap.remove(userId)
mutableAccountTokensFlowMap.remove(userId) mutableAccountTokensFlowMap.remove(userId)
mutablePinProtectedUserKeyEnvelopesFlowMap.remove(userId)
} }
private fun getMutablePinProtectedUserKeyEnvelopeFlow( private fun getMutablePinProtectedUserKeyEnvelopeFlow(

View File

@ -33,13 +33,6 @@ import java.time.ZonedDateTime
@ExtendWith(MainDispatcherExtension::class) @ExtendWith(MainDispatcherExtension::class)
class UserLogoutManagerTest { class UserLogoutManagerTest {
private val authDiskSource: AuthDiskSource = mockk { private val authDiskSource: AuthDiskSource = mockk {
every { storeAccountTokens(userId = any(), accountTokens = null) } just runs
every {
storePinProtectedUserKeyEnvelope(
userId = any(),
pinProtectedUserKeyEnvelope = any(),
)
} just runs
every { userState = any() } just runs every { userState = any() } just runs
every { clearData(any()) } just runs every { clearData(any()) } just runs
} }
@ -149,7 +142,6 @@ class UserLogoutManagerTest {
userLogoutManager.softLogout(userId = userId, reason = LogoutReason.Timeout) userLogoutManager.softLogout(userId = userId, reason = LogoutReason.Timeout)
verify { authDiskSource.storeAccountTokens(userId = userId, accountTokens = null) }
assertDataCleared(userId = userId) assertDataCleared(userId = userId)
verify(exactly = 1) { verify(exactly = 1) {
@ -170,10 +162,6 @@ class UserLogoutManagerTest {
userId = userId, userId = userId,
vaultTimeoutAction = vaultTimeoutAction, vaultTimeoutAction = vaultTimeoutAction,
) )
authDiskSource.storePinProtectedUserKeyEnvelope(
userId = userId,
pinProtectedUserKeyEnvelope = pinProtectedUserKey,
)
} }
} }
@ -198,7 +186,6 @@ class UserLogoutManagerTest {
userLogoutManager.softLogout(userId = userId, reason = LogoutReason.Timeout) userLogoutManager.softLogout(userId = userId, reason = LogoutReason.Timeout)
verify(exactly = 1) { verify(exactly = 1) {
authDiskSource.storeAccountTokens(userId = userId, accountTokens = null)
authDiskSource.userState = UserStateJson( authDiskSource.userState = UserStateJson(
activeUserId = USER_ID_2, activeUserId = USER_ID_2,
accounts = MULTI_USER_STATE.accounts, accounts = MULTI_USER_STATE.accounts,
@ -212,10 +199,6 @@ class UserLogoutManagerTest {
userId = userId, userId = userId,
vaultTimeoutAction = vaultTimeoutAction, vaultTimeoutAction = vaultTimeoutAction,
) )
authDiskSource.storePinProtectedUserKeyEnvelope(
userId = userId,
pinProtectedUserKeyEnvelope = pinProtectedUserKey,
)
} }
} }
@ -241,7 +224,6 @@ class UserLogoutManagerTest {
userLogoutManager.softLogout(userId = userId, reason = LogoutReason.SecurityStamp) userLogoutManager.softLogout(userId = userId, reason = LogoutReason.SecurityStamp)
verify(exactly = 1) { verify(exactly = 1) {
authDiskSource.storeAccountTokens(userId = userId, accountTokens = null)
authDiskSource.userState = UserStateJson( authDiskSource.userState = UserStateJson(
activeUserId = USER_ID_2, activeUserId = USER_ID_2,
accounts = MULTI_USER_STATE.accounts, accounts = MULTI_USER_STATE.accounts,
@ -255,10 +237,6 @@ class UserLogoutManagerTest {
userId = userId, userId = userId,
vaultTimeoutAction = vaultTimeoutAction, vaultTimeoutAction = vaultTimeoutAction,
) )
authDiskSource.storePinProtectedUserKeyEnvelope(
userId = userId,
pinProtectedUserKeyEnvelope = pinProtectedUserKey,
)
} }
} }

View File

@ -0,0 +1,279 @@
package com.x8bit.bitwarden.data.vault.manager
import com.bitwarden.core.EnrollPinResponse
import com.bitwarden.core.data.util.asFailure
import com.bitwarden.core.data.util.asSuccess
import com.x8bit.bitwarden.data.auth.datasource.disk.util.FakeAuthDiskSource
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test
class PinProtectedUserKeyManagerTest {
private val fakeAuthDiskSource: FakeAuthDiskSource = FakeAuthDiskSource()
private val vaultSdkSource: VaultSdkSource = mockk()
private val pinProtectedUserKeyManager: PinProtectedUserKeyManager =
PinProtectedUserKeyManagerImpl(
authDiskSource = fakeAuthDiskSource,
vaultSdkSource = vaultSdkSource,
)
@Test
fun `deriveTemporaryPinProtectedUserKeyIfNecessary without encryptedKey does nothing`() =
runTest {
fakeAuthDiskSource.storeEncryptedPin(userId = USER_ID, encryptedPin = null)
pinProtectedUserKeyManager.deriveTemporaryPinProtectedUserKeyIfNecessary(
userId = USER_ID,
)
coVerify(exactly = 0) {
vaultSdkSource.enrollPinWithEncryptedPin(userId = any(), encryptedPin = any())
}
}
@Suppress("MaxLineLength")
@Test
fun `deriveTemporaryPinProtectedUserKeyIfNecessary with encrypted key and existing pinProtectedUserKeyEnvelope does nothing`() =
runTest {
fakeAuthDiskSource.storeEncryptedPin(userId = USER_ID, encryptedPin = "encryptedPin")
fakeAuthDiskSource.storePinProtectedUserKeyEnvelope(
userId = USER_ID,
pinProtectedUserKeyEnvelope = "pinProtectedUserKeyEnvelope",
)
pinProtectedUserKeyManager.deriveTemporaryPinProtectedUserKeyIfNecessary(
userId = USER_ID,
)
coVerify(exactly = 0) {
vaultSdkSource.enrollPinWithEncryptedPin(userId = any(), encryptedPin = any())
}
}
@Suppress("MaxLineLength")
@Test
fun `deriveTemporaryPinProtectedUserKeyIfNecessary with enrollment success should store new pin data`() =
runTest {
val encryptedPin = "encryptedPin"
val pinProtectedUserKeyEnvelope = "pinProtectedUserKeyEnvelope"
val userKeyEncryptedPin = "userKeyEncryptedPin"
val enrollPinResponse = EnrollPinResponse(
pinProtectedUserKeyEnvelope = pinProtectedUserKeyEnvelope,
userKeyEncryptedPin = userKeyEncryptedPin,
)
fakeAuthDiskSource.storeEncryptedPin(userId = USER_ID, encryptedPin = encryptedPin)
fakeAuthDiskSource.storePinProtectedUserKeyEnvelope(
userId = USER_ID,
pinProtectedUserKeyEnvelope = null,
)
coEvery {
vaultSdkSource.enrollPinWithEncryptedPin(
userId = USER_ID,
encryptedPin = encryptedPin,
)
} returns enrollPinResponse.asSuccess()
pinProtectedUserKeyManager.deriveTemporaryPinProtectedUserKeyIfNecessary(
userId = USER_ID,
)
coVerify(exactly = 1) {
vaultSdkSource.enrollPinWithEncryptedPin(
userId = USER_ID,
encryptedPin = encryptedPin,
)
}
fakeAuthDiskSource.assertEncryptedPin(
userId = USER_ID,
encryptedPin = userKeyEncryptedPin,
)
fakeAuthDiskSource.assertPinProtectedUserKeyEnvelope(
userId = USER_ID,
pinProtectedUserKeyEnvelope = pinProtectedUserKeyEnvelope,
inMemoryOnly = true,
)
fakeAuthDiskSource.assertPinProtectedUserKey(
userId = USER_ID,
pinProtectedUserKey = null,
inMemoryOnly = false,
)
}
@Suppress("MaxLineLength")
@Test
fun `deriveTemporaryPinProtectedUserKeyIfNecessary with enrollment failure should clear all pin data`() =
runTest {
val encryptedPin = "encryptedPin"
val error = Throwable("Fail!")
fakeAuthDiskSource.storeEncryptedPin(userId = USER_ID, encryptedPin = encryptedPin)
fakeAuthDiskSource.storePinProtectedUserKeyEnvelope(
userId = USER_ID,
pinProtectedUserKeyEnvelope = null,
)
coEvery {
vaultSdkSource.enrollPinWithEncryptedPin(
userId = USER_ID,
encryptedPin = encryptedPin,
)
} returns error.asFailure()
pinProtectedUserKeyManager.deriveTemporaryPinProtectedUserKeyIfNecessary(
userId = USER_ID,
)
coVerify(exactly = 1) {
vaultSdkSource.enrollPinWithEncryptedPin(
userId = USER_ID,
encryptedPin = encryptedPin,
)
}
fakeAuthDiskSource.assertEncryptedPin(
userId = USER_ID,
encryptedPin = null,
)
fakeAuthDiskSource.assertPinProtectedUserKeyEnvelope(
userId = USER_ID,
pinProtectedUserKeyEnvelope = null,
inMemoryOnly = false,
)
fakeAuthDiskSource.assertPinProtectedUserKey(
userId = USER_ID,
pinProtectedUserKey = null,
inMemoryOnly = false,
)
}
@Test
fun `migratePinProtectedUserKeyIfNeeded without encryptedKey does nothing`() =
runTest {
fakeAuthDiskSource.storeEncryptedPin(userId = USER_ID, encryptedPin = null)
pinProtectedUserKeyManager.migratePinProtectedUserKeyIfNeeded(userId = USER_ID)
coVerify(exactly = 0) {
vaultSdkSource.enrollPinWithEncryptedPin(userId = any(), encryptedPin = any())
}
}
@Suppress("MaxLineLength")
@Test
fun `migratePinProtectedUserKeyIfNeeded with encrypted key and existing pinProtectedUserKeyEnvelope does nothing`() =
runTest {
fakeAuthDiskSource.storeEncryptedPin(userId = USER_ID, encryptedPin = "encryptedPin")
fakeAuthDiskSource.storePinProtectedUserKeyEnvelope(
userId = USER_ID,
pinProtectedUserKeyEnvelope = "pinProtectedUserKeyEnvelope",
)
pinProtectedUserKeyManager.migratePinProtectedUserKeyIfNeeded(userId = USER_ID)
coVerify(exactly = 0) {
vaultSdkSource.enrollPinWithEncryptedPin(userId = any(), encryptedPin = any())
}
}
@Suppress("MaxLineLength")
@Test
fun `migratePinProtectedUserKeyIfNeeded with enrollment success should store new pin data in memory`() =
runTest {
val encryptedPin = "encryptedPin"
val pinProtectedUserKeyEnvelope = "pinProtectedUserKeyEnvelope"
val userKeyEncryptedPin = "userKeyEncryptedPin"
val enrollPinResponse = EnrollPinResponse(
pinProtectedUserKeyEnvelope = pinProtectedUserKeyEnvelope,
userKeyEncryptedPin = userKeyEncryptedPin,
)
fakeAuthDiskSource.storeEncryptedPin(userId = USER_ID, encryptedPin = encryptedPin)
fakeAuthDiskSource.storePinProtectedUserKeyEnvelope(
userId = USER_ID,
pinProtectedUserKeyEnvelope = null,
)
fakeAuthDiskSource.storePinProtectedUserKey(
userId = USER_ID,
pinProtectedUserKey = null,
)
coEvery {
vaultSdkSource.enrollPinWithEncryptedPin(
userId = USER_ID,
encryptedPin = encryptedPin,
)
} returns enrollPinResponse.asSuccess()
pinProtectedUserKeyManager.migratePinProtectedUserKeyIfNeeded(userId = USER_ID)
coVerify(exactly = 1) {
vaultSdkSource.enrollPinWithEncryptedPin(
userId = USER_ID,
encryptedPin = encryptedPin,
)
}
fakeAuthDiskSource.assertEncryptedPin(
userId = USER_ID,
encryptedPin = userKeyEncryptedPin,
)
fakeAuthDiskSource.assertPinProtectedUserKeyEnvelope(
userId = USER_ID,
pinProtectedUserKeyEnvelope = pinProtectedUserKeyEnvelope,
inMemoryOnly = true,
)
fakeAuthDiskSource.assertPinProtectedUserKey(
userId = USER_ID,
pinProtectedUserKey = null,
inMemoryOnly = false,
)
}
@Suppress("MaxLineLength")
@Test
fun `migratePinProtectedUserKeyIfNeeded with enrollment failure should clear all pin data at disk level`() =
runTest {
val encryptedPin = "encryptedPin"
val pinProtectedUserKey = "pinProtectedUserKey"
val error = Throwable("Fail!")
fakeAuthDiskSource.storeEncryptedPin(userId = USER_ID, encryptedPin = encryptedPin)
fakeAuthDiskSource.storePinProtectedUserKeyEnvelope(
userId = USER_ID,
pinProtectedUserKeyEnvelope = null,
)
fakeAuthDiskSource.storePinProtectedUserKey(
userId = USER_ID,
pinProtectedUserKey = pinProtectedUserKey,
)
coEvery {
vaultSdkSource.enrollPinWithEncryptedPin(
userId = USER_ID,
encryptedPin = encryptedPin,
)
} returns error.asFailure()
pinProtectedUserKeyManager.migratePinProtectedUserKeyIfNeeded(userId = USER_ID)
coVerify(exactly = 1) {
vaultSdkSource.enrollPinWithEncryptedPin(
userId = USER_ID,
encryptedPin = encryptedPin,
)
}
fakeAuthDiskSource.assertEncryptedPin(
userId = USER_ID,
encryptedPin = null,
)
fakeAuthDiskSource.assertPinProtectedUserKeyEnvelope(
userId = USER_ID,
pinProtectedUserKeyEnvelope = null,
inMemoryOnly = false,
)
fakeAuthDiskSource.assertPinProtectedUserKey(
userId = USER_ID,
pinProtectedUserKey = null,
inMemoryOnly = false,
)
}
}
private const val USER_ID: String = "user_id"

View File

@ -115,6 +115,9 @@ class VaultLockManagerTest {
updateKdfToMinimumsIfNeeded(password = any()) updateKdfToMinimumsIfNeeded(password = any())
} returns UpdateKdfMinimumsResult.Success } returns UpdateKdfMinimumsResult.Success
} }
private val pinProtectedUserKeyManager: PinProtectedUserKeyManager = mockk {
coEvery { migratePinProtectedUserKeyIfNeeded(userId = any()) } just runs
}
private val vaultLockManager: VaultLockManager = VaultLockManagerImpl( private val vaultLockManager: VaultLockManager = VaultLockManagerImpl(
context = context, context = context,
@ -129,6 +132,7 @@ class VaultLockManagerTest {
trustedDeviceManager = trustedDeviceManager, trustedDeviceManager = trustedDeviceManager,
dispatcherManager = fakeDispatcherManager, dispatcherManager = fakeDispatcherManager,
kdfManager = kdfManager, kdfManager = kdfManager,
pinProtectedUserKeyManager = pinProtectedUserKeyManager,
) )
@Test @Test
@ -1678,12 +1682,6 @@ class VaultLockManagerTest {
request = InitOrgCryptoRequest(organizationKeys = organizationKeys), request = InitOrgCryptoRequest(organizationKeys = organizationKeys),
) )
} returns InitializeCryptoResult.Success.asSuccess() } returns InitializeCryptoResult.Success.asSuccess()
coEvery {
vaultSdkSource.enrollPinWithEncryptedPin(
userId = USER_ID,
encryptedPin = userKeyEncryptedPin,
)
} returns enrollResponse.asSuccess()
coEvery { coEvery {
trustedDeviceManager.trustThisDeviceIfNecessary(userId = USER_ID) trustedDeviceManager.trustThisDeviceIfNecessary(userId = USER_ID)
} returns false.asSuccess() } returns false.asSuccess()
@ -1692,18 +1690,6 @@ class VaultLockManagerTest {
vaultLockManager.vaultUnlockDataStateFlow.value, vaultLockManager.vaultUnlockDataStateFlow.value,
) )
mutableVaultTimeoutStateFlow.value = VaultTimeout.ThirtyMinutes 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( val result = vaultLockManager.unlockVault(
userId = USER_ID, userId = USER_ID,
@ -1730,19 +1716,6 @@ class VaultLockManagerTest {
vaultLockManager.vaultUnlockDataStateFlow.value, 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) { coVerify(exactly = 1) {
vaultSdkSource.initializeCrypto( vaultSdkSource.initializeCrypto(
userId = USER_ID, userId = USER_ID,
@ -1764,6 +1737,7 @@ class VaultLockManagerTest {
request = InitOrgCryptoRequest(organizationKeys = organizationKeys), request = InitOrgCryptoRequest(organizationKeys = organizationKeys),
) )
trustedDeviceManager.trustThisDeviceIfNecessary(userId = USER_ID) trustedDeviceManager.trustThisDeviceIfNecessary(userId = USER_ID)
pinProtectedUserKeyManager.migratePinProtectedUserKeyIfNeeded(userId = USER_ID)
} }
} }

View File

@ -3,7 +3,6 @@ package com.x8bit.bitwarden.data.vault.repository
import app.cash.turbine.test import app.cash.turbine.test
import com.bitwarden.collections.CollectionView import com.bitwarden.collections.CollectionView
import com.bitwarden.core.DateTime import com.bitwarden.core.DateTime
import com.bitwarden.core.EnrollPinResponse
import com.bitwarden.core.InitUserCryptoMethod import com.bitwarden.core.InitUserCryptoMethod
import com.bitwarden.core.data.manager.dispatcher.DispatcherManager import com.bitwarden.core.data.manager.dispatcher.DispatcherManager
import com.bitwarden.core.data.manager.dispatcher.FakeDispatcherManager import com.bitwarden.core.data.manager.dispatcher.FakeDispatcherManager
@ -42,6 +41,7 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkFolder
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkSend import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkSend
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSendView import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSendView
import com.x8bit.bitwarden.data.vault.manager.CredentialExchangeImportManager import com.x8bit.bitwarden.data.vault.manager.CredentialExchangeImportManager
import com.x8bit.bitwarden.data.vault.manager.PinProtectedUserKeyManager
import com.x8bit.bitwarden.data.vault.manager.TotpCodeManager import com.x8bit.bitwarden.data.vault.manager.TotpCodeManager
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
import com.x8bit.bitwarden.data.vault.manager.VaultSyncManager import com.x8bit.bitwarden.data.vault.manager.VaultSyncManager
@ -111,6 +111,9 @@ class VaultRepositoryTest {
every { sendDataStateFlow } returns mutableSendDataStateFlow every { sendDataStateFlow } returns mutableSendDataStateFlow
} }
private val credentialExchangeImportManager: CredentialExchangeImportManager = mockk() private val credentialExchangeImportManager: CredentialExchangeImportManager = mockk()
private val pinProtectedUserKeyManager: PinProtectedUserKeyManager = mockk {
coEvery { deriveTemporaryPinProtectedUserKeyIfNecessary(userId = any()) } just runs
}
private val vaultRepository = VaultRepositoryImpl( private val vaultRepository = VaultRepositoryImpl(
vaultDiskSource = vaultDiskSource, vaultDiskSource = vaultDiskSource,
@ -124,6 +127,7 @@ class VaultRepositoryTest {
sendManager = mockk(), sendManager = mockk(),
vaultSyncManager = vaultSyncManager, vaultSyncManager = vaultSyncManager,
credentialExchangeImportManager = credentialExchangeImportManager, credentialExchangeImportManager = credentialExchangeImportManager,
pinProtectedUserKeyManager = pinProtectedUserKeyManager,
) )
@BeforeEach @BeforeEach
@ -186,69 +190,6 @@ class VaultRepositoryTest {
) )
} }
@Suppress("MaxLineLength")
@Test
fun `unlockVaultWithBiometrics with VaultLockManager Success and no encrypted PIN should unlock for the current user and return Success`() =
runTest {
val userId = MOCK_USER_STATE.activeUserId
val privateKey = "mockPrivateKey-1"
val biometricsKey = "asdf1234"
fakeAuthDiskSource.userState = MOCK_USER_STATE
val encryptedBytes = byteArrayOf(1, 1)
val initVector = byteArrayOf(2, 2)
val cipher = mockk<Cipher> {
every { doFinal(any()) } returns encryptedBytes
every { iv } returns initVector
}
coEvery {
vaultLockManager.unlockVault(
userId = userId,
email = "email",
kdf = MOCK_PROFILE.toSdkParams(),
privateKey = privateKey,
signingKey = null,
securityState = null,
initUserCryptoMethod = InitUserCryptoMethod.DecryptedKey(
decryptedUserKey = biometricsKey,
),
organizationKeys = null,
)
} returns VaultUnlockResult.Success
fakeAuthDiskSource.apply {
storeUserBiometricInitVector(userId = userId, iv = null)
storeUserBiometricUnlockKey(userId = userId, biometricsKey = biometricsKey)
storePrivateKey(userId = userId, privateKey = privateKey)
}
val result = vaultRepository.unlockVaultWithBiometrics(cipher = cipher)
assertEquals(VaultUnlockResult.Success, result)
coVerify {
vaultLockManager.unlockVault(
userId = userId,
email = "email",
kdf = MOCK_PROFILE.toSdkParams(),
privateKey = privateKey,
signingKey = null,
securityState = null,
initUserCryptoMethod = InitUserCryptoMethod.DecryptedKey(
decryptedUserKey = biometricsKey,
),
organizationKeys = null,
)
}
coVerify(exactly = 0) {
vaultSdkSource.enrollPinWithEncryptedPin(any(), any())
}
fakeAuthDiskSource.apply {
assertBiometricsKey(
userId = userId,
biometricsKey = encryptedBytes.toString(Charsets.ISO_8859_1),
)
assertBiometricInitVector(userId = userId, iv = initVector)
}
}
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `unlockVaultWithBiometrics with failure to decode biometrics key should return BiometricDecodingError`() = fun `unlockVaultWithBiometrics with failure to decode biometrics key should return BiometricDecodingError`() =
@ -275,9 +216,6 @@ class VaultRepositoryTest {
), ),
result, result,
) )
coVerify(exactly = 0) {
vaultSdkSource.enrollPinWithEncryptedPin(any(), any())
}
} }
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@ -302,9 +240,6 @@ class VaultRepositoryTest {
VaultUnlockResult.BiometricDecodingError(error = error), VaultUnlockResult.BiometricDecodingError(error = error),
result, result,
) )
coVerify(exactly = 0) {
vaultSdkSource.enrollPinWithEncryptedPin(any(), any())
}
} }
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@ -357,9 +292,6 @@ class VaultRepositoryTest {
organizationKeys = null, organizationKeys = null,
) )
} }
coVerify(exactly = 0) {
vaultSdkSource.enrollPinWithEncryptedPin(any(), any())
}
} }
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@ -367,13 +299,7 @@ class VaultRepositoryTest {
fun `unlockVaultWithBiometrics with VaultLockManager Success and a stored encrypted pin should unlock for the current user, derive a new pin-protected key, and return Success`() = fun `unlockVaultWithBiometrics with VaultLockManager Success and a stored encrypted pin should unlock for the current user, derive a new pin-protected key, and return Success`() =
runTest { runTest {
val userId = MOCK_USER_STATE.activeUserId val userId = MOCK_USER_STATE.activeUserId
val encryptedPin = "encryptedPin"
val privateKey = "mockPrivateKey-1" val privateKey = "mockPrivateKey-1"
val pinProtectedUserKeyEnvelope = "pinProtectedUserKeyEnvelope"
val enrollResponse = EnrollPinResponse(
pinProtectedUserKeyEnvelope = pinProtectedUserKeyEnvelope,
userKeyEncryptedPin = encryptedPin,
)
val biometricsKey = "asdf1234" val biometricsKey = "asdf1234"
fakeAuthDiskSource.userState = MOCK_USER_STATE fakeAuthDiskSource.userState = MOCK_USER_STATE
val encryptedBytes = byteArrayOf(1, 1) val encryptedBytes = byteArrayOf(1, 1)
@ -382,12 +308,6 @@ class VaultRepositoryTest {
every { doFinal(any()) } returns encryptedBytes every { doFinal(any()) } returns encryptedBytes
every { iv } returns initVector every { iv } returns initVector
} }
coEvery {
vaultSdkSource.enrollPinWithEncryptedPin(
userId = userId,
encryptedPin = encryptedPin,
)
} returns enrollResponse.asSuccess()
coEvery { coEvery {
vaultLockManager.unlockVault( vaultLockManager.unlockVault(
userId = userId, userId = userId,
@ -406,32 +326,11 @@ class VaultRepositoryTest {
storeUserBiometricInitVector(userId = userId, iv = null) storeUserBiometricInitVector(userId = userId, iv = null)
storeUserBiometricUnlockKey(userId = userId, biometricsKey = biometricsKey) storeUserBiometricUnlockKey(userId = userId, biometricsKey = biometricsKey)
storePrivateKey(userId = userId, privateKey = privateKey) storePrivateKey(userId = userId, privateKey = privateKey)
storeEncryptedPin(userId = userId, encryptedPin = encryptedPin)
storePinProtectedUserKey(
userId = userId,
pinProtectedUserKey = null,
inMemoryOnly = true,
)
storePinProtectedUserKeyEnvelope(
userId = userId,
pinProtectedUserKeyEnvelope = null,
inMemoryOnly = true,
)
} }
val result = vaultRepository.unlockVaultWithBiometrics(cipher = cipher) val result = vaultRepository.unlockVaultWithBiometrics(cipher = cipher)
assertEquals(VaultUnlockResult.Success, result) assertEquals(VaultUnlockResult.Success, result)
fakeAuthDiskSource.assertPinProtectedUserKey(
userId = userId,
pinProtectedUserKey = null,
inMemoryOnly = true,
)
fakeAuthDiskSource.assertPinProtectedUserKeyEnvelope(
userId = userId,
pinProtectedUserKeyEnvelope = pinProtectedUserKeyEnvelope,
inMemoryOnly = true,
)
coVerify { coVerify {
vaultLockManager.unlockVault( vaultLockManager.unlockVault(
userId = userId, userId = userId,
@ -445,11 +344,8 @@ class VaultRepositoryTest {
), ),
organizationKeys = null, organizationKeys = null,
) )
} pinProtectedUserKeyManager.deriveTemporaryPinProtectedUserKeyIfNecessary(
coEvery {
vaultSdkSource.enrollPinWithEncryptedPin(
userId = userId, userId = userId,
encryptedPin = encryptedPin,
) )
} }
fakeAuthDiskSource.apply { fakeAuthDiskSource.apply {
@ -616,100 +512,13 @@ class VaultRepositoryTest {
) )
} }
@Suppress("MaxLineLength")
@Test
fun `unlockVaultWithMasterPassword with VaultLockManager Success and no encrypted PIN should unlock for the current user and return Success`() =
runTest {
val userId = "mockId-1"
val mockVaultUnlockResult = VaultUnlockResult.Success
val userKeyEncryptedPin = "encryptedPin"
val pinProtectedUserKeyEnvelope = "pinProtectedUserKeyEnvelope"
val enrollResponse = EnrollPinResponse(
pinProtectedUserKeyEnvelope = pinProtectedUserKeyEnvelope,
userKeyEncryptedPin = userKeyEncryptedPin,
)
coEvery {
vaultSdkSource.enrollPinWithEncryptedPin(any(), any())
} returns enrollResponse.asSuccess()
prepareStateForUnlocking(unlockResult = mockVaultUnlockResult)
fakeAuthDiskSource.apply {
storeEncryptedPin(
userId = userId,
encryptedPin = null,
)
storePinProtectedUserKey(
userId = userId,
pinProtectedUserKey = null,
inMemoryOnly = true,
)
storePinProtectedUserKeyEnvelope(
userId = userId,
pinProtectedUserKeyEnvelope = null,
inMemoryOnly = true,
)
}
val result = vaultRepository.unlockVaultWithMasterPassword(
masterPassword = "mockPassword-1",
)
assertEquals(
mockVaultUnlockResult,
result,
)
coVerify {
vaultLockManager.unlockVault(
userId = userId,
email = "email",
kdf = MOCK_PROFILE.toSdkParams(),
privateKey = "mockPrivateKey-1",
signingKey = null,
securityState = null,
initUserCryptoMethod = InitUserCryptoMethod.Password(
password = "mockPassword-1",
userKey = "mockKey-1",
),
organizationKeys = createMockOrganizationKeys(number = 1),
)
}
coVerify(exactly = 0) { vaultSdkSource.enrollPinWithEncryptedPin(any(), any()) }
}
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
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`() = 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 { runTest {
val userId = "mockId-1" val userId = "mockId-1"
val pinProtectedUserKey = "pinProtectedUserkeyEnvelope"
val encryptedPin = "userKeyEncryptedPin"
val enrollResponse = EnrollPinResponse(
pinProtectedUserKeyEnvelope = pinProtectedUserKey,
userKeyEncryptedPin = encryptedPin,
)
val mockVaultUnlockResult = VaultUnlockResult.Success val mockVaultUnlockResult = VaultUnlockResult.Success
coEvery {
vaultSdkSource.enrollPinWithEncryptedPin(
userId = userId,
encryptedPin = encryptedPin,
)
} returns enrollResponse.asSuccess()
prepareStateForUnlocking(unlockResult = mockVaultUnlockResult) prepareStateForUnlocking(unlockResult = mockVaultUnlockResult)
fakeAuthDiskSource.apply {
storeEncryptedPin(
userId = userId,
encryptedPin = encryptedPin,
)
storePinProtectedUserKey(
userId = userId,
pinProtectedUserKey = null,
inMemoryOnly = true,
)
storePinProtectedUserKeyEnvelope(
userId = userId,
pinProtectedUserKeyEnvelope = null,
inMemoryOnly = true,
)
}
val result = vaultRepository.unlockVaultWithMasterPassword( val result = vaultRepository.unlockVaultWithMasterPassword(
masterPassword = "mockPassword-1", masterPassword = "mockPassword-1",
@ -719,16 +528,6 @@ class VaultRepositoryTest {
mockVaultUnlockResult, mockVaultUnlockResult,
result, result,
) )
fakeAuthDiskSource.assertPinProtectedUserKey(
userId = userId,
pinProtectedUserKey = null,
inMemoryOnly = true,
)
fakeAuthDiskSource.assertPinProtectedUserKeyEnvelope(
userId = userId,
pinProtectedUserKeyEnvelope = pinProtectedUserKey,
inMemoryOnly = true,
)
coVerify { coVerify {
vaultLockManager.unlockVault( vaultLockManager.unlockVault(
userId = userId, userId = userId,
@ -743,11 +542,8 @@ class VaultRepositoryTest {
), ),
organizationKeys = createMockOrganizationKeys(number = 1), organizationKeys = createMockOrganizationKeys(number = 1),
) )
} pinProtectedUserKeyManager.deriveTemporaryPinProtectedUserKeyIfNecessary(
coEvery {
vaultSdkSource.enrollPinWithEncryptedPin(
userId = userId, userId = userId,
encryptedPin = encryptedPin,
) )
} }
} }