Merge pull request #5584 from ByteHamster/authenticate-before-password

Authenticate user before showing password
This commit is contained in:
cketti 2021-08-19 19:42:19 +02:00 committed by GitHub
commit 3b42061377
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 149 additions and 6 deletions

View file

@ -5,11 +5,13 @@ dependencies {
implementation project(":app:core")
api "androidx.appcompat:appcompat:${versions.androidxAppCompat}"
api "com.google.android.material:material:${versions.materialComponents}"
api "androidx.navigation:navigation-fragment-ktx:${versions.androidxNavigation}"
api "androidx.navigation:navigation-ui-ktx:${versions.androidxNavigation}"
api "androidx.lifecycle:lifecycle-livedata-ktx:${versions.androidxLifecycle}"
implementation "androidx.core:core-ktx:${versions.androidxCore}"
implementation "androidx.biometric:biometric:${versions.androidxBiometric}"
implementation "com.jakewharton.timber:timber:${versions.timber}"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:${versions.kotlinCoroutines}"
}

View file

@ -0,0 +1,119 @@
@file:JvmName("TextInputLayoutHelper")
package com.fsck.k9.ui.base.extensions
import android.annotation.SuppressLint
import android.text.method.PasswordTransformationMethod
import android.view.WindowManager.LayoutParams.FLAG_SECURE
import android.widget.EditText
import android.widget.Toast
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK
import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.google.android.material.textfield.TextInputLayout
/**
* Configures a [TextInputLayout] so the password can only be revealed after authentication.
*/
fun TextInputLayout.configureAuthenticatedPasswordToggle(
activity: FragmentActivity,
title: String,
subtitle: String,
needScreenLockMessage: String,
) {
val viewModel = ViewModelProvider(activity).get(AuthenticatedPasswordToggleViewModel::class.java)
viewModel.textInputLayout = this
viewModel.activity = activity
fun authenticateUserAndShowPassword(activity: FragmentActivity) {
val mainExecutor = ContextCompat.getMainExecutor(activity)
val context = activity.applicationContext
val authenticationCallback = object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
// The Activity might have been recreated since this callback object was created (e.g. due to an
// orientation change). So we fetch the (new) references from the ViewModel.
viewModel.isAuthenticated = true
viewModel.activity?.setSecure(true)
viewModel.textInputLayout?.editText?.showPassword()
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
if (errorCode == BiometricPrompt.ERROR_HW_NOT_PRESENT ||
errorCode == BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL ||
errorCode == BiometricPrompt.ERROR_NO_BIOMETRICS
) {
Toast.makeText(context, needScreenLockMessage, Toast.LENGTH_SHORT).show()
} else if (errString.isNotEmpty()) {
Toast.makeText(context, errString, Toast.LENGTH_SHORT).show()
}
}
}
BiometricPrompt(activity, mainExecutor, authenticationCallback).authenticate(
BiometricPrompt.PromptInfo.Builder()
.setAllowedAuthenticators(BIOMETRIC_STRONG or BIOMETRIC_WEAK or DEVICE_CREDENTIAL)
.setTitle(title)
.setSubtitle(subtitle)
.build()
)
}
val editText = this.editText ?: error("TextInputLayout.editText == null")
setEndIconOnClickListener {
if (editText.isPasswordHidden) {
if (viewModel.isAuthenticated) {
activity.setSecure(true)
editText.showPassword()
} else {
authenticateUserAndShowPassword(activity)
}
} else {
viewModel.isAuthenticated = false
editText.hidePassword()
activity.setSecure(false)
}
}
}
private val EditText.isPasswordHidden: Boolean
get() = transformationMethod is PasswordTransformationMethod
private fun EditText.showPassword() {
transformationMethod = null
}
private fun EditText.hidePassword() {
transformationMethod = PasswordTransformationMethod.getInstance()
}
private fun FragmentActivity.setSecure(secure: Boolean) {
window.setFlags(if (secure) FLAG_SECURE else 0, FLAG_SECURE)
}
@SuppressLint("StaticFieldLeak")
class AuthenticatedPasswordToggleViewModel : ViewModel() {
var isAuthenticated = false
var textInputLayout: TextInputLayout? = null
var activity: FragmentActivity? = null
set(value) {
field = value
value?.lifecycle?.addObserver(object : LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun removeReferences() {
textInputLayout = null
field = null
}
})
}
}

View file

