FIDO2 WebAuthn support for mobile (#1519)

* FIDO2 / WebAuthn support for mobile

* fixes
This commit is contained in:
Matt Portune 2021-08-30 12:44:12 -04:00 committed by GitHub
parent d050215ebc
commit 307a5a5843
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 276 additions and 155 deletions

View File

@ -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)

View File

@ -45,5 +45,6 @@ namespace Bit.App.Abstractions
bool UsingDarkTheme();
long GetActiveTime();
void CloseMainApp();
bool SupportsFido2();
}
}

View File

@ -82,7 +82,7 @@
</Grid>
</StackLayout>
<StackLayout Padding="10, 0">
<Button Text="{u:I18n LogIn}" Clicked="LogIn_Clicked" IsEnabled="{Binding LoginEnabled}"/>
<Button Text="{u:I18n LogIn}" Clicked="LogIn_Clicked" />
</StackLayout>
</StackLayout>
</ScrollView>

View File

@ -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<IStorageService>("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;
}
}

View File

@ -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()

View File

@ -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);
}
}

View File

@ -21,7 +21,7 @@
<ContentPage.ToolbarItems>
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
<ToolbarItem Text="{u:I18n Submit}" Clicked="Submit_Clicked" IsEnabled="{Binding SubmitEnabled}"/>
<ToolbarItem Text="{u:I18n Submit}" Clicked="Submit_Clicked" />
</ContentPage.ToolbarItems>
<ScrollView>

View File

@ -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<IMessagingService>("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)

View File

@ -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<string, object>
{
@ -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(),

View File

@ -102,6 +102,30 @@
</StackLayout>
</StackLayout>
</StackLayout>
<StackLayout Spacing="20" Padding="0" IsVisible="{Binding Fido2Method, Mode=OneWay}">
<Label
Text="{u:I18n Fido2Instruction}"
Margin="10, 20, 10, 0"
HorizontalTextAlignment="Center" />
<Image
Source="yubikey.png"
Margin="10, 0"
WidthRequest="266"
HeightRequest="160"
HorizontalOptions="Center" />
<StackLayout StyleClass="box">
<StackLayout StyleClass="box-row, box-row-switch">
<Label
Text="{u:I18n RememberMe}"
StyleClass="box-label-regular"
HorizontalOptions="StartAndExpand" />
<Switch
IsToggled="{Binding Remember}"
StyleClass="box-value"
HorizontalOptions="End" />
</StackLayout>
</StackLayout>
</StackLayout>
<StackLayout Spacing="0" Padding="0" IsVisible="{Binding DuoMethod, Mode=OneWay}"
VerticalOptions="FillAndExpand">
<controls:HybridWebView

View File

@ -168,11 +168,15 @@ namespace Bit.App.Pages
}
}
private void TryAgain_Clicked(object sender, EventArgs e)
private async void TryAgain_Clicked(object sender, EventArgs e)
{
if (DoOnce())
{
if (_vm.YubikeyMethod)
if (_vm.Fido2Method)
{
await _vm.Fido2AuthenticateAsync();
}
else if (_vm.YubikeyMethod)
{
_messagingService.Send("listenYubiKeyOTP", true);
}

View File

@ -7,9 +7,13 @@ using Bit.Core.Exceptions;
using Bit.Core.Models.Request;
using Bit.Core.Utilities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using Bit.App.Utilities;
using Newtonsoft.Json;
using Xamarin.Essentials;
using Xamarin.Forms;
namespace Bit.App.Pages
@ -27,7 +31,6 @@ namespace Bit.App.Pages
private readonly IBroadcasterService _broadcasterService;
private readonly IStateService _stateService;
private bool _u2fSupported = false;
private TwoFactorProviderType? _selectedProviderType;
private string _totpInstruction;
private string _webVaultUrl = "https://vault.bitwarden.com";
@ -65,6 +68,8 @@ namespace Bit.App.Pages
public bool DuoMethod => 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<string, object> 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();

View File

@ -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<bool> 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));
}
}
}

View File

@ -137,8 +137,6 @@
AutomationProperties.Name="{u:I18n File}"
Grid.Column="0">
<VisualStateManager.VisualStateGroups>
<!-- Rider users, if the x:Name values below are red, it's a known issue: -->
<!-- https://youtrack.jetbrains.com/issue/RSRP-479388 -->
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal">
<VisualState.Setters>
@ -163,8 +161,6 @@
AutomationProperties.Name="{u:I18n Text}"
Grid.Column="1">
<VisualStateManager.VisualStateGroups>
<!-- Rider users, if the x:Name values below are red, it's a known issue: -->
<!-- https://youtrack.jetbrains.com/issue/RSRP-479388 -->
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal">
<VisualState.Setters>

View File

@ -1,7 +1,6 @@
//------------------------------------------------------------------------------
// <auto-generated>
// 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);
}
}
}
}

View File

@ -2016,4 +2016,19 @@
<data name="CaptchaFailed" xml:space="preserve">
<value>Captcha Failed. Please try again.</value>
</data>
<data name="Fido2Title" xml:space="preserve">
<value>FIDO2 WebAuthn</value>
</data>
<data name="Fido2Instruction" xml:space="preserve">
<value>To continue, have your FIDO2 WebAuthn enabled security key ready, then follow the instructions after clicking 'Authenticate WebAuthn' on the next screen.</value>
</data>
<data name="Fido2Desc" xml:space="preserve">
<value>Authentication using FIDO2 WebAuthn, you can authenticate using an external security key.</value>
</data>
<data name="Fido2AuthenticateWebAuthn" xml:space="preserve">
<value>Authenticate WebAuthn</value>
</data>
<data name="Fido2SomethingWentWrong" xml:space="preserve">
<value>Something Went Wrong. Try again.</value>
</data>
</root>

View File

@ -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<string, object> options = null)

View File

@ -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));
}
}
}

View File

@ -17,7 +17,7 @@ namespace Bit.Core.Abstractions
Dictionary<TwoFactorProviderType, TwoFactorProvider> TwoFactorProviders { get; set; }
Dictionary<TwoFactorProviderType, Dictionary<string, object>> TwoFactorProvidersData { get; set; }
TwoFactorProviderType? GetDefaultTwoFactorProvider(bool u2fSupported);
TwoFactorProviderType? GetDefaultTwoFactorProvider(bool fido2Supported);
bool AuthingWithSso();
bool AuthingWithPassword();
List<TwoFactorProvider> GetSupportedTwoFactorProviders();

View File

@ -25,7 +25,7 @@ namespace Bit.Core.Abstractions
Task<bool> ShowPasswordDialogAsync(string title, string body, Func<string, Task<bool>> validator);
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 SupportsFido2();
bool SupportsDuo();
Task<bool> SupportsBiometricAsync();
Task<bool> AuthenticateBiometricAsync(string text = null, string fallbackText = null, Action fallback = null);

View File

@ -8,6 +8,7 @@
YubiKey = 3,
U2f = 4,
Remember = 5,
OrganizationDuo = 6
OrganizationDuo = 6,
Fido2WebAuthn = 7,
}
}

View File

@ -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;
}

View File

@ -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)

View File

@ -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);