mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 07:05:35 +03:00
BITAU-103 Implement symmetric key creation and storage (#3905)
This commit is contained in:
parent
4b53358c67
commit
f89b053d2e
8 changed files with 89 additions and 4 deletions
Binary file not shown.
Binary file not shown.
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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 = """
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue