Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Unverified Commit 3b420613 authored by cketti's avatar cketti Committed by GitHub
Browse files

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

Authenticate user before showing password
parents f6cb1f90 cff6041d
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -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}"
}
+119 −0
Original line number Diff line number Diff line
@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
                }
            })
        }
}
+0 −1
Original line number Diff line number Diff line
@@ -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"
+10 −2
Original line number Diff line number Diff line
@@ -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();
+12 −0
Original line number Diff line number Diff line
@@ -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();

Loading