Skip to content

Instantly share code, notes, and snippets.

@justintoth
Created October 29, 2018 14:25
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save justintoth/49484bb0c3a0494666442f3e4ea014c0 to your computer and use it in GitHub Desktop.
Save justintoth/49484bb0c3a0494666442f3e4ea014c0 to your computer and use it in GitHub Desktop.
BiometricPrompt implementation for Xamarin Android
using System;
using Android.Content.PM;
using Android.Hardware.Biometrics;
using Android.OS;
using Android.Runtime;
using Android.Security.Keystore;
using Android.Util;
using Java.Lang;
using Java.Security;
using Java.Security.Spec;
using RPR.Mobile.Shared.Composition;
using RPR.Mobile.Shared.Contracts;
using Signature = Java.Security.Signature;
namespace RPR.Mobile.Droid.Helpers {
public class BiometricHelper {
private const string KEY_STORE_NAME = "AndroidKeyStore";
private const string KEY_NAME = "BiometricKey";
private const string REPLAY_ID = "12345";// TODO: Set random value?
private const string SIGNATURE_ALGORITHM = "SHA256withECDSA";
private BiometricPrompt biometricPrompt;
private string signatureMessage;
private readonly ILoggingService log;
public BiometricHelper () {
log = ServiceLocator.Current.Resolve<ILoggingService> ();
}
private void RegisterOrAuthenticate () {
if (DeviceHelper.OSFlavor < BuildVersionCodes.P)
throw new NotSupportedException ("Fingerprint not supported below Android 9 Pie.");
// TODO: How do we determine whether to register or authenticate?
var alreadyRegistered = false;
if (!alreadyRegistered)
Register ();
else
Authenticate ();
}
private void Register () {
if (IsSupported) {
// Generate key pair and init signature
Signature signature;
try {
// Before generating a key pair, we have to check enrollment of biometrics on the device but there is no such method on new biometric prompt API
// Note that this method will throw an exception if there is no enrolled biometric on the device
// This issue is reported to Android issue tracker: https://issuetracker.google.com/issues/112495828
KeyPair keyPair = GenerateKeyPair (KEY_NAME, true);
// Send public key part of key pair to the server, to be used for authentication
signatureMessage = "{0}:{1}:{2}"
.WithFormat (Base64.EncodeToString (keyPair.Public.GetEncoded (), Base64Flags.UrlSafe), KEY_NAME, REPLAY_ID);
signature = InitSignature (KEY_NAME);
} catch (Java.Lang.Exception e) {
throw new RuntimeException (e);
}
if (signature != null)
ShowBiometricPrompt (signature);
}
}
private void Authenticate () {
if (IsSupported) {
// Init signature
Signature signature;
try {
// Send key name and challenge to the server, this message will be verified with registered public key on the server
signatureMessage = "{0}:{1}".WithFormat (KEY_NAME, REPLAY_ID);
signature = InitSignature (KEY_NAME);
} catch (Java.Lang.Exception e) {
throw new RuntimeException (e);
}
if (signature != null)
ShowBiometricPrompt (signature);
}
}
private void ShowBiometricPrompt (Signature signature) {
// Create biometric prompt
var activity = ApplicationContext.Activity;
biometricPrompt = new BiometricPrompt.Builder (activity)
.SetDescription ("Description")// TODO:
.SetTitle ("Title")// TODO:
.SetSubtitle ("Subtitle")// TODO:
/*.SetNegativeButton ("Cancel", GetMainExecutor (), new DialogInterface.OnClickListener () {
@Override
public void onClick (DialogInterface dialogInterface, int i) {
Log.i (TAG, "Cancel button clicked");
}
})*/
.Build ();
// Show biometric prompt
var cancellationSignal = new CancellationSignal ();
var authenticationCallback = GetAuthenticationCallback ();
biometricPrompt.Authenticate (new BiometricPrompt.CryptoObject (signature), cancellationSignal, activity.MainExecutor, authenticationCallback);
}
private BiometricPrompt.AuthenticationCallback GetAuthenticationCallback () {
// Callback for biometric authentication result
var callback = new BiometricAuthenticationCallback {
Success = (BiometricPrompt.AuthenticationResult result) => {
var signature = result.CryptoObject.Signature;
try {
signature.Update (signatureMessage.GetBytes ());
var signatureString = Base64.EncodeToString (signature.Sign (), Base64Flags.UrlSafe);
// Normally, ToBeSignedMessage and Signature are sent to the server and then verified
// TODO: Toast.MakeText (getApplicationContext (), signatureMessage + ":" + signatureString, Toast.LENGTH_SHORT).show ();
} catch (SignatureException) {
throw new RuntimeException ();
}
},
Failed = () => {
// TODO: Show error.
},
Help = (BiometricAcquiredStatus helpCode, ICharSequence helpString) => {
// TODO: What do we do here?
}
};
return callback;
}
private KeyPair GenerateKeyPair (string keyName, bool invalidatedByBiometricEnrollment) {
var keyPairGenerator = KeyPairGenerator.GetInstance (KeyProperties.KeyAlgorithmEc, KEY_STORE_NAME);
var builder = new KeyGenParameterSpec.Builder (keyName, KeyStorePurpose.Sign)
.SetAlgorithmParameterSpec (new ECGenParameterSpec ("secp256r1"))
.SetDigests (KeyProperties.DigestSha256, KeyProperties.DigestSha384, KeyProperties.DigestSha512)
// Require the user to authenticate with a biometric to authorize every use of the key
.SetUserAuthenticationRequired (true)
// Generated keys will be invalidated if the biometric templates are added more to user device
.SetInvalidatedByBiometricEnrollment (invalidatedByBiometricEnrollment);
keyPairGenerator.Initialize (builder.Build ());
return keyPairGenerator.GenerateKeyPair ();
}
private KeyPair GetKeyPair (string keyName) {
var keyStore = KeyStore.GetInstance(KEY_STORE_NAME);
keyStore.Load (null);
if (keyStore.ContainsAlias(keyName)) {
// Get public key
var publicKey = keyStore.GetCertificate (keyName).PublicKey;
// Get private key
var privateKey = (IPrivateKey)keyStore.GetKey (keyName, null);
// Return a key pair
return new KeyPair (publicKey, privateKey);
}
return null;
}
private Signature InitSignature (string keyName) {
var keyPair = GetKeyPair(keyName);
if (keyPair != null) {
var signature = Signature.GetInstance (SIGNATURE_ALGORITHM);
signature.InitSign (keyPair.Private);
return signature;
}
return null;
}
/*
* Before generating a key pair with biometric prompt, we need to check that the device supports fingerprint, iris, or face.
* Currently, there are no FEATURE_IRIS or FEATURE_FACE constants on PackageManager.
*/
private bool IsSupported {
get {
var packageManager = ApplicationContext.Activity.PackageManager;
return packageManager.HasSystemFeature (PackageManager.FeatureFingerprint);
}
}
class BiometricAuthenticationCallback : BiometricPrompt.AuthenticationCallback {
public Action<BiometricPrompt.AuthenticationResult> Success;
public Action Failed;
public Action<BiometricAcquiredStatus, ICharSequence> Help;
public override void OnAuthenticationSucceeded (BiometricPrompt.AuthenticationResult result) {
base.OnAuthenticationSucceeded (result);
Success (result);
}
public override void OnAuthenticationFailed () {
base.OnAuthenticationFailed ();
Failed ();
}
public override void OnAuthenticationHelp ([GeneratedEnum] BiometricAcquiredStatus helpCode, ICharSequence helpString) {
base.OnAuthenticationHelp (helpCode, helpString);
Help (helpCode, helpString);
}
}
}
}
@pvsfair
Copy link

pvsfair commented May 10, 2019

What about the "OnAuthenticationError" callback?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment