mirror of
https://github.com/bitwarden/mobile.git
synced 2024-11-22 11:35:21 +01:00
FIDO2 WebAuthn support for mobile (#1519)
* FIDO2 / WebAuthn support for mobile * fixes
This commit is contained in:
parent
d050215ebc
commit
307a5a5843
@ -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)
|
||||
|
@ -45,5 +45,6 @@ namespace Bit.App.Abstractions
|
||||
bool UsingDarkTheme();
|
||||
long GetActiveTime();
|
||||
void CloseMainApp();
|
||||
bool SupportsFido2();
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
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()
|
||||
|
@ -123,32 +123,18 @@ 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();
|
||||
cancelled = true;
|
||||
return;
|
||||
}
|
||||
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))
|
||||
{
|
||||
@ -161,7 +147,6 @@ namespace Bit.App.Pages
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string GetResultCode(WebAuthenticatorResult authResult, string state)
|
||||
{
|
||||
|
@ -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>
|
||||
|
@ -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();
|
||||
if (!_inputFocused)
|
||||
{
|
||||
RequestFocus(_email);
|
||||
_inputFocused = true;
|
||||
}
|
||||
}
|
||||
|
||||
private async void Submit_Clicked(object sender, EventArgs e)
|
||||
|
@ -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)
|
||||
{
|
||||
@ -144,6 +133,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();
|
||||
var kdf = KdfType.PBKDF2_SHA256;
|
||||
@ -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;
|
||||
};
|
||||
}
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
Loading = false;
|
||||
if (e?.Error != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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)
|
||||
{
|
||||
@ -212,8 +278,11 @@ namespace Bit.App.Pages
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
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();
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
32
src/App/Resources/AppResources.Designer.cs
generated
32
src/App/Resources/AppResources.Designer.cs
generated
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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)
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
|
@ -8,6 +8,7 @@
|
||||
YubiKey = 3,
|
||||
U2f = 4,
|
||||
Remember = 5,
|
||||
OrganizationDuo = 6
|
||||
OrganizationDuo = 6,
|
||||
Fido2WebAuthn = 7,
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user