Skip to content

Commit

Permalink
Add support for biometric auth for payments.
Browse files Browse the repository at this point in the history
  • Loading branch information
varsha888 authored and greyson-signal committed Aug 24, 2022
1 parent 7162297 commit 372f939
Show file tree
Hide file tree
Showing 13 changed files with 390 additions and 100 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package org.thoughtcrime.securesms

import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Build
import androidx.activity.result.contract.ActivityResultContract
import androidx.annotation.RequiresApi
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.biometric.BiometricPrompt.PromptInfo
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.util.ServiceUtil

/**
* Authentication using phone biometric (face, fingerprint recognition) or device lock (pattern, pin or passphrase).
*/
class BiometricDeviceAuthentication(
private val biometricManager: BiometricManager,
private val biometricPrompt: BiometricPrompt,
private val biometricPromptInfo: PromptInfo
) {
companion object {
const val AUTHENTICATED = 1
const val NOT_AUTHENTICATED = -1
const val TAG: String = "BiometricDeviceAuth"
const val BIOMETRIC_AUTHENTICATORS = BiometricManager.Authenticators.BIOMETRIC_STRONG or BiometricManager.Authenticators.BIOMETRIC_WEAK
const val ALLOWED_AUTHENTICATORS = BIOMETRIC_AUTHENTICATORS or BiometricManager.Authenticators.DEVICE_CREDENTIAL
}

fun authenticate(context: Context, force: Boolean, showConfirmDeviceCredentialIntent: () -> Unit): Boolean {
val isKeyGuardSecure = ServiceUtil.getKeyguardManager(context).isKeyguardSecure

if (!isKeyGuardSecure) {
Log.w(TAG, "Keyguard not secure...")
return false
}

return if (Build.VERSION.SDK_INT != 29 && biometricManager.canAuthenticate(ALLOWED_AUTHENTICATORS) == BiometricManager.BIOMETRIC_SUCCESS) {
if (force) {
Log.i(TAG, "Listening for biometric authentication...")
biometricPrompt.authenticate(biometricPromptInfo)
} else {
Log.i(TAG, "Skipping show system biometric or device lock dialog unless forced")
}
true
} else if (force) {
if (force) {
Log.i(TAG, "firing intent...")
showConfirmDeviceCredentialIntent()
} else {
Log.i(TAG, "Skipping firing intent unless forced")
}
true
} else {
Log.w(TAG, "Not compatible...")
false
}
}

fun cancelAuthentication() {
biometricPrompt.cancelAuthentication()
}
}

class BiometricDeviceLockContract : ActivityResultContract<String, Int>() {

@RequiresApi(api = 21)
override fun createIntent(context: Context, input: String): Intent {
val keyguardManager = ServiceUtil.getKeyguardManager(context)
return keyguardManager.createConfirmDeviceCredentialIntent(input, "")
}

override fun parseResult(resultCode: Int, intent: Intent?) =
if (resultCode != Activity.RESULT_OK) {
BiometricDeviceAuthentication.NOT_AUTHENTICATED
} else {
BiometricDeviceAuthentication.AUTHENTICATED
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@
import androidx.annotation.NonNull;
import androidx.appcompat.widget.Toolbar;
import androidx.biometric.BiometricManager;
import androidx.biometric.BiometricManager.Authenticators;
import androidx.biometric.BiometricPrompt;

import org.signal.core.util.ThreadUtil;
Expand All @@ -64,6 +63,8 @@
import org.thoughtcrime.securesms.util.SupportEmailUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;

import kotlin.Unit;

/**
* Activity that prompts for a user's passphrase.
*
Expand All @@ -72,8 +73,6 @@
public class PassphrasePromptActivity extends PassphraseActivity {

private static final String TAG = Log.tag(PassphrasePromptActivity.class);
private static final int BIOMETRIC_AUTHENTICATORS = Authenticators.BIOMETRIC_STRONG | Authenticators.BIOMETRIC_WEAK;
private static final int ALLOWED_AUTHENTICATORS = BIOMETRIC_AUTHENTICATORS | Authenticators.DEVICE_CREDENTIAL;
private static final short AUTHENTICATE_REQUEST_CODE = 1007;
private static final String BUNDLE_ALREADY_SHOWN = "bundle_already_shown";
public static final String FROM_FOREGROUND = "from_foreground";
Expand All @@ -90,9 +89,9 @@ public class PassphrasePromptActivity extends PassphraseActivity {
private ImageButton hideButton;
private AnimatingToggle visibilityToggle;

private BiometricManager biometricManager;
private BiometricPrompt biometricPrompt;
private BiometricPrompt.PromptInfo biometricPromptInfo;
private BiometricManager biometricManager;
private BiometricPrompt biometricPrompt;
private BiometricDeviceAuthentication biometricAuth;

private boolean authenticated;
private boolean hadFailure;
Expand Down Expand Up @@ -249,12 +248,12 @@ private void initializeResources() {
lockScreenButton = findViewById(R.id.lock_screen_auth_container);
biometricManager = BiometricManager.from(this);
biometricPrompt = new BiometricPrompt(this, new BiometricAuthenticationListener());
biometricPromptInfo = new BiometricPrompt.PromptInfo
.Builder()
.setAllowedAuthenticators(ALLOWED_AUTHENTICATORS)
.setTitle(getString(R.string.PassphrasePromptActivity_unlock_signal))
.build();

BiometricPrompt.PromptInfo biometricPromptInfo = new BiometricPrompt.PromptInfo
.Builder()
.setAllowedAuthenticators(BiometricDeviceAuthentication.ALLOWED_AUTHENTICATORS)
.setTitle(getString(R.string.PassphrasePromptActivity_unlock_signal))
.build();
biometricAuth = new BiometricDeviceAuthentication(biometricManager, biometricPrompt, biometricPromptInfo);
setSupportActionBar(toolbar);
getSupportActionBar().setTitle("");

Expand All @@ -279,7 +278,7 @@ private void initializeResources() {
private void setLockTypeVisibility() {
if (TextSecurePreferences.isScreenLockEnabled(this)) {
passphraseAuthContainer.setVisibility(View.GONE);
fingerprintPrompt.setVisibility(biometricManager.canAuthenticate(BIOMETRIC_AUTHENTICATORS) == BiometricManager.BIOMETRIC_SUCCESS ? View.VISIBLE
fingerprintPrompt.setVisibility(biometricManager.canAuthenticate(BiometricDeviceAuthentication.BIOMETRIC_AUTHENTICATORS) == BiometricManager.BIOMETRIC_SUCCESS ? View.VISIBLE
: View.GONE);
lockScreenButton.setVisibility(View.VISIBLE);
} else {
Expand All @@ -290,33 +289,7 @@ private void setLockTypeVisibility() {
}

private void resumeScreenLock(boolean force) {
KeyguardManager keyguardManager = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);

assert keyguardManager != null;

if (!keyguardManager.isKeyguardSecure()) {
Log.w(TAG ,"Keyguard not secure...");
handleAuthenticated();
return;
}

if (Build.VERSION.SDK_INT != 29 && biometricManager.canAuthenticate(ALLOWED_AUTHENTICATORS) == BiometricManager.BIOMETRIC_SUCCESS) {
if (force) {
Log.i(TAG, "Listening for biometric authentication...");
biometricPrompt.authenticate(biometricPromptInfo);
} else {
Log.i(TAG, "Skipping show system biometric dialog unless forced");
}
} else if (Build.VERSION.SDK_INT >= 21) {
if (force) {
Log.i(TAG, "firing intent...");
Intent intent = keyguardManager.createConfirmDeviceCredentialIntent(getString(R.string.PassphrasePromptActivity_unlock_signal), "");
startActivityForResult(intent, AUTHENTICATE_REQUEST_CODE);
} else {
Log.i(TAG, "Skipping firing intent unless forced");
}
} else {
Log.w(TAG, "Not compatible...");
if (!biometricAuth.authenticate(getApplicationContext(), force, this::showConfirmDeviceCredentialIntent)) {
handleAuthenticated();
}
}
Expand All @@ -332,6 +305,16 @@ private void sendEmailToSupport() {
body);
}

public Unit showConfirmDeviceCredentialIntent() {
KeyguardManager keyguardManager = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
Intent intent = null;
if (Build.VERSION.SDK_INT >= 21) {
intent = keyguardManager.createConfirmDeviceCredentialIntent(getString(R.string.PassphrasePromptActivity_unlock_signal), "");
}
startActivityForResult(intent, AUTHENTICATE_REQUEST_CODE);
return Unit.INSTANCE;
}

private class PassphraseActionListener implements TextView.OnEditorActionListener {
@Override
public boolean onEditorAction(TextView exampleView, int actionId, KeyEvent keyEvent) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.components.settings.app.privacy
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.provider.Settings
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.text.style.TextAppearanceSpan
Expand All @@ -16,6 +17,7 @@ import androidx.lifecycle.ViewModelProvider
import androidx.navigation.Navigation
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.preference.PreferenceManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import mobi.upod.timedurationpicker.TimeDurationPicker
Expand Down Expand Up @@ -78,9 +80,15 @@ class PrivacySettingsFragment : DSLSettingsFragment(R.string.preferences__privac
val repository = PrivacySettingsRepository()
val factory = PrivacySettingsViewModel.Factory(sharedPreferences, repository)
viewModel = ViewModelProvider(this, factory)[PrivacySettingsViewModel::class.java]
val args: PrivacySettingsFragmentArgs by navArgs()
var showPaymentLock = true

viewModel.state.observe(viewLifecycleOwner) { state ->
adapter.submitList(getConfiguration(state).toMappingModelList())
if (args.showPaymentLock && showPaymentLock) {
showPaymentLock = false
recyclerView?.scrollToPosition(adapter.itemCount - 1)
}
}
}

Expand Down Expand Up @@ -304,6 +312,23 @@ class PrivacySettingsFragment : DSLSettingsFragment(R.string.preferences__privac

dividerPref()

sectionHeaderPref(R.string.preferences_app_protection__payments)

switchPref(
title = DSLSettingsText.from(R.string.preferences__payment_lock),
summary = DSLSettingsText.from(R.string.PrivacySettingsFragment__payment_lock_require_lock),
isChecked = state.paymentLock && ServiceUtil.getKeyguardManager(requireContext()).isKeyguardSecure,
onClick = {
if (!ServiceUtil.getKeyguardManager(requireContext()).isKeyguardSecure) {
showGoToPhoneSettings()
} else {
viewModel.togglePaymentLock()
}
}
)

dividerPref()

clickPref(
title = DSLSettingsText.from(R.string.preferences__advanced),
summary = DSLSettingsText.from(R.string.PrivacySettingsFragment__signal_message_and_calls),
Expand All @@ -314,6 +339,16 @@ class PrivacySettingsFragment : DSLSettingsFragment(R.string.preferences__privac
}
}

private fun showGoToPhoneSettings() {
MaterialAlertDialogBuilder(requireContext()).apply {
setTitle(getString(R.string.PrivacySettingsFragment__cant_enable_title))
setMessage(getString(R.string.PrivacySettingsFragment__cant_enable_description))
setPositiveButton(R.string.PaymentsHomeFragment__enable) { _, _ -> startActivity(Intent(Settings.ACTION_BIOMETRIC_ENROLL)) }
setNegativeButton(R.string.PaymentsHomeFragment__not_now) { _, _ -> }
show()
}
}

private fun getScreenLockInactivityTimeoutSummary(timeoutSeconds: Long): String {
val hours = TimeUnit.SECONDS.toHours(timeoutSeconds)
val minutes = TimeUnit.SECONDS.toMinutes(timeoutSeconds) - hours * 60
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ data class PrivacySettingsState(
val screenLockActivityTimeout: Long,
val screenSecurity: Boolean,
val incognitoKeyboard: Boolean,
val paymentLock: Boolean,
val isObsoletePasswordEnabled: Boolean,
val isObsoletePasswordTimeoutEnabled: Boolean,
val obsoletePasswordTimeout: Int,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ class PrivacySettingsViewModel(
refresh()
}

fun togglePaymentLock() {
SignalStore.paymentsValues().paymentLock = state.value?.let { !it.paymentLock } ?: false
refresh()
}

fun setObsoletePasswordTimeoutEnabled(enabled: Boolean) {
sharedPreferences.edit().putBoolean(TextSecurePreferences.PASSPHRASE_TIMEOUT_PREF, enabled).apply()
refresh()
Expand All @@ -97,6 +102,7 @@ class PrivacySettingsViewModel(
screenLockActivityTimeout = TextSecurePreferences.getScreenLockTimeout(ApplicationDependencies.getApplication()),
screenSecurity = TextSecurePreferences.isScreenSecurityEnabled(ApplicationDependencies.getApplication()),
incognitoKeyboard = TextSecurePreferences.isIncognitoKeyboardEnabled(ApplicationDependencies.getApplication()),
paymentLock = SignalStore.paymentsValues().paymentLock,
seeMyPhoneNumber = SignalStore.phoneNumberPrivacy().phoneNumberSharingMode,
findMeByPhoneNumber = SignalStore.phoneNumberPrivacy().phoneNumberListingMode,
isObsoletePasswordEnabled = !TextSecurePreferences.isPasswordDisabled(ApplicationDependencies.getApplication()),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,28 @@ internal class PaymentsValues internal constructor(store: KeyValueStore) : Signa
private const val SHOW_CASHING_OUT_INFO_CARD = "mob_payments_show_cashing_out_info_card"
private const val SHOW_RECOVERY_PHRASE_INFO_CARD = "mob_payments_show_recovery_phrase_info_card"
private const val SHOW_UPDATE_PIN_INFO_CARD = "mob_payments_show_update_pin_info_card"
private const val PAYMENT_LOCK_ENABLED = "mob_payments_payment_lock_enabled"
private const val PAYMENT_LOCK_TIMESTAMP = "mob_payments_payment_lock_timestamp"
private const val PAYMENT_LOCK_SKIP_COUNT = "mob_payments_payment_lock_skip_count"

private val LARGE_BALANCE_THRESHOLD = Money.mobileCoin(BigDecimal.valueOf(500))

@VisibleForTesting
const val MOB_PAYMENTS_ENABLED = "mob_payments_enabled"
}

var paymentLock
get() = getBoolean(PAYMENT_LOCK_ENABLED, false)
set(enabled) = putBoolean(PAYMENT_LOCK_ENABLED, enabled)

var paymentLockTimestamp
get() = getLong(PAYMENT_LOCK_TIMESTAMP, 0)
set(timestamp) = putLong(PAYMENT_LOCK_TIMESTAMP, timestamp)

var paymentLockSkipCount
get() = getInteger(PAYMENT_LOCK_SKIP_COUNT, 0)
set(count) = putInteger(PAYMENT_LOCK_SKIP_COUNT, count)

private val liveCurrentCurrency: MutableLiveData<Currency> by lazy { MutableLiveData(currentCurrency()) }
private val liveMobileCoinLedger: MutableLiveData<MobileCoinLedgerWrapper> by lazy { MutableLiveData(mobileCoinLatestFullLedger()) }
private val liveMobileCoinBalance: LiveData<Balance> by lazy { Transformations.map(liveMobileCoinLedger) { obj: MobileCoinLedgerWrapper -> obj.balance } }
Expand Down

0 comments on commit 372f939

Please sign in to comment.