diff --git a/src/Android/Services/DeviceActionService.cs b/src/Android/Services/DeviceActionService.cs index 93061a336..5e6932b4d 100644 --- a/src/Android/Services/DeviceActionService.cs +++ b/src/Android/Services/DeviceActionService.cs @@ -8,9 +8,11 @@ using Android.App; using Android.App.Assist; using Android.Content; using Android.Content.PM; +using Android.Hardware.Biometrics; using Android.Nfc; using Android.OS; using Android.Provider; +using Android.Runtime; using Android.Support.V4.App; using Android.Support.V4.Content; using Android.Text; @@ -28,6 +30,7 @@ using Bit.Core.Models.View; using Bit.Core.Utilities; using Bit.Droid.Autofill; using Plugin.CurrentActivity; +using Plugin.Fingerprint; namespace Bit.Droid.Services { @@ -335,11 +338,72 @@ namespace Bit.Droid.Services Application.Context.PackageName, 0).VersionCode.ToString(); } - public bool SupportsFaceId() + public bool SupportsFaceBiometric() { return false; } + public Task SupportsFaceBiometricAsync() + { + return Task.FromResult(SupportsFaceBiometric()); + } + + public async Task 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 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(); + 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() { var activity = (MainActivity)CrossCurrentActivity.Current.Activity; @@ -715,5 +779,41 @@ namespace Bit.Droid.Services Context.ClipboardService) as Android.Content.ClipboardManager; clipboardManager.Text = text; } + + private class BiometricAuthenticationCallback : BiometricPrompt.AuthenticationCallback + { + public Action Success { get; set; } + public Action Failed { get; set; } + public Action 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(); + } + } } } diff --git a/src/App/Abstractions/IDeviceActionService.cs b/src/App/Abstractions/IDeviceActionService.cs index 85c92e71a..4833e7ed0 100644 --- a/src/App/Abstractions/IDeviceActionService.cs +++ b/src/App/Abstractions/IDeviceActionService.cs @@ -20,7 +20,11 @@ namespace Bit.App.Abstractions string okButtonText = null, string cancelButtonText = null, bool numericKeyboard = false, bool autofocus = true); void RateApp(); - bool SupportsFaceId(); + bool SupportsFaceBiometric(); + Task SupportsFaceBiometricAsync(); + Task BiometricAvailableAsync(); + bool UseNativeBiometric(); + Task AuthenticateBiometricAsync(string text = null); bool SupportsNfc(); bool SupportsCamera(); bool SupportsAutofillService(); diff --git a/src/App/Pages/Accounts/LockPageViewModel.cs b/src/App/Pages/Accounts/LockPageViewModel.cs index 73b7008a9..4b3d3a78d 100644 --- a/src/App/Pages/Accounts/LockPageViewModel.cs +++ b/src/App/Pages/Accounts/LockPageViewModel.cs @@ -126,7 +126,8 @@ namespace Bit.App.Pages if(FingerprintLock) { - FingerprintButtonText = _deviceActionService.SupportsFaceId() ? AppResources.UseFaceIDToUnlock : + var supportsFace = await _deviceActionService.SupportsFaceBiometricAsync(); + FingerprintButtonText = supportsFace ? AppResources.UseFaceIDToUnlock : AppResources.UseFingerprintToUnlock; if(autoPromptFingerprint) { @@ -266,7 +267,7 @@ namespace Bit.App.Pages { return; } - var success = await _platformUtilsService.AuthenticateFingerprintAsync(null, + var success = await _platformUtilsService.AuthenticateBiometricAsync(null, PinLock ? AppResources.PIN : AppResources.MasterPassword, () => { var page = Page as LockPage; diff --git a/src/App/Pages/Settings/SettingsPage/SettingsPage.xaml.cs b/src/App/Pages/Settings/SettingsPage/SettingsPage.xaml.cs index ee1b35778..4d6beb41d 100644 --- a/src/App/Pages/Settings/SettingsPage/SettingsPage.xaml.cs +++ b/src/App/Pages/Settings/SettingsPage/SettingsPage.xaml.cs @@ -142,8 +142,8 @@ namespace Bit.App.Pages var fingerprintName = AppResources.Fingerprint; if(Device.RuntimePlatform == Device.iOS) { - fingerprintName = _deviceActionService.SupportsFaceId() ? - AppResources.FaceID : AppResources.TouchID; + var supportsFace = await _deviceActionService.SupportsFaceBiometricAsync(); + fingerprintName = supportsFace ? AppResources.FaceID : AppResources.TouchID; } if(item.Name == string.Format(AppResources.UnlockWith, fingerprintName)) { diff --git a/src/App/Pages/Settings/SettingsPage/SettingsPageViewModel.cs b/src/App/Pages/Settings/SettingsPage/SettingsPageViewModel.cs index 43d0e3eb6..9202c54cb 100644 --- a/src/App/Pages/Settings/SettingsPage/SettingsPageViewModel.cs +++ b/src/App/Pages/Settings/SettingsPage/SettingsPageViewModel.cs @@ -62,7 +62,7 @@ namespace Bit.App.Pages public async Task InitAsync() { - _supportsFingerprint = await _platformUtilsService.SupportsFingerprintAsync(); + _supportsFingerprint = await _platformUtilsService.SupportsBiometricAsync(); var lastSync = await _syncService.GetLastSyncAsync(); if(lastSync != null) { @@ -255,9 +255,9 @@ namespace Bit.App.Pages { _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); } if(_fingerprint == current) @@ -328,8 +328,8 @@ namespace Bit.App.Pages var fingerprintName = AppResources.Fingerprint; if(Device.RuntimePlatform == Device.iOS) { - fingerprintName = _deviceActionService.SupportsFaceId() ? - AppResources.FaceID : AppResources.TouchID; + fingerprintName = _deviceActionService.SupportsFaceBiometric() ? AppResources.FaceID : + AppResources.TouchID; } var item = new SettingsPageListItem { diff --git a/src/App/Resources/AppResources.Designer.cs b/src/App/Resources/AppResources.Designer.cs index ae4f790db..6fb3e6d44 100644 --- a/src/App/Resources/AppResources.Designer.cs +++ b/src/App/Resources/AppResources.Designer.cs @@ -537,6 +537,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Use biometrics to verify.. + /// + public static string BiometricDirection { + get { + return ResourceManager.GetString("BiometricDirection", resourceCulture); + } + } + /// /// Looks up a localized string similar to Bitwarden. /// diff --git a/src/App/Resources/AppResources.resx b/src/App/Resources/AppResources.resx index b7810545d..9b683e584 100644 --- a/src/App/Resources/AppResources.resx +++ b/src/App/Resources/AppResources.resx @@ -1578,4 +1578,7 @@ Your login session has expired. + + Use biometrics to verify. + \ No newline at end of file diff --git a/src/App/Services/MobilePlatformUtilsService.cs b/src/App/Services/MobilePlatformUtilsService.cs index 1772a0fca..13e463e6c 100644 --- a/src/App/Services/MobilePlatformUtilsService.cs +++ b/src/App/Services/MobilePlatformUtilsService.cs @@ -197,45 +197,45 @@ namespace Bit.App.Services return await Clipboard.GetTextAsync(); } - public async Task SupportsFingerprintAsync() + public async Task SupportsBiometricAsync() { - try + return await _deviceActionService.BiometricAvailableAsync(); + } + + public async Task 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; } } - - public async Task 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; - } } } diff --git a/src/Core/Abstractions/IPlatformUtilsService.cs b/src/Core/Abstractions/IPlatformUtilsService.cs index b0ac5354a..5ee188135 100644 --- a/src/Core/Abstractions/IPlatformUtilsService.cs +++ b/src/Core/Abstractions/IPlatformUtilsService.cs @@ -26,7 +26,7 @@ namespace Bit.Core.Abstractions void ShowToast(string type, string title, string[] text, Dictionary options = null); bool SupportsU2f(); bool SupportsDuo(); - Task SupportsFingerprintAsync(); - Task AuthenticateFingerprintAsync(string text = null, string fallbackText = null, Action fallback = null); + Task SupportsBiometricAsync(); + Task AuthenticateBiometricAsync(string text = null, string fallbackText = null, Action fallback = null); } } \ No newline at end of file diff --git a/src/iOS.Core/Controllers/LockPasswordViewController.cs b/src/iOS.Core/Controllers/LockPasswordViewController.cs index ad0c17ce7..5015f4f92 100644 --- a/src/iOS.Core/Controllers/LockPasswordViewController.cs +++ b/src/iOS.Core/Controllers/LockPasswordViewController.cs @@ -216,7 +216,7 @@ namespace Bit.iOS.Core.Controllers { return; } - var success = await _platformUtilsService.AuthenticateFingerprintAsync(null, + var success = await _platformUtilsService.AuthenticateBiometricAsync(null, _pinLock ? AppResources.PIN : AppResources.MasterPassword, () => MasterPasswordCell.TextField.BecomeFirstResponder()); _lockService.FingerprintLocked = !success; @@ -261,7 +261,7 @@ namespace Bit.iOS.Core.Controllers { if(indexPath.Row == 0) { - var fingerprintButtonText = _controller._deviceActionService.SupportsFaceId() ? + var fingerprintButtonText = _controller._deviceActionService.SupportsFaceBiometric() ? AppResources.UseFaceIDToUnlock : AppResources.UseFingerprintToUnlock; var cell = new ExtendedUITableViewCell(); cell.TextLabel.TextColor = ThemeHelpers.PrimaryColor; diff --git a/src/iOS.Core/Services/DeviceActionService.cs b/src/iOS.Core/Services/DeviceActionService.cs index cb272cdea..16c571498 100644 --- a/src/iOS.Core/Services/DeviceActionService.cs +++ b/src/iOS.Core/Services/DeviceActionService.cs @@ -15,6 +15,7 @@ using Foundation; using LocalAuthentication; using MobileCoreServices; using Photos; +using Plugin.Fingerprint; using UIKit; using Xamarin.Forms; @@ -243,18 +244,47 @@ namespace Bit.iOS.Core.Services Device.OpenUri(new Uri(uri)); } - public bool SupportsFaceId() + public bool SupportsFaceBiometric() { if(SystemMajorVersion() < 11) { return false; } - var context = new LAContext(); - if(!context.CanEvaluatePolicy(LAPolicy.DeviceOwnerAuthenticationWithBiometrics, out NSError e)) + using(var context = new LAContext()) + { + if(!context.CanEvaluatePolicy(LAPolicy.DeviceOwnerAuthenticationWithBiometrics, out var e)) + { + return false; + } + return context.BiometryType == LABiometryType.FaceId; + } + } + + public Task SupportsFaceBiometricAsync() + { + return Task.FromResult(SupportsFaceBiometric()); + } + + public async Task BiometricAvailableAsync() + { + try + { + return await CrossFingerprint.Current.IsAvailableAsync(); + } + catch { return false; } - return context.BiometryType == LABiometryType.FaceId; + } + + public bool UseNativeBiometric() + { + return false; + } + + public Task AuthenticateBiometricAsync(string text = null) + { + throw new NotSupportedException(); } public bool SupportsNfc()