diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceImpl.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceImpl.kt index 477b1e1c1c..27375bf45c 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceImpl.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceImpl.kt @@ -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 diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/manager/UserLogoutManagerImpl.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/manager/UserLogoutManagerImpl.kt index 43df7f74c7..d87def0ef7 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/manager/UserLogoutManagerImpl.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/manager/UserLogoutManagerImpl.kt @@ -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) } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultSyncManagerImpl.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultSyncManagerImpl.kt index b2356ccfb9..4f3fbf4e51 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultSyncManagerImpl.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultSyncManagerImpl.kt @@ -310,7 +310,7 @@ class VaultSyncManagerImpl( localSecurityStamp?.let { if (serverSecurityStamp != localSecurityStamp) { // Ensure UserLogoutManager is available - userLogoutManager.softLogout( + userLogoutManager.logout( userId = userId, reason = LogoutReason.SecurityStamp, ) diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceTest.kt index 3be18acfed..52a0c91a44 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceTest.kt @@ -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 diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/datasource/disk/util/FakeAuthDiskSource.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/datasource/disk/util/FakeAuthDiskSource.kt index c3000891c7..a85a7331c8 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/datasource/disk/util/FakeAuthDiskSource.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/datasource/disk/util/FakeAuthDiskSource.kt @@ -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( diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/manager/UserLogoutManagerTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/manager/UserLogoutManagerTest.kt index 13427bd77b..6bfd4e8b68 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/manager/UserLogoutManagerTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/manager/UserLogoutManagerTest.kt @@ -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) } } diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultSyncManagerTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultSyncManagerTest.kt index 678552e4a3..265fa64ce3 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultSyncManagerTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultSyncManagerTest.kt @@ -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 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) {