1
0
mirror of https://github.com/bitwarden/mobile.git synced 2024-11-25 12:05:59 +01:00

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"); _messagingService.Send("finishMainActivity");
} }
public bool SupportsFido2()
{
return true;
}
private bool DeleteDir(Java.IO.File dir) private bool DeleteDir(Java.IO.File dir)
{ {
if (dir != null && dir.IsDirectory) if (dir != null && dir.IsDirectory)

View File

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

View File

@ -82,7 +82,7 @@
</Grid> </Grid>
</StackLayout> </StackLayout>
<StackLayout Padding="10, 0"> <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>
</StackLayout> </StackLayout>
</ScrollView> </ScrollView>

View File

@ -16,6 +16,8 @@ namespace Bit.App.Pages
private readonly LoginPageViewModel _vm; private readonly LoginPageViewModel _vm;
private readonly AppOptions _appOptions; private readonly AppOptions _appOptions;
private bool _inputFocused;
public LoginPage(string email = null, AppOptions appOptions = null) public LoginPage(string email = null, AppOptions appOptions = null)
{ {
_storageService = ServiceContainer.Resolve<IStorageService>("storageService"); _storageService = ServiceContainer.Resolve<IStorageService>("storageService");
@ -58,13 +60,10 @@ namespace Bit.App.Pages
{ {
base.OnAppearing(); base.OnAppearing();
await _vm.InitAsync(); await _vm.InitAsync();
if (string.IsNullOrWhiteSpace(_vm.Email)) if (!_inputFocused)
{ {
RequestFocus(_email); RequestFocus(string.IsNullOrWhiteSpace(_vm.Email) ? _email : _masterPassword);
} _inputFocused = true;
else
{
RequestFocus(_masterPassword);
} }
} }

View File

@ -33,7 +33,6 @@ namespace Bit.App.Pages
private bool _showPassword; private bool _showPassword;
private string _email; private string _email;
private string _masterPassword; private string _masterPassword;
private bool _loginEnabled = true;
public LoginPageViewModel() public LoginPageViewModel()
{ {
@ -73,16 +72,6 @@ namespace Bit.App.Pages
set => SetProperty(ref _masterPassword, value); 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 LogInCommand { get; }
public Command TogglePasswordCommand { get; } public Command TogglePasswordCommand { get; }
public string ShowPasswordIcon => ShowPassword ? "" : ""; public string ShowPasswordIcon => ShowPassword ? "" : "";
@ -106,7 +95,7 @@ namespace Bit.App.Pages
RememberEmail = rememberEmail.GetValueOrDefault(true); RememberEmail = rememberEmail.GetValueOrDefault(true);
} }
public async Task LogInAsync() public async Task LogInAsync(bool showLoading = true)
{ {
if (Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.None) if (Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.None)
{ {
@ -140,10 +129,9 @@ namespace Bit.App.Pages
ShowPassword = false; ShowPassword = false;
try try
{ {
if (!Loading) if (showLoading)
{ {
await _deviceActionService.ShowLoadingAsync(AppResources.LoggingIn); await _deviceActionService.ShowLoadingAsync(AppResources.LoggingIn);
Loading = true;
} }
var response = await _authService.LogInAsync(Email, MasterPassword, _captchaToken); var response = await _authService.LogInAsync(Email, MasterPassword, _captchaToken);
@ -156,25 +144,21 @@ namespace Bit.App.Pages
await _storageService.RemoveAsync(Keys_RememberedEmail); await _storageService.RemoveAsync(Keys_RememberedEmail);
} }
await AppHelpers.ResetInvalidUnlockAttemptsAsync(); await AppHelpers.ResetInvalidUnlockAttemptsAsync();
await _deviceActionService.HideLoadingAsync();
if (response.CaptchaNeeded) if (response.CaptchaNeeded)
{ {
if (await HandleCaptchaAsync(response.CaptchaSiteKey)) if (await HandleCaptchaAsync(response.CaptchaSiteKey))
{ {
await LogInAsync(); await LogInAsync(false);
_captchaToken = null; _captchaToken = null;
return;
}
else
{
Loading = false;
return;
} }
return;
} }
MasterPassword = string.Empty; MasterPassword = string.Empty;
_captchaToken = null; _captchaToken = null;
await _deviceActionService.HideLoadingAsync();
if (response.TwoFactor) if (response.TwoFactor)
{ {
StartTwoFactorAction?.Invoke(); StartTwoFactorAction?.Invoke();
@ -198,7 +182,6 @@ namespace Bit.App.Pages
AppResources.AnErrorHasOccurred); AppResources.AnErrorHasOccurred);
} }
} }
Loading = false;
} }
public void TogglePassword() public void TogglePassword()

View File

@ -123,43 +123,28 @@ namespace Bit.App.Pages
"domain_hint=" + Uri.EscapeDataString(OrgIdentifier); "domain_hint=" + Uri.EscapeDataString(OrgIdentifier);
WebAuthenticatorResult authResult = null; WebAuthenticatorResult authResult = null;
bool cancelled = false;
try try
{ {
authResult = await WebAuthenticator.AuthenticateAsync(new Uri(url), authResult = await WebAuthenticator.AuthenticateAsync(new Uri(url),
new Uri(redirectUri)); 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(); await _deviceActionService.HideLoadingAsync();
cancelled = true; await _platformUtilsService.ShowDialogAsync(AppResources.LoginSsoError,
} AppResources.AnErrorHasOccurred);
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);
}
} }
} }

