For #5074 - Sync Logins, Uses KeySharedPreferences for Passwords Encryption Key
parent
ee4e1c8f39
commit
c43f96096e
|
@ -402,6 +402,7 @@ dependencies {
|
||||||
|
|
||||||
implementation Deps.mozilla_lib_crash
|
implementation Deps.mozilla_lib_crash
|
||||||
implementation Deps.mozilla_lib_push_firebase
|
implementation Deps.mozilla_lib_push_firebase
|
||||||
|
implementation Deps.mozilla_lib_dataprotect
|
||||||
debugImplementation Deps.leakcanary
|
debugImplementation Deps.leakcanary
|
||||||
|
|
||||||
implementation Deps.androidx_legacy
|
implementation Deps.androidx_legacy
|
||||||
|
|
|
@ -39,5 +39,5 @@ object FeatureFlags {
|
||||||
/**
|
/**
|
||||||
* Gives option in Settings to see logins and sync logins
|
* Gives option in Settings to see logins and sync logins
|
||||||
*/
|
*/
|
||||||
const val logins = false
|
val logins = Config.channel.isNightlyOrDebug
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@ import mozilla.components.feature.push.PushConfig
|
||||||
import mozilla.components.feature.push.PushSubscriptionObserver
|
import mozilla.components.feature.push.PushSubscriptionObserver
|
||||||
import mozilla.components.feature.push.PushType
|
import mozilla.components.feature.push.PushType
|
||||||
import mozilla.components.lib.crash.CrashReporter
|
import mozilla.components.lib.crash.CrashReporter
|
||||||
|
import mozilla.components.lib.dataprotect.SecureAbove22Preferences
|
||||||
import mozilla.components.service.fxa.DeviceConfig
|
import mozilla.components.service.fxa.DeviceConfig
|
||||||
import mozilla.components.service.fxa.ServerConfig
|
import mozilla.components.service.fxa.ServerConfig
|
||||||
import mozilla.components.service.fxa.SyncConfig
|
import mozilla.components.service.fxa.SyncConfig
|
||||||
|
@ -33,6 +34,7 @@ import mozilla.components.service.fxa.SyncEngine
|
||||||
import mozilla.components.service.fxa.manager.FxaAccountManager
|
import mozilla.components.service.fxa.manager.FxaAccountManager
|
||||||
import mozilla.components.service.fxa.manager.SCOPE_SYNC
|
import mozilla.components.service.fxa.manager.SCOPE_SYNC
|
||||||
import mozilla.components.service.fxa.sync.GlobalSyncableStoreProvider
|
import mozilla.components.service.fxa.sync.GlobalSyncableStoreProvider
|
||||||
|
import mozilla.components.service.sync.logins.SyncableLoginsStore
|
||||||
import mozilla.components.support.base.log.logger.Logger
|
import mozilla.components.support.base.log.logger.Logger
|
||||||
import org.mozilla.fenix.Experiments
|
import org.mozilla.fenix.Experiments
|
||||||
import org.mozilla.fenix.R
|
import org.mozilla.fenix.R
|
||||||
|
@ -53,7 +55,9 @@ class BackgroundServices(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
crashReporter: CrashReporter,
|
crashReporter: CrashReporter,
|
||||||
historyStorage: PlacesHistoryStorage,
|
historyStorage: PlacesHistoryStorage,
|
||||||
bookmarkStorage: PlacesBookmarksStorage
|
bookmarkStorage: PlacesBookmarksStorage,
|
||||||
|
passwordsStorage: SyncableLoginsStore,
|
||||||
|
secureAbove22Preferences: SecureAbove22Preferences
|
||||||
) {
|
) {
|
||||||
// // A malformed string is causing crashes.
|
// // A malformed string is causing crashes.
|
||||||
// This will be removed when the string is fixed. See #5552
|
// This will be removed when the string is fixed. See #5552
|
||||||
|
@ -87,8 +91,9 @@ class BackgroundServices(
|
||||||
val syncConfig = if (context.isInExperiment(Experiments.asFeatureSyncDisabled)) {
|
val syncConfig = if (context.isInExperiment(Experiments.asFeatureSyncDisabled)) {
|
||||||
null
|
null
|
||||||
} else {
|
} else {
|
||||||
// TODO Add Passwords Here Waiting On https://github.com/mozilla-mobile/android-components/issues/4741
|
SyncConfig(
|
||||||
SyncConfig(setOf(SyncEngine.History, SyncEngine.Bookmarks), syncPeriodInMinutes = 240L) // four hours
|
setOf(SyncEngine.History, SyncEngine.Bookmarks, SyncEngine.Passwords),
|
||||||
|
syncPeriodInMinutes = 240L) // four hours
|
||||||
}
|
}
|
||||||
|
|
||||||
private val pushService by lazy { FirebasePush() }
|
private val pushService by lazy { FirebasePush() }
|
||||||
|
@ -96,11 +101,11 @@ class BackgroundServices(
|
||||||
val push by lazy { makePushConfig()?.let { makePush(it) } }
|
val push by lazy { makePushConfig()?.let { makePush(it) } }
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// Make the "history", "bookmark", and "logins" stores accessible to workers spawned by the sync manager.
|
// Make the "history", "bookmark", and "passwords" stores accessible to workers spawned by the sync manager.
|
||||||
GlobalSyncableStoreProvider.configureStore(SyncEngine.History to historyStorage)
|
GlobalSyncableStoreProvider.configureStore(SyncEngine.History to historyStorage)
|
||||||
GlobalSyncableStoreProvider.configureStore(SyncEngine.Bookmarks to bookmarkStorage)
|
GlobalSyncableStoreProvider.configureStore(SyncEngine.Bookmarks to bookmarkStorage)
|
||||||
// TODO Add Passwords Here Waiting On https://github.com/mozilla-mobile/android-components/issues/4741
|
GlobalSyncableStoreProvider.configureStore(SyncEngine.Passwords to passwordsStorage)
|
||||||
// GlobalSyncableStoreProvider.configureStore(SyncEngine.Passwords to loginsStorage)
|
GlobalSyncableStoreProvider.configureKeyStorage(secureAbove22Preferences)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val deviceEventObserver = object : DeviceEventsObserver {
|
private val deviceEventObserver = object : DeviceEventsObserver {
|
||||||
|
@ -170,7 +175,11 @@ class BackgroundServices(
|
||||||
).also { accountManager ->
|
).also { accountManager ->
|
||||||
// TODO this needs to change once we have a SyncManager
|
// TODO this needs to change once we have a SyncManager
|
||||||
context.settings().fxaHasSyncedItems = syncConfig?.supportedEngines?.isNotEmpty() ?: false
|
context.settings().fxaHasSyncedItems = syncConfig?.supportedEngines?.isNotEmpty() ?: false
|
||||||
accountManager.registerForDeviceEvents(deviceEventObserver, ProcessLifecycleOwner.get(), false)
|
accountManager.registerForDeviceEvents(
|
||||||
|
deviceEventObserver,
|
||||||
|
ProcessLifecycleOwner.get(),
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
// Register a telemetry account observer to keep track of FxA auth metrics.
|
// Register a telemetry account observer to keep track of FxA auth metrics.
|
||||||
accountManager.register(telemetryAccountObserver)
|
accountManager.register(telemetryAccountObserver)
|
||||||
|
|
|
@ -15,7 +15,14 @@ import org.mozilla.fenix.utils.ClipboardHandler
|
||||||
@Mockable
|
@Mockable
|
||||||
class Components(private val context: Context) {
|
class Components(private val context: Context) {
|
||||||
val backgroundServices by lazy {
|
val backgroundServices by lazy {
|
||||||
BackgroundServices(context, analytics.crashReporter, core.historyStorage, core.bookmarksStorage)
|
BackgroundServices(
|
||||||
|
context,
|
||||||
|
analytics.crashReporter,
|
||||||
|
core.historyStorage,
|
||||||
|
core.bookmarksStorage,
|
||||||
|
core.passwordsStorage,
|
||||||
|
core.secureAbove22Preferences
|
||||||
|
)
|
||||||
}
|
}
|
||||||
val services by lazy { Services(context, backgroundServices.accountManager) }
|
val services by lazy { Services(context, backgroundServices.accountManager) }
|
||||||
val core by lazy { Core(context) }
|
val core by lazy { Core(context) }
|
||||||
|
|
|
@ -7,6 +7,7 @@ package org.mozilla.fenix.components
|
||||||
import GeckoProvider
|
import GeckoProvider
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
|
import io.sentry.Sentry
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
@ -34,6 +35,8 @@ import mozilla.components.feature.pwa.ManifestStorage
|
||||||
import mozilla.components.feature.pwa.WebAppShortcutManager
|
import mozilla.components.feature.pwa.WebAppShortcutManager
|
||||||
import mozilla.components.feature.session.HistoryDelegate
|
import mozilla.components.feature.session.HistoryDelegate
|
||||||
import mozilla.components.feature.webcompat.WebCompatFeature
|
import mozilla.components.feature.webcompat.WebCompatFeature
|
||||||
|
import mozilla.components.lib.dataprotect.SecureAbove22Preferences
|
||||||
|
import mozilla.components.lib.dataprotect.generateEncryptionKey
|
||||||
import mozilla.components.service.sync.logins.AsyncLoginsStorageAdapter
|
import mozilla.components.service.sync.logins.AsyncLoginsStorageAdapter
|
||||||
import mozilla.components.service.sync.logins.SyncableLoginsStore
|
import mozilla.components.service.sync.logins.SyncableLoginsStore
|
||||||
import org.mozilla.fenix.AppRequestInterceptor
|
import org.mozilla.fenix.AppRequestInterceptor
|
||||||
|
@ -171,7 +174,7 @@ class Core(private val context: Context) {
|
||||||
|
|
||||||
val webAppManifestStorage by lazy { ManifestStorage(context) }
|
val webAppManifestStorage by lazy { ManifestStorage(context) }
|
||||||
|
|
||||||
val loginsStorage by lazy {
|
val passwordsStorage by lazy {
|
||||||
SyncableLoginsStore(
|
SyncableLoginsStore(
|
||||||
AsyncLoginsStorageAdapter.forDatabase(
|
AsyncLoginsStorageAdapter.forDatabase(
|
||||||
File(
|
File(
|
||||||
|
@ -180,10 +183,29 @@ class Core(private val context: Context) {
|
||||||
).canonicalPath
|
).canonicalPath
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
CompletableDeferred("very-insecure-key")
|
CompletableDeferred(passwordsEncryptionKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared Preferences that encrypt/decrypt using Android KeyStore and lib-dataprotect for 23+
|
||||||
|
* otherwise simply stored
|
||||||
|
*/
|
||||||
|
val secureAbove22Preferences by lazy {
|
||||||
|
SecureAbove22Preferences(context, KEY_STORAGE_NAME)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val passwordsEncryptionKey =
|
||||||
|
secureAbove22Preferences.getString(PASSWORDS_KEY)
|
||||||
|
?: generateEncryptionKey(KEY_STRENGTH).also {
|
||||||
|
if (context.settings().passwordsEncryptionKeyGenerated) {
|
||||||
|
// We already had previously generated an encryption key, but we have lost it
|
||||||
|
Sentry.capture("Passwords encryption key for passwords storage was lost and we generated a new one")
|
||||||
|
}
|
||||||
|
context.settings().recordPasswordsEncryptionKeyGenerated()
|
||||||
|
secureAbove22Preferences.putString(PASSWORDS_KEY, it)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a [TrackingProtectionPolicy] based on current preferences.
|
* Constructs a [TrackingProtectionPolicy] based on current preferences.
|
||||||
*
|
*
|
||||||
|
@ -223,4 +245,10 @@ class Core(private val context: Context) {
|
||||||
else -> PreferredColorScheme.Light
|
else -> PreferredColorScheme.Light
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val KEY_STRENGTH = 256
|
||||||
|
private const val KEY_STORAGE_NAME = "core_prefs"
|
||||||
|
private const val PASSWORDS_KEY = "passwords"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,7 +80,7 @@ class SavedLoginsFragment : Fragment() {
|
||||||
private fun loadAndMapLogins() {
|
private fun loadAndMapLogins() {
|
||||||
lifecycleScope.launch(IO) {
|
lifecycleScope.launch(IO) {
|
||||||
val syncedLogins = async {
|
val syncedLogins = async {
|
||||||
context!!.components.core.loginsStorage.withUnlocked {
|
context!!.components.core.passwordsStorage.withUnlocked {
|
||||||
it.list().await().map { item ->
|
it.list().await().map { item ->
|
||||||
SavedLoginsItem(
|
SavedLoginsItem(
|
||||||
item.hostname,
|
item.hostname,
|
||||||
|
|
|
@ -213,6 +213,16 @@ class Settings private constructor(
|
||||||
fun shouldDeleteAnyDataOnQuit() =
|
fun shouldDeleteAnyDataOnQuit() =
|
||||||
DeleteBrowsingDataOnQuitType.values().any { getDeleteDataOnQuit(it) }
|
DeleteBrowsingDataOnQuitType.values().any { getDeleteDataOnQuit(it) }
|
||||||
|
|
||||||
|
val passwordsEncryptionKeyGenerated by booleanPreference(
|
||||||
|
appContext.getPreferenceKey(R.string.pref_key_encryption_key_generated),
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
|
fun recordPasswordsEncryptionKeyGenerated() = preferences.edit().putBoolean(
|
||||||
|
appContext.getPreferenceKey(R.string.pref_key_encryption_key_generated),
|
||||||
|
true
|
||||||
|
).apply()
|
||||||
|
|
||||||
val themeSettingString: String
|
val themeSettingString: String
|
||||||
get() = when {
|
get() = when {
|
||||||
shouldFollowDeviceTheme -> appContext.getString(R.string.preference_follow_device_theme)
|
shouldFollowDeviceTheme -> appContext.getString(R.string.preference_follow_device_theme)
|
||||||
|
|
|
@ -117,4 +117,6 @@
|
||||||
<string name="pref_key_testing_stage" translatable="false">pref_key_testing_stage</string>
|
<string name="pref_key_testing_stage" translatable="false">pref_key_testing_stage</string>
|
||||||
|
|
||||||
<string name="pref_key_total_uri" translatable="false">pref_key_total_uri</string>
|
<string name="pref_key_total_uri" translatable="false">pref_key_total_uri</string>
|
||||||
|
|
||||||
|
<string name="pref_key_encryption_key_generated" translatable="false">pref_key_encryption_key_generated</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -50,15 +50,24 @@ class BackgroundServicesTest {
|
||||||
val context = mockk<Context>(relaxed = true)
|
val context = mockk<Context>(relaxed = true)
|
||||||
|
|
||||||
every { context.isInExperiment(eq(Experiments.asFeatureWebChannelsDisabled)) } returns false
|
every { context.isInExperiment(eq(Experiments.asFeatureWebChannelsDisabled)) } returns false
|
||||||
assertEquals("urn:ietf:wg:oauth:2.0:oob:oauth-redirect-webchannel", FxaServer.redirectUrl(context))
|
assertEquals(
|
||||||
|
"urn:ietf:wg:oauth:2.0:oob:oauth-redirect-webchannel",
|
||||||
|
FxaServer.redirectUrl(context)
|
||||||
|
)
|
||||||
|
|
||||||
every { context.isInExperiment(eq(Experiments.asFeatureWebChannelsDisabled)) } returns true
|
every { context.isInExperiment(eq(Experiments.asFeatureWebChannelsDisabled)) } returns true
|
||||||
assertEquals("https://accounts.firefox.com/oauth/success/a2270f727f45f648", FxaServer.redirectUrl(context))
|
assertEquals(
|
||||||
|
"https://accounts.firefox.com/oauth/success/a2270f727f45f648",
|
||||||
|
FxaServer.redirectUrl(context)
|
||||||
|
)
|
||||||
|
|
||||||
every { context.isInExperiment(eq(Experiments.asFeatureSyncDisabled)) } returns false
|
every { context.isInExperiment(eq(Experiments.asFeatureSyncDisabled)) } returns false
|
||||||
var backgroundServices = TestableBackgroundServices(context)
|
var backgroundServices = TestableBackgroundServices(context)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
SyncConfig(setOf(SyncEngine.History, SyncEngine.Bookmarks), syncPeriodInMinutes = 240L),
|
SyncConfig(
|
||||||
|
setOf(SyncEngine.History, SyncEngine.Bookmarks, SyncEngine.Passwords),
|
||||||
|
syncPeriodInMinutes = 240L
|
||||||
|
),
|
||||||
backgroundServices.syncConfig
|
backgroundServices.syncConfig
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -150,6 +150,7 @@ object Deps {
|
||||||
|
|
||||||
const val mozilla_lib_crash = "org.mozilla.components:lib-crash:${Versions.mozilla_android_components}"
|
const val mozilla_lib_crash = "org.mozilla.components:lib-crash:${Versions.mozilla_android_components}"
|
||||||
const val mozilla_lib_push_firebase = "org.mozilla.components:lib-push-firebase:${Versions.mozilla_android_components}"
|
const val mozilla_lib_push_firebase = "org.mozilla.components:lib-push-firebase:${Versions.mozilla_android_components}"
|
||||||
|
const val mozilla_lib_dataprotect = "org.mozilla.components:lib-dataprotect:${Versions.mozilla_android_components}"
|
||||||
|
|
||||||
const val mozilla_ui_publicsuffixlist = "org.mozilla.components:lib-publicsuffixlist:${Versions.mozilla_android_components}"
|
const val mozilla_ui_publicsuffixlist = "org.mozilla.components:lib-publicsuffixlist:${Versions.mozilla_android_components}"
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue