1
0
Fork 0

For #5547 - Add top level auth when accessing passwords

master
Emily Kager 2019-10-29 17:14:12 -07:00 committed by Emily Kager
parent 218763f9be
commit 3e2b88cc91
11 changed files with 329 additions and 12 deletions

View File

@ -405,6 +405,7 @@ dependencies {
debugImplementation Deps.leakcanary
implementation Deps.androidx_legacy
implementation Deps.androidx_biometric
implementation Deps.androidx_paging
implementation Deps.androidx_preference
implementation Deps.androidx_fragment

View File

@ -12,6 +12,7 @@
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT"/>
<uses-permission android:name="android.permission.USE_BIOMETRIC" android:requiredFeature="false"/>
<!-- Needed to prompt the user to give permission to install a downloaded apk -->
<uses-permission-sdk-23 android:name="android.permission.REQUEST_INSTALL_PACKAGES" />

View File

@ -9,6 +9,7 @@ import android.text.InputType
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.fragment_saved_login_site_info.*
import org.mozilla.fenix.R
@ -24,6 +25,12 @@ class SavedLoginSiteInfoFragment : Fragment(R.layout.fragment_saved_login_site_i
).savedLoginItem
}
override fun onPause() {
// If we pause this fragment, we want to pop users back to reauth
findNavController().popBackStack(R.id.loginsFragment, false)
super.onPause()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

View File

@ -63,6 +63,12 @@ class SavedLoginsFragment : Fragment() {
}
}
override fun onPause() {
// If we pause this fragment, we want to pop users back to reauth
findNavController().popBackStack(R.id.loginsFragment, false)
super.onPause()
}
private fun itemClicked(item: SavedLoginsItem) {
val directions =
SavedLoginsFragmentDirections.actionSavedLoginsFragmentToSavedLoginSiteInfoFragment(item)

View File

@ -4,11 +4,25 @@
package org.mozilla.fenix.settings
import android.annotation.TargetApi
import android.app.Activity.RESULT_OK
import android.app.KeyguardManager
import android.content.Context.KEYGUARD_SERVICE
import android.content.DialogInterface
import android.content.Intent
import android.os.Build
import android.os.Build.VERSION_CODES.M
import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.launch
import mozilla.components.concept.sync.AccountObserver
import mozilla.components.concept.sync.AuthType
import mozilla.components.concept.sync.OAuthAccount
@ -17,6 +31,8 @@ import mozilla.components.service.fxa.manager.SyncEnginesStorage
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.getPreferenceKey
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.settings
import java.util.concurrent.Executors
@Suppress("TooManyFunctions")
class LoginsFragment : PreferenceFragmentCompat(), AccountObserver {
@ -31,7 +47,11 @@ class LoginsFragment : PreferenceFragmentCompat(), AccountObserver {
val savedLoginsKey = getPreferenceKey(R.string.pref_key_saved_logins)
findPreference<Preference>(savedLoginsKey)?.setOnPreferenceClickListener {
navigateToLoginsSettingsFragment()
if (Build.VERSION.SDK_INT >= M && isHardwareAvailable && hasBiometricEnrolled) {
showBiometricPrompt()
} else {
verifyPinOrShowSetupWarning()
}
true
}
@ -54,6 +74,31 @@ class LoginsFragment : PreferenceFragmentCompat(), AccountObserver {
override fun onAuthenticationProblems() = updateSyncPreferenceNeedsReauth()
val isHardwareAvailable: Boolean by lazy {
if (Build.VERSION.SDK_INT >= M) {
context?.let {
val bm = BiometricManager.from(it)
val canAuthenticate = bm.canAuthenticate()
!(canAuthenticate == BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE ||
canAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE)
} ?: false
} else {
false
}
}
val hasBiometricEnrolled: Boolean by lazy {
if (Build.VERSION.SDK_INT >= M) {
context?.let {
val bm = BiometricManager.from(it)
val canAuthenticate = bm.canAuthenticate()
(canAuthenticate == BiometricManager.BIOMETRIC_SUCCESS)
} ?: false
} else {
false
}
}
private fun updateSyncPreferenceStatus() {
val syncLogins = getPreferenceKey(R.string.pref_key_password_sync_logins)
findPreference<Preference>(syncLogins)?.apply {
@ -92,7 +137,93 @@ class LoginsFragment : PreferenceFragmentCompat(), AccountObserver {
}
}
private fun navigateToLoginsSettingsFragment() {
@TargetApi(M)
private fun showBiometricPrompt() {
val biometricPromptCallback = object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
// Authentication Error
}
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
lifecycleScope.launch(Main) {
navigateToSavedLoginsFragment()
}
}
override fun onAuthenticationFailed() {
// Authenticated Failed
}
}
val executor = Executors.newSingleThreadExecutor()
val biometricPrompt = BiometricPrompt(this, executor, biometricPromptCallback)
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle(getString(R.string.logins_biometric_prompt_message))
.setDeviceCredentialAllowed(true)
.build()
biometricPrompt.authenticate(promptInfo)
}
private fun verifyPinOrShowSetupWarning() {
val manager = activity?.getSystemService(KEYGUARD_SERVICE) as KeyguardManager
if (manager.isKeyguardSecure) {
showPinVerification()
} else {
if (context?.settings()?.shouldShowSecurityPinWarning == true) {
showPinDialogWarning()
} else {
navigateToSavedLoginsFragment()
}
}
}
private fun showPinDialogWarning() {
context?.let {
AlertDialog.Builder(it).apply {
setTitle(getString(R.string.logins_warning_dialog_title))
setMessage(
getString(R.string.logins_warning_dialog_message)
)
setNegativeButton(getString(R.string.logins_warning_dialog_later)) { _: DialogInterface, _ ->
navigateToSavedLoginsFragment()
}
setPositiveButton(getString(R.string.logins_warning_dialog_set_up_now)) { it: DialogInterface, _ ->
it.dismiss()
val intent = Intent(
android.provider.Settings.ACTION_SECURITY_SETTINGS
)
startActivity(intent)
}
create()
}.show()
it.settings().incrementShowLoginsSecureWarningCount()
}
}
private fun showPinVerification() {
val manager = activity?.getSystemService(KEYGUARD_SERVICE) as KeyguardManager
val intent = manager.createConfirmDeviceCredentialIntent(
getString(R.string.logins_biometric_prompt_message_pin),
getString(R.string.logins_biometric_prompt_message)
)
startActivityForResult(intent, PIN_REQUEST)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == PIN_REQUEST && resultCode == RESULT_OK) {
navigateToSavedLoginsFragment()
} else {
super.onActivityResult(requestCode, resultCode, data)
}
}
private fun navigateToSavedLoginsFragment() {
val directions = LoginsFragmentDirections.actionLoginsFragmentToSavedLoginsFragment()
findNavController().navigate(directions)
}
@ -111,4 +242,8 @@ class LoginsFragment : PreferenceFragmentCompat(), AccountObserver {
val directions = LoginsFragmentDirections.actionLoginsFragmentToTurnOnSyncFragment()
findNavController().navigate(directions)
}
companion object {
const val PIN_REQUEST = 303
}
}

View File

@ -4,10 +4,16 @@
package org.mozilla.fenix.settings.account
import android.app.KeyguardManager
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.os.Bundle
import android.provider.Settings
import android.text.InputFilter
import android.text.format.DateUtils
import android.view.View
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
@ -36,6 +42,7 @@ import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getPreferenceKey
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.settings
@SuppressWarnings("TooManyFunctions", "LargeClass")
class AccountSettingsFragment : PreferenceFragmentCompat() {
@ -180,14 +187,27 @@ class AccountSettingsFragment : PreferenceFragmentCompat() {
val loginsNameKey = getPreferenceKey(R.string.pref_key_sync_logins)
findPreference<CheckBoxPreference>(loginsNameKey)?.apply {
setOnPreferenceChangeListener { _, newValue ->
SyncEnginesStorage(context).setStatus(SyncEngine.Passwords, newValue as Boolean)
@Suppress("DeferredResultUnused")
context.components.backgroundServices.accountManager.syncNowAsync(SyncReason.EngineChange)
val manager =
activity?.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
if (manager.isKeyguardSecure ||
newValue == false ||
!context.settings().shouldShowSecurityPinWarningSync
) {
SyncEnginesStorage(context).setStatus(SyncEngine.Passwords, newValue as Boolean)
@Suppress("DeferredResultUnused")
context.components.backgroundServices.accountManager.syncNowAsync(SyncReason.EngineChange)
} else {
showPinDialogWarning(newValue as Boolean)
}
true
}
}
deviceConstellation?.registerDeviceObserver(deviceConstellationObserver, owner = this, autoPause = true)
deviceConstellation?.registerDeviceObserver(
deviceConstellationObserver,
owner = this,
autoPause = true
)
// NB: ObserverRegistry will take care of cleaning up internal references to 'observer' and
// 'owner' when appropriate.
@ -196,6 +216,33 @@ class AccountSettingsFragment : PreferenceFragmentCompat() {
)
}
private fun showPinDialogWarning(newValue: Boolean) {
context?.let {
AlertDialog.Builder(it).apply {
setTitle(getString(R.string.logins_warning_dialog_title))
setMessage(
getString(R.string.logins_warning_dialog_message)
)
setNegativeButton(getString(R.string.logins_warning_dialog_later)) { _: DialogInterface, _ ->
SyncEnginesStorage(context).setStatus(SyncEngine.Passwords, newValue)
@Suppress("DeferredResultUnused")
context.components.backgroundServices.accountManager.syncNowAsync(SyncReason.EngineChange)
}
setPositiveButton(getString(R.string.logins_warning_dialog_set_up_now)) { it: DialogInterface, _ ->
it.dismiss()
val intent = Intent(
Settings.ACTION_SECURITY_SETTINGS
)
startActivity(intent)
}
create()
}.show()
it.settings().incrementShowLoginsSecureWarningSyncCount()
}
}
private fun updateSyncEngineStates() {
val syncEnginesStatus = SyncEnginesStorage(context!!).getStatus()
val bookmarksNameKey = getPreferenceKey(R.string.pref_key_sync_bookmarks)

View File

@ -37,6 +37,8 @@ class Settings private constructor(
private val isCrashReportEnabledInBuild: Boolean
) : PreferencesHolder {
companion object {
const val showLoginsSecureWarningSyncMaxCount = 1
const val showLoginsSecureWarningMaxCount = 1
const val autoBounceMaximumCount = 2
const val trackingProtectionOnboardingMaximumCount = 2
const val FENIX_PREFERENCES = "fenix_preferences"
@ -133,6 +135,12 @@ class Settings private constructor(
val shouldAutoBounceQuickActionSheet: Boolean
get() = autoBounceQuickActionSheetCount < autoBounceMaximumCount
val shouldShowSecurityPinWarningSync: Boolean
get() = loginsSecureWarningSyncCount < showLoginsSecureWarningSyncMaxCount
val shouldShowSecurityPinWarning: Boolean
get() = loginsSecureWarningCount < showLoginsSecureWarningMaxCount
var shouldUseLightTheme by booleanPreference(
appContext.getPreferenceKey(R.string.pref_key_light_theme),
default = false
@ -249,6 +257,32 @@ class Settings private constructor(
default = 0
)
@VisibleForTesting(otherwise = PRIVATE)
internal val loginsSecureWarningSyncCount by intPreference(
appContext.getPreferenceKey(R.string.pref_key_logins_secure_warning_sync),
default = 0
)
@VisibleForTesting(otherwise = PRIVATE)
internal val loginsSecureWarningCount by intPreference(
appContext.getPreferenceKey(R.string.pref_key_logins_secure_warning),
default = 0
)
fun incrementShowLoginsSecureWarningCount() {
preferences.edit().putInt(
appContext.getPreferenceKey(R.string.pref_key_logins_secure_warning),
loginsSecureWarningCount + 1
).apply()
}
fun incrementShowLoginsSecureWarningSyncCount() {
preferences.edit().putInt(
appContext.getPreferenceKey(R.string.pref_key_logins_secure_warning_sync),
loginsSecureWarningSyncCount + 1
).apply()
}
fun incrementAutomaticBounceQuickActionSheetCount() {
preferences.edit().putInt(
appContext.getPreferenceKey(R.string.pref_key_bounce_quick_action),

View File

@ -104,6 +104,8 @@
<!-- Logins Settings -->
<string name="pref_key_saved_logins" translatable="false">pref_key_saved_logins</string>
<string name="pref_key_password_sync_logins" translatable="false">pref_key_password_sync_logins</string>
<string name="pref_key_logins_secure_warning_sync" translatable="false">pref_key_logins_secure_warning_sync</string>
<string name="pref_key_logins_secure_warning" translatable="false">pref_key_logins_secure_warning</string>
<!-- Privacy Settings -->
<string name="pref_key_open_links_in_a_private_tab" translatable="false">pref_key_open_links_in_a_private_tab</string>

View File

@ -1009,4 +1009,16 @@
<string name="saved_login_reveal_password">Show password</string>
<!-- Content Description (for screenreaders etc) read for the button to hide a password in logins -->
<string name="saved_login_hide_password">Hide password</string>
<!-- Message displayed in biometric prompt displayed for authentication before allowing users to view their logins -->
<string name="logins_biometric_prompt_message">Unlock to view your saved logins</string>
<!-- Title of warning dialog if users have no device authentication set up -->
<string name="logins_warning_dialog_title">Secure your logins and passwords</string>
<!-- Message of warning dialog if users have no device authentication set up -->
<string name="logins_warning_dialog_message">Set up a device lock pattern, PIN, or password to protect your saved logins and passwords from being accessed if someone else has your device.</string>
<!-- Negative button to ignore warning dialog if users have no device authentication set up -->
<string name="logins_warning_dialog_later">Later</string>
<!-- Positive button to send users to set up a pin of warning dialog if users have no device authentication set up -->
<string name="logins_warning_dialog_set_up_now">Set up now</string>
<!-- Title of PIN verification dialog to direct users to re-enter their device credentials to access their logins -->
<string name="logins_biometric_prompt_message_pin">Unlock your device</string>
</resources>

View File

@ -173,6 +173,58 @@ class SettingsTest {
assertFalse(settings.shouldAutoBounceQuickActionSheet)
}
@Test
fun showLoginsDialogWarningSync() {
// When just created
// Then
assertEquals(0, settings.loginsSecureWarningSyncCount)
// When
settings.incrementShowLoginsSecureWarningSyncCount()
// Then
assertEquals(1, settings.loginsSecureWarningSyncCount)
}
@Test
fun shouldShowLoginsDialogWarningSync() {
// When just created
// Then
assertTrue(settings.shouldShowSecurityPinWarningSync)
// When
settings.incrementShowLoginsSecureWarningSyncCount()
// Then
assertFalse(settings.shouldShowSecurityPinWarningSync)
}
@Test
fun showLoginsDialogWarning() {
// When just created
// Then
assertEquals(0, settings.loginsSecureWarningCount)
// When
settings.incrementShowLoginsSecureWarningCount()
// Then
assertEquals(1, settings.loginsSecureWarningCount)
}
@Test
fun shouldShowLoginsDialogWarning() {
// When just created
// Then
assertTrue(settings.shouldShowSecurityPinWarning)
// When
settings.incrementShowLoginsSecureWarningCount()
// Then
assertFalse(settings.shouldShowSecurityPinWarning)
}
@Test
fun shouldUseLightTheme() {
// When just created
@ -298,7 +350,10 @@ class SettingsTest {
fun sitePermissionsPhoneFeatureCameraAction() {
// When just created
// Then
assertEquals(ASK_TO_ALLOW, settings.getSitePermissionsPhoneFeatureAction(PhoneFeature.CAMERA))
assertEquals(
ASK_TO_ALLOW,
settings.getSitePermissionsPhoneFeatureAction(PhoneFeature.CAMERA)
)
// When
settings.setSitePermissionsPhoneFeatureAction(PhoneFeature.CAMERA, BLOCKED)
@ -311,33 +366,48 @@ class SettingsTest {
fun sitePermissionsPhoneFeatureMicrophoneAction() {
// When just created
// Then
assertEquals(ASK_TO_ALLOW, settings.getSitePermissionsPhoneFeatureAction(PhoneFeature.MICROPHONE))
assertEquals(
ASK_TO_ALLOW,
settings.getSitePermissionsPhoneFeatureAction(PhoneFeature.MICROPHONE)
)
// When
settings.setSitePermissionsPhoneFeatureAction(PhoneFeature.MICROPHONE, BLOCKED)
// Then
assertEquals(BLOCKED, settings.getSitePermissionsPhoneFeatureAction(PhoneFeature.MICROPHONE))
assertEquals(
BLOCKED,
settings.getSitePermissionsPhoneFeatureAction(PhoneFeature.MICROPHONE)
)
}
@Test
fun sitePermissionsPhoneFeatureNotificationAction() {
// When just created
// Then
assertEquals(ASK_TO_ALLOW, settings.getSitePermissionsPhoneFeatureAction(PhoneFeature.NOTIFICATION))
assertEquals(
ASK_TO_ALLOW,
settings.getSitePermissionsPhoneFeatureAction(PhoneFeature.NOTIFICATION)
)
// When
settings.setSitePermissionsPhoneFeatureAction(PhoneFeature.NOTIFICATION, BLOCKED)
// Then
assertEquals(BLOCKED, settings.getSitePermissionsPhoneFeatureAction(PhoneFeature.NOTIFICATION))
assertEquals(
BLOCKED,
settings.getSitePermissionsPhoneFeatureAction(PhoneFeature.NOTIFICATION)
)
}
@Test
fun sitePermissionsPhoneFeatureLocation() {
// When just created
// Then
assertEquals(ASK_TO_ALLOW, settings.getSitePermissionsPhoneFeatureAction(PhoneFeature.LOCATION))
assertEquals(
ASK_TO_ALLOW,
settings.getSitePermissionsPhoneFeatureAction(PhoneFeature.LOCATION)
)
// When
settings.setSitePermissionsPhoneFeatureAction(PhoneFeature.LOCATION, BLOCKED)

View File

@ -18,6 +18,7 @@ object Versions {
const val osslicenses_library = "17.0.0"
const val androidx_appcompat = "1.1.0"
const val androidx_biometric = "1.0.0-rc02"
const val androidx_coordinator_layout = "1.1.0-beta01"
const val androidx_constraint_layout = "2.0.0-beta2"
const val androidx_preference = "1.1.0"
@ -166,6 +167,7 @@ object Deps {
const val leanplum = "com.leanplum:leanplum-core:${Versions.leanplum}"
const val androidx_annotation = "androidx.annotation:annotation:${Versions.androidx_annotation}"
const val androidx_biometric = "androidx.biometric:biometric:${Versions.androidx_biometric}"
const val androidx_fragment = "androidx.fragment:fragment-ktx:${Versions.androidx_fragment}"
const val androidx_appcompat = "androidx.appcompat:appcompat:${Versions.androidx_appcompat}"
const val androidx_coordinatorlayout = "androidx.coordinatorlayout:coordinatorlayout:${Versions.androidx_coordinator_layout}"