diff --git a/app/src/main/kotlin/com/bitwarden/authenticator/data/auth/datasource/disk/AuthDiskSource.kt b/app/src/main/kotlin/com/bitwarden/authenticator/data/auth/datasource/disk/AuthDiskSource.kt index 513521c17a..6ba4d84c01 100644 --- a/app/src/main/kotlin/com/bitwarden/authenticator/data/auth/datasource/disk/AuthDiskSource.kt +++ b/app/src/main/kotlin/com/bitwarden/authenticator/data/auth/datasource/disk/AuthDiskSource.kt @@ -30,4 +30,9 @@ interface AuthDiskSource { * Stores the biometrics key. */ fun storeUserBiometricUnlockKey(biometricsKey: String?) + + /** + * Stores the symmetric key data used for encrypting TOTP data. + */ + var authenticatorBridgeSymmetricSyncKey: ByteArray? } diff --git a/app/src/main/kotlin/com/bitwarden/authenticator/data/auth/datasource/disk/AuthDiskSourceImpl.kt b/app/src/main/kotlin/com/bitwarden/authenticator/data/auth/datasource/disk/AuthDiskSourceImpl.kt index 48c8636e96..2c25f8098f 100644 --- a/app/src/main/kotlin/com/bitwarden/authenticator/data/auth/datasource/disk/AuthDiskSourceImpl.kt +++ b/app/src/main/kotlin/com/bitwarden/authenticator/data/auth/datasource/disk/AuthDiskSourceImpl.kt @@ -5,6 +5,8 @@ import com.bitwarden.authenticator.data.platform.datasource.disk.BaseDiskSource. import com.bitwarden.authenticator.data.platform.datasource.disk.BaseEncryptedDiskSource import com.bitwarden.authenticator.data.platform.datasource.disk.BaseEncryptedDiskSource.Companion.ENCRYPTED_BASE_KEY +private const val AUTHENTICATOR_SYNC_SYMMETRIC_KEY = + "$ENCRYPTED_BASE_KEY:authenticatorSyncSymmetricKey" private const val LAST_ACTIVE_TIME_KEY = "$BASE_KEY:lastActiveTime" private const val BIOMETRICS_UNLOCK_KEY = "$ENCRYPTED_BASE_KEY:userKeyBiometricUnlock" @@ -43,4 +45,12 @@ class AuthDiskSourceImpl( value = biometricsKey, ) } + + override var authenticatorBridgeSymmetricSyncKey: ByteArray? + set(value) { + val asString = value?.let { value.toString(Charsets.ISO_8859_1) } + putEncryptedString(AUTHENTICATOR_SYNC_SYMMETRIC_KEY, asString) + } + get() = getEncryptedString(AUTHENTICATOR_SYNC_SYMMETRIC_KEY) + ?.toByteArray(Charsets.ISO_8859_1) } diff --git a/app/src/main/kotlin/com/bitwarden/authenticator/data/authenticator/repository/di/AuthenticatorBridgeModule.kt b/app/src/main/kotlin/com/bitwarden/authenticator/data/authenticator/repository/di/AuthenticatorBridgeModule.kt index 19d5b9f201..fae43aeb19 100644 --- a/app/src/main/kotlin/com/bitwarden/authenticator/data/authenticator/repository/di/AuthenticatorBridgeModule.kt +++ b/app/src/main/kotlin/com/bitwarden/authenticator/data/authenticator/repository/di/AuthenticatorBridgeModule.kt @@ -1,10 +1,11 @@ package com.bitwarden.authenticator.data.authenticator.repository.di import android.content.Context +import com.bitwarden.authenticator.data.auth.datasource.disk.AuthDiskSource +import com.bitwarden.authenticator.data.authenticator.repository.util.SymmetricKeyStorageProviderImpl import com.bitwarden.authenticatorbridge.factory.AuthenticatorBridgeFactory import com.bitwarden.authenticatorbridge.manager.AuthenticatorBridgeManager import com.bitwarden.authenticatorbridge.manager.model.AuthenticatorBridgeConnectionType -import com.bitwarden.authenticatorbridge.model.SymmetricEncryptionKeyData import com.bitwarden.authenticatorbridge.provider.SymmetricKeyStorageProvider import dagger.Module import dagger.Provides @@ -31,12 +32,17 @@ object AuthenticatorBridgeModule { @Singleton fun provideAuthenticatorBridgeManager( factory: AuthenticatorBridgeFactory, + symmetricKeyStorageProvider: SymmetricKeyStorageProvider, ): AuthenticatorBridgeManager = factory.getAuthenticatorBridgeManager( connectionType = AuthenticatorBridgeConnectionType.DEV, - symmetricKeyStorageProvider = object : SymmetricKeyStorageProvider { - - // TODO: Implement symmetric key storage: BITAU-70 - override var symmetricKey: SymmetricEncryptionKeyData? = null - }, + symmetricKeyStorageProvider = symmetricKeyStorageProvider, ) + + @Provides + fun providesSymmetricKeyStorageProvider( + authDiskSource: AuthDiskSource, + ): SymmetricKeyStorageProvider = + SymmetricKeyStorageProviderImpl( + authDiskSource = authDiskSource, + ) } diff --git a/app/src/main/kotlin/com/bitwarden/authenticator/data/authenticator/repository/util/SymmetricKeyStorageProviderImpl.kt b/app/src/main/kotlin/com/bitwarden/authenticator/data/authenticator/repository/util/SymmetricKeyStorageProviderImpl.kt new file mode 100644 index 0000000000..1efc07d35a --- /dev/null +++ b/app/src/main/kotlin/com/bitwarden/authenticator/data/authenticator/repository/util/SymmetricKeyStorageProviderImpl.kt @@ -0,0 +1,22 @@ +package com.bitwarden.authenticator.data.authenticator.repository.util + +import com.bitwarden.authenticator.data.auth.datasource.disk.AuthDiskSource +import com.bitwarden.authenticatorbridge.model.SymmetricEncryptionKeyData +import com.bitwarden.authenticatorbridge.provider.SymmetricKeyStorageProvider +import com.bitwarden.authenticatorbridge.util.toSymmetricEncryptionKeyData + +/** + * Implementation of [SymmetricKeyStorageProvider] that stores symmetric key data in encrypted + * shared preferences. + */ +class SymmetricKeyStorageProviderImpl( + private val authDiskSource: AuthDiskSource, +) : SymmetricKeyStorageProvider { + + override var symmetricKey: SymmetricEncryptionKeyData? + get() = authDiskSource.authenticatorBridgeSymmetricSyncKey?.toSymmetricEncryptionKeyData() + set(value) { + authDiskSource.authenticatorBridgeSymmetricSyncKey = + value?.symmetricEncryptionKey?.byteArray + } +} diff --git a/app/src/test/java/com/bitwarden/authenticator/data/auth/datasource/disk/AuthDiskSourceTest.kt b/app/src/test/java/com/bitwarden/authenticator/data/auth/datasource/disk/AuthDiskSourceTest.kt new file mode 100644 index 0000000000..b738d1d97f --- /dev/null +++ b/app/src/test/java/com/bitwarden/authenticator/data/auth/datasource/disk/AuthDiskSourceTest.kt @@ -0,0 +1,40 @@ +package com.bitwarden.authenticator.data.auth.datasource.disk + +import com.bitwarden.authenticator.data.platform.base.FakeSharedPreferences +import com.bitwarden.authenticatorbridge.util.generateSecretKey +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +class AuthDiskSourceTest { + + private val fakeEncryptedSharedPreferences = FakeSharedPreferences() + private val fakeSharedPreferences = FakeSharedPreferences() + + private val authDiskSource = AuthDiskSourceImpl( + encryptedSharedPreferences = fakeEncryptedSharedPreferences, + sharedPreferences = fakeSharedPreferences, + ) + + @Test + @Suppress("MaxLineLength") + fun `authenticatorBridgeSymmetricSyncKey should store and update from EncryptedSharedPreferences`() { + val sharedPrefsKey = "bwSecureStorage:authenticatorSyncSymmetricKey" + + // Shared preferences and the repository start with the same value: + assertNull(authDiskSource.authenticatorBridgeSymmetricSyncKey) + assertNull(fakeEncryptedSharedPreferences.getString(sharedPrefsKey, null)) + + // Updating the repository updates shared preferences: + val symmetricKey = generateSecretKey().getOrThrow().encoded + authDiskSource.authenticatorBridgeSymmetricSyncKey = symmetricKey + assertEquals( + symmetricKey.toString(Charsets.ISO_8859_1), + fakeEncryptedSharedPreferences.getString(sharedPrefsKey, null), + ) + + // Retrieving the key from repository should give same byte array despite String conversion: + assertTrue(authDiskSource.authenticatorBridgeSymmetricSyncKey.contentEquals(symmetricKey)) + } +} diff --git a/app/src/test/java/com/bitwarden/authenticator/data/auth/datasource/disk/util/FakeAuthDiskSource.kt b/app/src/test/java/com/bitwarden/authenticator/data/auth/datasource/disk/util/FakeAuthDiskSource.kt new file mode 100644 index 0000000000..f5cb87320c --- /dev/null +++ b/app/src/test/java/com/bitwarden/authenticator/data/auth/datasource/disk/util/FakeAuthDiskSource.kt @@ -0,0 +1,23 @@ +package com.bitwarden.authenticator.data.auth.datasource.disk.util + +import com.bitwarden.authenticator.data.auth.datasource.disk.AuthDiskSource + +class FakeAuthDiskSource : AuthDiskSource { + + private var lastActiveTimeMillis: Long? = null + private var userBiometricUnlockKey: String? = null + + override fun getLastActiveTimeMillis(): Long? = lastActiveTimeMillis + + override fun storeLastActiveTimeMillis(lastActiveTimeMillis: Long?) { + this@FakeAuthDiskSource.lastActiveTimeMillis = lastActiveTimeMillis + } + + override fun getUserBiometricUnlockKey(): String? = userBiometricUnlockKey + + override fun storeUserBiometricUnlockKey(biometricsKey: String?) { + this@FakeAuthDiskSource.userBiometricUnlockKey = biometricsKey + } + + override var authenticatorBridgeSymmetricSyncKey: ByteArray? = null +} diff --git a/app/src/test/java/com/bitwarden/authenticator/data/authenticator/repository/util/SymmetricKeyStorageProviderTest.kt b/app/src/test/java/com/bitwarden/authenticator/data/authenticator/repository/util/SymmetricKeyStorageProviderTest.kt new file mode 100644 index 0000000000..c75bff7161 --- /dev/null +++ b/app/src/test/java/com/bitwarden/authenticator/data/authenticator/repository/util/SymmetricKeyStorageProviderTest.kt @@ -0,0 +1,50 @@ +package com.bitwarden.authenticator.data.authenticator.repository.util + +import com.bitwarden.authenticator.data.auth.datasource.disk.util.FakeAuthDiskSource +import com.bitwarden.authenticatorbridge.util.generateSecretKey +import com.bitwarden.authenticatorbridge.util.toSymmetricEncryptionKeyData +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +class SymmetricKeyStorageProviderTest { + + private val fakeAuthDiskSource = FakeAuthDiskSource() + + private val provider = SymmetricKeyStorageProviderImpl( + authDiskSource = fakeAuthDiskSource, + ) + + @Test + fun `symmetricKey get should return null when disk source has no symmetric key`() { + fakeAuthDiskSource.authenticatorBridgeSymmetricSyncKey = null + assertNull(provider.symmetricKey) + } + + @Test + fun `symmetricKey get should return symmetric key when disk source has symmetric key`() { + val key = generateSecretKey().getOrThrow() + fakeAuthDiskSource.authenticatorBridgeSymmetricSyncKey = key.encoded + assertEquals( + key.encoded.toSymmetricEncryptionKeyData(), + provider.symmetricKey, + ) + } + + @Test + fun `symmetricKey set should store key in AuthDiskSource`() { + val key = generateSecretKey().getOrThrow() + fakeAuthDiskSource.authenticatorBridgeSymmetricSyncKey = null + + provider.symmetricKey = key.encoded.toSymmetricEncryptionKeyData() + assertTrue( + key.encoded.contentEquals( + fakeAuthDiskSource.authenticatorBridgeSymmetricSyncKey, + ), + ) + + provider.symmetricKey = null + assertNull(fakeAuthDiskSource.authenticatorBridgeSymmetricSyncKey) + } +}