use native biomatrics on Android

This commit is contained in:
Kyle Spearrin 2019-10-23 09:11:48 -04:00
parent aed3ec5474
commit 4b989b01e9
11 changed files with 199 additions and 52 deletions

View file

@ -8,9 +8,11 @@ using Android.App;
using Android.App.Assist; using Android.App.Assist;
using Android.Content; using Android.Content;
using Android.Content.PM; using Android.Content.PM;
using Android.Hardware.Biometrics;
using Android.Nfc; using Android.Nfc;
using Android.OS; using Android.OS;
using Android.Provider; using Android.Provider;
using Android.Runtime;
using Android.Support.V4.App; using Android.Support.V4.App;
using Android.Support.V4.Content; using Android.Support.V4.Content;
using Android.Text; using Android.Text;
@ -28,6 +30,7 @@ using Bit.Core.Models.View;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Bit.Droid.Autofill; using Bit.Droid.Autofill;
using Plugin.CurrentActivity; using Plugin.CurrentActivity;
using Plugin.Fingerprint;
namespace Bit.Droid.Services namespace Bit.Droid.Services
{ {
@ -335,11 +338,72 @@ namespace Bit.Droid.Services
Application.Context.PackageName, 0).VersionCode.ToString(); Application.Context.PackageName, 0).VersionCode.ToString();
} }
public bool SupportsFaceId() public bool SupportsFaceBiometric()
{ {
return false; return false;
} }
public Task<bool> SupportsFaceBiometricAsync()
{
return Task.FromResult(SupportsFaceBiometric());
}
public async Task<bool> BiometricAvailableAsync()
{
if(UseNativeBiometric())
{
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
var manager = activity.GetSystemService(Context.BiometricService) as BiometricManager;
return manager.CanAuthenticate() == BiometricCode.Success;
}
else
{
try
{
return await CrossFingerprint.Current.IsAvailableAsync();
}
catch
{
return false;
}
}
}
public bool UseNativeBiometric()
{
return (int)Build.VERSION.SdkInt >= 28;
}
public Task<bool> AuthenticateBiometricAsync(string text = null)
{
if(string.IsNullOrWhiteSpace(text))
{
text = AppResources.BiometricDirection;
}
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
using(var builder = new BiometricPrompt.Builder(activity))
{
builder.SetTitle(text);
builder.SetConfirmationRequired(false);
builder.SetNegativeButton(AppResources.Cancel, activity.MainExecutor,
new DialogInterfaceOnClickListener
{
Clicked = () => { }
});
var prompt = builder.Build();
var result = new TaskCompletionSource<bool>();
prompt.Authenticate(new CancellationSignal(), activity.MainExecutor,
new BiometricAuthenticationCallback
{
Success = authResult => result.TrySetResult(true),
Failed = () => result.TrySetResult(false),
Help = (helpCode, helpString) => { }
});
return result.Task;
}
}
public bool SupportsNfc() public bool SupportsNfc()
{ {
var activity = (MainActivity)CrossCurrentActivity.Current.Activity; var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
@ -715,5 +779,41 @@ namespace Bit.Droid.Services
Context.ClipboardService) as Android.Content.ClipboardManager; Context.ClipboardService) as Android.Content.ClipboardManager;
clipboardManager.Text = text; clipboardManager.Text = text;
} }
private class BiometricAuthenticationCallback : BiometricPrompt.AuthenticationCallback
{
public Action<BiometricPrompt.AuthenticationResult> Success { get; set; }
public Action Failed { get; set; }
public Action<BiometricAcquiredStatus, Java.Lang.ICharSequence> Help { get; set; }
public override void OnAuthenticationSucceeded(BiometricPrompt.AuthenticationResult authResult)
{
base.OnAuthenticationSucceeded(authResult);
Success?.Invoke(authResult);
}
public override void OnAuthenticationFailed()
{
base.OnAuthenticationFailed();
Failed?.Invoke();
}
public override void OnAuthenticationHelp([GeneratedEnum] BiometricAcquiredStatus helpCode,
Java.Lang.ICharSequence helpString)
{
base.OnAuthenticationHelp(helpCode, helpString);
Help?.Invoke(helpCode, helpString);
}
}
private class DialogInterfaceOnClickListener : Java.Lang.Object, IDialogInterfaceOnClickListener
{
public Action Clicked { get; set; }
public void OnClick(IDialogInterface dialog, int which)
{
Clicked?.Invoke();
}
}
} }
} }

View file

@ -20,7 +20,11 @@ namespace Bit.App.Abstractions
string okButtonText = null, string cancelButtonText = null, bool numericKeyboard = false, string okButtonText = null, string cancelButtonText = null, bool numericKeyboard = false,
bool autofocus = true); bool autofocus = true);
void RateApp(); void RateApp();
bool SupportsFaceId(); bool SupportsFaceBiometric();
Task<bool> SupportsFaceBiometricAsync();
Task<bool> BiometricAvailableAsync();
bool UseNativeBiometric();
Task<bool> AuthenticateBiometricAsync(string text = null);
bool SupportsNfc(); bool SupportsNfc();
bool SupportsCamera(); bool SupportsCamera();
bool SupportsAutofillService(); bool SupportsAutofillService();

View file

@ -126,7 +126,8 @@ namespace Bit.App.Pages
if(FingerprintLock) if(FingerprintLock)
{ {
FingerprintButtonText = _deviceActionService.SupportsFaceId() ? AppResources.UseFaceIDToUnlock : var supportsFace = await _deviceActionService.SupportsFaceBiometricAsync();
FingerprintButtonText = supportsFace ? AppResources.UseFaceIDToUnlock :
AppResources.UseFingerprintToUnlock; AppResources.UseFingerprintToUnlock;
if(autoPromptFingerprint) if(autoPromptFingerprint)
{ {
@ -266,7 +267,7 @@ namespace Bit.App.Pages
{ {
return; return;
} }
var success = await _platformUtilsService.AuthenticateFingerprintAsync(null, var success = await _platformUtilsService.AuthenticateBiometricAsync(null,
PinLock ? AppResources.PIN : AppResources.MasterPassword, () => PinLock ? AppResources.PIN : AppResources.MasterPassword, () =>
{ {
var page = Page as LockPage; var page = Page as LockPage;

View file

@ -142,8 +142,8 @@ namespace Bit.App.Pages
var fingerprintName = AppResources.Fingerprint; var fingerprintName = AppResources.Fingerprint;
if(Device.RuntimePlatform == Device.iOS) if(Device.RuntimePlatform == Device.iOS)
{ {
fingerprintName = _deviceActionService.SupportsFaceId() ? var supportsFace = await _deviceActionService.SupportsFaceBiometricAsync();
AppResources.FaceID : AppResources.TouchID; fingerprintName = supportsFace ? AppResources.FaceID : AppResources.TouchID;
} }
if(item.Name == string.Format(AppResources.UnlockWith, fingerprintName)) if(item.Name == string.Format(AppResources.UnlockWith, fingerprintName))
{ {

View file

@ -62,7 +62,7 @@ namespace Bit.App.Pages
public async Task InitAsync() public async Task InitAsync()
{ {
_supportsFingerprint = await _platformUtilsService.SupportsFingerprintAsync(); _supportsFingerprint = await _platformUtilsService.SupportsBiometricAsync();
var lastSync = await _syncService.GetLastSyncAsync(); var lastSync = await _syncService.GetLastSyncAsync();
if(lastSync != null) if(lastSync != null)
{ {
@ -255,9 +255,9 @@ namespace Bit.App.Pages
{ {
_fingerprint = false; _fingerprint = false;
} }
else if(await _platformUtilsService.SupportsFingerprintAsync()) else if(await _platformUtilsService.SupportsBiometricAsync())
{ {
_fingerprint = await _platformUtilsService.AuthenticateFingerprintAsync(null, _fingerprint = await _platformUtilsService.AuthenticateBiometricAsync(null,
_deviceActionService.DeviceType == Core.Enums.DeviceType.Android ? "." : null); _deviceActionService.DeviceType == Core.Enums.DeviceType.Android ? "." : null);
} }
if(_fingerprint == current) if(_fingerprint == current)
@ -328,8 +328,8 @@ namespace Bit.App.Pages
var fingerprintName = AppResources.Fingerprint; var fingerprintName = AppResources.Fingerprint;
if(Device.RuntimePlatform == Device.iOS) if(Device.RuntimePlatform == Device.iOS)
{ {
fingerprintName = _deviceActionService.SupportsFaceId() ? fingerprintName = _deviceActionService.SupportsFaceBiometric() ? AppResources.FaceID :
AppResources.FaceID : AppResources.TouchID; AppResources.TouchID;
} }
var item = new SettingsPageListItem var item = new SettingsPageListItem
{ {

View file

@ -537,6 +537,15 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Use biometrics to verify..
/// </summary>
public static string BiometricDirection {
get {
return ResourceManager.GetString("BiometricDirection", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Bitwarden. /// Looks up a localized string similar to Bitwarden.
/// </summary> /// </summary>

View file

@ -1578,4 +1578,7 @@
<data name="LoginExpired" xml:space="preserve"> <data name="LoginExpired" xml:space="preserve">
<value>Your login session has expired.</value> <value>Your login session has expired.</value>
</data> </data>
<data name="BiometricDirection" xml:space="preserve">
<value>Use biometrics to verify.</value>
</data>
</root> </root>

View file

@ -197,45 +197,45 @@ namespace Bit.App.Services
return await Clipboard.GetTextAsync(); return await Clipboard.GetTextAsync();
} }
public async Task<bool> SupportsFingerprintAsync() public async Task<bool> SupportsBiometricAsync()
{ {
try return await _deviceActionService.BiometricAvailableAsync();
}
public async Task<bool> AuthenticateBiometricAsync(string text = null, string fallbackText = null,
Action fallback = null)
{
if(_deviceActionService.UseNativeBiometric())
{ {
return await CrossFingerprint.Current.IsAvailableAsync(); return await _deviceActionService.AuthenticateBiometricAsync(text);
} }
catch else
{ {
try
{
if(text == null)
{
var supportsFace = await _deviceActionService.SupportsFaceBiometricAsync();
text = supportsFace ? AppResources.FaceIDDirection : AppResources.FingerprintDirection;
}
var fingerprintRequest = new AuthenticationRequestConfiguration(text)
{
CancelTitle = AppResources.Cancel,
FallbackTitle = fallbackText
};
var result = await CrossFingerprint.Current.AuthenticateAsync(fingerprintRequest);
if(result.Authenticated)
{
return true;
}
else if(result.Status == FingerprintAuthenticationResultStatus.FallbackRequested)
{
fallback?.Invoke();
}
}
catch { }
return false; return false;
} }
} }
public async Task<bool> AuthenticateFingerprintAsync(string text = null, string fallbackText = null,
Action fallback = null)
{
try
{
if(text == null)
{
text = _deviceActionService.SupportsFaceId() ? AppResources.FaceIDDirection :
AppResources.FingerprintDirection;
}
var fingerprintRequest = new AuthenticationRequestConfiguration(text)
{
CancelTitle = AppResources.Cancel,
FallbackTitle = fallbackText
};
var result = await CrossFingerprint.Current.AuthenticateAsync(fingerprintRequest);
if(result.Authenticated)
{
return true;
}
else if(result.Status == FingerprintAuthenticationResultStatus.FallbackRequested)
{
fallback?.Invoke();
}
}
catch { }
return false;
}
} }
} }

View file

@ -26,7 +26,7 @@ namespace Bit.Core.Abstractions
void ShowToast(string type, string title, string[] text, Dictionary<string, object> options = null); void ShowToast(string type, string title, string[] text, Dictionary<string, object> options = null);
bool SupportsU2f(); bool SupportsU2f();
bool SupportsDuo(); bool SupportsDuo();
Task<bool> SupportsFingerprintAsync(); Task<bool> SupportsBiometricAsync();
Task<bool> AuthenticateFingerprintAsync(string text = null, string fallbackText = null, Action fallback = null); Task<bool> AuthenticateBiometricAsync(string text = null, string fallbackText = null, Action fallback = null);
} }
} }

View file

@ -216,7 +216,7 @@ namespace Bit.iOS.Core.Controllers
{ {
return; return;
} }
var success = await _platformUtilsService.AuthenticateFingerprintAsync(null, var success = await _platformUtilsService.AuthenticateBiometricAsync(null,
_pinLock ? AppResources.PIN : AppResources.MasterPassword, _pinLock ? AppResources.PIN : AppResources.MasterPassword,
() => MasterPasswordCell.TextField.BecomeFirstResponder()); () => MasterPasswordCell.TextField.BecomeFirstResponder());
_lockService.FingerprintLocked = !success; _lockService.FingerprintLocked = !success;
@ -261,7 +261,7 @@ namespace Bit.iOS.Core.Controllers
{ {
if(indexPath.Row == 0) if(indexPath.Row == 0)
{ {
var fingerprintButtonText = _controller._deviceActionService.SupportsFaceId() ? var fingerprintButtonText = _controller._deviceActionService.SupportsFaceBiometric() ?
AppResources.UseFaceIDToUnlock : AppResources.UseFingerprintToUnlock; AppResources.UseFaceIDToUnlock : AppResources.UseFingerprintToUnlock;
var cell = new ExtendedUITableViewCell(); var cell = new ExtendedUITableViewCell();
cell.TextLabel.TextColor = ThemeHelpers.PrimaryColor; cell.TextLabel.TextColor = ThemeHelpers.PrimaryColor;

View file

@ -15,6 +15,7 @@ using Foundation;
using LocalAuthentication; using LocalAuthentication;
using MobileCoreServices; using MobileCoreServices;
using Photos; using Photos;
using Plugin.Fingerprint;
using UIKit; using UIKit;
using Xamarin.Forms; using Xamarin.Forms;
@ -243,18 +244,47 @@ namespace Bit.iOS.Core.Services
Device.OpenUri(new Uri(uri)); Device.OpenUri(new Uri(uri));
} }
public bool SupportsFaceId() public bool SupportsFaceBiometric()
{ {
if(SystemMajorVersion() < 11) if(SystemMajorVersion() < 11)
{ {
return false; return false;
} }
var context = new LAContext(); using(var context = new LAContext())
if(!context.CanEvaluatePolicy(LAPolicy.DeviceOwnerAuthenticationWithBiometrics, out NSError e)) {
if(!context.CanEvaluatePolicy(LAPolicy.DeviceOwnerAuthenticationWithBiometrics, out var e))
{
return false;
}
return context.BiometryType == LABiometryType.FaceId;
}
}
public Task<bool> SupportsFaceBiometricAsync()
{
return Task.FromResult(SupportsFaceBiometric());
}
public async Task<bool> BiometricAvailableAsync()
{
try
{
return await CrossFingerprint.Current.IsAvailableAsync();
}
catch
{ {
return false; return false;
} }
return context.BiometryType == LABiometryType.FaceId; }
public bool UseNativeBiometric()
{
return false;
}
public Task<bool> AuthenticateBiometricAsync(string text = null)
{
throw new NotSupportedException();
} }
public bool SupportsNfc() public bool SupportsNfc()