@ -29,7 +29,6 @@ dependencies {
implementation "androidx.cardview:cardview:${versions.androidxCardView}"
implementation "androidx.localbroadcastmanager:localbroadcastmanager:${versions.androidxLocalBroadcastManager}"
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
implementation "com.google.android.material:material:${versions.materialComponents}"
implementation "de.cketti.library.changelog:ckchangelog-core:2.0.0-beta02"
implementation "com.splitwise:tokenautocomplete:4.0.0-beta01"
implementation "de.cketti.safecontentresolver:safe-content-resolver-v21:1.0.0"

View file

@ -19,11 +19,9 @@ import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
import com.fsck.k9.Account;
import com.fsck.k9.Account.FolderMode;
import com.fsck.k9.DI;
import com.fsck.k9.LocalKeyStoreManager;
import com.fsck.k9.Preferences;
@ -44,6 +42,7 @@ import com.fsck.k9.mail.store.imap.ImapStoreSettings;
import com.fsck.k9.mail.store.webdav.WebDavStoreSettings;
import com.fsck.k9.preferences.Protocols;
import com.fsck.k9.ui.R;
import com.fsck.k9.ui.base.extensions.TextInputLayoutHelper;
import com.fsck.k9.view.ClientCertificateSpinner;
import com.fsck.k9.view.ClientCertificateSpinner.OnClientCertificateChangedListener;
@ -181,6 +180,15 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
}
boolean editSettings = Intent.ACTION_EDIT.equals(getIntent().getAction());
if (editSettings) {
TextInputLayoutHelper.configureAuthenticatedPasswordToggle(
mPasswordLayoutView,
this,
getString(R.string.account_setup_basics_show_password_biometrics_title),
getString(R.string.account_setup_basics_show_password_biometrics_subtitle),
getString(R.string.account_setup_basics_show_password_need_lock)
);
}
try {
ServerSettings settings = mAccount.getIncomingServerSettings();

View file

@ -34,6 +34,7 @@ import com.fsck.k9.mail.AuthType;
import com.fsck.k9.mail.ConnectionSecurity;
import com.fsck.k9.mail.MailServerDirection;
import com.fsck.k9.mail.ServerSettings;
import com.fsck.k9.ui.base.extensions.TextInputLayoutHelper;
import com.fsck.k9.view.ClientCertificateSpinner;
import com.fsck.k9.view.ClientCertificateSpinner.OnClientCertificateChangedListener;
import com.google.android.material.textfield.TextInputEditText;
@ -148,6 +149,17 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener,
mAccount = Preferences.getPreferences(this).getAccount(accountUuid);
}
boolean editSettings = Intent.ACTION_EDIT.equals(getIntent().getAction());
if (editSettings) {
TextInputLayoutHelper.configureAuthenticatedPasswordToggle(
mPasswordLayoutView,
this,
getString(R.string.account_setup_basics_show_password_biometrics_title),
getString(R.string.account_setup_basics_show_password_biometrics_subtitle),
getString(R.string.account_setup_basics_show_password_need_lock)
);
}
try {
ServerSettings settings = mAccount.getOutgoingServerSettings();

View file

@ -97,7 +97,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/account_setup_margin_between_items_incoming_and_outgoing"
app:passwordToggleEnabled="true">
app:endIconMode="password_toggle">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/account_password"

View file

@ -111,7 +111,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/account_setup_margin_between_items_incoming_and_outgoing"
app:passwordToggleEnabled="true">
app:endIconMode="password_toggle">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/account_password"

View file

@ -355,6 +355,9 @@ Please submit bug reports, contribute new features and ask questions at
<string name="account_setup_basics_email_hint">Email address</string>
<string name="account_setup_basics_password_hint">Password</string>
<string name="account_setup_basics_show_password">Show password</string>
<string name="account_setup_basics_show_password_need_lock">To view your password here, enable screen lock on this device.</string>
<string name="account_setup_basics_show_password_biometrics_title">Verify it\'s you</string>
<string name="account_setup_basics_show_password_biometrics_subtitle">Unlock to view your password</string>
<string name="account_setup_basics_manual_setup_action">Manual setup</string>
<string name="account_setup_check_settings_title"/>

View file

@ -10,7 +10,6 @@ dependencies {
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${versions.androidxLifecycle}"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:${versions.androidxLifecycle}"
implementation "androidx.constraintlayout:constraintlayout:${versions.androidxConstraintLayout}"
implementation "com.google.android.material:material:${versions.materialComponents}"
implementation "androidx.core:core-ktx:${versions.androidxCore}"
implementation "com.jakewharton.timber:timber:${versions.timber}"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:${versions.kotlinCoroutines}"

View file

@ -17,6 +17,7 @@ buildscript {
'androidxRecyclerView': '1.1.0',
'androidxLifecycle': '2.3.1',
'androidxAnnotation': '1.2.0',
'androidxBiometric': '1.1.0',
'androidxNavigation': '2.3.5',
'androidxConstraintLayout': '2.0.4',
'androidxWorkManager': '2.5.0',