From db92034a8a85ac33d42235e87f6fce9bc9738475 Mon Sep 17 00:00:00 2001 From: Harris Date: Fri, 3 Jun 2022 09:53:40 +1000 Subject: [PATCH] feat: handle KeyStore backed fingerprint verification --- .../securesms/PassphrasePromptActivity.java | 38 ++++++++-- .../crypto/BiometricSecretProvider.kt | 75 +++++++++++++++++++ 2 files changed, 107 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/crypto/BiometricSecretProvider.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/PassphrasePromptActivity.java b/app/src/main/java/org/thoughtcrime/securesms/PassphrasePromptActivity.java index 7cbb0533f0..09f8efafe6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/PassphrasePromptActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/PassphrasePromptActivity.java @@ -39,11 +39,14 @@ import android.widget.ImageView; import androidx.core.hardware.fingerprint.FingerprintManagerCompat; import androidx.core.os.CancellationSignal; -import org.thoughtcrime.securesms.util.AnimationCompleteListener; -import org.thoughtcrime.securesms.components.AnimatingToggle; -import org.session.libsignal.utilities.Log; -import org.thoughtcrime.securesms.service.KeyCachingService; import org.session.libsession.utilities.TextSecurePreferences; +import org.session.libsignal.utilities.Log; +import org.thoughtcrime.securesms.components.AnimatingToggle; +import org.thoughtcrime.securesms.crypto.BiometricSecretProvider; +import org.thoughtcrime.securesms.service.KeyCachingService; +import org.thoughtcrime.securesms.util.AnimationCompleteListener; + +import java.security.Signature; import network.loki.messenger.R; @@ -61,6 +64,8 @@ public class PassphrasePromptActivity extends BaseActionBarActivity { private CancellationSignal fingerprintCancellationSignal; private FingerprintListener fingerprintListener; + private final BiometricSecretProvider biometricSecretProvider = new BiometricSecretProvider(); + private boolean authenticated; private boolean failure; @@ -200,7 +205,7 @@ public class PassphrasePromptActivity extends BaseActionBarActivity { if (fingerprintManager.isHardwareDetected() && fingerprintManager.hasEnrolledFingerprints()) { Log.i(TAG, "Listening for fingerprints..."); fingerprintCancellationSignal = new CancellationSignal(); - fingerprintManager.authenticate(null, 0, fingerprintCancellationSignal, fingerprintListener, null); + fingerprintManager.authenticate(new FingerprintManagerCompat.CryptoObject(biometricSecretProvider.getOrCreateBiometricSignature(this)), 0, fingerprintCancellationSignal, fingerprintListener, null); } else { Log.i(TAG, "firing intent..."); Intent intent = keyguardManager.createConfirmDeviceCredentialIntent("Unlock Session", ""); @@ -224,6 +229,27 @@ public class PassphrasePromptActivity extends BaseActionBarActivity { @Override public void onAuthenticationSucceeded(FingerprintManagerCompat.AuthenticationResult result) { Log.i(TAG, "onAuthenticationSucceeded"); + if (result.getCryptoObject() == null || result.getCryptoObject().getSignature() == null) { + // authentication failed + onAuthenticationFailed(); + return; + } + // Signature object now successfully unlocked + boolean authenticationSucceeded = false; + try { + Signature signature = result.getCryptoObject().getSignature(); + byte[] random = biometricSecretProvider.getRandomData(); + signature.update(random); + byte[] signed = signature.sign(); + authenticationSucceeded = biometricSecretProvider.verifySignature(random, signed); + } catch (Exception e) { + Log.e(TAG, "onAuthentication signature generation and verification failed", e); + } + if (!authenticationSucceeded) { + onAuthenticationFailed(); + return; + } + fingerprintPrompt.setImageResource(R.drawable.ic_check_white_48dp); fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.green_500), PorterDuff.Mode.SRC_IN); fingerprintPrompt.animate().setInterpolator(new BounceInterpolator()).scaleX(1.1f).scaleY(1.1f).setDuration(500).setListener(new AnimationCompleteListener() { @@ -239,7 +265,7 @@ public class PassphrasePromptActivity extends BaseActionBarActivity { @Override public void onAuthenticationFailed() { - Log.w(TAG, "onAuthenticatoinFailed()"); + Log.w(TAG, "onAuthenticationFailed()"); fingerprintPrompt.setImageResource(R.drawable.ic_close_white_48dp); fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.red_500), PorterDuff.Mode.SRC_IN); diff --git a/app/src/main/java/org/thoughtcrime/securesms/crypto/BiometricSecretProvider.kt b/app/src/main/java/org/thoughtcrime/securesms/crypto/BiometricSecretProvider.kt new file mode 100644 index 0000000000..fe9c0c7fcf --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/crypto/BiometricSecretProvider.kt @@ -0,0 +1,75 @@ +package org.thoughtcrime.securesms.crypto + +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyProperties +import org.session.libsession.utilities.Util +import java.security.KeyPairGenerator +import java.security.KeyStore +import java.security.PrivateKey +import java.security.Signature + +class BiometricSecretProvider { + + companion object { + private const val BIOMETRIC_ASYM_KEY_ALIAS = "Session-biometric-asym" + private const val ANDROID_KEYSTORE = "AndroidKeyStore" + private const val SIGNATURE_ALGORITHM = "SHA512withECDSA" + } + + fun getRandomData() = Util.getSecretBytes(32) + + private fun createAsymmetricKey(context: Context) { + val keyGenerator = KeyPairGenerator.getInstance( + KeyProperties.KEY_ALGORITHM_EC, ANDROID_KEYSTORE + ) + + val builder = KeyGenParameterSpec.Builder(BIOMETRIC_ASYM_KEY_ALIAS, + KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY + ) + .setDigests( + KeyProperties.DIGEST_SHA256, + KeyProperties.DIGEST_SHA512 + ) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_OAEP) + .setUserAuthenticationRequired(true) + .setUserAuthenticationValidityDurationSeconds(-1) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + builder.setUnlockedDeviceRequired(true) + if (context.packageManager.hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE)) { + builder.setIsStrongBoxBacked(true) + } + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + builder.setInvalidatedByBiometricEnrollment(true) + } + keyGenerator.initialize(builder.build()) + keyGenerator.generateKeyPair() + } + + fun getOrCreateBiometricSignature(context: Context): Signature { + val ks = KeyStore.getInstance(ANDROID_KEYSTORE) + ks.load(null) + if (!ks.containsAlias(BIOMETRIC_ASYM_KEY_ALIAS)) { + createAsymmetricKey(context) + } + val key = ks.getKey(BIOMETRIC_ASYM_KEY_ALIAS, null) as PrivateKey + val signature = Signature.getInstance(SIGNATURE_ALGORITHM) + signature.initSign(key) + return signature + } + + fun verifySignature(data: ByteArray, signedData: ByteArray): Boolean { + val ks = KeyStore.getInstance(ANDROID_KEYSTORE) + ks.load(null) + val certificate = ks.getCertificate(BIOMETRIC_ASYM_KEY_ALIAS) + val signature = Signature.getInstance(SIGNATURE_ALGORITHM) + signature.initVerify(certificate) + signature.update(data) + return signature.verify(signedData) + } +} \ No newline at end of file