Skip to content

Instantly share code, notes, and snippets.

@justintoth
Created December 7, 2018 13:21
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/c6fc5cc8c647819e351a554b1f9d7aff to your computer and use it in GitHub Desktop.
Save justintoth/c6fc5cc8c647819e351a554b1f9d7aff to your computer and use it in GitHub Desktop.
using System;
using Android.Content;
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;
var negativeButtonListener = new DialogInterfaceOnClickListener (() => {
// TODO: Do something here.
});
biometricPrompt = new BiometricPrompt.Builder (activity)
.SetDescription ("Description")// TODO:
.SetTitle ("Title")// TODO:
.SetSubtitle ("Subtitle")// TODO:
.SetNegativeButton ("Cancel", activity.MainExecutor, negativeButtonListener)
.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);
}
}
}
class DialogInterfaceOnClickListener : Java.Lang.Object, IDialogInterfaceOnClickListener {
private Action click;
public DialogInterfaceOnClickListener (Action click) {
this.click = click;
}
public void OnClick (IDialogInterface dialog, int which) {
click ();
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment