From 8849ffd76cfe3055fec506de2b0bc2cfce0e1c95 Mon Sep 17 00:00:00 2001 From: Grisha Kruglov Date: Fri, 8 Nov 2019 11:53:18 -0800 Subject: [PATCH] Closes #2229: Encrypted-at-rest FxA state storage support This patch adds a version of `AccountStorage` which is backed by an encrypted-at-rest shared-prefs implementation, `SecureAbove22Preferences`. As the name suggests, encryption at rest is enabled only for Android API levels 23+. Otherwise, plaintext storage is used. `SecureAbove22Preferences` will handle API level upgrades behind the scenes, if necessary. In order to support rolling this out, `SecureAbove22AccountStorage` automatically migrates account state if it was present in `SharedPrefAccountStorage`. And vice-versa, `SharedPrefAccountStorage` will automatically migrate account state if it was present in `SecureAbove22AccountStorage`. This allows applications to easily switch between two implementations, without any ill-effects. In order to monitor storage implementations for abnormalities (such as disappearing encryption keys), an optional `CrashReporter` instance may be configured now via FxaAccountManager. `DeviceConfig` gained a `secureStateAtRest` flag, which allows applications to specify if they'd like to encrypt account state. This config object isn't a perfect fit for this flag, but it's close enough conceptually. --- .../FirefoxAccountsAuthFeatureTest.kt | 10 ++ .../service/firefox-accounts/build.gradle | 24 +++ .../components/service/fxa/AccountStorage.kt | 101 ++++++++++- .../mozilla/components/service/fxa/Config.kt | 12 +- .../service/fxa/manager/FxaAccountManager.kt | 13 +- .../service/fxa/AccountStorageTest.kt | 164 ++++++++++++++++++ .../service/fxa/FxaAccountManagerTest.kt | 4 +- docs/changelog.md | 7 + .../org/mozilla/samples/sync/MainActivity.kt | 3 +- 9 files changed, 329 insertions(+), 9 deletions(-) create mode 100644 components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/AccountStorageTest.kt diff --git a/components/feature/accounts/src/test/java/mozilla/components/feature/accounts/FirefoxAccountsAuthFeatureTest.kt b/components/feature/accounts/src/test/java/mozilla/components/feature/accounts/FirefoxAccountsAuthFeatureTest.kt index b082ab94bfd..901a2ef7227 100644 --- a/components/feature/accounts/src/test/java/mozilla/components/feature/accounts/FirefoxAccountsAuthFeatureTest.kt +++ b/components/feature/accounts/src/test/java/mozilla/components/feature/accounts/FirefoxAccountsAuthFeatureTest.kt @@ -30,6 +30,7 @@ import org.junit.Assert.assertEquals import org.junit.Assert.assertNull import org.mockito.Mockito.never import org.mockito.Mockito.verify +import org.robolectric.annotation.Config // Same as the actual account manager, except we get to control how FirefoxAccountShaped instances // are created. This is necessary because due to some build issues (native dependencies not available @@ -57,6 +58,10 @@ class FirefoxAccountsAuthFeatureTest { } } + // Note that tests that involve secure storage specify API=21, because of issues testing secure storage on + // 23+ API levels. See https://github.com/mozilla-mobile/android-components/issues/4956 + + @Config(sdk = [22]) @Test fun `begin authentication`() { val manager = prepareAccountManagerForSuccessfulAuthentication() @@ -75,6 +80,7 @@ class FirefoxAccountsAuthFeatureTest { assertEquals("auth://url", authLabmda.url) } + @Config(sdk = [22]) @Test fun `begin pairing authentication`() { val manager = prepareAccountManagerForSuccessfulAuthentication() @@ -93,6 +99,7 @@ class FirefoxAccountsAuthFeatureTest { assertEquals("auth://url", authLabmda.url) } + @Config(sdk = [22]) @Test fun `begin authentication with errors`() { val manager = prepareAccountManagerForFailedAuthentication() @@ -112,6 +119,7 @@ class FirefoxAccountsAuthFeatureTest { assertEquals("https://accounts.firefox.com/signin", authLambda.url) } + @Config(sdk = [22]) @Test fun `begin pairing authentication with errors`() { val manager = prepareAccountManagerForFailedAuthentication() @@ -203,6 +211,7 @@ class FirefoxAccountsAuthFeatureTest { ) } + @Config(sdk = [22]) private fun prepareAccountManagerForSuccessfulAuthentication(): TestableFxaAccountManager { val mockAccount: OAuthAccount = mock() val profile = Profile(uid = "testUID", avatar = null, email = "test@example.com", displayName = "test profile") @@ -227,6 +236,7 @@ class FirefoxAccountsAuthFeatureTest { return manager } + @Config(sdk = [22]) private fun prepareAccountManagerForFailedAuthentication(): TestableFxaAccountManager { val mockAccount: OAuthAccount = mock() val profile = Profile(uid = "testUID", avatar = null, email = "test@example.com", displayName = "test profile") diff --git a/components/service/firefox-accounts/build.gradle b/components/service/firefox-accounts/build.gradle index 909efda4e3c..a355c40d14d 100644 --- a/components/service/firefox-accounts/build.gradle +++ b/components/service/firefox-accounts/build.gradle @@ -27,6 +27,23 @@ android { } } +configurations { + // There's an interaction between Gradle's resolution of dependencies with different types + // (@jar, @aar) for `implementation` and `testImplementation` and with Android Studio's built-in + // JUnit test runner. The runtime classpath in the built-in JUnit test runner gets the + // dependency from the `implementation`, which is type @aar, and therefore the JNA dependency + // doesn't provide the JNI dispatch libraries in the correct Java resource directories. I think + // what's happening is that @aar type in `implementation` resolves to the @jar type in + // `testImplementation`, and that it wins the dependency resolution battle. + // + // A workaround is to add a new configuration which depends on the @jar type and to reference + // the underlying JAR file directly in `testImplementation`. This JAR file doesn't resolve to + // the @aar type in `implementation`. This works when invoked via `gradle`, but also sets the + // correct runtime classpath when invoked with Android Studio's built-in JUnit test runner. + // Success! + jnaForTest +} + dependencies { // Types defined in concept-sync are part of the public API of this module. api project(':concept-sync') @@ -40,6 +57,8 @@ dependencies { implementation project(':support-sync-telemetry') implementation project(':support-ktx') implementation project(':lib-dataprotect') + // CrashReporter is part of the public API. + api project(':lib-crash') implementation Dependencies.kotlin_stdlib implementation Dependencies.kotlin_coroutines @@ -53,6 +72,11 @@ dependencies { testImplementation Dependencies.testing_robolectric testImplementation Dependencies.testing_mockito testImplementation Dependencies.testing_coroutines + + jnaForTest Dependencies.thirdparty_jna + testImplementation files(configurations.jnaForTest.copyRecursive().files) + + testImplementation Dependencies.mozilla_full_megazord_forUnitTests } apply from: '../../../publish.gradle' diff --git a/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/AccountStorage.kt b/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/AccountStorage.kt index 20a86915451..9658a0551ca 100644 --- a/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/AccountStorage.kt +++ b/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/AccountStorage.kt @@ -7,21 +7,46 @@ package mozilla.components.service.fxa import android.content.Context import android.content.SharedPreferences import mozilla.components.concept.sync.OAuthAccount +import mozilla.components.lib.crash.CrashReporter +import mozilla.components.lib.dataprotect.SecureAbove22Preferences const val FXA_STATE_PREFS_KEY = "fxaAppState" const val FXA_STATE_KEY = "fxaState" -interface AccountStorage { +internal interface AccountStorage { @Throws(Exception::class) fun read(): OAuthAccount? fun write(accountState: String) fun clear() } -class SharedPrefAccountStorage(val context: Context) : AccountStorage { +/** + * Account storage layer which uses plaintext storage implementation. + * + * Migration from [SecureAbove22AccountStorage] will happen upon initialization, + * unless disabled via [migrateFromSecureStorage]. + */ +internal class SharedPrefAccountStorage( + val context: Context, + crashReporter: CrashReporter? = null, + migrateFromSecureStorage: Boolean = true +) : AccountStorage { + init { + if (migrateFromSecureStorage) { + // In case we switched from SecureAbove22AccountStorage to this implementation, migrate persisted account + // and clear out the old storage layer. + val secureStorage = SecureAbove22AccountStorage(context, crashReporter, migrateFromPlaintextStorage = false) + secureStorage.read()?.let { secureAccount -> + this.write(secureAccount.toJSONString()) + secureStorage.clear() + } + } + } + /** * @throws FxaException if JSON failed to parse into a [FirefoxAccount]. */ + @Throws(FxaException::class) override fun read(): OAuthAccount? { val savedJSON = accountPreferences().getString(FXA_STATE_KEY, null) ?: return null @@ -48,3 +73,75 @@ class SharedPrefAccountStorage(val context: Context) : AccountStorage { return context.getSharedPreferences(FXA_STATE_PREFS_KEY, Context.MODE_PRIVATE) } } + +/** + * A base class for exceptions describing abnormal account storage behaviour. + */ +internal abstract class AbnormalAccountStorageEvent : Exception() { + /** + * Account state was expected to be present, but it wasn't. + */ + internal class UnexpectedlyMissingAccountState : AbnormalAccountStorageEvent() +} + +/** + * Account storage layer which uses encrypted-at-rest storage implementation for supported API levels (23+). + * On older API versions account state is stored in plaintext. + * + * Migration from [SharedPrefAccountStorage] will happen upon initialization, + * unless disabled via [migrateFromPlaintextStorage]. + */ +internal class SecureAbove22AccountStorage( + context: Context, + private val crashReporter: CrashReporter? = null, + migrateFromPlaintextStorage: Boolean = true +) : AccountStorage { + companion object { + private const val STORAGE_NAME = "fxaStateAC" + private const val KEY_ACCOUNT_STATE = "fxaState" + private const val PREF_NAME = "fxaStatePrefAC" + private const val PREF_KEY_HAS_STATE = "fxaStatePresent" + } + + private val store = SecureAbove22Preferences(context, STORAGE_NAME) + // Prefs are used here to keep track of abnormal storage behaviour - namely, account state disappearing without + // being cleared first through this class. Note that clearing application data will clear both 'store' and 'prefs'. + private val prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) + + init { + if (migrateFromPlaintextStorage) { + // In case we switched from SharedPrefAccountStorage to this implementation, migrate persisted account + // and clear out the old storage layer. + val plaintextStorage = SharedPrefAccountStorage(context, migrateFromSecureStorage = false) + plaintextStorage.read()?.let { plaintextAccount -> + this.write(plaintextAccount.toJSONString()) + plaintextStorage.clear() + } + } + } + + /** + * @throws FxaException if JSON failed to parse into a [FirefoxAccount]. + */ + @Throws(FxaException::class) + override fun read(): OAuthAccount? { + return store.getString(KEY_ACCOUNT_STATE).also { + // If account state is missing, but we expected it to be present, report an exception. + if (it == null && prefs.getBoolean(PREF_KEY_HAS_STATE, false)) { + crashReporter?.submitCaughtException(AbnormalAccountStorageEvent.UnexpectedlyMissingAccountState()) + // Clear prefs to make sure we only submit this exception once. + prefs.edit().clear().apply() + } + }?.let { FirefoxAccount.fromJSONString(it) } + } + + override fun write(accountState: String) { + store.putString(KEY_ACCOUNT_STATE, accountState) + prefs.edit().putBoolean(PREF_KEY_HAS_STATE, true).apply() + } + + override fun clear() { + store.clear() + prefs.edit().clear().apply() + } +} diff --git a/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/Config.kt b/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/Config.kt index b468fb1144b..1b800f2a15b 100644 --- a/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/Config.kt +++ b/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/Config.kt @@ -24,11 +24,21 @@ typealias ServerConfig = mozilla.appservices.fxaclient.Config * @property capabilities A set of device capabilities, such as SEND_TAB. This set can be expanded by * re-initializing [FxaAccountManager] with a new set (e.g. on app restart). * Shrinking a set of capabilities is currently not supported. + * + * @property secureStateAtRest A flag indicating whether or not to use encrypted storage for the persisted account + * state. If set to `true`, [SecureAbove22AccountStorage] will be used as a storage layer. As the name suggests, + * account state will only by encrypted on Android API 23+. Otherwise, even if this flag is set to `true`, account state + * will be stored in plaintext. + * + * Default value of `false` configures the plaintext version of account storage to be used, [SharedPrefAccountStorage]. + * + * Switching of this flag's values is supported; account state will be migrated between the underlying storage layers. */ data class DeviceConfig( val name: String, val type: DeviceType, - val capabilities: Set + val capabilities: Set, + val secureStateAtRest: Boolean = false ) /** diff --git a/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/manager/FxaAccountManager.kt b/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/manager/FxaAccountManager.kt index 456b53cc4d3..904a58517f8 100644 --- a/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/manager/FxaAccountManager.kt +++ b/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/manager/FxaAccountManager.kt @@ -27,6 +27,7 @@ import mozilla.components.concept.sync.DeviceEventsObserver import mozilla.components.concept.sync.OAuthAccount import mozilla.components.concept.sync.Profile import mozilla.components.concept.sync.StatePersistenceCallback +import mozilla.components.lib.crash.CrashReporter import mozilla.components.service.fxa.AccountStorage import mozilla.components.service.fxa.DeviceConfig import mozilla.components.service.fxa.FxaDeviceSettingsCache @@ -34,6 +35,7 @@ import mozilla.components.service.fxa.FirefoxAccount import mozilla.components.service.fxa.FxaAuthData import mozilla.components.service.fxa.FxaException import mozilla.components.service.fxa.FxaPanicException +import mozilla.components.service.fxa.SecureAbove22AccountStorage import mozilla.components.service.fxa.ServerConfig import mozilla.components.service.fxa.SharedPrefAccountStorage import mozilla.components.service.fxa.SyncAuthInfoCache @@ -116,6 +118,7 @@ open class FxaAccountManager( private val deviceConfig: DeviceConfig, @Volatile private var syncConfig: SyncConfig?, private val applicationScopes: Set = emptySet(), + private val crashReporter: CrashReporter? = null, // We want a single-threaded execution model for our account-related "actions" (state machine side-effects). // That is, we want to ensure a sequential execution flow, but on a background thread. private val coroutineContext: CoroutineContext = Executors @@ -889,13 +892,17 @@ open class FxaAccountManager( } @VisibleForTesting - open fun createSyncManager(config: SyncConfig): SyncManager { + internal open fun createSyncManager(config: SyncConfig): SyncManager { return WorkManagerSyncManager(context, config) } @VisibleForTesting - open fun getAccountStorage(): AccountStorage { - return SharedPrefAccountStorage(context) + internal open fun getAccountStorage(): AccountStorage { + return if (deviceConfig.secureStateAtRest) { + SecureAbove22AccountStorage(context, crashReporter) + } else { + SharedPrefAccountStorage(context, crashReporter) + } } /** diff --git a/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/AccountStorageTest.kt b/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/AccountStorageTest.kt new file mode 100644 index 00000000000..f2e3bb30319 --- /dev/null +++ b/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/AccountStorageTest.kt @@ -0,0 +1,164 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.service.fxa + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.lib.crash.CrashReporter +import mozilla.components.lib.dataprotect.SecureAbove22Preferences +import mozilla.components.support.test.argumentCaptor +import mozilla.components.support.test.mock +import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.reset +import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyZeroInteractions +import org.robolectric.annotation.Config +import kotlin.reflect.KClass + +// Note that tests that involve secure storage specify API=21, because of issues testing secure storage on +// 23+ API levels. See https://github.com/mozilla-mobile/android-components/issues/4956 + +@RunWith(AndroidJUnit4::class) +class SharedPrefAccountStorageTest { + @Config(sdk = [21]) + @Test + fun `plain storage crud`() { + val storage = SharedPrefAccountStorage(testContext) + val account = FirefoxAccount( + mozilla.appservices.fxaclient.Config.release("someId", "http://www.firefox.com") + ) + assertNull(storage.read()) + storage.write(account.toJSONString()) + assertNotNull(storage.read()) + storage.clear() + assertNull(storage.read()) + } + + @Config(sdk = [21]) + @Test + fun `migration from SecureAbove22AccountStorage`() { + val secureStorage = SecureAbove22AccountStorage(testContext) + val account = FirefoxAccount( + mozilla.appservices.fxaclient.Config.release("someId", "http://www.firefox.com") + ) + + assertNull(secureStorage.read()) + secureStorage.write(account.toJSONString()) + assertNotNull(secureStorage.read()) + + // Now that we have account state in secureStorage, it should be migrated over to plainStorage when it's init'd. + val plainStorage = SharedPrefAccountStorage(testContext) + assertNotNull(plainStorage.read()) + // And secureStorage must have been cleared during this migration. + assertNull(secureStorage.read()) + } + + @Config(sdk = [21]) + @Test + fun `missing state is reported during a migration`() { + val secureStorage = SecureAbove22AccountStorage(testContext) + val account = FirefoxAccount( + mozilla.appservices.fxaclient.Config.release("someId", "http://www.firefox.com") + ) + secureStorage.write(account.toJSONString()) + + // Clear the underlying storage layer "behind the back" of account storage. + SecureAbove22Preferences(testContext, "fxaStateAC").clear() + + val crashReporter: CrashReporter = mock() + val plainStorage = SharedPrefAccountStorage(testContext, crashReporter) + assertCaughtException(crashReporter, AbnormalAccountStorageEvent.UnexpectedlyMissingAccountState::class) + + assertNull(plainStorage.read()) + + reset(crashReporter) + assertNull(secureStorage.read()) + verifyZeroInteractions(crashReporter) + } +} + +@RunWith(AndroidJUnit4::class) +class SecureAbove22AccountStorageTest { + @Config(sdk = [21]) + @Test + fun `secure storage crud`() { + val crashReporter: CrashReporter = mock() + val storage = SecureAbove22AccountStorage(testContext, crashReporter) + val account = FirefoxAccount( + mozilla.appservices.fxaclient.Config.release("someId", "http://www.firefox.com") + ) + assertNull(storage.read()) + storage.write(account.toJSONString()) + assertNotNull(storage.read()) + storage.clear() + assertNull(storage.read()) + verifyZeroInteractions(crashReporter) + } + + @Config(sdk = [21]) + @Test + fun `migration from SharedPrefAccountStorage`() { + val plainStorage = SharedPrefAccountStorage(testContext) + val account = FirefoxAccount( + mozilla.appservices.fxaclient.Config.release("someId", "http://www.firefox.com") + ) + + assertNull(plainStorage.read()) + plainStorage.write(account.toJSONString()) + assertNotNull(plainStorage.read()) + + // Now that we have account state in plainStorage, it should be migrated over to secureStorage when it's init'd. + val crashReporter: CrashReporter = mock() + val secureStorage = SecureAbove22AccountStorage(testContext, crashReporter) + assertNotNull(secureStorage.read()) + // And plainStorage must have been cleared during this migration. + assertNull(plainStorage.read()) + verifyZeroInteractions(crashReporter) + } + + @Config(sdk = [21]) + @Test + fun `missing state is reported`() { + val crashReporter: CrashReporter = mock() + val storage = SecureAbove22AccountStorage(testContext, crashReporter) + val account = FirefoxAccount( + mozilla.appservices.fxaclient.Config.release("someId", "http://www.firefox.com") + ) + storage.write(account.toJSONString()) + + // Clear the underlying storage layer "behind the back" of account storage. + SecureAbove22Preferences(testContext, "fxaStateAC").clear() + assertNull(storage.read()) + assertCaughtException(crashReporter, AbnormalAccountStorageEvent.UnexpectedlyMissingAccountState::class) + // Make sure exception is only reported once per "incident". + reset(crashReporter) + assertNull(storage.read()) + verifyZeroInteractions(crashReporter) + } + + @Config(sdk = [21]) + @Test + fun `missing state is ignored without a configured crash reporter`() { + val storage = SecureAbove22AccountStorage(testContext) + val account = FirefoxAccount( + mozilla.appservices.fxaclient.Config.release("someId", "http://www.firefox.com") + ) + storage.write(account.toJSONString()) + + // Clear the underlying storage layer "behind the back" of account storage. + SecureAbove22Preferences(testContext, "fxaStateAC").clear() + assertNull(storage.read()) + } +} + +private fun assertCaughtException(crashReporter: CrashReporter, type: KClass) { + val captor = argumentCaptor() + verify(crashReporter).submitCaughtException(captor.capture()) + Assert.assertEquals(type, captor.value::class) +} diff --git a/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/FxaAccountManagerTest.kt b/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/FxaAccountManagerTest.kt index 2647508c384..12a0a98888e 100644 --- a/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/FxaAccountManagerTest.kt +++ b/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/FxaAccountManagerTest.kt @@ -71,7 +71,7 @@ import kotlin.coroutines.CoroutineContext // are created. This is necessary because due to some build issues (native dependencies not available // within the test environment) we can't use fxaclient supplied implementation of FirefoxAccountShaped. // Instead, we express all of our account-related operations over an interface. -open class TestableFxaAccountManager( +internal open class TestableFxaAccountManager( context: Context, config: ServerConfig, private val storage: AccountStorage, @@ -79,7 +79,7 @@ open class TestableFxaAccountManager( syncConfig: SyncConfig? = null, coroutineContext: CoroutineContext, private val block: () -> OAuthAccount = { mock() } -) : FxaAccountManager(context, config, DeviceConfig("test", DeviceType.UNKNOWN, capabilities), syncConfig, emptySet(), coroutineContext) { +) : FxaAccountManager(context, config, DeviceConfig("test", DeviceType.UNKNOWN, capabilities), syncConfig, emptySet(), null, coroutineContext) { override fun createAccount(config: ServerConfig): OAuthAccount { return block() } diff --git a/docs/changelog.md b/docs/changelog.md index 0cd3165f0e6..f7bd8293239 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -28,6 +28,13 @@ permalink: /changelog/ val addOns = addOnsProvider.getAvailableAddOns(allowCache = false) ``` +* **service-firefox-accounts** + * For supported Android API levels (23+), `FxaAccountManager` can now be configured to encrypt persisted FxA state, via `secureStateAtRest` flag on passed-in `DeviceConfig`. Defaults to `false`. For lower API levels, setting `secureStateAtRest` will continue storing FxA state in plaintext. If the device is later upgraded to 23+, FxA state will be automatically migrated to an encrypted storage. + * FxA state is stored in application's data directory, in plaintext or encrypted-at-rest if configured via the `secureStateAtRest` flag. This state contains everything that's necessary to download and decrypt data stored in Firefox Sync. + * An instance of a `CrashReporter` may now be passed to the `FxaAccountManager`'s constructor. If configured, it will be used to report any detected abnormalities. + * ⚠️ **This is a breaking change**: + * Several `FxaAccountManager` methods have been made internal, and are no longer part of the public API of this module: `createSyncManager`, `getAccountStorage`. + # 21.0.0 * [Commits](https://github.com/mozilla-mobile/android-components/compare/v20.0.0...v21.0.0) diff --git a/samples/sync/src/main/java/org/mozilla/samples/sync/MainActivity.kt b/samples/sync/src/main/java/org/mozilla/samples/sync/MainActivity.kt index f33cca5404b..79e9c210bde 100644 --- a/samples/sync/src/main/java/org/mozilla/samples/sync/MainActivity.kt +++ b/samples/sync/src/main/java/org/mozilla/samples/sync/MainActivity.kt @@ -73,7 +73,8 @@ class MainActivity : DeviceConfig( name = "A-C Sync Sample - ${System.currentTimeMillis()}", type = DeviceType.MOBILE, - capabilities = setOf(DeviceCapability.SEND_TAB) + capabilities = setOf(DeviceCapability.SEND_TAB), + secureStateAtRest = true ), SyncConfig(setOf(SyncEngine.History, SyncEngine.Bookmarks), syncPeriodInMinutes = 15L) )