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")
|
||||
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
|
||||
* 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
|
||||
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 BIOMETRICS_UNLOCK_KEY = "userKeyBiometricUnlock"
|
||||
private const val USER_AUTO_UNLOCK_KEY_KEY = "userKeyAutoUnlock"
|
||||
|
@ -93,6 +94,14 @@ class AuthDiskSourceImpl(
|
|||
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
|
||||
get() = getString(key = UNIQUE_APP_ID_KEY) ?: generateAndStoreUniqueAppId()
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package com.x8bit.bitwarden.data.platform.repository
|
||||
|
||||
import android.view.autofill.AutofillManager
|
||||
import com.bitwarden.bridge.util.generateSecretKey
|
||||
import com.x8bit.bitwarden.BuildConfig
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation
|
||||
|
@ -119,7 +120,9 @@ class SettingsRepositoryImpl(
|
|||
return
|
||||
}
|
||||
// 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 {
|
||||
vaultSdkSource
|
||||
.getUserEncryptionKey(userId = userId)
|
||||
|
@ -535,6 +538,20 @@ class SettingsRepositoryImpl(
|
|||
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
|
||||
* 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 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.AccountTokensJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.EnvironmentUrlDataJson
|
||||
|
@ -1133,6 +1134,25 @@ class AuthDiskSourceTest {
|
|||
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 = """
|
||||
|
|
|
@ -14,6 +14,8 @@ import org.junit.Assert.assertEquals
|
|||
|
||||
class FakeAuthDiskSource : AuthDiskSource {
|
||||
|
||||
override var authenticatorSyncSymmetricKey: ByteArray? = null
|
||||
|
||||
override val uniqueAppId: String = "testUniqueAppId"
|
||||
|
||||
override var rememberedEmailAddress: String? = null
|
||||
|
|
|
@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.platform.repository
|
|||
|
||||
import android.view.autofill.AutofillManager
|
||||
import app.cash.turbine.test
|
||||
import com.bitwarden.bridge.util.generateSecretKey
|
||||
import com.bitwarden.core.DerivePinKeyResponse
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.EnvironmentUrlDataJson
|
||||
|
@ -42,6 +43,7 @@ import io.mockk.verify
|
|||
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.assertNotNull
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
|
@ -1060,12 +1062,13 @@ class SettingsRepositoryTest {
|
|||
}
|
||||
|
||||
@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 {
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
coEvery { vaultSdkSource.getUserEncryptionKey(USER_ID) }
|
||||
.returns(AUTHENTICATION_SYNC_KEY.asSuccess())
|
||||
|
||||
fakeAuthDiskSource.authenticatorSyncSymmetricKey = null
|
||||
assertNull(fakeAuthDiskSource.getAuthenticatorSyncUnlockKey(USER_ID))
|
||||
|
||||
settingsRepository.isAuthenticatorSyncEnabled = true
|
||||
|
@ -1075,13 +1078,39 @@ class SettingsRepositoryTest {
|
|||
AUTHENTICATION_SYNC_KEY,
|
||||
fakeAuthDiskSource.getAuthenticatorSyncUnlockKey(USER_ID),
|
||||
)
|
||||
assertNotNull(fakeAuthDiskSource.authenticatorSyncSymmetricKey)
|
||||
coVerify { vaultSdkSource.getUserEncryptionKey(USER_ID) }
|
||||
}
|
||||
|
||||
@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 {
|
||||
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)
|
||||
|
||||
assertTrue(settingsRepository.isAuthenticatorSyncEnabled)
|
||||
|
@ -1090,6 +1119,7 @@ class SettingsRepositoryTest {
|
|||
|
||||
assertFalse(settingsRepository.isAuthenticatorSyncEnabled)
|
||||
assertNull(fakeAuthDiskSource.getAuthenticatorSyncUnlockKey(USER_ID))
|
||||
assertTrue(fakeAuthDiskSource.authenticatorSyncSymmetricKey.contentEquals(syncSymmetricKey))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
Loading…
Reference in a new issue