From 307a5a584391c248f338befb7f5e7b371fa843b0 Mon Sep 17 00:00:00 2001
From: Matt Portune <59324545+mportune-bw@users.noreply.github.com>
Date: Mon, 30 Aug 2021 12:44:12 -0400
Subject: [PATCH] FIDO2 WebAuthn support for mobile (#1519)
* FIDO2 / WebAuthn support for mobile
* fixes
---
src/Android/Services/DeviceActionService.cs | 5 +
src/App/Abstractions/IDeviceActionService.cs | 1 +
src/App/Pages/Accounts/LoginPage.xaml | 2 +-
src/App/Pages/Accounts/LoginPage.xaml.cs | 11 +--
src/App/Pages/Accounts/LoginPageViewModel.cs | 29 ++----
.../Pages/Accounts/LoginSsoPageViewModel.cs | 45 +++------
src/App/Pages/Accounts/RegisterPage.xaml | 2 +-
src/App/Pages/Accounts/RegisterPage.xaml.cs | 8 +-
.../Pages/Accounts/RegisterPageViewModel.cs | 35 ++-----
src/App/Pages/Accounts/TwoFactorPage.xaml | 24 +++++
src/App/Pages/Accounts/TwoFactorPage.xaml.cs | 8 +-
.../Pages/Accounts/TwoFactorPageViewModel.cs | 91 ++++++++++++++++---
src/App/Pages/CaptchaProtectedViewModel.cs | 42 ++-------
src/App/Pages/Send/SendAddEditPage.xaml | 4 -
src/App/Resources/AppResources.Designer.cs | 32 ++++++-
src/App/Resources/AppResources.resx | 15 +++
.../Services/MobilePlatformUtilsService.cs | 4 +-
src/App/Utilities/AppHelpers.cs | 15 +++
src/Core/Abstractions/IAuthService.cs | 2 +-
.../Abstractions/IPlatformUtilsService.cs | 2 +-
src/Core/Enums/TwoFactorProviderType.cs | 3 +-
src/Core/Services/AuthService.cs | 17 ++--
src/iOS.Core/Services/DeviceActionService.cs | 24 ++++-
src/iOS/AppDelegate.cs | 10 ++
24 files changed, 276 insertions(+), 155 deletions(-)
diff --git a/src/Android/Services/DeviceActionService.cs b/src/Android/Services/DeviceActionService.cs
index 68523ca77..45fd32f27 100644
--- a/src/Android/Services/DeviceActionService.cs
+++ b/src/Android/Services/DeviceActionService.cs
@@ -776,6 +776,11 @@ namespace Bit.Droid.Services
_messagingService.Send("finishMainActivity");
}
+ public bool SupportsFido2()
+ {
+ return true;
+ }
+
private bool DeleteDir(Java.IO.File dir)
{
if (dir != null && dir.IsDirectory)
diff --git a/src/App/Abstractions/IDeviceActionService.cs b/src/App/Abstractions/IDeviceActionService.cs
index 2f32a7da9..3c78759ba 100644
--- a/src/App/Abstractions/IDeviceActionService.cs
+++ b/src/App/Abstractions/IDeviceActionService.cs
@@ -45,5 +45,6 @@ namespace Bit.App.Abstractions
bool UsingDarkTheme();
long GetActiveTime();
void CloseMainApp();
+ bool SupportsFido2();
}
}
diff --git a/src/App/Pages/Accounts/LoginPage.xaml b/src/App/Pages/Accounts/LoginPage.xaml
index e43026992..1363785ce 100644
--- a/src/App/Pages/Accounts/LoginPage.xaml
+++ b/src/App/Pages/Accounts/LoginPage.xaml
@@ -82,7 +82,7 @@
-
+
diff --git a/src/App/Pages/Accounts/LoginPage.xaml.cs b/src/App/Pages/Accounts/LoginPage.xaml.cs
index 8d887c7da..66c0ce596 100644
--- a/src/App/Pages/Accounts/LoginPage.xaml.cs
+++ b/src/App/Pages/Accounts/LoginPage.xaml.cs
@@ -16,6 +16,8 @@ namespace Bit.App.Pages
private readonly LoginPageViewModel _vm;
private readonly AppOptions _appOptions;
+ private bool _inputFocused;
+
public LoginPage(string email = null, AppOptions appOptions = null)
{
_storageService = ServiceContainer.Resolve("storageService");
@@ -58,13 +60,10 @@ namespace Bit.App.Pages
{
base.OnAppearing();
await _vm.InitAsync();
- if (string.IsNullOrWhiteSpace(_vm.Email))
+ if (!_inputFocused)
{
- RequestFocus(_email);
- }
- else
- {
- RequestFocus(_masterPassword);
+ RequestFocus(string.IsNullOrWhiteSpace(_vm.Email) ? _email : _masterPassword);
+ _inputFocused = true;
}
}
diff --git a/src/App/Pages/Accounts/LoginPageViewModel.cs b/src/App/Pages/Accounts/LoginPageViewModel.cs
index 0f989217c..58dd75a36 100644
--- a/src/App/Pages/Accounts/LoginPageViewModel.cs
+++ b/src/App/Pages/Accounts/LoginPageViewModel.cs
@@ -33,7 +33,6 @@ namespace Bit.App.Pages
private bool _showPassword;
private string _email;
private string _masterPassword;
- private bool _loginEnabled = true;
public LoginPageViewModel()
{
@@ -73,16 +72,6 @@ namespace Bit.App.Pages
set => SetProperty(ref _masterPassword, value);
}
- public bool LoginEnabled {
- get => _loginEnabled;
- set => SetProperty(ref _loginEnabled, value);
- }
- public bool Loading
- {
- get => !LoginEnabled;
- set => LoginEnabled = !value;
- }
-
public Command LogInCommand { get; }
public Command TogglePasswordCommand { get; }
public string ShowPasswordIcon => ShowPassword ? "" : "";
@@ -106,7 +95,7 @@ namespace Bit.App.Pages
RememberEmail = rememberEmail.GetValueOrDefault(true);
}
- public async Task LogInAsync()
+ public async Task LogInAsync(bool showLoading = true)
{
if (Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.None)
{
@@ -140,10 +129,9 @@ namespace Bit.App.Pages
ShowPassword = false;
try
{
- if (!Loading)
+ if (showLoading)
{
await _deviceActionService.ShowLoadingAsync(AppResources.LoggingIn);
- Loading = true;
}
var response = await _authService.LogInAsync(Email, MasterPassword, _captchaToken);
@@ -156,25 +144,21 @@ namespace Bit.App.Pages
await _storageService.RemoveAsync(Keys_RememberedEmail);
}
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
- await _deviceActionService.HideLoadingAsync();
if (response.CaptchaNeeded)
{
if (await HandleCaptchaAsync(response.CaptchaSiteKey))
{
- await LogInAsync();
+ await LogInAsync(false);
_captchaToken = null;
- return;
- }
- else
- {
- Loading = false;
- return;
}
+ return;
}
MasterPassword = string.Empty;
_captchaToken = null;
+ await _deviceActionService.HideLoadingAsync();
+
if (response.TwoFactor)
{
StartTwoFactorAction?.Invoke();
@@ -198,7 +182,6 @@ namespace Bit.App.Pages
AppResources.AnErrorHasOccurred);
}
}
- Loading = false;
}
public void TogglePassword()
diff --git a/src/App/Pages/Accounts/LoginSsoPageViewModel.cs b/src/App/Pages/Accounts/LoginSsoPageViewModel.cs
index 24c3ea763..d3416571e 100644
--- a/src/App/Pages/Accounts/LoginSsoPageViewModel.cs
+++ b/src/App/Pages/Accounts/LoginSsoPageViewModel.cs
@@ -123,43 +123,28 @@ namespace Bit.App.Pages
"domain_hint=" + Uri.EscapeDataString(OrgIdentifier);
WebAuthenticatorResult authResult = null;
- bool cancelled = false;
try
{
authResult = await WebAuthenticator.AuthenticateAsync(new Uri(url),
new Uri(redirectUri));
}
- catch (TaskCanceledException taskCanceledException)
+ catch (TaskCanceledException)
+ {
+ // user canceled
+ await _deviceActionService.HideLoadingAsync();
+ return;
+ }
+
+ var code = GetResultCode(authResult, state);
+ if (!string.IsNullOrEmpty(code))
+ {
+ await LogIn(code, codeVerifier, redirectUri);
+ }
+ else
{
await _deviceActionService.HideLoadingAsync();
- cancelled = true;
- }
- catch (Exception e)
- {
- // WebAuthenticator throws NSErrorException if iOS flow is cancelled - by setting cancelled to true
- // here we maintain the appearance of a clean cancellation (we don't want to do this across the board
- // because we still want to present legitimate errors). If/when this is fixed, we can remove this
- // particular catch block (catching taskCanceledException above must remain)
- // https://github.com/xamarin/Essentials/issues/1240
- if (Device.RuntimePlatform == Device.iOS)
- {
- await _deviceActionService.HideLoadingAsync();
- cancelled = true;
- }
- }
- if (!cancelled)
- {
- var code = GetResultCode(authResult, state);
- if (!string.IsNullOrEmpty(code))
- {
- await LogIn(code, codeVerifier, redirectUri);
- }
- else
- {
- await _deviceActionService.HideLoadingAsync();
- await _platformUtilsService.ShowDialogAsync(AppResources.LoginSsoError,
- AppResources.AnErrorHasOccurred);
- }
+ await _platformUtilsService.ShowDialogAsync(AppResources.LoginSsoError,
+ AppResources.AnErrorHasOccurred);
}
}
diff --git a/src/App/Pages/Accounts/RegisterPage.xaml b/src/App/Pages/Accounts/RegisterPage.xaml
index 342f0dd29..19d2a30fc 100644
--- a/src/App/Pages/Accounts/RegisterPage.xaml
+++ b/src/App/Pages/Accounts/RegisterPage.xaml
@@ -21,7 +21,7 @@
-
+
diff --git a/src/App/Pages/Accounts/RegisterPage.xaml.cs b/src/App/Pages/Accounts/RegisterPage.xaml.cs
index 1589da34f..b3d1f47ab 100644
--- a/src/App/Pages/Accounts/RegisterPage.xaml.cs
+++ b/src/App/Pages/Accounts/RegisterPage.xaml.cs
@@ -11,6 +11,8 @@ namespace Bit.App.Pages
private readonly IMessagingService _messagingService;
private readonly RegisterPageViewModel _vm;
+ private bool _inputFocused;
+
public RegisterPage(HomePage homePage)
{
_messagingService = ServiceContainer.Resolve("messagingService");
@@ -45,7 +47,11 @@ namespace Bit.App.Pages
protected override void OnAppearing()
{
base.OnAppearing();
- RequestFocus(_email);
+ if (!_inputFocused)
+ {
+ RequestFocus(_email);
+ _inputFocused = true;
+ }
}
private async void Submit_Clicked(object sender, EventArgs e)
diff --git a/src/App/Pages/Accounts/RegisterPageViewModel.cs b/src/App/Pages/Accounts/RegisterPageViewModel.cs
index 37a67c5fb..e06d9306d 100644
--- a/src/App/Pages/Accounts/RegisterPageViewModel.cs
+++ b/src/App/Pages/Accounts/RegisterPageViewModel.cs
@@ -22,7 +22,6 @@ namespace Bit.App.Pages
private readonly IPlatformUtilsService _platformUtilsService;
private bool _showPassword;
private bool _acceptPolicies;
- private bool _submitEnabled = true;
public RegisterPageViewModel()
{
@@ -60,16 +59,6 @@ namespace Bit.App.Pages
get => _acceptPolicies;
set => SetProperty(ref _acceptPolicies, value);
}
- public bool SubmitEnabled
- {
- get => _submitEnabled;
- set => SetProperty(ref _submitEnabled, value);
- }
- public bool Loading
- {
- get => !SubmitEnabled;
- set => SubmitEnabled = !value;
- }
public Thickness SwitchMargin
{
@@ -96,7 +85,7 @@ namespace Bit.App.Pages
protected override IDeviceActionService deviceActionService => _deviceActionService;
protected override IPlatformUtilsService platformUtilsService => _platformUtilsService;
- public async Task SubmitAsync()
+ public async Task SubmitAsync(bool showLoading = true)
{
if (Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.None)
{
@@ -143,6 +132,11 @@ namespace Bit.App.Pages
}
// TODO: Password strength check?
+
+ if (showLoading)
+ {
+ await _deviceActionService.ShowLoadingAsync(AppResources.CreatingAccount);
+ }
Name = string.IsNullOrWhiteSpace(Name) ? null : Name;
Email = Email.Trim().ToLower();
@@ -172,14 +166,8 @@ namespace Bit.App.Pages
try
{
- if (!Loading)
- {
- await _deviceActionService.ShowLoadingAsync(AppResources.CreatingAccount);
- Loading = true;
- }
await _apiService.PostRegisterAsync(request);
await _deviceActionService.HideLoadingAsync();
- Loading = false;
_platformUtilsService.ShowToast("success", null, AppResources.AccountCreated,
new System.Collections.Generic.Dictionary
{
@@ -193,19 +181,12 @@ namespace Bit.App.Pages
{
if (await HandleCaptchaAsync(e.Error.CaptchaSiteKey))
{
- await SubmitAsync();
+ await SubmitAsync(false);
_captchaToken = null;
- return;
}
- else
- {
- await _deviceActionService.HideLoadingAsync();
- Loading = false;
- return;
- };
+ return;
}
await _deviceActionService.HideLoadingAsync();
- Loading = false;
if (e?.Error != null)
{
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
diff --git a/src/App/Pages/Accounts/TwoFactorPage.xaml b/src/App/Pages/Accounts/TwoFactorPage.xaml
index c3235ec72..8a2eea9c6 100644
--- a/src/App/Pages/Accounts/TwoFactorPage.xaml
+++ b/src/App/Pages/Accounts/TwoFactorPage.xaml
@@ -102,6 +102,30 @@
+
+
+
+
+
+
+
+
+
+
SelectedProviderType == TwoFactorProviderType.Duo ||
SelectedProviderType == TwoFactorProviderType.OrganizationDuo;
+ public bool Fido2Method => SelectedProviderType == TwoFactorProviderType.Fido2WebAuthn;
+
public bool YubikeyMethod => SelectedProviderType == TwoFactorProviderType.YubiKey;
public bool AuthenticatorMethod => SelectedProviderType == TwoFactorProviderType.Authenticator;
@@ -73,7 +78,7 @@ namespace Bit.App.Pages
public bool TotpMethod => AuthenticatorMethod || EmailMethod;
- public bool ShowTryAgain => YubikeyMethod && Device.RuntimePlatform == Device.iOS;
+ public bool ShowTryAgain => (YubikeyMethod && Device.RuntimePlatform == Device.iOS) || Fido2Method;
public bool ShowContinue
{
@@ -97,6 +102,7 @@ namespace Bit.App.Pages
{
nameof(EmailMethod),
nameof(DuoMethod),
+ nameof(Fido2Method),
nameof(YubikeyMethod),
nameof(AuthenticatorMethod),
nameof(TotpMethod),
@@ -128,10 +134,7 @@ namespace Bit.App.Pages
_webVaultUrl = _environmentService.WebVaultUrl;
}
- // TODO: init U2F
- _u2fSupported = false;
-
- SelectedProviderType = _authService.GetDefaultTwoFactorProvider(_u2fSupported);
+ SelectedProviderType = _authService.GetDefaultTwoFactorProvider(_platformUtilsService.SupportsFido2());
Load();
}
@@ -147,8 +150,8 @@ namespace Bit.App.Pages
var providerData = _authService.TwoFactorProvidersData[SelectedProviderType.Value];
switch (SelectedProviderType.Value)
{
- case TwoFactorProviderType.U2f:
- // TODO
+ case TwoFactorProviderType.Fido2WebAuthn:
+ Fido2AuthenticateAsync(providerData);
break;
case TwoFactorProviderType.YubiKey:
_messagingService.Send("listenYubiKeyOTP", true);
@@ -183,10 +186,73 @@ namespace Bit.App.Pages
{
_messagingService.Send("listenYubiKeyOTP", false);
}
- ShowContinue = !(SelectedProviderType == null || DuoMethod);
+ ShowContinue = !(SelectedProviderType == null || DuoMethod || Fido2Method);
}
- public async Task SubmitAsync()
+ public async Task Fido2AuthenticateAsync(Dictionary providerData = null)
+ {
+ await _deviceActionService.ShowLoadingAsync(AppResources.Validating);
+
+ if (providerData == null)
+ {
+ providerData = _authService.TwoFactorProvidersData[TwoFactorProviderType.Fido2WebAuthn];
+ }
+
+ var callbackUri = "bitwarden://webauthn-callback";
+ var data = AppHelpers.EncodeDataParameter(new
+ {
+ callbackUri = callbackUri,
+ data = JsonConvert.SerializeObject(providerData),
+ btnText = AppResources.Fido2AuthenticateWebAuthn,
+ });
+
+ var url = _webVaultUrl + "/webauthn-mobile-connector.html?" + "data=" + data +
+ "&parent=" + Uri.EscapeDataString(callbackUri) + "&v=2";
+
+ WebAuthenticatorResult authResult = null;
+ try
+ {
+ var options = new WebAuthenticatorOptions
+ {
+ Url = new Uri(url),
+ CallbackUrl = new Uri(callbackUri),
+ PrefersEphemeralWebBrowserSession = true,
+ };
+ authResult = await WebAuthenticator.AuthenticateAsync(options);
+ }
+ catch (TaskCanceledException)
+ {
+ // user canceled
+ await _deviceActionService.HideLoadingAsync();
+ return;
+ }
+
+ string response = null;
+ if (authResult != null && authResult.Properties.TryGetValue("data", out var resultData))
+ {
+ response = Uri.UnescapeDataString(resultData);
+ }
+ if (!string.IsNullOrWhiteSpace(response))
+ {
+ Token = response;
+ await SubmitAsync(false);
+ }
+ else
+ {
+ await _deviceActionService.HideLoadingAsync();
+ if (authResult != null && authResult.Properties.TryGetValue("error", out var resultError))
+ {
+ await _platformUtilsService.ShowDialogAsync(resultError, AppResources.AnErrorHasOccurred);
+ }
+ else
+ {
+ await _platformUtilsService.ShowDialogAsync(AppResources.Fido2SomethingWentWrong,
+ AppResources.AnErrorHasOccurred);
+ }
+ }
+ }
+
+ public async Task SubmitAsync(bool showLoading = true)
{
if (SelectedProviderType == null)
{
@@ -213,7 +279,10 @@ namespace Bit.App.Pages
try
{
- await _deviceActionService.ShowLoadingAsync(AppResources.Validating);
+ if (showLoading)
+ {
+ await _deviceActionService.ShowLoadingAsync(AppResources.Validating);
+ }
var result = await _authService.LogInTwoFactorAsync(SelectedProviderType.Value, Token, Remember);
var task = Task.Run(() => _syncService.FullSyncAsync(true));
await _deviceActionService.HideLoadingAsync();
diff --git a/src/App/Pages/CaptchaProtectedViewModel.cs b/src/App/Pages/CaptchaProtectedViewModel.cs
index 26a48b52a..22a4349a1 100644
--- a/src/App/Pages/CaptchaProtectedViewModel.cs
+++ b/src/App/Pages/CaptchaProtectedViewModel.cs
@@ -1,13 +1,10 @@
using System;
-using System.Text;
-using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Bit.App.Abstractions;
using Bit.App.Resources;
+using Bit.App.Utilities;
using Bit.Core.Abstractions;
-using Newtonsoft.Json;
using Xamarin.Essentials;
-using Xamarin.Forms;
namespace Bit.App.Pages
{
@@ -22,7 +19,7 @@ namespace Bit.App.Pages
protected async Task HandleCaptchaAsync(string CaptchaSiteKey)
{
var callbackUri = "bitwarden://captcha-callback";
- var data = EncodeDataParameter(new
+ var data = AppHelpers.EncodeDataParameter(new
{
siteKey = CaptchaSiteKey,
locale = i18nService.Culture.TwoLetterISOLanguageName,
@@ -37,27 +34,19 @@ namespace Bit.App.Pages
bool cancelled = false;
try
{
- authResult = await WebAuthenticator.AuthenticateAsync(new Uri(url),
- new Uri(callbackUri));
+ var options = new WebAuthenticatorOptions
+ {
+ Url = new Uri(url),
+ CallbackUrl = new Uri(callbackUri),
+ PrefersEphemeralWebBrowserSession = true,
+ };
+ authResult = await WebAuthenticator.AuthenticateAsync(options);
}
catch (TaskCanceledException)
{
await deviceActionService.HideLoadingAsync();
cancelled = true;
}
- catch (Exception e)
- {
- // WebAuthenticator throws NSErrorException if iOS flow is cancelled - by setting cancelled to true
- // here we maintain the appearance of a clean cancellation (we don't want to do this across the board
- // because we still want to present legitimate errors). If/when this is fixed, we can remove this
- // particular catch block (catching taskCanceledException above must remain)
- // https://github.com/xamarin/Essentials/issues/1240
- if (Device.RuntimePlatform == Device.iOS)
- {
- await deviceActionService.HideLoadingAsync();
- cancelled = true;
- }
- }
if (cancelled == false && authResult != null &&
authResult.Properties.TryGetValue("token", out _captchaToken))
@@ -71,18 +60,5 @@ namespace Bit.App.Pages
return false;
}
}
-
- private string EncodeDataParameter(object obj)
- {
- string EncodeMultibyte(Match match)
- {
- return Convert.ToChar(Convert.ToUInt32($"0x{match.Groups[1].Value}", 16)).ToString();
- }
-
- var escaped = Uri.EscapeDataString(JsonConvert.SerializeObject(obj));
- var multiByteEscaped = Regex.Replace(escaped, "%([0-9A-F]{2})", EncodeMultibyte);
- return Convert.ToBase64String(Encoding.UTF8.GetBytes(multiByteEscaped));
- }
-
}
}
diff --git a/src/App/Pages/Send/SendAddEditPage.xaml b/src/App/Pages/Send/SendAddEditPage.xaml
index 5ab0fc4a2..55bbb3a1c 100644
--- a/src/App/Pages/Send/SendAddEditPage.xaml
+++ b/src/App/Pages/Send/SendAddEditPage.xaml
@@ -137,8 +137,6 @@
AutomationProperties.Name="{u:I18n File}"
Grid.Column="0">
-
-
@@ -163,8 +161,6 @@
AutomationProperties.Name="{u:I18n Text}"
Grid.Column="1">
-
-
diff --git a/src/App/Resources/AppResources.Designer.cs b/src/App/Resources/AppResources.Designer.cs
index 9b761f3d4..b7dee1148 100644
--- a/src/App/Resources/AppResources.Designer.cs
+++ b/src/App/Resources/AppResources.Designer.cs
@@ -1,7 +1,6 @@
//------------------------------------------------------------------------------
//
// This code was generated by a tool.
-// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
@@ -10,7 +9,6 @@
namespace Bit.App.Resources {
using System;
- using System.Reflection;
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
@@ -3562,5 +3560,35 @@ namespace Bit.App.Resources {
return ResourceManager.GetString("CaptchaFailed", resourceCulture);
}
}
+
+ public static string Fido2Title {
+ get {
+ return ResourceManager.GetString("Fido2Title", resourceCulture);
+ }
+ }
+
+ public static string Fido2Instruction {
+ get {
+ return ResourceManager.GetString("Fido2Instruction", resourceCulture);
+ }
+ }
+
+ public static string Fido2Desc {
+ get {
+ return ResourceManager.GetString("Fido2Desc", resourceCulture);
+ }
+ }
+
+ public static string Fido2AuthenticateWebAuthn {
+ get {
+ return ResourceManager.GetString("Fido2AuthenticateWebAuthn", resourceCulture);
+ }
+ }
+
+ public static string Fido2SomethingWentWrong {
+ get {
+ return ResourceManager.GetString("Fido2SomethingWentWrong", resourceCulture);
+ }
+ }
}
}
diff --git a/src/App/Resources/AppResources.resx b/src/App/Resources/AppResources.resx
index 21b4af715..5301e085d 100644
--- a/src/App/Resources/AppResources.resx
+++ b/src/App/Resources/AppResources.resx
@@ -2016,4 +2016,19 @@
Captcha Failed. Please try again.
+
+ FIDO2 WebAuthn
+
+
+ To continue, have your FIDO2 WebAuthn enabled security key ready, then follow the instructions after clicking 'Authenticate WebAuthn' on the next screen.
+
+
+ Authentication using FIDO2 WebAuthn, you can authenticate using an external security key.
+
+
+ Authenticate WebAuthn
+
+
+ Something Went Wrong. Try again.
+
diff --git a/src/App/Services/MobilePlatformUtilsService.cs b/src/App/Services/MobilePlatformUtilsService.cs
index 1875e2fe7..bc061f786 100644
--- a/src/App/Services/MobilePlatformUtilsService.cs
+++ b/src/App/Services/MobilePlatformUtilsService.cs
@@ -129,9 +129,9 @@ namespace Bit.App.Services
return true;
}
- public bool SupportsU2f()
+ public bool SupportsFido2()
{
- return false;
+ return _deviceActionService.SupportsFido2();
}
public void ShowToast(string type, string title, string text, Dictionary options = null)
diff --git a/src/App/Utilities/AppHelpers.cs b/src/App/Utilities/AppHelpers.cs
index 6d3d34606..ace0719e8 100644
--- a/src/App/Utilities/AppHelpers.cs
+++ b/src/App/Utilities/AppHelpers.cs
@@ -8,10 +8,13 @@ using Bit.Core.Models.View;
using Bit.Core.Utilities;
using System.Collections.Generic;
using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Bit.App.Models;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
+using Newtonsoft.Json;
using Xamarin.Essentials;
using Xamarin.Forms;
@@ -470,5 +473,17 @@ namespace Bit.App.Utilities
}
return false;
}
+
+ public static string EncodeDataParameter(object obj)
+ {
+ string EncodeMultibyte(Match match)
+ {
+ return Convert.ToChar(Convert.ToUInt32($"0x{match.Groups[1].Value}", 16)).ToString();
+ }
+
+ var escaped = Uri.EscapeDataString(JsonConvert.SerializeObject(obj));
+ var multiByteEscaped = Regex.Replace(escaped, "%([0-9A-F]{2})", EncodeMultibyte);
+ return Convert.ToBase64String(Encoding.UTF8.GetBytes(multiByteEscaped));
+ }
}
}
diff --git a/src/Core/Abstractions/IAuthService.cs b/src/Core/Abstractions/IAuthService.cs
index d2c88f678..4cd9fbf51 100644
--- a/src/Core/Abstractions/IAuthService.cs
+++ b/src/Core/Abstractions/IAuthService.cs
@@ -17,7 +17,7 @@ namespace Bit.Core.Abstractions
Dictionary TwoFactorProviders { get; set; }
Dictionary> TwoFactorProvidersData { get; set; }
- TwoFactorProviderType? GetDefaultTwoFactorProvider(bool u2fSupported);
+ TwoFactorProviderType? GetDefaultTwoFactorProvider(bool fido2Supported);
bool AuthingWithSso();
bool AuthingWithPassword();
List GetSupportedTwoFactorProviders();
diff --git a/src/Core/Abstractions/IPlatformUtilsService.cs b/src/Core/Abstractions/IPlatformUtilsService.cs
index e0a060901..5d85634f7 100644
--- a/src/Core/Abstractions/IPlatformUtilsService.cs
+++ b/src/Core/Abstractions/IPlatformUtilsService.cs
@@ -25,7 +25,7 @@ namespace Bit.Core.Abstractions
Task ShowPasswordDialogAsync(string title, string body, Func> validator);
void ShowToast(string type, string title, string text, Dictionary options = null);
void ShowToast(string type, string title, string[] text, Dictionary options = null);
- bool SupportsU2f();
+ bool SupportsFido2();
bool SupportsDuo();
Task SupportsBiometricAsync();
Task AuthenticateBiometricAsync(string text = null, string fallbackText = null, Action fallback = null);
diff --git a/src/Core/Enums/TwoFactorProviderType.cs b/src/Core/Enums/TwoFactorProviderType.cs
index 1a1b7ffc9..70ac4de0f 100644
--- a/src/Core/Enums/TwoFactorProviderType.cs
+++ b/src/Core/Enums/TwoFactorProviderType.cs
@@ -8,6 +8,7 @@
YubiKey = 3,
U2f = 4,
Remember = 5,
- OrganizationDuo = 6
+ OrganizationDuo = 6,
+ Fido2WebAuthn = 7,
}
}
diff --git a/src/Core/Services/AuthService.cs b/src/Core/Services/AuthService.cs
index e8c9e9675..59790989c 100644
--- a/src/Core/Services/AuthService.cs
+++ b/src/Core/Services/AuthService.cs
@@ -76,9 +76,9 @@ namespace Bit.Core.Services
Priority = 10,
Sort = 4
});
- TwoFactorProviders.Add(TwoFactorProviderType.U2f, new TwoFactorProvider
+ TwoFactorProviders.Add(TwoFactorProviderType.Fido2WebAuthn, new TwoFactorProvider
{
- Type = TwoFactorProviderType.U2f,
+ Type = TwoFactorProviderType.Fido2WebAuthn,
Priority = 4,
Sort = 5,
Premium = true
@@ -114,8 +114,8 @@ namespace Bit.Core.Services
string.Format("Duo ({0})", _i18nService.T("Organization"));
TwoFactorProviders[TwoFactorProviderType.OrganizationDuo].Description =
_i18nService.T("DuoOrganizationDesc");
- TwoFactorProviders[TwoFactorProviderType.U2f].Name = _i18nService.T("U2fTitle");
- TwoFactorProviders[TwoFactorProviderType.U2f].Description = _i18nService.T("U2fDesc");
+ TwoFactorProviders[TwoFactorProviderType.Fido2WebAuthn].Name = _i18nService.T("Fido2Title");
+ TwoFactorProviders[TwoFactorProviderType.Fido2WebAuthn].Description = _i18nService.T("Fido2Desc");
TwoFactorProviders[TwoFactorProviderType.YubiKey].Name = _i18nService.T("YubiKeyTitle");
TwoFactorProviders[TwoFactorProviderType.YubiKey].Description = _i18nService.T("YubiKeyDesc");
}
@@ -192,9 +192,10 @@ namespace Bit.Core.Services
{
providers.Add(TwoFactorProviders[TwoFactorProviderType.Duo]);
}
- if (TwoFactorProvidersData.ContainsKey(TwoFactorProviderType.U2f) && _platformUtilsService.SupportsU2f())
+ if (TwoFactorProvidersData.ContainsKey(TwoFactorProviderType.Fido2WebAuthn) &&
+ _platformUtilsService.SupportsFido2())
{
- providers.Add(TwoFactorProviders[TwoFactorProviderType.U2f]);
+ providers.Add(TwoFactorProviders[TwoFactorProviderType.Fido2WebAuthn]);
}
if (TwoFactorProvidersData.ContainsKey(TwoFactorProviderType.Email))
{
@@ -203,7 +204,7 @@ namespace Bit.Core.Services
return providers;
}
- public TwoFactorProviderType? GetDefaultTwoFactorProvider(bool u2fSupported)
+ public TwoFactorProviderType? GetDefaultTwoFactorProvider(bool fido2Supported)
{
if (TwoFactorProvidersData == null)
{
@@ -223,7 +224,7 @@ namespace Bit.Core.Services
var provider = TwoFactorProviders[providerKvp.Key];
if (provider.Priority > providerPriority)
{
- if (providerKvp.Key == TwoFactorProviderType.U2f && !u2fSupported)
+ if (providerKvp.Key == TwoFactorProviderType.Fido2WebAuthn && !fido2Supported)
{
continue;
}
diff --git a/src/iOS.Core/Services/DeviceActionService.cs b/src/iOS.Core/Services/DeviceActionService.cs
index 6c37ea37b..bc512c14e 100644
--- a/src/iOS.Core/Services/DeviceActionService.cs
+++ b/src/iOS.Core/Services/DeviceActionService.cs
@@ -88,7 +88,8 @@ namespace Bit.iOS.Core.Services
var loadingIndicator = new UIActivityIndicatorView(new CGRect(10, 5, 50, 50));
loadingIndicator.HidesWhenStopped = true;
- loadingIndicator.ActivityIndicatorViewStyle = UIActivityIndicatorViewStyle.Gray;
+ loadingIndicator.ActivityIndicatorViewStyle = ThemeHelpers.LightTheme ? UIActivityIndicatorViewStyle.Gray :
+ UIActivityIndicatorViewStyle.White;
loadingIndicator.StartAnimating();
_progressAlert = UIAlertController.Create(null, text, UIAlertControllerStyle.Alert);
@@ -446,6 +447,27 @@ namespace Bit.iOS.Core.Services
throw new NotImplementedException();
}
+ public bool SupportsFido2()
+ {
+ // FIDO2 WebAuthn supported on 13.3+
+ var versionParts = UIDevice.CurrentDevice.SystemVersion.Split('.');
+ if (versionParts.Length > 0 && int.TryParse(versionParts[0], out var version))
+ {
+ if (version == 13)
+ {
+ if (versionParts.Length > 1 && int.TryParse(versionParts[1], out var minorVersion))
+ {
+ return minorVersion >= 3;
+ }
+ }
+ else if (version > 13)
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
private void ImagePicker_FinishedPickingMedia(object sender, UIImagePickerMediaPickedEventArgs e)
{
if (sender is UIImagePickerController picker)
diff --git a/src/iOS/AppDelegate.cs b/src/iOS/AppDelegate.cs
index e56e02cf9..dbedf8142 100644
--- a/src/iOS/AppDelegate.cs
+++ b/src/iOS/AppDelegate.cs
@@ -239,6 +239,16 @@ namespace Bit.iOS
return base.OpenUrl(app, url, options);
}
+ public override bool ContinueUserActivity(UIApplication application, NSUserActivity userActivity,
+ UIApplicationRestorationHandler completionHandler)
+ {
+ if (Xamarin.Essentials.Platform.ContinueUserActivity(application, userActivity, completionHandler))
+ {
+ return true;
+ }
+ return base.ContinueUserActivity(application, userActivity, completionHandler);
+ }
+
public override void FailedToRegisterForRemoteNotifications(UIApplication application, NSError error)
{
_pushHandler?.OnErrorReceived(error);