BITAU-103 Implement symmetric key creation and storage (#3905)

This commit is contained in:
Andrew Haisting 2024-09-18 13:47:10 -05:00 committed by GitHub
parent 4b53358c67
commit f89b053d2e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 89 additions and 4 deletions

Binary file not shown.

View file

@ -12,6 +12,13 @@ import kotlinx.coroutines.flow.Flow
*/ */
@Suppress("TooManyFunctions") @Suppress("TooManyFunctions")
interface AuthDiskSource { interface AuthDiskSource {
/**
* The currently persisted authenticator sync symmetric key. This key is used for
* encrypting IPC traffic.
*/
var authenticatorSyncSymmetricKey: ByteArray?
/** /**
* Retrieves a unique ID for the application that is stored locally. This will generate a new * Retrieves a unique ID for the application that is stored locally. This will generate a new
* one if it does not yet exist and it will only be reset for new installs or when clearing * one if it does not yet exist and it will only be reset for new installs or when clearing

View file

@ -19,6 +19,7 @@ import java.util.UUID
// These keys should be encrypted // These keys should be encrypted
private const val ACCOUNT_TOKENS_KEY = "accountTokens" private const val ACCOUNT_TOKENS_KEY = "accountTokens"
private const val AUTHENTICATOR_SYNC_SYMMETRIC_KEY = "authenticatorSyncSymmetric"
private const val AUTHENTICATOR_SYNC_UNLOCK_KEY = "authenticatorSyncUnlock" private const val AUTHENTICATOR_SYNC_UNLOCK_KEY = "authenticatorSyncUnlock"
private const val BIOMETRICS_UNLOCK_KEY = "userKeyBiometricUnlock" private const val BIOMETRICS_UNLOCK_KEY = "userKeyBiometricUnlock"
private const val USER_AUTO_UNLOCK_KEY_KEY = "userKeyAutoUnlock" private const val USER_AUTO_UNLOCK_KEY_KEY = "userKeyAutoUnlock"
@ -93,6 +94,14 @@ class AuthDiskSourceImpl(
migrateAccountTokens() migrateAccountTokens()
} }
override var authenticatorSyncSymmetricKey: 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)
override val uniqueAppId: String override val uniqueAppId: String
get() = getString(key = UNIQUE_APP_ID_KEY) ?: generateAndStoreUniqueAppId() get() = getString(key = UNIQUE_APP_ID_KEY) ?: generateAndStoreUniqueAppId()

View file

@ -1,6 +1,7 @@
package com.x8bit.bitwarden.data.platform.repository package com.x8bit.bitwarden.data.platform.repository
import android.view.autofill.AutofillManager import android.view.autofill.AutofillManager
import com.bitwarden.bridge.util.generateSecretKey
import com.x8bit.bitwarden.BuildConfig import com.x8bit.bitwarden.BuildConfig
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation
@ -119,7 +120,9 @@ class SettingsRepositoryImpl(
return return
} }
// When turning on authenticator sync, get a user encryption key from the vault SDK // When turning on authenticator sync, get a user encryption key from the vault SDK
// and store it as a authenticator sync unlock key: // and store it as a authenticator sync unlock key. Also, generate a
// symmetric sync key if needed:
generateSymmetricSyncKeyIfNecessary()
unconfinedScope.launch { unconfinedScope.launch {
vaultSdkSource vaultSdkSource
.getUserEncryptionKey(userId = userId) .getUserEncryptionKey(userId = userId)
@ -535,6 +538,20 @@ class SettingsRepositoryImpl(
settingsDiskSource.storeUseHasLoggedInPreviously(userId) settingsDiskSource.storeUseHasLoggedInPreviously(userId)
} }
/**
* If there isn't already one generated, generate a symmetric sync key that would be used
* for communicating via IPC.
*/
private fun generateSymmetricSyncKeyIfNecessary() {
// If there is already an authenticator sync symmetric key, do nothing:
if (authDiskSource.authenticatorSyncSymmetricKey != null) {
return
}
// Otherwise, generate and store a key:
val secretKey = generateSecretKey().getOrNull() ?: return
authDiskSource.authenticatorSyncSymmetricKey = secretKey.encoded
}
/** /**
* Check the parameters of the vault unlock policy against the user's * Check the parameters of the vault unlock policy against the user's
* settings to determine whether to update the user's settings. * settings to determine whether to update the user's settings.

View file

@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.auth.datasource.disk
import androidx.core.content.edit import androidx.core.content.edit
import app.cash.turbine.test import app.cash.turbine.test
import com.bitwarden.bridge.util.generateSecretKey
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.EnvironmentUrlDataJson import com.x8bit.bitwarden.data.auth.datasource.disk.model.EnvironmentUrlDataJson
@ -1133,6 +1134,25 @@ class AuthDiskSourceTest {
assertEquals(OnboardingStatus.AUTOFILL_SETUP, awaitItem()) assertEquals(OnboardingStatus.AUTOFILL_SETUP, awaitItem())
} }
} }
fun `authenticatorSyncSymmetricKey should store and update from EncryptedSharedPreferences`() {
val sharedPrefsKey = "bwSecureStorage:authenticatorSyncSymmetric"
// Shared preferences and the repository start with the same value:
assertNull(authDiskSource.authenticatorSyncSymmetricKey)
assertNull(fakeEncryptedSharedPreferences.getString(sharedPrefsKey, null))
// Updating the repository updates shared preferences:
val symmetricKey = generateSecretKey().getOrThrow().encoded
authDiskSource.authenticatorSyncSymmetricKey = 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.authenticatorSyncSymmetricKey.contentEquals(symmetricKey))
}
} }
private const val USER_STATE_JSON = """ private const val USER_STATE_JSON = """

View file

@ -14,6 +14,8 @@ import org.junit.Assert.assertEquals
class FakeAuthDiskSource : AuthDiskSource { class FakeAuthDiskSource : AuthDiskSource {
override var authenticatorSyncSymmetricKey: ByteArray? = null
override val uniqueAppId: String = "testUniqueAppId" override val uniqueAppId: String = "testUniqueAppId"
override var rememberedEmailAddress: String? = null override var rememberedEmailAddress: String? = null

View file

@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.platform.repository
import android.view.autofill.AutofillManager import android.view.autofill.AutofillManager
import app.cash.turbine.test import app.cash.turbine.test
import com.bitwarden.bridge.util.generateSecretKey
import com.bitwarden.core.DerivePinKeyResponse import com.bitwarden.core.DerivePinKeyResponse
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.EnvironmentUrlDataJson import com.x8bit.bitwarden.data.auth.datasource.disk.model.EnvironmentUrlDataJson
@ -42,6 +43,7 @@ import io.mockk.verify
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
@ -1060,12 +1062,13 @@ class SettingsRepositoryTest {
} }
@Test @Test
fun `setting isAuthenticatorSyncEnabled to true should generate an authenticator sync key`() = @Suppress("MaxLineLength")
fun `isAuthenticatorSyncEnabled set to true should generate an authenticator sync key and also a symmetric key if none exists`() =
runTest { runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE fakeAuthDiskSource.userState = MOCK_USER_STATE
coEvery { vaultSdkSource.getUserEncryptionKey(USER_ID) } coEvery { vaultSdkSource.getUserEncryptionKey(USER_ID) }
.returns(AUTHENTICATION_SYNC_KEY.asSuccess()) .returns(AUTHENTICATION_SYNC_KEY.asSuccess())
fakeAuthDiskSource.authenticatorSyncSymmetricKey = null
assertNull(fakeAuthDiskSource.getAuthenticatorSyncUnlockKey(USER_ID)) assertNull(fakeAuthDiskSource.getAuthenticatorSyncUnlockKey(USER_ID))
settingsRepository.isAuthenticatorSyncEnabled = true settingsRepository.isAuthenticatorSyncEnabled = true
@ -1075,13 +1078,39 @@ class SettingsRepositoryTest {
AUTHENTICATION_SYNC_KEY, AUTHENTICATION_SYNC_KEY,
fakeAuthDiskSource.getAuthenticatorSyncUnlockKey(USER_ID), fakeAuthDiskSource.getAuthenticatorSyncUnlockKey(USER_ID),
) )
assertNotNull(fakeAuthDiskSource.authenticatorSyncSymmetricKey)
coVerify { vaultSdkSource.getUserEncryptionKey(USER_ID) } coVerify { vaultSdkSource.getUserEncryptionKey(USER_ID) }
} }
@Test @Test
fun `setting isAuthenticatorSyncEnabled to false should clear authenticator sync key`() = @Suppress("MaxLineLength")
fun `isAuthenticatorSyncEnabled set to true should generate an authenticator sync key and leave symmetric key untouched if already set`() =
runTest { runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE fakeAuthDiskSource.userState = MOCK_USER_STATE
coEvery { vaultSdkSource.getUserEncryptionKey(USER_ID) }
.returns(AUTHENTICATION_SYNC_KEY.asSuccess())
val symmetricKey = generateSecretKey().getOrThrow().encoded
fakeAuthDiskSource.authenticatorSyncSymmetricKey = symmetricKey
assertNull(fakeAuthDiskSource.getAuthenticatorSyncUnlockKey(USER_ID))
settingsRepository.isAuthenticatorSyncEnabled = true
assertTrue(settingsRepository.isAuthenticatorSyncEnabled)
assertEquals(
AUTHENTICATION_SYNC_KEY,
fakeAuthDiskSource.getAuthenticatorSyncUnlockKey(USER_ID),
)
fakeAuthDiskSource.authenticatorSyncSymmetricKey.contentEquals(symmetricKey)
coVerify { vaultSdkSource.getUserEncryptionKey(USER_ID) }
}
@Test
@Suppress("MaxLineLength")
fun `isAuthenticatorSyncEnabled set to false should clear authenticator sync key and leave symmetric sync key untouched`() =
runTest {
val syncSymmetricKey = generateSecretKey().getOrThrow().encoded
fakeAuthDiskSource.userState = MOCK_USER_STATE
fakeAuthDiskSource.authenticatorSyncSymmetricKey = syncSymmetricKey
fakeAuthDiskSource.storeAuthenticatorSyncUnlockKey(USER_ID, AUTHENTICATION_SYNC_KEY) fakeAuthDiskSource.storeAuthenticatorSyncUnlockKey(USER_ID, AUTHENTICATION_SYNC_KEY)
assertTrue(settingsRepository.isAuthenticatorSyncEnabled) assertTrue(settingsRepository.isAuthenticatorSyncEnabled)
@ -1090,6 +1119,7 @@ class SettingsRepositoryTest {
assertFalse(settingsRepository.isAuthenticatorSyncEnabled) assertFalse(settingsRepository.isAuthenticatorSyncEnabled)
assertNull(fakeAuthDiskSource.getAuthenticatorSyncUnlockKey(USER_ID)) assertNull(fakeAuthDiskSource.getAuthenticatorSyncUnlockKey(USER_ID))
assertTrue(fakeAuthDiskSource.authenticatorSyncSymmetricKey.contentEquals(syncSymmetricKey))
} }
@Test @Test