mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 23:25:45 +03:00
use native biomatrics on Android
This commit is contained in:
parent
aed3ec5474
commit
4b989b01e9
11 changed files with 199 additions and 52 deletions
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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))
|
||||||
{
|
{
|
||||||
|
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
9
src/App/Resources/AppResources.Designer.cs
generated
9
src/App/Resources/AppResources.Designer.cs
generated
|
@ -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>
|
||||||
|
|
|
@ -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>
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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;
|
||||||
|
|
|
@ -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()
|
||||||
|
|
Loading…
Reference in a new issue