mirror of
https://github.com/bitwarden/android.git
synced 2026-11-09 10:34:18 -06:00
PM-40154: feat: Migrate user data to KeystoreEncryptedSharedPreferences (#7158)
This commit is contained in:
@@ -2,8 +2,10 @@ package com.x8bit.bitwarden
|
||||
|
||||
import android.app.Application
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
import com.bitwarden.data.manager.flightrecorder.FlightRecorderManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.AuthRequestNotificationManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.FillAssistManager
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.legacy.DiskSourceMigrationLogger
|
||||
import com.x8bit.bitwarden.data.platform.manager.LogsManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.network.NetworkConfigManager
|
||||
@@ -22,11 +24,17 @@ class BitwardenApplication : Application() {
|
||||
// Inject classes here that must be triggered on startup but are not otherwise consumed by
|
||||
// other callers.
|
||||
@Inject
|
||||
lateinit var fillAssistManager: FillAssistManager
|
||||
lateinit var flightRecorder: FlightRecorderManager
|
||||
|
||||
@Inject
|
||||
lateinit var logsManager: LogsManager
|
||||
|
||||
@Inject
|
||||
lateinit var diskSourceMigrationLogger: DiskSourceMigrationLogger
|
||||
|
||||
@Inject
|
||||
lateinit var fillAssistManager: FillAssistManager
|
||||
|
||||
@Inject
|
||||
lateinit var networkConfigManager: NetworkConfigManager
|
||||
|
||||
|
||||
+24
-5
@@ -21,6 +21,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.onSubscription
|
||||
import kotlinx.serialization.builtins.serializer
|
||||
import kotlinx.serialization.json.Json
|
||||
import timber.log.Timber
|
||||
import java.time.Instant
|
||||
import java.util.UUID
|
||||
|
||||
@@ -33,6 +34,7 @@ private const val BIOMETRICS_UNLOCK_KEY = "userKeyBiometricUnlock"
|
||||
private const val USER_AUTO_UNLOCK_KEY_KEY = "userKeyAutoUnlock"
|
||||
private const val DEVICE_KEY_KEY = "deviceKey"
|
||||
private const val PENDING_ADMIN_AUTH_REQUEST_KEY = "pendingAdminAuthRequest"
|
||||
private const val ACCOUNT_CRYPTOGRAPHIC_STATE_KEY = "accountCryptographicState"
|
||||
|
||||
// These keys should not be encrypted
|
||||
private const val UNIQUE_APP_ID_KEY = "appId"
|
||||
@@ -58,7 +60,6 @@ private const val ONBOARDING_STATUS_KEY = "onboardingStatus"
|
||||
private const val SHOW_IMPORT_LOGINS_KEY = "showImportLogins"
|
||||
private const val LAST_LOCK_TIMESTAMP = "lastLockTimestamp"
|
||||
private const val PROFILE_ACCOUNT_KEYS_KEY = "profileAccountKeys"
|
||||
private const val ACCOUNT_CRYPTOGRAPHIC_STATE_KEY = "accountCryptographicState"
|
||||
private const val V2_UPGRADE_TOKEN = "v2UpgradeToken"
|
||||
|
||||
/**
|
||||
@@ -67,11 +68,13 @@ private const val V2_UPGRADE_TOKEN = "v2UpgradeToken"
|
||||
@Suppress("TooManyFunctions")
|
||||
class AuthDiskSourceImpl(
|
||||
encryptedSharedPreferences: SharedPreferences,
|
||||
keystoreEncryptedPreferences: SharedPreferences,
|
||||
sharedPreferences: SharedPreferences,
|
||||
legacySecureStorageMigrator: LegacySecureStorageMigrator,
|
||||
private val json: Json,
|
||||
) : BaseEncryptedDiskSource(
|
||||
encryptedSharedPreferences = encryptedSharedPreferences,
|
||||
keystoreEncryptedPreferences = keystoreEncryptedPreferences,
|
||||
sharedPreferences = sharedPreferences,
|
||||
),
|
||||
AuthDiskSource {
|
||||
@@ -132,6 +135,9 @@ class AuthDiskSourceImpl(
|
||||
// We must migrate the Private Key and Account Keys to use the Account Cryptographic state
|
||||
// from now on.
|
||||
migrateAccountKeys()
|
||||
|
||||
// Migrate to the Keystore Encrypted SharedPreferences.
|
||||
migrateToKeystoreEncryption()
|
||||
}
|
||||
|
||||
override var authenticatorSyncSymmetricKey: ByteArray?
|
||||
@@ -285,10 +291,7 @@ class AuthDiskSourceImpl(
|
||||
}
|
||||
|
||||
override fun getUserAutoUnlockKey(userId: String): String? =
|
||||
getEncryptedString(
|
||||
key = USER_AUTO_UNLOCK_KEY_KEY.appendIdentifier(userId),
|
||||
default = null,
|
||||
)
|
||||
getEncryptedString(key = USER_AUTO_UNLOCK_KEY_KEY.appendIdentifier(userId))
|
||||
|
||||
override fun storeUserAutoUnlockKey(
|
||||
userId: String,
|
||||
@@ -740,4 +743,20 @@ class AuthDiskSourceImpl(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun migrateToKeystoreEncryption() {
|
||||
var isMigrated = false
|
||||
isMigrated = migrateKeyByPrefix(keyPrefix = AUTHENTICATOR_SYNC_SYMMETRIC_KEY) || isMigrated
|
||||
isMigrated = migrateKeyByPrefix(keyPrefix = AUTHENTICATOR_SYNC_UNLOCK_KEY) || isMigrated
|
||||
isMigrated = migrateKeyByPrefix(keyPrefix = ACCOUNT_CRYPTOGRAPHIC_STATE_KEY) || isMigrated
|
||||
isMigrated = migrateKeyByPrefix(keyPrefix = USER_AUTO_UNLOCK_KEY_KEY) || isMigrated
|
||||
isMigrated = migrateKeyByPrefix(keyPrefix = DEVICE_KEY_KEY) || isMigrated
|
||||
isMigrated = migrateKeyByPrefix(keyPrefix = PENDING_ADMIN_AUTH_REQUEST_KEY) || isMigrated
|
||||
isMigrated = migrateKeyByPrefix(keyPrefix = BIOMETRICS_INIT_VECTOR_KEY) || isMigrated
|
||||
isMigrated = migrateKeyByPrefix(keyPrefix = BIOMETRICS_UNLOCK_KEY) || isMigrated
|
||||
isMigrated = migrateKeyByPrefix(keyPrefix = ACCOUNT_TOKENS_KEY) || isMigrated
|
||||
if (isMigrated) {
|
||||
Timber.d("AuthDiskSource has been migrated to keystore encrypted shared preferences")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.auth.datasource.disk.di
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import com.bitwarden.data.datasource.disk.di.EncryptedPreferences
|
||||
import com.bitwarden.data.datasource.disk.di.KeystoreEncryptedPreferences
|
||||
import com.bitwarden.data.datasource.disk.di.UnencryptedPreferences
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSourceImpl
|
||||
@@ -24,12 +25,14 @@ object AuthDiskModule {
|
||||
@Singleton
|
||||
fun provideAuthDiskSource(
|
||||
@EncryptedPreferences encryptedSharedPreferences: SharedPreferences,
|
||||
@KeystoreEncryptedPreferences keystoreEncryptedPreferences: SharedPreferences,
|
||||
@UnencryptedPreferences sharedPreferences: SharedPreferences,
|
||||
legacySecureStorageMigrator: LegacySecureStorageMigrator,
|
||||
json: Json,
|
||||
): AuthDiskSource =
|
||||
AuthDiskSourceImpl(
|
||||
encryptedSharedPreferences = encryptedSharedPreferences,
|
||||
keystoreEncryptedPreferences = keystoreEncryptedPreferences,
|
||||
sharedPreferences = sharedPreferences,
|
||||
legacySecureStorageMigrator = legacySecureStorageMigrator,
|
||||
json = json,
|
||||
|
||||
+20
-5
@@ -6,9 +6,9 @@ import com.bitwarden.core.data.util.decodeFromStringOrNull
|
||||
import com.bitwarden.data.datasource.disk.BaseEncryptedDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.model.CookieConfigurationData
|
||||
import kotlinx.serialization.json.Json
|
||||
import timber.log.Timber
|
||||
|
||||
private const val CONFIG_PREFIX = "elb_cookie_config"
|
||||
private const val ENCRYPTED_PREFIX = "bwSecureStorage:$CONFIG_PREFIX"
|
||||
|
||||
/**
|
||||
* Implementation of [CookieDiskSource] using encrypted SharedPreferences.
|
||||
@@ -17,14 +17,21 @@ private const val ENCRYPTED_PREFIX = "bwSecureStorage:$CONFIG_PREFIX"
|
||||
*/
|
||||
class CookieDiskSourceImpl(
|
||||
sharedPreferences: SharedPreferences,
|
||||
private val encryptedSharedPreferences: SharedPreferences,
|
||||
private val keystoreEncryptedPreferences: SharedPreferences,
|
||||
encryptedSharedPreferences: SharedPreferences,
|
||||
private val json: Json,
|
||||
) : CookieDiskSource,
|
||||
BaseEncryptedDiskSource(
|
||||
sharedPreferences = sharedPreferences,
|
||||
keystoreEncryptedPreferences = keystoreEncryptedPreferences,
|
||||
encryptedSharedPreferences = encryptedSharedPreferences,
|
||||
) {
|
||||
|
||||
init {
|
||||
// Migrate to the Keystore Encrypted SharedPreferences.
|
||||
migrateToKeystoreEncryption()
|
||||
}
|
||||
|
||||
override fun getCookieConfig(hostname: String): CookieConfigurationData? {
|
||||
val key = CONFIG_PREFIX.appendIdentifier(hostname)
|
||||
return getEncryptedString(key)
|
||||
@@ -37,12 +44,20 @@ class CookieDiskSourceImpl(
|
||||
}
|
||||
|
||||
override fun clearCookies() {
|
||||
val keysToRemove = encryptedSharedPreferences
|
||||
val keysToRemove = keystoreEncryptedPreferences
|
||||
.all
|
||||
.keys
|
||||
.filter { it.startsWith(ENCRYPTED_PREFIX) }
|
||||
encryptedSharedPreferences.edit {
|
||||
.filter { it.startsWith(CONFIG_PREFIX) }
|
||||
keystoreEncryptedPreferences.edit {
|
||||
keysToRemove.forEach { key -> remove(key) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun migrateToKeystoreEncryption() {
|
||||
var isMigrated = false
|
||||
isMigrated = migrateKeyByPrefix(keyPrefix = CONFIG_PREFIX) || isMigrated
|
||||
if (isMigrated) {
|
||||
Timber.d("CookieDiskSource has been migrated to keystore encrypted shared preferences")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+20
@@ -7,7 +7,9 @@ import androidx.room.Room
|
||||
import com.bitwarden.core.data.manager.dispatcher.DispatcherManager
|
||||
import com.bitwarden.data.datasource.disk.FlightRecorderDiskSource
|
||||
import com.bitwarden.data.datasource.disk.di.EncryptedPreferences
|
||||
import com.bitwarden.data.datasource.disk.di.KeystoreEncryptedPreferences
|
||||
import com.bitwarden.data.datasource.disk.di.UnencryptedPreferences
|
||||
import com.bitwarden.data.manager.flightrecorder.FlightRecorderManager
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.CookieDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.CookieDiskSourceImpl
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.EnvironmentDiskSource
|
||||
@@ -22,6 +24,8 @@ import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSourceImpl
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.dao.OrganizationEventDao
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.database.PlatformDatabase
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.legacy.DiskSourceMigrationLogger
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.legacy.DiskSourceMigrationLoggerImpl
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.legacy.LegacyAppCenterMigrator
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.legacy.LegacyAppCenterMigratorImpl
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.legacy.LegacySecureStorage
|
||||
@@ -114,6 +118,20 @@ object PlatformDiskModule {
|
||||
encryptedSharedPreferences = encryptedSharedPreferences,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideDiskSourceMigrationLogger(
|
||||
flightRecorderManager: FlightRecorderManager,
|
||||
@EncryptedPreferences encryptedPreferences: SharedPreferences,
|
||||
@KeystoreEncryptedPreferences keystoreEncryptedPreferences: SharedPreferences,
|
||||
dispatcherManager: DispatcherManager,
|
||||
): DiskSourceMigrationLogger = DiskSourceMigrationLoggerImpl(
|
||||
flightRecorderManager = flightRecorderManager,
|
||||
encryptedPreferences = encryptedPreferences,
|
||||
keystoreEncryptedPreferences = keystoreEncryptedPreferences,
|
||||
dispatcherManager = dispatcherManager,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideLegacyAppCenterMigrator(
|
||||
@@ -162,10 +180,12 @@ object PlatformDiskModule {
|
||||
@Singleton
|
||||
fun provideCookieDiskSource(
|
||||
@UnencryptedPreferences sharedPreferences: SharedPreferences,
|
||||
@KeystoreEncryptedPreferences keystoreEncryptedPreferences: SharedPreferences,
|
||||
@EncryptedPreferences encryptedSharedPreferences: SharedPreferences,
|
||||
json: Json,
|
||||
): CookieDiskSource = CookieDiskSourceImpl(
|
||||
sharedPreferences = sharedPreferences,
|
||||
keystoreEncryptedPreferences = keystoreEncryptedPreferences,
|
||||
encryptedSharedPreferences = encryptedSharedPreferences,
|
||||
json = json,
|
||||
)
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.platform.datasource.disk.legacy
|
||||
|
||||
/**
|
||||
* Adds a log indicating if the data encrypted data stored in shared preferences has been migrated.
|
||||
*/
|
||||
interface DiskSourceMigrationLogger
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package com.x8bit.bitwarden.data.platform.datasource.disk.legacy
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
import com.bitwarden.core.data.manager.dispatcher.DispatcherManager
|
||||
import com.bitwarden.data.manager.flightrecorder.FlightRecorderManager
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* The default implementation of the [DiskSourceMigrationLogger].
|
||||
*/
|
||||
@OmitFromCoverage
|
||||
internal class DiskSourceMigrationLoggerImpl(
|
||||
keystoreEncryptedPreferences: SharedPreferences,
|
||||
encryptedPreferences: SharedPreferences,
|
||||
flightRecorderManager: FlightRecorderManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
) : DiskSourceMigrationLogger {
|
||||
private val unconfinedScope: CoroutineScope = CoroutineScope(dispatcherManager.unconfined)
|
||||
|
||||
init {
|
||||
flightRecorderManager
|
||||
.isLoggingReadyFlow
|
||||
.filter { it }
|
||||
.onEach {
|
||||
// The older encrypted shared preferences has its own values written to disk,
|
||||
// so it is normal for there to be 2 values still present.
|
||||
if (keystoreEncryptedPreferences.all.isNotEmpty() &&
|
||||
encryptedPreferences.all.size <= 2
|
||||
) {
|
||||
Timber.d("Encrypted data has been migrated to Keystore encryption.")
|
||||
}
|
||||
}
|
||||
.launchIn(unconfinedScope)
|
||||
}
|
||||
}
|
||||
+80
-42
@@ -39,6 +39,7 @@ import java.time.Instant
|
||||
@Suppress("LargeClass")
|
||||
class AuthDiskSourceTest {
|
||||
private val fakeEncryptedSharedPreferences = FakeSharedPreferences()
|
||||
private val fakeKeystoreEncryptedPreferences = FakeSharedPreferences()
|
||||
private val fakeSharedPreferences = FakeSharedPreferences().apply {
|
||||
edit(commit = true) {
|
||||
putString("bwPreferencesStorage:masterKeyEncryptedUserKey", "testUserKey")
|
||||
@@ -52,6 +53,7 @@ class AuthDiskSourceTest {
|
||||
|
||||
private val authDiskSource = AuthDiskSourceImpl(
|
||||
encryptedSharedPreferences = fakeEncryptedSharedPreferences,
|
||||
keystoreEncryptedPreferences = fakeKeystoreEncryptedPreferences,
|
||||
sharedPreferences = fakeSharedPreferences,
|
||||
legacySecureStorageMigrator = legacySecureStorageMigrator,
|
||||
json = json,
|
||||
@@ -62,6 +64,42 @@ class AuthDiskSourceTest {
|
||||
verify(exactly = 1) { legacySecureStorageMigrator.migrateIfNecessary() }
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `initialization should migrate legacy encrypted values to the keystore encrypted preferences`() {
|
||||
val mockUserId = "mockUserId"
|
||||
val legacyValues = mapOf(
|
||||
"authenticatorSyncSymmetric" to "mockSyncSymmetricKey",
|
||||
"authenticatorSyncUnlock_$mockUserId" to "mockSyncUnlockKey",
|
||||
"accountCryptographicState_$mockUserId" to "mockCryptographicState",
|
||||
"userKeyAutoUnlock_$mockUserId" to "mockUserAutoUnlockKey",
|
||||
"deviceKey_$mockUserId" to "mockDeviceKey",
|
||||
"pendingAdminAuthRequest_$mockUserId" to "mockPendingAuthRequest",
|
||||
"biometricInitializationVector_$mockUserId" to "mockInitVector",
|
||||
"userKeyBiometricUnlock_$mockUserId" to "mockBiometricsKey",
|
||||
"accountTokens_$mockUserId" to "mockAccountTokens",
|
||||
)
|
||||
fakeEncryptedSharedPreferences.edit {
|
||||
legacyValues.forEach { (key, value) ->
|
||||
putString("bwSecureStorage:$key", value)
|
||||
}
|
||||
}
|
||||
|
||||
AuthDiskSourceImpl(
|
||||
encryptedSharedPreferences = fakeEncryptedSharedPreferences,
|
||||
keystoreEncryptedPreferences = fakeKeystoreEncryptedPreferences,
|
||||
sharedPreferences = fakeSharedPreferences,
|
||||
legacySecureStorageMigrator = legacySecureStorageMigrator,
|
||||
json = json,
|
||||
)
|
||||
|
||||
// The values are moved to the keystore encrypted preferences without the legacy prefix.
|
||||
assertTrue(fakeEncryptedSharedPreferences.all.isEmpty())
|
||||
legacyValues.forEach { (key, value) ->
|
||||
assertEquals(value, fakeKeystoreEncryptedPreferences.getString(key, null))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `initialization should trigger a legacy user key removal`() {
|
||||
assertNull(
|
||||
@@ -455,12 +493,12 @@ class AuthDiskSourceTest {
|
||||
|
||||
@Test
|
||||
fun `getAccountCryptographicState should pull from SharedPreferences`() {
|
||||
val accountKeysBaseKey = "bwSecureStorage:accountCryptographicState"
|
||||
val accountKeysBaseKey = "accountCryptographicState"
|
||||
val mockUserId = "mockUserId"
|
||||
val mockAccountCryptographicState = WrappedAccountCryptographicState.V1(
|
||||
privateKey = "privateKey",
|
||||
)
|
||||
fakeEncryptedSharedPreferences.edit {
|
||||
fakeKeystoreEncryptedPreferences.edit {
|
||||
putString(
|
||||
"${accountKeysBaseKey}_$mockUserId",
|
||||
json.encodeToString(
|
||||
@@ -478,7 +516,7 @@ class AuthDiskSourceTest {
|
||||
|
||||
@Test
|
||||
fun `storeAccountCryptographicState should update sharedPreferences`() {
|
||||
val accountKeysBaseKey = "bwSecureStorage:accountCryptographicState"
|
||||
val accountKeysBaseKey = "accountCryptographicState"
|
||||
val mockUserId = "mockUserId"
|
||||
val mockAccountCryptographicState = WrappedAccountCryptographicState.V1(
|
||||
privateKey = "privateKey",
|
||||
@@ -487,7 +525,7 @@ class AuthDiskSourceTest {
|
||||
userId = mockUserId,
|
||||
accountCryptographicState = mockAccountCryptographicState,
|
||||
)
|
||||
val actual = fakeEncryptedSharedPreferences.getString(
|
||||
val actual = fakeKeystoreEncryptedPreferences.getString(
|
||||
"${accountKeysBaseKey}_$mockUserId",
|
||||
null,
|
||||
)
|
||||
@@ -580,10 +618,10 @@ class AuthDiskSourceTest {
|
||||
|
||||
@Test
|
||||
fun `getUserAutoUnlockKey should pull from SharedPreferences`() {
|
||||
val userAutoUnlockKeyBaseKey = "bwSecureStorage:userKeyAutoUnlock"
|
||||
val userAutoUnlockKeyBaseKey = "userKeyAutoUnlock"
|
||||
val mockUserId = "mockUserId"
|
||||
val mockUserAutoUnlockKey = "mockUserAutoUnlockKey"
|
||||
fakeEncryptedSharedPreferences
|
||||
fakeKeystoreEncryptedPreferences
|
||||
.edit {
|
||||
putString(
|
||||
"${userAutoUnlockKeyBaseKey}_$mockUserId",
|
||||
@@ -599,14 +637,14 @@ class AuthDiskSourceTest {
|
||||
|
||||
@Test
|
||||
fun `storeUserAutoUnlockKey should update SharedPreferences`() {
|
||||
val userAutoUnlockKeyBaseKey = "bwSecureStorage:userKeyAutoUnlock"
|
||||
val userAutoUnlockKeyBaseKey = "userKeyAutoUnlock"
|
||||
val mockUserId = "mockUserId"
|
||||
val mockUserAutoUnlockKey = "mockUserAutoUnlockKey"
|
||||
authDiskSource.storeUserAutoUnlockKey(
|
||||
userId = mockUserId,
|
||||
userAutoUnlockKey = mockUserAutoUnlockKey,
|
||||
)
|
||||
val actual = fakeEncryptedSharedPreferences
|
||||
val actual = fakeKeystoreEncryptedPreferences
|
||||
.getString(
|
||||
"${userAutoUnlockKeyBaseKey}_$mockUserId",
|
||||
null,
|
||||
@@ -619,23 +657,23 @@ class AuthDiskSourceTest {
|
||||
|
||||
@Test
|
||||
fun `getDeviceKey should pull from SharedPreferences`() {
|
||||
val deviceKeyBaseKey = "bwSecureStorage:deviceKey"
|
||||
val deviceKeyBaseKey = "deviceKey"
|
||||
val mockUserId = "mockUserId"
|
||||
val deviceKeyKey = "${deviceKeyBaseKey}_$mockUserId"
|
||||
val devicesKey = "1234"
|
||||
fakeEncryptedSharedPreferences.edit { putString(deviceKeyKey, devicesKey) }
|
||||
fakeKeystoreEncryptedPreferences.edit { putString(deviceKeyKey, devicesKey) }
|
||||
val actual = authDiskSource.getDeviceKey(userId = mockUserId)
|
||||
assertEquals(devicesKey, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `storeDeviceKey for non-null values should update SharedPreferences`() {
|
||||
val deviceKeyBaseKey = "bwSecureStorage:deviceKey"
|
||||
val deviceKeyBaseKey = "deviceKey"
|
||||
val mockUserId = "mockUserId"
|
||||
val deviceKeyKey = "${deviceKeyBaseKey}_$mockUserId"
|
||||
val devicesKey = "1234"
|
||||
authDiskSource.storeDeviceKey(userId = mockUserId, deviceKey = devicesKey)
|
||||
val actual = fakeEncryptedSharedPreferences.getString(
|
||||
val actual = fakeKeystoreEncryptedPreferences.getString(
|
||||
key = deviceKeyKey,
|
||||
defaultValue = null,
|
||||
)
|
||||
@@ -644,21 +682,21 @@ class AuthDiskSourceTest {
|
||||
|
||||
@Test
|
||||
fun `storeDeviceKey for null values should clear SharedPreferences`() {
|
||||
val deviceKeyBaseKey = "bwSecureStorage:deviceKey"
|
||||
val deviceKeyBaseKey = "deviceKey"
|
||||
val mockUserId = "mockUserId"
|
||||
val deviceKeyKey = "${deviceKeyBaseKey}_$mockUserId"
|
||||
val deviceKey = "1234"
|
||||
fakeEncryptedSharedPreferences.edit { putString(deviceKeyKey, deviceKey) }
|
||||
fakeKeystoreEncryptedPreferences.edit { putString(deviceKeyKey, deviceKey) }
|
||||
authDiskSource.storeDeviceKey(userId = mockUserId, deviceKey = null)
|
||||
assertFalse(fakeEncryptedSharedPreferences.contains(deviceKeyKey))
|
||||
assertFalse(fakeKeystoreEncryptedPreferences.contains(deviceKeyKey))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getPendingAuthRequest should pull from SharedPreferences`() {
|
||||
val pendingAdminAuthRequestBaseKey = "bwSecureStorage:pendingAdminAuthRequest"
|
||||
val pendingAdminAuthRequestBaseKey = "pendingAdminAuthRequest"
|
||||
val mockUserId = "mockUserId"
|
||||
val pendingAdminAuthRequestKey = "${pendingAdminAuthRequestBaseKey}_$mockUserId"
|
||||
fakeEncryptedSharedPreferences.edit {
|
||||
fakeKeystoreEncryptedPreferences.edit {
|
||||
putString(
|
||||
pendingAdminAuthRequestKey,
|
||||
"""
|
||||
@@ -685,7 +723,7 @@ class AuthDiskSourceTest {
|
||||
|
||||
@Test
|
||||
fun `storePendingAuthRequest for non-null values should update SharedPreferences`() {
|
||||
val pendingAdminAuthRequestKeyBaseKey = "bwSecureStorage:pendingAdminAuthRequest"
|
||||
val pendingAdminAuthRequestKeyBaseKey = "pendingAdminAuthRequest"
|
||||
val mockUserId = "mockUserId"
|
||||
val pendingAuthRequestKey = "${pendingAdminAuthRequestKeyBaseKey}_$mockUserId"
|
||||
val pendingAdminAuthRequest = PendingAuthRequestJson(
|
||||
@@ -698,7 +736,7 @@ class AuthDiskSourceTest {
|
||||
userId = mockUserId,
|
||||
pendingAuthRequest = pendingAdminAuthRequest,
|
||||
)
|
||||
val actual = fakeEncryptedSharedPreferences.getString(
|
||||
val actual = fakeKeystoreEncryptedPreferences.getString(
|
||||
key = pendingAuthRequestKey,
|
||||
defaultValue = null,
|
||||
)
|
||||
@@ -720,11 +758,11 @@ class AuthDiskSourceTest {
|
||||
|
||||
@Test
|
||||
fun `getUserBiometricUnlockKey should pull from SharedPreferences`() {
|
||||
val biometricsKeyBaseKey = "bwSecureStorage:userKeyBiometricUnlock"
|
||||
val biometricsKeyBaseKey = "userKeyBiometricUnlock"
|
||||
val mockUserId = "mockUserId"
|
||||
val biometricsKeyKey = "${biometricsKeyBaseKey}_$mockUserId"
|
||||
val biometricsKey = "1234"
|
||||
fakeEncryptedSharedPreferences.edit {
|
||||
fakeKeystoreEncryptedPreferences.edit {
|
||||
putString(biometricsKeyKey, biometricsKey)
|
||||
}
|
||||
val actual = authDiskSource.getUserBiometricUnlockKey(userId = mockUserId)
|
||||
@@ -752,12 +790,12 @@ class AuthDiskSourceTest {
|
||||
|
||||
@Test
|
||||
fun `storeUserBiometricInitVector for non-null values should update SharedPreferences`() {
|
||||
val biometricsInitVectorBaseKey = "bwSecureStorage:biometricInitializationVector"
|
||||
val biometricsInitVectorBaseKey = "biometricInitializationVector"
|
||||
val mockUserId = "mockUserId"
|
||||
val biometricsInitVectorKey = "${biometricsInitVectorBaseKey}_$mockUserId"
|
||||
val initVector = byteArrayOf(1, 2)
|
||||
authDiskSource.storeUserBiometricInitVector(userId = mockUserId, iv = initVector)
|
||||
val actual = fakeEncryptedSharedPreferences.getString(
|
||||
val actual = fakeKeystoreEncryptedPreferences.getString(
|
||||
key = biometricsInitVectorKey,
|
||||
defaultValue = null,
|
||||
)
|
||||
@@ -766,20 +804,20 @@ class AuthDiskSourceTest {
|
||||
|
||||
@Test
|
||||
fun `storeUserBiometricInitVector for null values should clear SharedPreferences`() {
|
||||
val biometricsInitVectorBaseKey = "bwSecureStorage:biometricInitializationVector"
|
||||
val biometricsInitVectorBaseKey = "biometricInitializationVector"
|
||||
val mockUserId = "mockUserId"
|
||||
val biometricsInitVectorKey = "${biometricsInitVectorBaseKey}_$mockUserId"
|
||||
val initVector = "1234"
|
||||
fakeEncryptedSharedPreferences.edit {
|
||||
fakeKeystoreEncryptedPreferences.edit {
|
||||
putString(biometricsInitVectorKey, initVector)
|
||||
}
|
||||
authDiskSource.storeUserBiometricInitVector(userId = mockUserId, iv = null)
|
||||
assertFalse(fakeEncryptedSharedPreferences.contains(biometricsInitVectorKey))
|
||||
assertFalse(fakeKeystoreEncryptedPreferences.contains(biometricsInitVectorKey))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `storeUserBiometricUnlockKey for non-null values should update SharedPreferences`() {
|
||||
val biometricsKeyBaseKey = "bwSecureStorage:userKeyBiometricUnlock"
|
||||
val biometricsKeyBaseKey = "userKeyBiometricUnlock"
|
||||
val mockUserId = "mockUserId"
|
||||
val biometricsKeyKey = "${biometricsKeyBaseKey}_$mockUserId"
|
||||
val biometricsKey = "1234"
|
||||
@@ -787,7 +825,7 @@ class AuthDiskSourceTest {
|
||||
userId = mockUserId,
|
||||
biometricsKey = biometricsKey,
|
||||
)
|
||||
val actual = fakeEncryptedSharedPreferences.getString(
|
||||
val actual = fakeKeystoreEncryptedPreferences.getString(
|
||||
key = biometricsKeyKey,
|
||||
defaultValue = null,
|
||||
)
|
||||
@@ -796,18 +834,18 @@ class AuthDiskSourceTest {
|
||||
|
||||
@Test
|
||||
fun `storeUserBiometricUnlockKey for null values should clear SharedPreferences`() {
|
||||
val biometricsKeyBaseKey = "bwSecureStorage:userKeyBiometricUnlock"
|
||||
val biometricsKeyBaseKey = "userKeyBiometricUnlock"
|
||||
val mockUserId = "mockUserId"
|
||||
val biometricsKeyKey = "${biometricsKeyBaseKey}_$mockUserId"
|
||||
val biometricsKey = "1234"
|
||||
fakeEncryptedSharedPreferences.edit {
|
||||
fakeKeystoreEncryptedPreferences.edit {
|
||||
putString(biometricsKeyKey, biometricsKey)
|
||||
}
|
||||
authDiskSource.storeUserBiometricUnlockKey(
|
||||
userId = mockUserId,
|
||||
biometricsKey = null,
|
||||
)
|
||||
assertFalse(fakeEncryptedSharedPreferences.contains(biometricsKeyKey))
|
||||
assertFalse(fakeKeystoreEncryptedPreferences.contains(biometricsKeyKey))
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@@ -1233,13 +1271,13 @@ class AuthDiskSourceTest {
|
||||
|
||||
@Test
|
||||
fun `getAccountTokens should pull from SharedPreferences`() {
|
||||
val baseKey = "bwSecureStorage:accountTokens"
|
||||
val baseKey = "accountTokens"
|
||||
val mockUserId = "mockUserId"
|
||||
val accountTokens = AccountTokensJson(
|
||||
accessToken = "accessToken",
|
||||
refreshToken = "refreshToken",
|
||||
)
|
||||
fakeEncryptedSharedPreferences.edit {
|
||||
fakeKeystoreEncryptedPreferences.edit {
|
||||
putString("${baseKey}_$mockUserId", json.encodeToString(accountTokens))
|
||||
}
|
||||
val actual = authDiskSource.getAccountTokens(userId = mockUserId)
|
||||
@@ -1276,7 +1314,7 @@ class AuthDiskSourceTest {
|
||||
|
||||
@Test
|
||||
fun `storeAccountTokens should update SharedPreferences`() {
|
||||
val baseKey = "bwSecureStorage:accountTokens"
|
||||
val baseKey = "accountTokens"
|
||||
val mockUserId = "mockUserId"
|
||||
val accountTokens = AccountTokensJson(
|
||||
accessToken = "accessToken",
|
||||
@@ -1286,7 +1324,7 @@ class AuthDiskSourceTest {
|
||||
userId = mockUserId,
|
||||
accountTokens = accountTokens,
|
||||
)
|
||||
val actual = fakeEncryptedSharedPreferences.getString(
|
||||
val actual = fakeKeystoreEncryptedPreferences.getString(
|
||||
key = "${baseKey}_$mockUserId",
|
||||
defaultValue = null,
|
||||
)
|
||||
@@ -1298,10 +1336,10 @@ class AuthDiskSourceTest {
|
||||
|
||||
@Test
|
||||
fun `getAuthenticatorSyncUnlockKey should pull from SharedPreferences`() {
|
||||
val authenticatorSyncUnlockKey = "bwSecureStorage:authenticatorSyncUnlock"
|
||||
val authenticatorSyncUnlockKey = "authenticatorSyncUnlock"
|
||||
val mockUserId = "mockUserId"
|
||||
val mockAuthenticatorSyncUnlockKey = "mockAuthSyncUnlockKey"
|
||||
fakeEncryptedSharedPreferences
|
||||
fakeKeystoreEncryptedPreferences
|
||||
.edit {
|
||||
putString(
|
||||
"${authenticatorSyncUnlockKey}_$mockUserId",
|
||||
@@ -1317,7 +1355,7 @@ class AuthDiskSourceTest {
|
||||
|
||||
@Test
|
||||
fun `storeAuthenticatorSyncUnlockKey should update SharedPreferences`() {
|
||||
val authenticatorSyncUnlockKey = "bwSecureStorage:authenticatorSyncUnlock"
|
||||
val authenticatorSyncUnlockKey = "authenticatorSyncUnlock"
|
||||
val mockUserId = "mockUserId"
|
||||
val mockAuthenticatorSyncUnlockKey = "mockAuthSyncUnlockKey"
|
||||
authDiskSource.storeAuthenticatorSyncUnlockKey(
|
||||
@@ -1325,7 +1363,7 @@ class AuthDiskSourceTest {
|
||||
authenticatorSyncUnlockKey = mockAuthenticatorSyncUnlockKey,
|
||||
)
|
||||
|
||||
val actual = fakeEncryptedSharedPreferences.getString(
|
||||
val actual = fakeKeystoreEncryptedPreferences.getString(
|
||||
key = "${authenticatorSyncUnlockKey}_$mockUserId",
|
||||
defaultValue = null,
|
||||
)
|
||||
@@ -1385,18 +1423,18 @@ class AuthDiskSourceTest {
|
||||
|
||||
@Test
|
||||
fun `authenticatorSyncSymmetricKey should store and update from EncryptedSharedPreferences`() {
|
||||
val sharedPrefsKey = "bwSecureStorage:authenticatorSyncSymmetric"
|
||||
val sharedPrefsKey = "authenticatorSyncSymmetric"
|
||||
|
||||
// Shared preferences and the repository start with the same value:
|
||||
assertNull(authDiskSource.authenticatorSyncSymmetricKey)
|
||||
assertNull(fakeEncryptedSharedPreferences.getString(sharedPrefsKey, null))
|
||||
assertNull(fakeKeystoreEncryptedPreferences.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),
|
||||
fakeKeystoreEncryptedPreferences.getString(sharedPrefsKey, null),
|
||||
)
|
||||
|
||||
// Retrieving the key from repository should give same byte array despite String conversion:
|
||||
|
||||
+44
@@ -1,5 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.platform.datasource.disk
|
||||
|
||||
import androidx.core.content.edit
|
||||
import com.bitwarden.core.di.CoreModule
|
||||
import com.bitwarden.data.datasource.disk.base.FakeSharedPreferences
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.model.CookieConfigurationData
|
||||
@@ -10,15 +11,58 @@ import org.junit.jupiter.api.Test
|
||||
|
||||
class CookieDiskSourceTest {
|
||||
private val fakeEncryptedSharedPreferences = FakeSharedPreferences()
|
||||
private val fakeKeystoreEncryptedPreferences = FakeSharedPreferences()
|
||||
private val fakeSharedPreferences = FakeSharedPreferences()
|
||||
private val json = CoreModule.providesJson(buildInfoManager = mockk(relaxed = true))
|
||||
|
||||
private val cookieDiskSource: CookieDiskSource = CookieDiskSourceImpl(
|
||||
sharedPreferences = fakeSharedPreferences,
|
||||
keystoreEncryptedPreferences = fakeKeystoreEncryptedPreferences,
|
||||
encryptedSharedPreferences = fakeEncryptedSharedPreferences,
|
||||
json = json,
|
||||
)
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `initialization should migrate legacy encrypted values to the keystore encrypted preferences`() {
|
||||
val hostname = "vault.bitwarden.com"
|
||||
val config = CookieConfigurationData(
|
||||
hostname = hostname,
|
||||
cookies = listOf(
|
||||
CookieConfigurationData.Cookie(
|
||||
name = "BW_SESSION",
|
||||
value = "encrypted_cookie_value",
|
||||
),
|
||||
),
|
||||
)
|
||||
val legacyKey = "bwSecureStorage:elb_cookie_config_$hostname"
|
||||
fakeEncryptedSharedPreferences.edit {
|
||||
putString(legacyKey, json.encodeToString(config))
|
||||
}
|
||||
|
||||
val diskSource = CookieDiskSourceImpl(
|
||||
sharedPreferences = fakeSharedPreferences,
|
||||
keystoreEncryptedPreferences = fakeKeystoreEncryptedPreferences,
|
||||
encryptedSharedPreferences = fakeEncryptedSharedPreferences,
|
||||
json = json,
|
||||
)
|
||||
|
||||
// The value is moved to the keystore encrypted preferences without the legacy prefix.
|
||||
assertNull(fakeEncryptedSharedPreferences.getString(legacyKey, null))
|
||||
assertEquals(
|
||||
json.parseToJsonElement(json.encodeToString(config)),
|
||||
json.parseToJsonElement(
|
||||
requireNotNull(
|
||||
fakeKeystoreEncryptedPreferences.getString(
|
||||
"elb_cookie_config_$hostname",
|
||||
null,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
assertEquals(config, diskSource.getCookieConfig(hostname))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getCookieConfig should return null when no config exists`() {
|
||||
assertNull(cookieDiskSource.getCookieConfig("example.com"))
|
||||
|
||||
+18
@@ -5,6 +5,7 @@ import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
|
||||
import com.bitwarden.data.datasource.disk.BaseEncryptedDiskSource
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.onSubscription
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
|
||||
private const val AUTHENTICATOR_SYNC_SYMMETRIC_KEY = "authenticatorSyncSymmetricKey"
|
||||
@@ -18,14 +19,21 @@ private const val UNIQUE_APP_ID_KEY = "appId"
|
||||
*/
|
||||
class AuthDiskSourceImpl(
|
||||
encryptedSharedPreferences: SharedPreferences,
|
||||
keystoreEncryptedPreferences: SharedPreferences,
|
||||
sharedPreferences: SharedPreferences,
|
||||
) : BaseEncryptedDiskSource(
|
||||
encryptedSharedPreferences = encryptedSharedPreferences,
|
||||
keystoreEncryptedPreferences = keystoreEncryptedPreferences,
|
||||
sharedPreferences = sharedPreferences,
|
||||
),
|
||||
AuthDiskSource {
|
||||
private val mutableUserBiometricUnlockKeyFlow = bufferedMutableSharedFlow<String?>(replay = 1)
|
||||
|
||||
init {
|
||||
// Migrate to the Keystore Encrypted SharedPreferences.
|
||||
migrateToKeystoreEncryption()
|
||||
}
|
||||
|
||||
override val uniqueAppId: String
|
||||
get() = getString(key = UNIQUE_APP_ID_KEY) ?: generateAndStoreUniqueAppId()
|
||||
|
||||
@@ -86,4 +94,14 @@ class AuthDiskSourceImpl(
|
||||
.also {
|
||||
putString(key = UNIQUE_APP_ID_KEY, value = it)
|
||||
}
|
||||
|
||||
private fun migrateToKeystoreEncryption() {
|
||||
var isMigrated = false
|
||||
isMigrated = migrateKeyByPrefix(keyPrefix = BIOMETRICS_UNLOCK_KEY) || isMigrated
|
||||
isMigrated = migrateKeyByPrefix(keyPrefix = BIOMETRICS_INIT_VECTOR_KEY) || isMigrated
|
||||
isMigrated = migrateKeyByPrefix(keyPrefix = AUTHENTICATOR_SYNC_SYMMETRIC_KEY) || isMigrated
|
||||
if (isMigrated) {
|
||||
Timber.d("AuthDiskSource has been migrated to keystore encrypted shared preferences")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
@@ -4,6 +4,7 @@ import android.content.SharedPreferences
|
||||
import com.bitwarden.authenticator.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.bitwarden.authenticator.data.auth.datasource.disk.AuthDiskSourceImpl
|
||||
import com.bitwarden.data.datasource.disk.di.EncryptedPreferences
|
||||
import com.bitwarden.data.datasource.disk.di.KeystoreEncryptedPreferences
|
||||
import com.bitwarden.data.datasource.disk.di.UnencryptedPreferences
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
@@ -22,10 +23,12 @@ object AuthDiskModule {
|
||||
@Singleton
|
||||
fun provideAuthDiskSource(
|
||||
@EncryptedPreferences encryptedSharedPreferences: SharedPreferences,
|
||||
@KeystoreEncryptedPreferences keystoreEncryptedPreferences: SharedPreferences,
|
||||
@UnencryptedPreferences sharedPreferences: SharedPreferences,
|
||||
): AuthDiskSource =
|
||||
AuthDiskSourceImpl(
|
||||
encryptedSharedPreferences = encryptedSharedPreferences,
|
||||
keystoreEncryptedPreferences = keystoreEncryptedPreferences,
|
||||
sharedPreferences = sharedPreferences,
|
||||
)
|
||||
}
|
||||
|
||||
+42
-8
@@ -11,28 +11,61 @@ import org.junit.jupiter.api.Test
|
||||
class AuthDiskSourceTest {
|
||||
|
||||
private val fakeEncryptedSharedPreferences = FakeSharedPreferences()
|
||||
private val fakeKeystoreEncryptedPreferences = FakeSharedPreferences()
|
||||
private val fakeSharedPreferences = FakeSharedPreferences()
|
||||
|
||||
private val authDiskSource = AuthDiskSourceImpl(
|
||||
encryptedSharedPreferences = fakeEncryptedSharedPreferences,
|
||||
keystoreEncryptedPreferences = fakeKeystoreEncryptedPreferences,
|
||||
sharedPreferences = fakeSharedPreferences,
|
||||
)
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `authenticatorBridgeSymmetricSyncKey should store and update from EncryptedSharedPreferences`() {
|
||||
val sharedPrefsKey = "bwSecureStorage:authenticatorSyncSymmetricKey"
|
||||
fun `initialization should migrate legacy encrypted values to the keystore encrypted preferences`() {
|
||||
fakeEncryptedSharedPreferences.edit {
|
||||
putString("bwSecureStorage:userKeyBiometricUnlock", "biometricsKey")
|
||||
putString("bwSecureStorage:biometricInitializationVector", "initVector")
|
||||
putString("bwSecureStorage:authenticatorSyncSymmetricKey", "symmetricKey")
|
||||
}
|
||||
|
||||
AuthDiskSourceImpl(
|
||||
encryptedSharedPreferences = fakeEncryptedSharedPreferences,
|
||||
keystoreEncryptedPreferences = fakeKeystoreEncryptedPreferences,
|
||||
sharedPreferences = fakeSharedPreferences,
|
||||
)
|
||||
|
||||
// The values are moved to the keystore encrypted preferences without the legacy prefix.
|
||||
assertTrue(fakeEncryptedSharedPreferences.all.isEmpty())
|
||||
assertEquals(
|
||||
"biometricsKey",
|
||||
fakeKeystoreEncryptedPreferences.getString("userKeyBiometricUnlock", null),
|
||||
)
|
||||
assertEquals(
|
||||
"initVector",
|
||||
fakeKeystoreEncryptedPreferences.getString("biometricInitializationVector", null),
|
||||
)
|
||||
assertEquals(
|
||||
"symmetricKey",
|
||||
fakeKeystoreEncryptedPreferences.getString("authenticatorSyncSymmetricKey", null),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `authenticatorBridgeSymmetricSyncKey should store and update from keystore encrypted preferences`() {
|
||||
val sharedPrefsKey = "authenticatorSyncSymmetricKey"
|
||||
|
||||
// Shared preferences and the repository start with the same value:
|
||||
assertNull(authDiskSource.authenticatorBridgeSymmetricSyncKey)
|
||||
assertNull(fakeEncryptedSharedPreferences.getString(sharedPrefsKey, null))
|
||||
assertNull(fakeKeystoreEncryptedPreferences.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),
|
||||
fakeKeystoreEncryptedPreferences.getString(sharedPrefsKey, null),
|
||||
)
|
||||
|
||||
// Retrieving the key from repository should give same byte array despite String conversion:
|
||||
@@ -40,19 +73,20 @@ class AuthDiskSourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `userBiometricKeyInitVector should store and update from EncryptedSharedPreferences`() {
|
||||
val sharedPrefsKey = "bwSecureStorage:biometricInitializationVector"
|
||||
@Suppress("MaxLineLength")
|
||||
fun `userBiometricKeyInitVector should store and update from keystore encrypted preferences`() {
|
||||
val sharedPrefsKey = "biometricInitializationVector"
|
||||
|
||||
// Shared preferences and the repository start with the same value:
|
||||
assertNull(authDiskSource.userBiometricKeyInitVector)
|
||||
assertNull(fakeEncryptedSharedPreferences.getString(sharedPrefsKey, null))
|
||||
assertNull(fakeKeystoreEncryptedPreferences.getString(sharedPrefsKey, null))
|
||||
|
||||
// Updating the repository updates shared preferences:
|
||||
val userBiometricKeyInitVector = byteArrayOf(3, 4)
|
||||
authDiskSource.userBiometricKeyInitVector = userBiometricKeyInitVector
|
||||
assertEquals(
|
||||
userBiometricKeyInitVector.toString(Charsets.ISO_8859_1),
|
||||
fakeEncryptedSharedPreferences.getString(sharedPrefsKey, null),
|
||||
fakeKeystoreEncryptedPreferences.getString(sharedPrefsKey, null),
|
||||
)
|
||||
|
||||
// Retrieving the key from repository should give same byte array despite String conversion:
|
||||
|
||||
+32
-14
@@ -2,31 +2,49 @@ package com.bitwarden.data.datasource.disk
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import androidx.core.content.edit
|
||||
import androidx.security.crypto.EncryptedSharedPreferences
|
||||
import timber.log.Timber
|
||||
|
||||
private const val LEGACY_PREFIX: String = "bwSecureStorage:"
|
||||
|
||||
/**
|
||||
* Base class for simplifying interactions with [SharedPreferences] and
|
||||
* [EncryptedSharedPreferences].
|
||||
* Base class for simplifying interactions with [SharedPreferences], this includes both regular
|
||||
* and encrypted storage.
|
||||
*/
|
||||
@Suppress("UnnecessaryAbstractClass")
|
||||
abstract class BaseEncryptedDiskSource(
|
||||
sharedPreferences: SharedPreferences,
|
||||
private val encryptedSharedPreferences: SharedPreferences,
|
||||
) : BaseDiskSource(
|
||||
sharedPreferences = sharedPreferences,
|
||||
) {
|
||||
private val keystoreEncryptedPreferences: SharedPreferences,
|
||||
) : BaseDiskSource(sharedPreferences = sharedPreferences) {
|
||||
protected fun getEncryptedString(
|
||||
key: String,
|
||||
default: String? = null,
|
||||
): String? = encryptedSharedPreferences.getString(key.withBase(), default)
|
||||
): String? = keystoreEncryptedPreferences.getString(key, null)
|
||||
|
||||
protected fun putEncryptedString(
|
||||
key: String,
|
||||
value: String?,
|
||||
): Unit = encryptedSharedPreferences.edit { putString(key.withBase(), value) }
|
||||
}
|
||||
) {
|
||||
keystoreEncryptedPreferences.edit { putString(key, value) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for prepending the key with the appropriate base storage key.
|
||||
*/
|
||||
private fun String.withBase(): String = "bwSecureStorage:$this"
|
||||
protected fun migrateKeyByPrefix(keyPrefix: String): Boolean {
|
||||
var result = false
|
||||
encryptedSharedPreferences
|
||||
.all
|
||||
.keys
|
||||
.filter { it.startsWith(prefix = "$LEGACY_PREFIX$keyPrefix") }
|
||||
.forEach { key ->
|
||||
Timber.d("Migrating $key to keystore encrypted shared preferences")
|
||||
result = true
|
||||
// Move the value to the new file without the base prefix.
|
||||
encryptedSharedPreferences.getString(key, null)?.let { value ->
|
||||
keystoreEncryptedPreferences.edit(commit = true) {
|
||||
putString(key.removePrefix(prefix = LEGACY_PREFIX), value)
|
||||
}
|
||||
}
|
||||
// Then ensure the original value is deleted.
|
||||
encryptedSharedPreferences.edit(commit = true) { remove(key) }
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
+7
@@ -18,6 +18,13 @@ interface FlightRecorderManager {
|
||||
*/
|
||||
val flightRecorderDataFlow: StateFlow<FlightRecorderDataSet>
|
||||
|
||||
/**
|
||||
* Emits `true` when the Flight Recorder is initialized and capable of logging, `false`
|
||||
* otherwise. This can emit true even is the Flight Recorder is disabled, it only indicates
|
||||
* that it could be logging.
|
||||
*/
|
||||
val isLoggingReadyFlow: StateFlow<Boolean>
|
||||
|
||||
/**
|
||||
* Dismisses the all flight recorder banners.
|
||||
*/
|
||||
|
||||
+5
@@ -13,6 +13,7 @@ import com.bitwarden.data.manager.model.FlightRecorderDuration
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
@@ -42,6 +43,9 @@ internal class FlightRecorderManagerImpl(
|
||||
private var cancellationJob: Job = Job().apply { complete() }
|
||||
private val expirationJobMap: MutableMap<String, Job> = concurrentMapOf()
|
||||
private val flightRecorderTree = FlightRecorderTree()
|
||||
private val mutableIsLoggingReadyFlow: MutableStateFlow<Boolean> = MutableStateFlow(false)
|
||||
|
||||
override val isLoggingReadyFlow: StateFlow<Boolean> get() = mutableIsLoggingReadyFlow
|
||||
|
||||
override val flightRecorderData: FlightRecorderDataSet
|
||||
get() = flightRecorderDiskSource
|
||||
@@ -71,6 +75,7 @@ internal class FlightRecorderManagerImpl(
|
||||
ScreenStateBroadcastReceiver(),
|
||||
IntentFilter(Intent.ACTION_SCREEN_ON),
|
||||
)
|
||||
mutableIsLoggingReadyFlow.value = true
|
||||
}
|
||||
|
||||
override fun dismissFlightRecorderBanner() {
|
||||
|
||||
Reference in New Issue
Block a user