Created
December 7, 2018 13:21
-
-
Save justintoth/c6fc5cc8c647819e351a554b1f9d7aff to your computer and use it in GitHub Desktop.
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; | |
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