Created
October 29, 2018 14:25
-
-
Save justintoth/49484bb0c3a0494666442f3e4ea014c0 to your computer and use it in GitHub Desktop.
BiometricPrompt implementation for Xamarin Android
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
What about the "OnAuthenticationError" callback?