PM-28355: Clear pin data on hard-logout or security stamp (#6232)

This commit is contained in:
David Perez 2025-12-08 10:51:18 -06:00 committed by GitHub
parent 28db795790
commit 2eb8ad4221
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 106 additions and 64 deletions

View File

@ -159,11 +159,11 @@ class AuthDiskSourceImpl(
storeAuthenticatorSyncUnlockKey(userId = userId, authenticatorSyncUnlockKey = null) storeAuthenticatorSyncUnlockKey(userId = userId, authenticatorSyncUnlockKey = null)
storeShowImportLogins(userId = userId, showImportLogins = null) storeShowImportLogins(userId = userId, showImportLogins = null)
storeLastLockTimestamp(userId = userId, lastLockTimestamp = null) storeLastLockTimestamp(userId = userId, lastLockTimestamp = null)
storeEncryptedPin(userId = userId, encryptedPin = null)
storePinProtectedUserKey(userId = userId, pinProtectedUserKey = null)
storePinProtectedUserKeyEnvelope(userId = userId, pinProtectedUserKeyEnvelope = null)
// Certain values are never removed as required by the feature requirements: // Certain values are never removed as required by the feature requirements:
// * EncryptedPin
// * PinProtectedUserKey
// * PinProtectedUserKeyEnvelope
// * DeviceKey // * DeviceKey
// * PendingAuthRequest // * PendingAuthRequest
// * OnboardingStatus // * OnboardingStatus

View File

@ -49,14 +49,14 @@ class UserLogoutManagerImpl(
override fun logout(userId: String, reason: LogoutReason) { override fun logout(userId: String, reason: LogoutReason) {
authDiskSource.userState ?: return authDiskSource.userState ?: return
Timber.d("logout reason=$reason") Timber.d("logout reason=$reason")
val isExpired = reason == LogoutReason.SecurityStamp val isSecurityStamp = reason == LogoutReason.SecurityStamp
if (isExpired) { if (isSecurityStamp) {
showToast(message = BitwardenString.login_expired) showToast(message = BitwardenString.login_expired)
} }
val ableToSwitchToNewAccount = switchUserIfAvailable( val ableToSwitchToNewAccount = switchUserIfAvailable(
currentUserId = userId, currentUserId = userId,
isExpired = isExpired, isSecurityStamp = isSecurityStamp,
removeCurrentUserFromAccounts = true, removeCurrentUserFromAccounts = true,
) )
@ -73,19 +73,24 @@ class UserLogoutManagerImpl(
override fun softLogout(userId: String, reason: LogoutReason) { override fun softLogout(userId: String, reason: LogoutReason) {
Timber.d("softLogout reason=$reason") Timber.d("softLogout reason=$reason")
val isExpired = reason == LogoutReason.SecurityStamp val isSecurityStamp = reason == LogoutReason.SecurityStamp
if (isExpired) { if (isSecurityStamp) {
showToast(message = BitwardenString.login_expired) showToast(message = BitwardenString.login_expired)
} }
// 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 data
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 encryptedPin = authDiskSource.getEncryptedPin(userId = userId)
val pinProtectedUserKey = authDiskSource.getPinProtectedUserKey(userId = userId)
val pinProtectedUserKeyEnvelope = authDiskSource.getPinProtectedUserKeyEnvelope(
userId = userId,
)
switchUserIfAvailable( switchUserIfAvailable(
currentUserId = userId, currentUserId = userId,
removeCurrentUserFromAccounts = false, removeCurrentUserFromAccounts = false,
isExpired = isExpired, isSecurityStamp = isSecurityStamp,
) )
clearData(userId = userId) clearData(userId = userId)
@ -102,6 +107,14 @@ class UserLogoutManagerImpl(
vaultTimeoutAction = vaultTimeoutAction, vaultTimeoutAction = vaultTimeoutAction,
) )
} }
authDiskSource.apply {
storeEncryptedPin(userId = userId, encryptedPin = encryptedPin)
storePinProtectedUserKey(userId = userId, pinProtectedUserKey = pinProtectedUserKey)
storePinProtectedUserKeyEnvelope(
userId = userId,
pinProtectedUserKeyEnvelope = pinProtectedUserKeyEnvelope,
)
}
} }
private fun clearData(userId: String) { private fun clearData(userId: String) {
@ -123,7 +136,7 @@ class UserLogoutManagerImpl(
private fun switchUserIfAvailable( private fun switchUserIfAvailable(
currentUserId: String, currentUserId: String,
removeCurrentUserFromAccounts: Boolean, removeCurrentUserFromAccounts: Boolean,
isExpired: Boolean = false, isSecurityStamp: Boolean,
): Boolean { ): Boolean {
val currentUserState = authDiskSource.userState ?: return false val currentUserState = authDiskSource.userState ?: return false
@ -135,7 +148,7 @@ class UserLogoutManagerImpl(
// Check if there is a new active user // Check if there is a new active user
return if (updatedAccounts.isNotEmpty()) { return if (updatedAccounts.isNotEmpty()) {
if (currentUserId == currentUserState.activeUserId && !isExpired) { if (currentUserId == currentUserState.activeUserId && !isSecurityStamp) {
showToast(message = BitwardenString.account_switched_automatically) showToast(message = BitwardenString.account_switched_automatically)
} }

View File

@ -310,7 +310,7 @@ class VaultSyncManagerImpl(
localSecurityStamp?.let { localSecurityStamp?.let {
if (serverSecurityStamp != localSecurityStamp) { if (serverSecurityStamp != localSecurityStamp) {
// Ensure UserLogoutManager is available // Ensure UserLogoutManager is available
userLogoutManager.softLogout( userLogoutManager.logout(
userId = userId, userId = userId,
reason = LogoutReason.SecurityStamp, reason = LogoutReason.SecurityStamp,
) )

View File

@ -267,15 +267,14 @@ class AuthDiskSourceTest {
userId = userId, userId = userId,
biometricsKey = "1234-9876-0192", biometricsKey = "1234-9876-0192",
) )
val pinProtectedUserKey = "pinProtectedUserKey" authDiskSource.storeEncryptedPin(userId = userId, encryptedPin = "encryptedPin")
authDiskSource.storePinProtectedUserKey( authDiskSource.storePinProtectedUserKey(
userId = userId, userId = userId,
pinProtectedUserKey = pinProtectedUserKey, pinProtectedUserKey = "pinProtectedUserKey",
) )
val pinProtectedUserKeyEnvelope = "pinProtectedUserKeyEnvelope"
authDiskSource.storePinProtectedUserKeyEnvelope( authDiskSource.storePinProtectedUserKeyEnvelope(
userId = userId, userId = userId,
pinProtectedUserKeyEnvelope = pinProtectedUserKeyEnvelope, pinProtectedUserKeyEnvelope = "pinProtectedUserKeyEnvelope",
) )
authDiskSource.storeInvalidUnlockAttempts( authDiskSource.storeInvalidUnlockAttempts(
userId = userId, userId = userId,
@ -310,8 +309,6 @@ class AuthDiskSourceTest {
refreshToken = "refreshToken", refreshToken = "refreshToken",
), ),
) )
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,
@ -333,12 +330,6 @@ 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))
@ -357,6 +348,9 @@ class AuthDiskSourceTest {
assertNull(authDiskSource.getIsTdeLoginComplete(userId = userId)) assertNull(authDiskSource.getIsTdeLoginComplete(userId = userId))
assertNull(authDiskSource.getAuthenticatorSyncUnlockKey(userId = userId)) assertNull(authDiskSource.getAuthenticatorSyncUnlockKey(userId = userId))
assertNull(authDiskSource.getShowImportLogins(userId = userId)) assertNull(authDiskSource.getShowImportLogins(userId = userId))
assertNull(authDiskSource.getEncryptedPin(userId = userId))
assertNull(authDiskSource.getPinProtectedUserKey(userId = userId))
assertNull(authDiskSource.getPinProtectedUserKeyEnvelope(userId = userId))
} }
@Test @Test

View File

@ -91,11 +91,14 @@ class FakeAuthDiskSource : AuthDiskSource {
storedBiometricKeys.remove(userId) storedBiometricKeys.remove(userId)
storedOrganizationKeys.remove(userId) storedOrganizationKeys.remove(userId)
storedPinProtectedUserKeyEnvelopes.remove(userId) storedPinProtectedUserKeyEnvelopes.remove(userId)
storedEncryptedPins.remove(userId)
storedPinProtectedUserKeys.remove(userId)
mutableShouldUseKeyConnectorFlowMap.remove(userId) mutableShouldUseKeyConnectorFlowMap.remove(userId)
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

@ -120,6 +120,21 @@ class UserLogoutManagerTest {
assertDataCleared(userId = userId) assertDataCleared(userId = userId)
} }
@Suppress("MaxLineLength")
@Test
fun `logout with security stamp reason should switch active user and display the login expired toast`() {
val userId = USER_ID_1
every { authDiskSource.userState } returns MULTI_USER_STATE
userLogoutManager.logout(userId = userId, reason = LogoutReason.SecurityStamp)
verify(exactly = 1) {
authDiskSource.userState = SINGLE_USER_STATE_2
toastManager.show(messageId = BitwardenString.login_expired)
}
assertDataCleared(userId = userId)
}
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `softLogout should clear most data associated with the given user and remove token data in the authDiskSource`() { fun `softLogout should clear most data associated with the given user and remove token data in the authDiskSource`() {
@ -127,6 +142,8 @@ class UserLogoutManagerTest {
val vaultTimeoutInMinutes = 360 val vaultTimeoutInMinutes = 360
val vaultTimeoutAction = VaultTimeoutAction.LOGOUT val vaultTimeoutAction = VaultTimeoutAction.LOGOUT
val pinProtectedUserKey = "pinProtectedUserKey" val pinProtectedUserKey = "pinProtectedUserKey"
val pinProtectedUserKeyEnvelope = "pinProtectedUserKeyEnvelope"
val encryptedPin = "encryptedPin"
every { authDiskSource.userState } returns MULTI_USER_STATE every { authDiskSource.userState } returns MULTI_USER_STATE
every { every {
@ -135,10 +152,26 @@ class UserLogoutManagerTest {
every { every {
settingsDiskSource.getVaultTimeoutAction(userId = userId) settingsDiskSource.getVaultTimeoutAction(userId = userId)
} returns vaultTimeoutAction } returns vaultTimeoutAction
every { every {
authDiskSource.getPinProtectedUserKeyEnvelope(userId = userId) authDiskSource.getPinProtectedUserKeyEnvelope(userId = userId)
} returns pinProtectedUserKey } returns pinProtectedUserKeyEnvelope
every {
authDiskSource.storePinProtectedUserKeyEnvelope(
userId = userId,
pinProtectedUserKeyEnvelope = pinProtectedUserKeyEnvelope,
)
} just runs
every { authDiskSource.getPinProtectedUserKey(userId = userId) } returns pinProtectedUserKey
every {
authDiskSource.storePinProtectedUserKey(
userId = userId,
pinProtectedUserKey = pinProtectedUserKey,
)
} just runs
every { authDiskSource.getEncryptedPin(userId = userId) } returns encryptedPin
every {
authDiskSource.storeEncryptedPin(userId = userId, encryptedPin = encryptedPin)
} just runs
userLogoutManager.softLogout(userId = userId, reason = LogoutReason.Timeout) userLogoutManager.softLogout(userId = userId, reason = LogoutReason.Timeout)
@ -162,6 +195,15 @@ class UserLogoutManagerTest {
userId = userId, userId = userId,
vaultTimeoutAction = vaultTimeoutAction, vaultTimeoutAction = vaultTimeoutAction,
) )
authDiskSource.storePinProtectedUserKeyEnvelope(
userId = userId,
pinProtectedUserKeyEnvelope = pinProtectedUserKeyEnvelope,
)
authDiskSource.storePinProtectedUserKey(
userId = userId,
pinProtectedUserKey = pinProtectedUserKey,
)
authDiskSource.storeEncryptedPin(userId = userId, encryptedPin = encryptedPin)
} }
} }
@ -171,6 +213,8 @@ class UserLogoutManagerTest {
val vaultTimeoutInMinutes = 360 val vaultTimeoutInMinutes = 360
val vaultTimeoutAction = VaultTimeoutAction.LOGOUT val vaultTimeoutAction = VaultTimeoutAction.LOGOUT
val pinProtectedUserKey = "pinProtectedUserKey" val pinProtectedUserKey = "pinProtectedUserKey"
val pinProtectedUserKeyEnvelope = "pinProtectedUserKeyEnvelope"
val encryptedPin = "encryptedPin"
every { authDiskSource.userState } returns MULTI_USER_STATE every { authDiskSource.userState } returns MULTI_USER_STATE
every { every {
@ -181,7 +225,24 @@ class UserLogoutManagerTest {
} returns vaultTimeoutAction } returns vaultTimeoutAction
every { every {
authDiskSource.getPinProtectedUserKeyEnvelope(userId = userId) authDiskSource.getPinProtectedUserKeyEnvelope(userId = userId)
} returns pinProtectedUserKey } returns pinProtectedUserKeyEnvelope
every {
authDiskSource.storePinProtectedUserKeyEnvelope(
userId = userId,
pinProtectedUserKeyEnvelope = pinProtectedUserKeyEnvelope,
)
} just runs
every { authDiskSource.getPinProtectedUserKey(userId = userId) } returns pinProtectedUserKey
every {
authDiskSource.storePinProtectedUserKey(
userId = userId,
pinProtectedUserKey = pinProtectedUserKey,
)
} just runs
every { authDiskSource.getEncryptedPin(userId = userId) } returns encryptedPin
every {
authDiskSource.storeEncryptedPin(userId = userId, encryptedPin = encryptedPin)
} just runs
userLogoutManager.softLogout(userId = userId, reason = LogoutReason.Timeout) userLogoutManager.softLogout(userId = userId, reason = LogoutReason.Timeout)
@ -199,44 +260,15 @@ class UserLogoutManagerTest {
userId = userId, userId = userId,
vaultTimeoutAction = vaultTimeoutAction, vaultTimeoutAction = vaultTimeoutAction,
) )
} authDiskSource.storePinProtectedUserKeyEnvelope(
}
@Suppress("MaxLineLength")
@Test
fun `softLogout with security stamp reason should switch active user and keep previous user in accounts list but display the login expired toast`() {
val userId = USER_ID_1
val vaultTimeoutInMinutes = 360
val vaultTimeoutAction = VaultTimeoutAction.LOGOUT
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
userLogoutManager.softLogout(userId = userId, reason = LogoutReason.SecurityStamp)
verify(exactly = 1) {
authDiskSource.userState = UserStateJson(
activeUserId = USER_ID_2,
accounts = MULTI_USER_STATE.accounts,
)
toastManager.show(messageId = BitwardenString.login_expired)
settingsDiskSource.storeVaultTimeoutInMinutes(
userId = userId, userId = userId,
vaultTimeoutInMinutes = vaultTimeoutInMinutes, pinProtectedUserKeyEnvelope = pinProtectedUserKeyEnvelope,
) )
settingsDiskSource.storeVaultTimeoutAction( authDiskSource.storePinProtectedUserKey(
userId = userId, userId = userId,
vaultTimeoutAction = vaultTimeoutAction, pinProtectedUserKey = pinProtectedUserKey,
) )
authDiskSource.storeEncryptedPin(userId = userId, encryptedPin = encryptedPin)
} }
} }

View File

@ -126,7 +126,7 @@ class VaultSyncManagerTest {
} }
} }
private val userLogoutManager: UserLogoutManager = mockk { private val userLogoutManager: UserLogoutManager = mockk {
every { softLogout(any(), any()) } just runs every { logout(userId = any(), reason = LogoutReason.SecurityStamp) } just runs
} }
private val userStateManager: UserStateManager = mockk { private val userStateManager: UserStateManager = mockk {
val blockSlot = slot<suspend () -> SyncVaultDataResult>() val blockSlot = slot<suspend () -> SyncVaultDataResult>()
@ -786,7 +786,7 @@ class VaultSyncManagerTest {
vaultSyncManager.sync() vaultSyncManager.sync()
coVerify(exactly = 1) { coVerify(exactly = 1) {
userLogoutManager.softLogout(userId = userId, reason = LogoutReason.SecurityStamp) userLogoutManager.logout(userId = userId, reason = LogoutReason.SecurityStamp)
} }
coVerify(exactly = 0) { coVerify(exactly = 0) {