View File

@ -21,7 +21,7 @@
<ContentPage.ToolbarItems> <ContentPage.ToolbarItems>
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" /> <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> </ContentPage.ToolbarItems>
<ScrollView> <ScrollView>

View File

@ -11,6 +11,8 @@ namespace Bit.App.Pages
private readonly IMessagingService _messagingService; private readonly IMessagingService _messagingService;
private readonly RegisterPageViewModel _vm; private readonly RegisterPageViewModel _vm;
private bool _inputFocused;
public RegisterPage(HomePage homePage) public RegisterPage(HomePage homePage)
{ {
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService"); _messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
@ -45,7 +47,11 @@ namespace Bit.App.Pages
protected override void OnAppearing() protected override void OnAppearing()
{ {
base.OnAppearing(); base.OnAppearing();
RequestFocus(_email); if (!_inputFocused)
{
RequestFocus(_email);
_inputFocused = true;
}
} }
private async void Submit_Clicked(object sender, EventArgs e) private async void Submit_Clicked(object sender, EventArgs e)

View File

@ -22,7 +22,6 @@ namespace Bit.App.Pages
private readonly IPlatformUtilsService _platformUtilsService; private readonly IPlatformUtilsService _platformUtilsService;
private bool _showPassword; private bool _showPassword;
private bool _acceptPolicies; private bool _acceptPolicies;
private bool _submitEnabled = true;
public RegisterPageViewModel() public RegisterPageViewModel()
{ {
@ -60,16 +59,6 @@ namespace Bit.App.Pages
get => _acceptPolicies; get => _acceptPolicies;
set => SetProperty(ref _acceptPolicies, value); 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 public Thickness SwitchMargin
{ {
@ -96,7 +85,7 @@ namespace Bit.App.Pages
protected override IDeviceActionService deviceActionService => _deviceActionService; protected override IDeviceActionService deviceActionService => _deviceActionService;
protected override IPlatformUtilsService platformUtilsService => _platformUtilsService; 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) if (Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.None)
{ {
@ -143,6 +132,11 @@ namespace Bit.App.Pages
} }
// TODO: Password strength check? // TODO: Password strength check?
if (showLoading)
{
await _deviceActionService.ShowLoadingAsync(AppResources.CreatingAccount);
}
Name = string.IsNullOrWhiteSpace(Name) ? null : Name; Name = string.IsNullOrWhiteSpace(Name) ? null : Name;
Email = Email.Trim().ToLower(); Email = Email.Trim().ToLower();
@ -172,14 +166,8 @@ namespace Bit.App.Pages
try try
{ {
if (!Loading)
{
await _deviceActionService.ShowLoadingAsync(AppResources.CreatingAccount);
Loading = true;
}
await _apiService.PostRegisterAsync(request); await _apiService.PostRegisterAsync(request);
await _deviceActionService.HideLoadingAsync(); await _deviceActionService.HideLoadingAsync();
Loading = false;
_platformUtilsService.ShowToast("success", null, AppResources.AccountCreated, _platformUtilsService.ShowToast("success", null, AppResources.AccountCreated,
new System.Collections.Generic.Dictionary<string, object> new System.Collections.Generic.Dictionary<string, object>
{ {
@ -193,19 +181,12 @@ namespace Bit.App.Pages
{ {
if (await HandleCaptchaAsync(e.Error.CaptchaSiteKey)) if (await HandleCaptchaAsync(e.Error.CaptchaSiteKey))
{ {
await SubmitAsync(); await SubmitAsync(false);
_captchaToken = null; _captchaToken = null;
return;
} }
else return;
{
await _deviceActionService.HideLoadingAsync();
Loading = false;
return;
};
} }
await _deviceActionService.HideLoadingAsync(); await _deviceActionService.HideLoadingAsync();
Loading = false;
if (e?.Error != null) if (e?.Error != null)
{ {
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(), await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),

View File

@ -102,6 +102,30 @@
</StackLayout> </StackLayout>
</StackLayout> </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}" <StackLayout Spacing="0" Padding="0" IsVisible="{Binding DuoMethod, Mode=OneWay}"
VerticalOptions="FillAndExpand"> VerticalOptions="FillAndExpand">
<controls:HybridWebView <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 (DoOnce())
{ {
if (_vm.YubikeyMethod) if (_vm.Fido2Method)
{
await _vm.Fido2AuthenticateAsync();
}
else if (_vm.YubikeyMethod)
{ {
_messagingService.Send("listenYubiKeyOTP", true); _messagingService.Send("listenYubiKeyOTP", true);
} }

View File

@ -7,9 +7,13 @@ using Bit.Core.Exceptions;
using Bit.Core.Models.Request; using Bit.Core.Models.Request;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.App.Utilities;
using Newtonsoft.Json;
using Xamarin.Essentials;
using Xamarin.Forms; using Xamarin.Forms;
namespace Bit.App.Pages namespace Bit.App.Pages
@ -27,7 +31,6 @@ namespace Bit.App.Pages
private readonly IBroadcasterService _broadcasterService; private readonly IBroadcasterService _broadcasterService;
private readonly IStateService _stateService; private readonly IStateService _stateService;
private bool _u2fSupported = false;
private TwoFactorProviderType? _selectedProviderType; private TwoFactorProviderType? _selectedProviderType;
private string _totpInstruction; private string _totpInstruction;
private string _webVaultUrl = "https://vault.bitwarden.com"; private string _webVaultUrl = "https://vault.bitwarden.com";
@ -65,6 +68,8 @@ namespace Bit.App.Pages
public bool DuoMethod => SelectedProviderType == TwoFactorProviderType.Duo || public bool DuoMethod => SelectedProviderType == TwoFactorProviderType.Duo ||
SelectedProviderType == TwoFactorProviderType.OrganizationDuo; SelectedProviderType == TwoFactorProviderType.OrganizationDuo;
public bool Fido2Method => SelectedProviderType == TwoFactorProviderType.Fido2WebAuthn;
public bool YubikeyMethod => SelectedProviderType == TwoFactorProviderType.YubiKey; public bool YubikeyMethod => SelectedProviderType == TwoFactorProviderType.YubiKey;
public bool AuthenticatorMethod => SelectedProviderType == TwoFactorProviderType.Authenticator; public bool AuthenticatorMethod => SelectedProviderType == TwoFactorProviderType.Authenticator;
@ -73,7 +78,7 @@ namespace Bit.App.Pages
public bool TotpMethod => AuthenticatorMethod || EmailMethod; 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 public bool ShowContinue
{ {
@ -97,6 +102,7 @@ namespace Bit.App.Pages
{ {
nameof(EmailMethod), nameof(EmailMethod),
nameof(DuoMethod), nameof(DuoMethod),
nameof(Fido2Method),
nameof(YubikeyMethod), nameof(YubikeyMethod),
nameof(AuthenticatorMethod), nameof(AuthenticatorMethod),
nameof(TotpMethod), nameof(TotpMethod),
@ -128,10 +134,7 @@ namespace Bit.App.Pages
_webVaultUrl = _environmentService.WebVaultUrl; _webVaultUrl = _environmentService.WebVaultUrl;
} }
// TODO: init U2F SelectedProviderType = _authService.GetDefaultTwoFactorProvider(_platformUtilsService.SupportsFido2());
_u2fSupported = false;
SelectedProviderType = _authService.GetDefaultTwoFactorProvider(_u2fSupported);
Load(); Load();
} }
@ -147,8 +150,8 @@ namespace Bit.App.Pages
var providerData = _authService.TwoFactorProvidersData[SelectedProviderType.Value]; var providerData = _authService.TwoFactorProvidersData[SelectedProviderType.Value];
switch (SelectedProviderType.Value) switch (SelectedProviderType.Value)
{ {
case TwoFactorProviderType.U2f: case TwoFactorProviderType.Fido2WebAuthn:
// TODO Fido2AuthenticateAsync(providerData);
break; break;
case TwoFactorProviderType.YubiKey: case TwoFactorProviderType.YubiKey:
_messagingService.Send("listenYubiKeyOTP", true); _messagingService.Send("listenYubiKeyOTP", true);
@ -183,10 +186,73 @@ namespace Bit.App.Pages
{ {
_messagingService.Send("listenYubiKeyOTP", false); _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) if (SelectedProviderType == null)
{ {
@ -213,7 +279,10 @@ namespace Bit.App.Pages
try try
{ {
await _deviceActionService.ShowLoadingAsync(AppResources.Validating); if (showLoading)
{
await _deviceActionService.ShowLoadingAsync(AppResources.Validating);
}
var result = await _authService.LogInTwoFactorAsync(SelectedProviderType.Value, Token, Remember); var result = await _authService.LogInTwoFactorAsync(SelectedProviderType.Value, Token, Remember);
var task = Task.Run(() => _syncService.FullSyncAsync(true)); var task = Task.Run(() => _syncService.FullSyncAsync(true));
await _deviceActionService.HideLoadingAsync(); await _deviceActionService.HideLoadingAsync();

View File

@ -1,13 +1,10 @@
using System; using System;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.App.Abstractions; using Bit.App.Abstractions;
using Bit.App.Resources; using Bit.App.Resources;
using Bit.App.Utilities;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Newtonsoft.Json;
using Xamarin.Essentials; using Xamarin.Essentials;
using Xamarin.Forms;
namespace Bit.App.Pages namespace Bit.App.Pages
{ {
@ -22,7 +19,7 @@ namespace Bit.App.Pages
protected async Task<bool> HandleCaptchaAsync(string CaptchaSiteKey) protected async Task<bool> HandleCaptchaAsync(string CaptchaSiteKey)
{ {
var callbackUri = "bitwarden://captcha-callback"; var callbackUri = "bitwarden://captcha-callback";
var data = EncodeDataParameter(new var data = AppHelpers.EncodeDataParameter(new
{ {
siteKey = CaptchaSiteKey, siteKey = CaptchaSiteKey,
locale = i18nService.Culture.TwoLetterISOLanguageName, locale = i18nService.Culture.TwoLetterISOLanguageName,
@ -37,27 +34,19 @@ namespace Bit.App.Pages
bool cancelled = false; bool cancelled = false;
try try
{ {
authResult = await WebAuthenticator.AuthenticateAsync(new Uri(url), var options = new WebAuthenticatorOptions
new Uri(callbackUri)); {
Url = new Uri(url),
CallbackUrl = new Uri(callbackUri),
PrefersEphemeralWebBrowserSession = true,
};
authResult = await WebAuthenticator.AuthenticateAsync(options);
} }
catch (TaskCanceledException) catch (TaskCanceledException)
{ {
await deviceActionService.HideLoadingAsync(); await deviceActionService.HideLoadingAsync();
cancelled = true; 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 && if (cancelled == false && authResult != null &&
authResult.Properties.TryGetValue("token", out _captchaToken)) authResult.Properties.TryGetValue("token", out _captchaToken))
@ -71,18 +60,5 @@ namespace Bit.App.Pages
return false; 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}" AutomationProperties.Name="{u:I18n File}"
Grid.Column="0"> Grid.Column="0">
<VisualStateManager.VisualStateGroups> <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"> <VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal"> <VisualState x:Name="Normal">
<VisualState.Setters> <VisualState.Setters>
@ -163,8 +161,6 @@
AutomationProperties.Name="{u:I18n Text}" AutomationProperties.Name="{u:I18n Text}"
Grid.Column="1"> Grid.Column="1">
<VisualStateManager.VisualStateGroups> <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"> <VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal"> <VisualState x:Name="Normal">
<VisualState.Setters> <VisualState.Setters>

View File

@ -1,7 +1,6 @@
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// <auto-generated> // <auto-generated>
// This code was generated by a tool. // 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 // Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated. // the code is regenerated.
@ -10,7 +9,6 @@
namespace Bit.App.Resources { namespace Bit.App.Resources {
using System; using System;
using System.Reflection;
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] [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); 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"> <data name="CaptchaFailed" xml:space="preserve">
<value>Captcha Failed. Please try again.</value> <value>Captcha Failed. Please try again.</value>
</data> </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> </root>

View File

@ -129,9 +129,9 @@ namespace Bit.App.Services
return true; 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) 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 Bit.Core.Utilities;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.App.Models; using Bit.App.Models;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Newtonsoft.Json;
using Xamarin.Essentials; using Xamarin.Essentials;
using Xamarin.Forms; using Xamarin.Forms;
@ -470,5 +473,17 @@ namespace Bit.App.Utilities
} }
return false; 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, TwoFactorProvider> TwoFactorProviders { get; set; }
Dictionary<TwoFactorProviderType, Dictionary<string, object>> TwoFactorProvidersData { get; set; } Dictionary<TwoFactorProviderType, Dictionary<string, object>> TwoFactorProvidersData { get; set; }
TwoFactorProviderType? GetDefaultTwoFactorProvider(bool u2fSupported); TwoFactorProviderType? GetDefaultTwoFactorProvider(bool fido2Supported);
bool AuthingWithSso(); bool AuthingWithSso();
bool AuthingWithPassword(); bool AuthingWithPassword();
List<TwoFactorProvider> GetSupportedTwoFactorProviders(); 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); 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);
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(); bool SupportsDuo();
Task<bool> SupportsBiometricAsync(); Task<bool> SupportsBiometricAsync();
Task<bool> AuthenticateBiometricAsync(string text = null, string fallbackText = null, Action fallback = null); Task<bool> AuthenticateBiometricAsync(string text = null, string fallbackText = null, Action fallback = null);

View File

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

View File

@ -76,9 +76,9 @@ namespace Bit.Core.Services
Priority = 10, Priority = 10,
Sort = 4 Sort = 4
}); });
TwoFactorProviders.Add(TwoFactorProviderType.U2f, new TwoFactorProvider TwoFactorProviders.Add(TwoFactorProviderType.Fido2WebAuthn, new TwoFactorProvider
{ {
Type = TwoFactorProviderType.U2f, Type = TwoFactorProviderType.Fido2WebAuthn,
Priority = 4, Priority = 4,
Sort = 5, Sort = 5,
Premium = true Premium = true
@ -114,8 +114,8 @@ namespace Bit.Core.Services
string.Format("Duo ({0})", _i18nService.T("Organization")); string.Format("Duo ({0})", _i18nService.T("Organization"));
TwoFactorProviders[TwoFactorProviderType.OrganizationDuo].Description = TwoFactorProviders[TwoFactorProviderType.OrganizationDuo].Description =
_i18nService.T("DuoOrganizationDesc"); _i18nService.T("DuoOrganizationDesc");
TwoFactorProviders[TwoFactorProviderType.U2f].Name = _i18nService.T("U2fTitle"); TwoFactorProviders[TwoFactorProviderType.Fido2WebAuthn].Name = _i18nService.T("Fido2Title");
TwoFactorProviders[TwoFactorProviderType.U2f].Description = _i18nService.T("U2fDesc"); TwoFactorProviders[TwoFactorProviderType.Fido2WebAuthn].Description = _i18nService.T("Fido2Desc");
TwoFactorProviders[TwoFactorProviderType.YubiKey].Name = _i18nService.T("YubiKeyTitle"); TwoFactorProviders[TwoFactorProviderType.YubiKey].Name = _i18nService.T("YubiKeyTitle");
TwoFactorProviders[TwoFactorProviderType.YubiKey].Description = _i18nService.T("YubiKeyDesc"); TwoFactorProviders[TwoFactorProviderType.YubiKey].Description = _i18nService.T("YubiKeyDesc");
} }
@ -192,9 +192,10 @@ namespace Bit.Core.Services
{ {
providers.Add(TwoFactorProviders[TwoFactorProviderType.Duo]); 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)) if (TwoFactorProvidersData.ContainsKey(TwoFactorProviderType.Email))
{ {
@ -203,7 +204,7 @@ namespace Bit.Core.Services
return providers; return providers;
} }
public TwoFactorProviderType? GetDefaultTwoFactorProvider(bool u2fSupported) public TwoFactorProviderType? GetDefaultTwoFactorProvider(bool fido2Supported)
{ {
if (TwoFactorProvidersData == null) if (TwoFactorProvidersData == null)
{ {
@ -223,7 +224,7 @@ namespace Bit.Core.Services
var provider = TwoFactorProviders[providerKvp.Key]; var provider = TwoFactorProviders[providerKvp.Key];
if (provider.Priority > providerPriority) if (provider.Priority > providerPriority)
{ {
if (providerKvp.Key == TwoFactorProviderType.U2f && !u2fSupported) if (providerKvp.Key == TwoFactorProviderType.Fido2WebAuthn && !fido2Supported)
{ {
continue; continue;
} }

View File

@ -88,7 +88,8 @@ namespace Bit.iOS.Core.Services
var loadingIndicator = new UIActivityIndicatorView(new CGRect(10, 5, 50, 50)); var loadingIndicator = new UIActivityIndicatorView(new CGRect(10, 5, 50, 50));
loadingIndicator.HidesWhenStopped = true; loadingIndicator.HidesWhenStopped = true;
loadingIndicator.ActivityIndicatorViewStyle = UIActivityIndicatorViewStyle.Gray; loadingIndicator.ActivityIndicatorViewStyle = ThemeHelpers.LightTheme ? UIActivityIndicatorViewStyle.Gray :
UIActivityIndicatorViewStyle.White;
loadingIndicator.StartAnimating(); loadingIndicator.StartAnimating();
_progressAlert = UIAlertController.Create(null, text, UIAlertControllerStyle.Alert); _progressAlert = UIAlertController.Create(null, text, UIAlertControllerStyle.Alert);
@ -446,6 +447,27 @@ namespace Bit.iOS.Core.Services
throw new NotImplementedException(); 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) private void ImagePicker_FinishedPickingMedia(object sender, UIImagePickerMediaPickedEventArgs e)
{ {
if (sender is UIImagePickerController picker) if (sender is UIImagePickerController picker)

View File

@ -239,6 +239,16 @@ namespace Bit.iOS
return base.OpenUrl(app, url, options); 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) public override void FailedToRegisterForRemoteNotifications(UIApplication application, NSError error)
{ {
_pushHandler?.OnErrorReceived(error); _pushHandler?.OnErrorReceived(error);