diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSource.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSource.kt index dfee0830d23..e63d9a2d445 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSource.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSource.kt @@ -138,6 +138,16 @@ interface AuthDiskSource : AppIdProvider { accountCryptographicState: WrappedAccountCryptographicState?, ) + /** + * Returns the id of the user key for the given [userId]. + */ + fun getUserKeyId(userId: String): String? + + /** + * Stores the id of the user key for the given [userId]. + */ + fun storeUserKeyId(userId: String, userKeyId: String?) + /** * Retrieves a user auto-unlock key for the given [userId]. */ 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 307df5d1231..3916af10f29 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 @@ -61,6 +61,7 @@ private const val SHOW_IMPORT_LOGINS_KEY = "showImportLogins" private const val LAST_LOCK_TIMESTAMP = "lastLockTimestamp" private const val PROFILE_ACCOUNT_KEYS_KEY = "profileAccountKeys" private const val V2_UPGRADE_TOKEN = "v2UpgradeToken" +private const val USER_KEY_ID_KEY = "userKeyId" /** * Primary implementation of [AuthDiskSource]. @@ -201,6 +202,7 @@ class AuthDiskSourceImpl( pinProtectedUserKeyEnvelope = null, ) storeV2UpgradeToken(userId = userId, v2UpgradeToken = null) + storeUserKeyId(userId = userId, userKeyId = null) // Certain values are never removed as required by the feature requirements: // * DeviceKey @@ -290,6 +292,13 @@ class AuthDiskSourceImpl( ) } + override fun getUserKeyId(userId: String): String? = + getString(key = USER_KEY_ID_KEY.appendIdentifier(userId)) + + override fun storeUserKeyId(userId: String, userKeyId: String?) { + putString(key = USER_KEY_ID_KEY.appendIdentifier(userId), value = userKeyId) + } + override fun getUserAutoUnlockKey(userId: String): String? = getEncryptedString(key = USER_AUTO_UNLOCK_KEY_KEY.appendIdentifier(userId)) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/sdk/statebridge/SdkStateBridge.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/sdk/statebridge/SdkStateBridge.kt index 6afbc5bd1b5..4d96553d2cd 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/sdk/statebridge/SdkStateBridge.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/sdk/statebridge/SdkStateBridge.kt @@ -39,6 +39,16 @@ internal class SdkStateBridge( inMemoryUserKey = null } + override suspend fun setUserKeyId(value: String) { + authDiskSource.storeUserKeyId(userId = userId, userKeyId = value) + } + + override suspend fun getUserKeyId(): String? = authDiskSource.getUserKeyId(userId = userId) + + override suspend fun clearUserKeyId() { + authDiskSource.storeUserKeyId(userId = userId, userKeyId = null) + } + override suspend fun setPersistentPinEnvelope(value: PasswordProtectedKeyEnvelope) { authDiskSource.storePersistentPinProtectedUserKeyEnvelope( userId = userId, 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 8306c33ec81..29d111827ce 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 @@ -377,6 +377,7 @@ class AuthDiskSourceTest { wrappedUserKey2 = "wrappedUserKey2", ), ) + authDiskSource.storeUserKeyId(userId = userId, userKeyId = "userKeyId") authDiskSource.clearData(userId = userId) @@ -410,6 +411,7 @@ class AuthDiskSourceTest { assertNull(authDiskSource.getEphemeralPinProtectedUserKeyEnvelope(userId = userId)) assertNull(authDiskSource.getPersistentPinProtectedUserKeyEnvelope(userId = userId)) assertNull(authDiskSource.getV2UpgradeToken(userId = userId)) + assertNull(authDiskSource.getUserKeyId(userId = userId)) } @Test @@ -538,6 +540,28 @@ class AuthDiskSourceTest { ) } + @Test + fun `getUserKeyId should pull from SharedPreferences`() { + val userKeyIdBaseKey = "bwPreferencesStorage:userKeyId" + val mockUserId = "mockUserId" + val mockUserKeyId = "mockUserKeyId" + fakeSharedPreferences.edit { + putString("${userKeyIdBaseKey}_$mockUserId", mockUserKeyId) + } + val actual = authDiskSource.getUserKeyId(userId = mockUserId) + assertEquals(mockUserKeyId, actual) + } + + @Test + fun `storeUserKeyId should update SharedPreferences`() { + val userKeyIdBaseKey = "bwPreferencesStorage:userKeyId" + val mockUserId = "mockUserId" + val mockUserKeyId = "mockUserKeyId" + authDiskSource.storeUserKeyId(userId = mockUserId, userKeyId = mockUserKeyId) + val actual = fakeSharedPreferences.getString("${userKeyIdBaseKey}_$mockUserId", null) + assertEquals(mockUserKeyId, actual) + } + @Test fun `getV2UpgradeToken should pull from SharedPreferences`() { val v2UpgradeTokenBaseKey = "bwPreferencesStorage:v2UpgradeToken" 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 594d23004fb..3bdecac2d18 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 @@ -73,6 +73,7 @@ class FakeAuthDiskSource : AuthDiskSource { private val mutablePersistentPinProtectedUserKeyEnvelopesFlowMap = mutableMapOf>() private val storedV2UpgradeTokens = mutableMapOf() + private val storedUserKeyIds = mutableMapOf() override var userState: UserStateJson? = null set(value) { @@ -100,6 +101,7 @@ class FakeAuthDiskSource : AuthDiskSource { storedEncryptedPins.remove(userId) storedPinProtectedUserKeys.remove(userId) storedV2UpgradeTokens.remove(userId) + storedUserKeyIds.remove(userId) mutableShouldUseKeyConnectorFlowMap.remove(userId) mutableOrganizationsFlowMap.remove(userId) @@ -163,6 +165,12 @@ class FakeAuthDiskSource : AuthDiskSource { storedAccountCryptographicState[userId] = accountCryptographicState } + override fun getUserKeyId(userId: String): String? = storedUserKeyIds[userId] + + override fun storeUserKeyId(userId: String, userKeyId: String?) { + storedUserKeyIds[userId] = userKeyId + } + override fun getV2UpgradeToken(userId: String): V2UpgradeTokenJson? = storedV2UpgradeTokens[userId] @@ -449,6 +457,13 @@ class FakeAuthDiskSource : AuthDiskSource { assertEquals(accountCryptographicState, storedAccountCryptographicState[userId]) } + /** + * Assert that the [userKeyId] was stored successfully using the [userId]. + */ + fun assertUserKeyId(userId: String, userKeyId: String?) { + assertEquals(userKeyId, storedUserKeyIds[userId]) + } + /** * Assert that the [v2UpgradeToken] was stored successfully using the [userId]. */ diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/sdk/statebridge/SdkStateBridgeTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/sdk/statebridge/SdkStateBridgeTest.kt index c6e7e4d6f83..d16eaf57d0e 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/sdk/statebridge/SdkStateBridgeTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/sdk/statebridge/SdkStateBridgeTest.kt @@ -56,6 +56,31 @@ class SdkStateBridgeTest { assertNull(stateBridge.getUserKey()) } + @Test + fun `setUserKeyId should store the user key id`() = runTest { + stateBridge.setUserKeyId(value = "userKeyId") + + authDiskSource.assertUserKeyId(userId = USER_ID, userKeyId = "userKeyId") + } + + @Test + fun `getUserKeyId should return the stored user key id`() = runTest { + assertNull(stateBridge.getUserKeyId()) + + authDiskSource.storeUserKeyId(userId = USER_ID, userKeyId = "userKeyId") + + assertEquals("userKeyId", stateBridge.getUserKeyId()) + } + + @Test + fun `clearUserKeyId should clear the stored user key id`() = runTest { + authDiskSource.storeUserKeyId(userId = USER_ID, userKeyId = "userKeyId") + + stateBridge.clearUserKeyId() + + authDiskSource.assertUserKeyId(userId = USER_ID, userKeyId = null) + } + @Test fun `setPersistentPinEnvelope should store the persistent pin envelope`() = runTest { stateBridge.setPersistentPinEnvelope(value = "pinEnvelope") diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1cf8fa02069..1c594d5e502 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -29,7 +29,7 @@ androidxRoom = "2.8.4" androidxSecurityCrypto = "1.1.0" androidxSplash = "1.2.0" androidxWork = "2.11.2" -bitwardenSdk = "3.0.0-8288-99ffb6ef" +bitwardenSdk = "3.0.0-8379-cc7daf10" crashlytics = "3.0.7" detekt = "1.23.8" firebaseBom = "34.15.0"