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)
storeShowImportLogins(userId = userId, showImportLogins = 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:
// * EncryptedPin
// * PinProtectedUserKey
// * PinProtectedUserKeyEnvelope
// * DeviceKey
// * PendingAuthRequest
// * OnboardingStatus

View File

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

View File

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

View File

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

View File

@ -91,11 +91,14 @@ class FakeAuthDiskSource : AuthDiskSource {
storedBiometricKeys.remove(userId)
storedOrganizationKeys.remove(userId)
storedPinProtectedUserKeyEnvelopes.remove(userId)
storedEncryptedPins.remove(userId)
storedPinProtectedUserKeys.remove(userId)
mutableShouldUseKeyConnectorFlowMap.remove(userId)
mutableOrganizationsFlowMap.remove(userId)
mutablePoliciesFlowMap.remove(userId)
mutableAccountTokensFlowMap.remove(userId)
mutablePinProtectedUserKeyEnvelopesFlowMap.remove(userId)
}
private fun getMutablePinProtectedUserKeyEnvelopeFlow(

View File

@ -120,6 +120,21 @@ class UserLogoutManagerTest {
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")
@Test
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 vaultTimeoutAction = VaultTimeoutAction.LOGOUT
val pinProtectedUserKey = "pinProtectedUserKey"
val pinProtectedUserKeyEnvelope = "pinProtectedUserKeyEnvelope"
val encryptedPin = "encryptedPin"
every { authDiskSource.userState } returns MULTI_USER_STATE
every {
@ -135,10 +152,26 @@ class UserLogoutManagerTest {
every {
settingsDiskSource.getVaultTimeoutAction(userId = userId)
} returns vaultTimeoutAction
every {
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)
@ -162,6 +195,15 @@ class UserLogoutManagerTest {
userId = userId,
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 vaultTimeoutAction = VaultTimeoutAction.LOGOUT
val pinProtectedUserKey = "pinProtectedUserKey"
val pinProtectedUserKeyEnvelope = "pinProtectedUserKeyEnvelope"
val encryptedPin = "encryptedPin"
every { authDiskSource.userState } returns MULTI_USER_STATE
every {
@ -181,7 +225,24 @@ class UserLogoutManagerTest {
} returns vaultTimeoutAction
every {
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)
@ -199,44 +260,15 @@ class UserLogoutManagerTest {
userId = userId,
vaultTimeoutAction = vaultTimeoutAction,
)
}
}
@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(
authDiskSource.storePinProtectedUserKeyEnvelope(
userId = userId,
vaultTimeoutInMinutes = vaultTimeoutInMinutes,
pinProtectedUserKeyEnvelope = pinProtectedUserKeyEnvelope,
)
settingsDiskSource.storeVaultTimeoutAction(
authDiskSource.storePinProtectedUserKey(
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 {
every { softLogout(any(), any()) } just runs
every { logout(userId = any(), reason = LogoutReason.SecurityStamp) } just runs
}
private val userStateManager: UserStateManager = mockk {
val blockSlot = slot<suspend () -> SyncVaultDataResult>()
@ -786,7 +786,7 @@ class VaultSyncManagerTest {
vaultSyncManager.sync()
coVerify(exactly = 1) {
userLogoutManager.softLogout(userId = userId, reason = LogoutReason.SecurityStamp)
userLogoutManager.logout(userId = userId, reason = LogoutReason.SecurityStamp)
}
coVerify(exactly = 0) {