diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/sdk/SdkRepositoryFactoryImpl.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/sdk/SdkRepositoryFactoryImpl.kt index 30f11cc50b..adeb833a95 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/sdk/SdkRepositoryFactoryImpl.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/sdk/SdkRepositoryFactoryImpl.kt @@ -7,6 +7,7 @@ import com.bitwarden.sdk.ServerCommunicationConfigRepository import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource import com.x8bit.bitwarden.data.platform.datasource.disk.CookieDiskSource import com.x8bit.bitwarden.data.platform.manager.sdk.repository.SdkCipherRepository +import com.x8bit.bitwarden.data.platform.manager.sdk.repository.SdkLocalUserDataKeyStateRepository import com.x8bit.bitwarden.data.platform.manager.sdk.repository.SdkTokenRepository import com.x8bit.bitwarden.data.platform.manager.sdk.repository.ServerCommunicationConfigRepositoryImpl import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource @@ -25,6 +26,10 @@ class SdkRepositoryFactoryImpl( cipher = getSdkRepository(userId = userId), folder = null, userKeyState = null, + localUserDataKeyState = SdkLocalUserDataKeyStateRepository( + authDiskSource = authDiskSource, + ), + ephemeralPinEnvelopeState = null, ) override fun getClientManagedTokens( diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/sdk/repository/SdkLocalUserDataKeyStateRepository.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/sdk/repository/SdkLocalUserDataKeyStateRepository.kt new file mode 100644 index 0000000000..d483553e03 --- /dev/null +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/sdk/repository/SdkLocalUserDataKeyStateRepository.kt @@ -0,0 +1,49 @@ +package com.x8bit.bitwarden.data.platform.manager.sdk.repository + +import com.bitwarden.core.LocalUserDataKeyState +import com.bitwarden.sdk.LocalUserDataKeyStateRepository +import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource + +/** + * An implementation of a Bitwarden SDK [LocalUserDataKeyStateRepository]. + */ +class SdkLocalUserDataKeyStateRepository( + private val authDiskSource: AuthDiskSource, +) : LocalUserDataKeyStateRepository { + override suspend fun get(id: String): LocalUserDataKeyState? { + return authDiskSource + .getLocalUserDataKey(userId = id) + ?.let { LocalUserDataKeyState(wrappedKey = it) } + } + + override suspend fun has( + id: String, + ): Boolean = authDiskSource.getLocalUserDataKey(userId = id) != null + + override suspend fun list(): List = + authDiskSource + .userState + ?.accounts + ?.mapNotNull { get(id = it.key) } + .orEmpty() + + override suspend fun remove(id: String) { + authDiskSource.storeLocalUserDataKey(userId = id, wrappedKey = null) + } + + override suspend fun removeAll() { + removeBulk(keys = authDiskSource.userState?.accounts.orEmpty().keys.toList()) + } + + override suspend fun removeBulk(keys: List) { + keys.forEach { remove(id = it) } + } + + override suspend fun set(id: String, value: LocalUserDataKeyState) { + authDiskSource.storeLocalUserDataKey(userId = id, value.wrappedKey) + } + + override suspend fun setBulk(values: Map) { + values.forEach { (id, value) -> set(id = id, value = value) } + } +} diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultLockManagerImpl.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultLockManagerImpl.kt index e5fd83d460..b6cf173e79 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultLockManagerImpl.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultLockManagerImpl.kt @@ -711,6 +711,7 @@ class VaultLockManagerImpl( is InitUserCryptoMethod.DecryptedKey, is InitUserCryptoMethod.DeviceKey, is InitUserCryptoMethod.KeyConnector, + is InitUserCryptoMethod.KeyConnectorUrl, is InitUserCryptoMethod.Pin, is InitUserCryptoMethod.PinEnvelope, -> return diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/util/InitUserCryptoMethodExtensions.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/util/InitUserCryptoMethodExtensions.kt index 22916e705b..edc5b1d004 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/util/InitUserCryptoMethodExtensions.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/util/InitUserCryptoMethodExtensions.kt @@ -14,5 +14,6 @@ val InitUserCryptoMethod.logTag: String is InitUserCryptoMethod.KeyConnector -> "Key Connector" is InitUserCryptoMethod.Pin -> "Pin" is InitUserCryptoMethod.PinEnvelope -> "Pin Envelope" + is InitUserCryptoMethod.KeyConnectorUrl -> "Key Connector Url" is InitUserCryptoMethod.MasterPasswordUnlock -> "Master Password Unlock" } diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/sdk/repository/SdkLocalUserDataKeyStateRepositoryTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/sdk/repository/SdkLocalUserDataKeyStateRepositoryTest.kt new file mode 100644 index 0000000000..37568595c2 --- /dev/null +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/sdk/repository/SdkLocalUserDataKeyStateRepositoryTest.kt @@ -0,0 +1,171 @@ +package com.x8bit.bitwarden.data.platform.manager.sdk.repository + +import com.bitwarden.core.LocalUserDataKeyState +import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson +import com.x8bit.bitwarden.data.auth.datasource.disk.util.FakeAuthDiskSource +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +class SdkLocalUserDataKeyStateRepositoryTest { + + private val fakeAuthDiskSource = FakeAuthDiskSource() + + private val repository = SdkLocalUserDataKeyStateRepository( + authDiskSource = fakeAuthDiskSource, + ) + + @Test + fun `get should return null when no key is stored for the given id`() = runTest { + assertNull(repository.get(id = USER_ID)) + } + + @Test + fun `get should return LocalUserDataKeyState when key is stored for the given id`() = runTest { + fakeAuthDiskSource.storeLocalUserDataKey(userId = USER_ID, wrappedKey = WRAPPED_KEY) + + assertEquals( + LocalUserDataKeyState(wrappedKey = WRAPPED_KEY), + repository.get(id = USER_ID), + ) + } + + @Test + fun `has should return false when no key is stored for the given id`() = runTest { + assertFalse(repository.has(id = USER_ID)) + } + + @Test + fun `has should return true when a key is stored for the given id`() = runTest { + fakeAuthDiskSource.storeLocalUserDataKey(userId = USER_ID, wrappedKey = WRAPPED_KEY) + + assertTrue(repository.has(id = USER_ID)) + } + + @Test + fun `list should return empty list when userState is null`() = runTest { + fakeAuthDiskSource.userState = null + + assertEquals(emptyList(), repository.list()) + } + + @Test + fun `list should return empty list when no keys are stored for any account`() = runTest { + fakeAuthDiskSource.userState = UserStateJson( + activeUserId = USER_ID, + accounts = mapOf(USER_ID to mockk()), + ) + + assertEquals(emptyList(), repository.list()) + } + + @Test + fun `list should return LocalUserDataKeyState for each account that has a stored key`() = + runTest { + fakeAuthDiskSource.userState = UserStateJson( + activeUserId = USER_ID, + accounts = mapOf( + USER_ID to mockk(), + USER_ID_2 to mockk(), + ), + ) + fakeAuthDiskSource.storeLocalUserDataKey(userId = USER_ID, wrappedKey = WRAPPED_KEY) + fakeAuthDiskSource.storeLocalUserDataKey(userId = USER_ID_2, wrappedKey = WRAPPED_KEY_2) + + assertEquals( + listOf( + LocalUserDataKeyState(wrappedKey = WRAPPED_KEY), + LocalUserDataKeyState(wrappedKey = WRAPPED_KEY_2), + ), + repository.list(), + ) + } + + @Test + fun `list should omit accounts that have no stored key`() = runTest { + fakeAuthDiskSource.userState = UserStateJson( + activeUserId = USER_ID, + accounts = mapOf(USER_ID to mockk(), USER_ID_2 to mockk()), + ) + fakeAuthDiskSource.storeLocalUserDataKey(userId = USER_ID, wrappedKey = WRAPPED_KEY) + + assertEquals( + listOf(LocalUserDataKeyState(wrappedKey = WRAPPED_KEY)), + repository.list(), + ) + } + + @Test + fun `remove should clear the stored key for the given id`() = runTest { + fakeAuthDiskSource.storeLocalUserDataKey(userId = USER_ID, wrappedKey = WRAPPED_KEY) + + repository.remove(id = USER_ID) + + assertNull(fakeAuthDiskSource.getLocalUserDataKey(userId = USER_ID)) + } + + @Test + fun `removeAll should clear the stored key for all accounts`() = runTest { + fakeAuthDiskSource.userState = UserStateJson( + activeUserId = USER_ID, + accounts = mapOf( + USER_ID to mockk(), + USER_ID_2 to mockk(), + ), + ) + fakeAuthDiskSource.storeLocalUserDataKey(userId = USER_ID, wrappedKey = WRAPPED_KEY) + fakeAuthDiskSource.storeLocalUserDataKey(userId = USER_ID_2, wrappedKey = WRAPPED_KEY_2) + + repository.removeAll() + + assertNull(fakeAuthDiskSource.getLocalUserDataKey(userId = USER_ID)) + assertNull(fakeAuthDiskSource.getLocalUserDataKey(userId = USER_ID_2)) + } + + @Test + fun `removeAll should do nothing when userState is null`() = runTest { + fakeAuthDiskSource.userState = null + + repository.removeAll() + } + + @Test + fun `removeBulk should clear the stored key for each given id`() = runTest { + fakeAuthDiskSource.storeLocalUserDataKey(userId = USER_ID, wrappedKey = WRAPPED_KEY) + fakeAuthDiskSource.storeLocalUserDataKey(userId = USER_ID_2, wrappedKey = WRAPPED_KEY_2) + + repository.removeBulk(keys = listOf(USER_ID, USER_ID_2)) + + assertNull(fakeAuthDiskSource.getLocalUserDataKey(userId = USER_ID)) + assertNull(fakeAuthDiskSource.getLocalUserDataKey(userId = USER_ID_2)) + } + + @Test + fun `set should store the wrapped key for the given id`() = runTest { + repository.set(id = USER_ID, value = LocalUserDataKeyState(wrappedKey = WRAPPED_KEY)) + + assertEquals(WRAPPED_KEY, fakeAuthDiskSource.getLocalUserDataKey(userId = USER_ID)) + } + + @Test + fun `setBulk should store the wrapped key for each given id`() = runTest { + repository.setBulk( + values = mapOf( + USER_ID to LocalUserDataKeyState(wrappedKey = WRAPPED_KEY), + USER_ID_2 to LocalUserDataKeyState(wrappedKey = WRAPPED_KEY_2), + ), + ) + + assertEquals(WRAPPED_KEY, fakeAuthDiskSource.getLocalUserDataKey(userId = USER_ID)) + assertEquals(WRAPPED_KEY_2, fakeAuthDiskSource.getLocalUserDataKey(userId = USER_ID_2)) + } +} + +private const val USER_ID: String = "userId" +private const val USER_ID_2: String = "userId2" +private const val WRAPPED_KEY: String = "wrappedKey" +private const val WRAPPED_KEY_2: String = "wrappedKey2" diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1d717cdc14..09bc669cc9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -30,7 +30,7 @@ androidxRoom = "2.8.4" androidxSecurityCrypto = "1.1.0" androidxSplash = "1.2.0" androidxWork = "2.11.1" -bitwardenSdk = "2.0.0-5451-c73f9161" +bitwardenSdk = "2.0.0-5676-14521973" crashlytics = "3.0.6" detekt = "1.23.8" firebaseBom = "34.10.0"