Added SSO flows and functionality (#1047)

* SSO login flow for pre-existing user and no 2FA

* 2FA progress

* 2FA support

* Added SSO flows and functionality

* Handle webauthenticator cancellation gracefully

* updates & bugfixes

* Added state validation to web auth response handling

* SSO auth, account registration, and environment settings support for iOS extensions

* Added SSO prevalidation to auth process

* prevalidation now hitting identity service base url

* additional error handling

* Requested changes

* fixed case
This commit is contained in:
Matt Portune 2020-09-03 12:30:40 -04:00 committed by GitHub
parent 3af08a4727
commit f1419a75f6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
46 changed files with 4368 additions and 4072 deletions

View file

@ -78,7 +78,7 @@
<Version>1.8.6.7</Version>
</PackageReference>
<PackageReference Include="Xamarin.Essentials">
<Version>1.5.3.1</Version>
<Version>1.5.3.2</Version>
</PackageReference>
<PackageReference Include="Xamarin.Firebase.Messaging">
<Version>71.1740.0</Version>
@ -142,6 +142,7 @@
<Compile Include="Tiles\MyVaultTileService.cs" />
<Compile Include="Utilities\AndroidHelpers.cs" />
<Compile Include="Utilities\AppCenterHelper.cs" />
<Compile Include="WebAuthCallbackActivity.cs" />
</ItemGroup>
<ItemGroup>
<AndroidAsset Include="Assets\FontAwesome.ttf" />

View file

@ -144,6 +144,7 @@ namespace Bit.Droid
protected override void OnResume()
{
base.OnResume();
Xamarin.Essentials.Platform.OnResume();
if (_deviceActionService.SupportsNfc())
{
try

View file

@ -0,0 +1,11 @@
using Android.App;
using Android.Content.PM;
namespace Bit.Droid
{
[Activity(NoHistory = true, LaunchMode = LaunchMode.SingleTop)]
[IntentFilter(new[] { Android.Content.Intent.ActionView },
Categories = new[] { Android.Content.Intent.CategoryDefault, Android.Content.Intent.CategoryBrowsable },
DataScheme = "bitwarden")]
public class WebAuthCallbackActivity : Xamarin.Essentials.WebAuthenticatorCallbackActivity { }
}

View file

@ -15,7 +15,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.AppCenter.Crashes" Version="3.2.1" />
<PackageReference Include="Plugin.Fingerprint" Version="2.1.1" />
<PackageReference Include="Xamarin.Essentials" Version="1.5.3.1" />
<PackageReference Include="Xamarin.Essentials" Version="1.5.3.2" />
<PackageReference Include="Xamarin.FFImageLoading.Forms" Version="2.4.11.982" />
<PackageReference Include="Xamarin.Forms" Version="4.5.0.725" />
<PackageReference Include="ZXing.Net.Mobile" Version="2.4.1" />
@ -111,6 +111,14 @@
<Compile Update="Pages\Vault\GroupingsPage\GroupingsPage.xaml.cs">
<DependentUpon>GroupingsPage.xaml</DependentUpon>
</Compile>
<Compile Update="Pages\Accounts\LoginSsoPage.xaml.cs">
<DependentUpon>LoginSsoPage.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Update="Pages\Accounts\SetPasswordPage.xaml.cs">
<DependentUpon>ResetMasterPasswordPage.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
</ItemGroup>
<ItemGroup>

View file

@ -19,5 +19,28 @@ namespace Bit.App.Models
public string SaveCardExpYear { get; set; }
public string SaveCardCode { get; set; }
public bool IosExtension { get; set; }
public void SetAllFrom(AppOptions o)
{
if (o == null)
{
return;
}
MyVaultTile = o.MyVaultTile;
GeneratorTile = o.GeneratorTile;
FromAutofillFramework = o.FromAutofillFramework;
FillType = o.FillType;
Uri = o.Uri;
SaveType = o.SaveType;
SaveName = o.SaveName;
SaveUsername = o.SaveUsername;
SavePassword = o.SavePassword;
SaveCardName = o.SaveCardName;
SaveCardNumber = o.SaveCardNumber;
SaveCardExpMonth = o.SaveCardExpMonth;
SaveCardExpYear = o.SaveCardExpYear;
SaveCardCode = o.SaveCardCode;
IosExtension = o.IosExtension;
}
}
}

View file

@ -1,17 +1,21 @@
using Bit.Core.Abstractions;
using Bit.Core.Utilities;
using System;
using System.Threading.Tasks;
using Bit.App.Resources;
using Xamarin.Forms;
namespace Bit.App.Pages
{
public partial class EnvironmentPage : BaseContentPage
{
private readonly IPlatformUtilsService _platformUtilsService;
private readonly IMessagingService _messagingService;
private readonly EnvironmentPageViewModel _vm;
public EnvironmentPage()
{
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
_messagingService.Send("showStatusBar", true);
InitializeComponent();
@ -28,6 +32,12 @@ namespace Bit.App.Pages
_apiEntry.ReturnCommand = new Command(() => _identityEntry.Focus());
_identityEntry.ReturnType = ReturnType.Next;
_identityEntry.ReturnCommand = new Command(() => _iconsEntry.Focus());
_vm.SubmitSuccessAction = () => Device.BeginInvokeOnMainThread(async () => await SubmitSuccessAsync());
_vm.CloseAction = async () =>
{
_messagingService.Send("showStatusBar", false);
await Navigation.PopModalAsync();
};
}
private async void Submit_Clicked(object sender, EventArgs e)
@ -38,12 +48,17 @@ namespace Bit.App.Pages
}
}
private async void Close_Clicked(object sender, EventArgs e)
private async Task SubmitSuccessAsync()
{
_platformUtilsService.ShowToast("success", null, AppResources.EnvironmentSaved);
await Navigation.PopModalAsync();
}
private void Close_Clicked(object sender, EventArgs e)
{
if (DoOnce())
{
_messagingService.Send("showStatusBar", false);
await Navigation.PopModalAsync();
_vm.CloseAction();
}
}
}

View file

@ -1,6 +1,7 @@
using Bit.App.Resources;
using Bit.Core.Abstractions;
using Bit.Core.Utilities;
using System;
using System.Threading.Tasks;
using Xamarin.Forms;
@ -8,12 +9,10 @@ namespace Bit.App.Pages
{
public class EnvironmentPageViewModel : BaseViewModel
{
private readonly IPlatformUtilsService _platformUtilsService;
private readonly IEnvironmentService _environmentService;
public EnvironmentPageViewModel()
{
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
_environmentService = ServiceContainer.Resolve<IEnvironmentService>("environmentService");
PageTitle = AppResources.Settings;
@ -33,6 +32,8 @@ namespace Bit.App.Pages
public string WebVaultUrl { get; set; }
public string IconsUrl { get; set; }
public string NotificationsUrls { get; set; }
public Action SubmitSuccessAction { get; set; }
public Action CloseAction { get; set; }
public async Task SubmitAsync()
{
@ -54,8 +55,7 @@ namespace Bit.App.Pages
IconsUrl = resUrls.Icons;
NotificationsUrls = resUrls.Notifications;
_platformUtilsService.ShowToast("success", null, AppResources.EnvironmentSaved);
await Page.Navigation.PopModalAsync();
SubmitSuccessAction?.Invoke();
}
}
}

View file

@ -11,12 +11,16 @@
<ContentPage.BindingContext>
<pages:HomeViewModel />
</ContentPage.BindingContext>
<ContentPage.ToolbarItems>
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
</ContentPage.ToolbarItems>
<StackLayout Spacing="0" Padding="10, 5">
<controls:FaButton Text="&#xf013;"
StyleClass="btn-muted, btn-icon, btn-icon-platform"
HorizontalOptions="Start"
Clicked="Settings_Clicked"
Clicked="Environment_Clicked"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Options}">
<controls:FaButton.Margin>
@ -39,6 +43,8 @@
Clicked="LogIn_Clicked"></Button>
<Button Text="{u:I18n CreateAccount}"
Clicked="Register_Clicked"></Button>
<Button Text="{u:I18n LogInSso}"
Clicked="LogInSso_Clicked"></Button>
</StackLayout>
</StackLayout>
</StackLayout>

View file

@ -10,6 +10,7 @@ namespace Bit.App.Pages
{
public partial class HomePage : BaseContentPage
{
private readonly HomeViewModel _vm;
private readonly AppOptions _appOptions;
private IMessagingService _messagingService;
@ -19,6 +20,12 @@ namespace Bit.App.Pages
_messagingService.Send("showStatusBar", false);
_appOptions = appOptions;
InitializeComponent();
_vm = BindingContext as HomeViewModel;
_vm.Page = this;
_vm.StartLoginAction = () => Device.BeginInvokeOnMainThread(async () => await StartLoginAsync());
_vm.StartRegisterAction = () => Device.BeginInvokeOnMainThread(async () => await StartRegisterAsync());
_vm.StartSsoLoginAction = () => Device.BeginInvokeOnMainThread(async () => await StartSsoLoginAsync());
_vm.StartEnvironmentAction = () => Device.BeginInvokeOnMainThread(async () => await StartEnvironmentAsync());
_logo.Source = !ThemeManager.UsingLightTheme ? "logo_white.png" : "logo.png";
}
@ -33,29 +40,69 @@ namespace Bit.App.Pages
base.OnAppearing();
_messagingService.Send("showStatusBar", false);
}
private void Close_Clicked(object sender, EventArgs e)
{
if (DoOnce())
{
_vm.CloseAction();
}
}
private void LogIn_Clicked(object sender, EventArgs e)
{
if (DoOnce())
{
Navigation.PushModalAsync(new NavigationPage(new LoginPage(null, _appOptions)));
_vm.StartLoginAction();
}
}
private async Task StartLoginAsync()
{
var page = new LoginPage(null, _appOptions);
await Navigation.PushModalAsync(new NavigationPage(page));
}
private void Register_Clicked(object sender, EventArgs e)
{
if (DoOnce())
{
Navigation.PushModalAsync(new NavigationPage(new RegisterPage(this)));
_vm.StartRegisterAction();
}
}
private async Task StartRegisterAsync()
{
var page = new RegisterPage(this);
await Navigation.PushModalAsync(new NavigationPage(page));
}
private void Settings_Clicked(object sender, EventArgs e)
private void LogInSso_Clicked(object sender, EventArgs e)
{
if (DoOnce())
{
Navigation.PushModalAsync(new NavigationPage(new EnvironmentPage()));
_vm.StartSsoLoginAction();
}
}
private async Task StartSsoLoginAsync()
{
var page = new LoginSsoPage(_appOptions);
await Navigation.PushModalAsync(new NavigationPage(page));
}
private void Environment_Clicked(object sender, EventArgs e)
{
if (DoOnce())
{
_vm.StartEnvironmentAction();
}
}
private async Task StartEnvironmentAsync()
{
var page = new EnvironmentPage();
await Navigation.PushModalAsync(new NavigationPage(page));
}
}
}

View file

@ -1,4 +1,5 @@
using System;
using Bit.App.Resources;
namespace Bit.App.Pages
{
@ -6,7 +7,13 @@ namespace Bit.App.Pages
{
public HomeViewModel()
{
PageTitle = "Home Page";
PageTitle = AppResources.Bitwarden;
}
public Action StartLoginAction { get; set; }
public Action StartRegisterAction { get; set; }
public Action StartSsoLoginAction { get; set; }
public Action StartEnvironmentAction { get; set; }
public Action CloseAction { get; set; }
}
}

View file

@ -1,9 +1,9 @@
using Bit.App.Models;
using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Core.Utilities;
using System;
using System.Threading.Tasks;
using Bit.App.Utilities;
using Xamarin.Forms;
namespace Bit.App.Pages
@ -99,24 +99,11 @@ namespace Bit.App.Pages
private async Task UnlockedAsync()
{
if (_appOptions != null)
if (AppHelpers.SetAlternateMainPage(_appOptions))
{
if (_appOptions.FromAutofillFramework && _appOptions.SaveType.HasValue)
{
Application.Current.MainPage = new NavigationPage(new AddEditPage(appOptions: _appOptions));
return;
}
if (_appOptions.Uri != null)
{
Application.Current.MainPage = new NavigationPage(new AutofillCiphersPage(_appOptions));
return;
}
}
var previousPage = await _storageService.GetAsync<PreviousPageInfo>(Constants.PreviousPageKey);
if (previousPage != null)
{
await _storageService.RemoveAsync(Constants.PreviousPageKey);
return;
}
var previousPage = await AppHelpers.ClearPreviousPage();
Application.Current.MainPage = new TabsPage(_appOptions, previousPage);
}
}

View file

@ -8,12 +8,14 @@ using Bit.Core.Models.Domain;
using Bit.Core.Utilities;
using System;
using System.Threading.Tasks;
using Bit.Core.Models.Request;
using Xamarin.Forms;
namespace Bit.App.Pages
{
public class LockPageViewModel : BaseViewModel
{
private readonly IApiService _apiService;
private readonly IPlatformUtilsService _platformUtilsService;
private readonly IDeviceActionService _deviceActionService;
private readonly IVaultTimeoutService _vaultTimeoutService;
@ -39,6 +41,7 @@ namespace Bit.App.Pages
public LockPageViewModel()
{
_apiService = ServiceContainer.Resolve<IApiService>("apiService");
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
@ -224,18 +227,33 @@ namespace Bit.App.Pages
{
var key = await _cryptoService.MakeKeyAsync(MasterPassword, _email, kdf, kdfIterations);
var keyHash = await _cryptoService.HashPasswordAsync(MasterPassword, key);
var storedKeyHash = await _cryptoService.GetKeyHashAsync();
if (storedKeyHash == null)
var passwordValid = false;
if (keyHash != null)
{
var oldKey = await _secureStorageService.GetAsync<string>("oldKey");
if (key.KeyB64 == oldKey)
var storedKeyHash = await _cryptoService.GetKeyHashAsync();
if (storedKeyHash != null)
{
await _secureStorageService.RemoveAsync("oldKey");
await _cryptoService.SetKeyHashAsync(keyHash);
storedKeyHash = keyHash;
passwordValid = storedKeyHash == keyHash;
}
else
{
await _deviceActionService.ShowLoadingAsync(AppResources.Loading);
var request = new PasswordVerificationRequest();
request.MasterPasswordHash = keyHash;
try
{
await _apiService.PostAccountVerifyPasswordAsync(request);
passwordValid = true;
await _cryptoService.SetKeyHashAsync(keyHash);
}
catch (Exception e)
{
System.Diagnostics.Debug.WriteLine(">>> {0}: {1}", e.GetType(), e.StackTrace);
}
await _deviceActionService.HideLoadingAsync();
}
}
if (storedKeyHash != null && keyHash != null && storedKeyHash == keyHash)
if (passwordValid)
{
if (_pinSet.Item1)
{

View file

@ -1,9 +1,9 @@
using Bit.App.Models;
using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Core.Utilities;
using System;
using System.Threading.Tasks;
using Bit.App.Utilities;
using Xamarin.Forms;
namespace Bit.App.Pages
@ -25,7 +25,7 @@ namespace Bit.App.Pages
_vm = BindingContext as LoginPageViewModel;
_vm.Page = this;
_vm.StartTwoFactorAction = () => Device.BeginInvokeOnMainThread(async () => await StartTwoFactorAsync());
_vm.LoggedInAction = () => Device.BeginInvokeOnMainThread(async () => await LoggedInAsync());
_vm.LogInSuccessAction = () => Device.BeginInvokeOnMainThread(async () => await LogInSuccessAsync());
_vm.CloseAction = async () =>
{
_messagingService.Send("showStatusBar", false);
@ -74,7 +74,7 @@ namespace Bit.App.Pages
}
}
private async void Close_Clicked(object sender, EventArgs e)
private void Close_Clicked(object sender, EventArgs e)
{
if (DoOnce())
{
@ -84,30 +84,17 @@ namespace Bit.App.Pages
private async Task StartTwoFactorAsync()
{
var page = new TwoFactorPage();
var page = new TwoFactorPage(false, _appOptions);
await Navigation.PushModalAsync(new NavigationPage(page));
}
private async Task LoggedInAsync()
private async Task LogInSuccessAsync()
{
if (_appOptions != null)
if (AppHelpers.SetAlternateMainPage(_appOptions))
{
if (_appOptions.FromAutofillFramework && _appOptions.SaveType.HasValue)
{
Application.Current.MainPage = new NavigationPage(new AddEditPage(appOptions: _appOptions));
return;
}
if (_appOptions.Uri != null)
{
Application.Current.MainPage = new NavigationPage(new AutofillCiphersPage(_appOptions));
return;
}
}
var previousPage = await _storageService.GetAsync<PreviousPageInfo>(Constants.PreviousPageKey);
if (previousPage != null)
{
await _storageService.RemoveAsync(Constants.PreviousPageKey);
return;
}
var previousPage = await AppHelpers.ClearPreviousPage();
Application.Current.MainPage = new TabsPage(_appOptions, previousPage);
}
}

View file

@ -68,7 +68,7 @@ namespace Bit.App.Pages
public string ShowPasswordIcon => ShowPassword ? "" : "";
public bool RememberEmail { get; set; }
public Action StartTwoFactorAction { get; set; }
public Action LoggedInAction { get; set; }
public Action LogInSuccessAction { get; set; }
public Action CloseAction { get; set; }
public bool HideHintButton
@ -142,7 +142,7 @@ namespace Bit.App.Pages
var disableFavicon = await _storageService.GetAsync<bool?>(Constants.DisableFaviconKey);
await _stateService.SaveAsync(Constants.DisableFaviconKey, disableFavicon.GetValueOrDefault());
var task = Task.Run(async () => await _syncService.FullSyncAsync(true));
LoggedInAction?.Invoke();
LogInSuccessAction?.Invoke();
}
}
catch (ApiException e)

View file

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8" ?>
<pages:BaseContentPage
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Bit.App.Pages.LoginSsoPage"
xmlns:pages="clr-namespace:Bit.App.Pages"
xmlns:u="clr-namespace:Bit.App.Utilities"
x:DataType="pages:LoginSsoPageViewModel"
Title="{Binding PageTitle}">
<ContentPage.BindingContext>
<pages:LoginSsoPageViewModel />
</ContentPage.BindingContext>
<ContentPage.ToolbarItems>
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
<ToolbarItem Text="{u:I18n LogIn}" Clicked="LogIn_Clicked" />
</ContentPage.ToolbarItems>
<ScrollView>
<StackLayout Spacing="20" Margin="0,10,0,0">
<StackLayout StyleClass="box">
<Label Text="{u:I18n LogInSsoSummary}"
StyleClass="text-md"
HorizontalTextAlignment="Start"></Label>
<StackLayout StyleClass="box-row">
<Label
Text="{u:I18n OrgIdentifier}"
StyleClass="box-label" />
<Entry
x:Name="_orgIdentifier"
Text="{Binding OrgIdentifier}"
Keyboard="Default"
StyleClass="box-value"
ReturnType="Go"
ReturnCommand="{Binding LogInCommand}" />
</StackLayout>
</StackLayout>
</StackLayout>
</ScrollView>
</pages:BaseContentPage>

View file

@ -0,0 +1,114 @@
using Bit.App.Models;
using Bit.Core.Abstractions;
using Bit.Core.Utilities;
using System;
using System.Threading.Tasks;
using Bit.App.Utilities;
using Xamarin.Forms;
namespace Bit.App.Pages
{
public partial class LoginSsoPage : BaseContentPage
{
private readonly IStorageService _storageService;
private readonly IMessagingService _messagingService;
private readonly IVaultTimeoutService _vaultTimeoutService;
private readonly LoginSsoPageViewModel _vm;
private readonly AppOptions _appOptions;
private AppOptions _appOptionsCopy;
public LoginSsoPage(AppOptions appOptions = null)
{
_storageService = ServiceContainer.Resolve<IStorageService>("storageService");
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
_messagingService.Send("showStatusBar", true);
_appOptions = appOptions;
InitializeComponent();
_vm = BindingContext as LoginSsoPageViewModel;
_vm.Page = this;
_vm.StartTwoFactorAction = () => Device.BeginInvokeOnMainThread(async () => await StartTwoFactorAsync());
_vm.StartSetPasswordAction = () =>
Device.BeginInvokeOnMainThread(async () => await StartSetPasswordAsync());
_vm.SsoAuthSuccessAction = () => Device.BeginInvokeOnMainThread(async () => await SsoAuthSuccessAsync());
_vm.CloseAction = async () =>
{
_messagingService.Send("showStatusBar", false);
await Navigation.PopModalAsync();
};
if (Device.RuntimePlatform == Device.Android)
{
ToolbarItems.RemoveAt(0);
}
}
protected override async void OnAppearing()
{
base.OnAppearing();
await _vm.InitAsync();
if (string.IsNullOrWhiteSpace(_vm.OrgIdentifier))
{
RequestFocus(_orgIdentifier);
}
}
private void CopyAppOptions()
{
if (_appOptions != null)
{
// create an object copy of _appOptions to persist values when app is exited during web auth flow
_appOptionsCopy = new AppOptions();
_appOptionsCopy.SetAllFrom(_appOptions);
}
}
private void RestoreAppOptionsFromCopy()
{
if (_appOptions != null)
{
// restore values to original readonly _appOptions object from copy
_appOptions.SetAllFrom(_appOptionsCopy);
_appOptionsCopy = null;
}
}
private async void LogIn_Clicked(object sender, EventArgs e)
{
if (DoOnce())
{
CopyAppOptions();
await _vm.LogInAsync();
}
}
private void Close_Clicked(object sender, EventArgs e)
{
if (DoOnce())
{
_vm.CloseAction();
}
}
private async Task StartTwoFactorAsync()
{
RestoreAppOptionsFromCopy();
var page = new TwoFactorPage(true, _appOptions);
await Navigation.PushModalAsync(new NavigationPage(page));
}
private async Task StartSetPasswordAsync()
{
RestoreAppOptionsFromCopy();
var page = new SetPasswordPage(_appOptions);
await Navigation.PushModalAsync(new NavigationPage(page));
}
private async Task SsoAuthSuccessAsync()
{
RestoreAppOptionsFromCopy();
await AppHelpers.ClearPreviousPage();
Application.Current.MainPage = new NavigationPage(new LockPage(_appOptions));
}
}
}

View file

@ -0,0 +1,218 @@
using Bit.App.Abstractions;
using Bit.App.Resources;
using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Core.Utilities;
using System;
using System.Threading.Tasks;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Domain;
using Xamarin.Essentials;
using Xamarin.Forms;
namespace Bit.App.Pages
{
public class LoginSsoPageViewModel : BaseViewModel
{
private const string Keys_RememberedOrgIdentifier = "rememberedOrgIdentifier";
private const string Keys_RememberOrgIdentifier = "rememberOrgIdentifier";
private readonly IDeviceActionService _deviceActionService;
private readonly IAuthService _authService;
private readonly ISyncService _syncService;
private readonly IApiService _apiService;
private readonly IPasswordGenerationService _passwordGenerationService;
private readonly ICryptoFunctionService _cryptoFunctionService;
private readonly IStorageService _storageService;
private readonly IPlatformUtilsService _platformUtilsService;
private readonly IStateService _stateService;
private string _orgIdentifier;
public LoginSsoPageViewModel()
{
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
_authService = ServiceContainer.Resolve<IAuthService>("authService");
_syncService = ServiceContainer.Resolve<ISyncService>("syncService");
_apiService = ServiceContainer.Resolve<IApiService>("apiService");
_passwordGenerationService =
ServiceContainer.Resolve<IPasswordGenerationService>("passwordGenerationService");
_cryptoFunctionService = ServiceContainer.Resolve<ICryptoFunctionService>("cryptoFunctionService");
_storageService = ServiceContainer.Resolve<IStorageService>("storageService");
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
PageTitle = AppResources.Bitwarden;
LogInCommand = new Command(async () => await LogInAsync());
}
public string OrgIdentifier
{
get => _orgIdentifier;
set => SetProperty(ref _orgIdentifier, value);
}
public Command LogInCommand { get; }
public bool RememberOrgIdentifier { get; set; }
public Action StartTwoFactorAction { get; set; }
public Action StartSetPasswordAction { get; set; }
public Action SsoAuthSuccessAction { get; set; }
public Action CloseAction { get; set; }
public async Task InitAsync()
{
if (string.IsNullOrWhiteSpace(OrgIdentifier))
{
OrgIdentifier = await _storageService.GetAsync<string>(Keys_RememberedOrgIdentifier);
}
var rememberOrgIdentifier = await _storageService.GetAsync<bool?>(Keys_RememberOrgIdentifier);
RememberOrgIdentifier = rememberOrgIdentifier.GetValueOrDefault(true);
}
public async Task LogInAsync()
{
if (Connectivity.NetworkAccess == NetworkAccess.None)
{
await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
AppResources.InternetConnectionRequiredTitle);
return;
}
if (string.IsNullOrWhiteSpace(OrgIdentifier))
{
await _platformUtilsService.ShowDialogAsync(
string.Format(AppResources.ValidationFieldRequired, AppResources.OrgIdentifier),
AppResources.AnErrorHasOccurred,
AppResources.Ok);
return;
}
await _deviceActionService.ShowLoadingAsync(AppResources.LoggingIn);
try
{
await _apiService.PreValidateSso(OrgIdentifier);
}
catch (ApiException e)
{
await _deviceActionService.HideLoadingAsync();
await _platformUtilsService.ShowDialogAsync(
(e?.Error != null ? e.Error.GetSingleMessage() : AppResources.LoginSsoError),
AppResources.AnErrorHasOccurred);
return;
}
var passwordOptions = new PasswordGenerationOptions(true);
passwordOptions.Length = 64;
var codeVerifier = await _passwordGenerationService.GeneratePasswordAsync(passwordOptions);
var codeVerifierHash = await _cryptoFunctionService.HashAsync(codeVerifier, CryptoHashAlgorithm.Sha256);
var codeChallenge = CoreHelpers.Base64UrlEncode(codeVerifierHash);
var state = await _passwordGenerationService.GeneratePasswordAsync(passwordOptions);
var redirectUri = "bitwarden://sso-callback";
var url = _apiService.IdentityBaseUrl + "/connect/authorize?" +
"client_id=" + _platformUtilsService.IdentityClientId + "&" +
"redirect_uri=" + Uri.EscapeDataString(redirectUri) + "&" +
"response_type=code&scope=api%20offline_access&" +
"state=" + state + "&code_challenge=" + codeChallenge + "&" +
"code_challenge_method=S256&response_mode=query&" +
"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)
{
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);
}
}
}
private string GetResultCode(WebAuthenticatorResult authResult, string state)
{
string code = null;
if (authResult != null)
{
authResult.Properties.TryGetValue("state", out var resultState);
if (resultState == state)
{
authResult.Properties.TryGetValue("code", out var resultCode);
code = resultCode;
}
}
return code;
}
private async Task LogIn(string code, string codeVerifier, string redirectUri)
{
try
{
var response = await _authService.LogInSsoAsync(code, codeVerifier, redirectUri);
if (RememberOrgIdentifier)
{
await _storageService.SaveAsync(Keys_RememberedOrgIdentifier, OrgIdentifier);
}
else
{
await _storageService.RemoveAsync(Keys_RememberedOrgIdentifier);
}
await _deviceActionService.HideLoadingAsync();
if (response.TwoFactor)
{
StartTwoFactorAction?.Invoke();
}
else if (response.ResetMasterPassword)
{
StartSetPasswordAction?.Invoke();
}
else
{
var disableFavicon = await _storageService.GetAsync<bool?>(Constants.DisableFaviconKey);
await _stateService.SaveAsync(Constants.DisableFaviconKey, disableFavicon.GetValueOrDefault());
var task = Task.Run(async () => await _syncService.FullSyncAsync(true));
SsoAuthSuccessAction?.Invoke();
}
}
catch (Exception e)
{
await _deviceActionService.HideLoadingAsync();
await _platformUtilsService.ShowDialogAsync(AppResources.LoginSsoError,
AppResources.AnErrorHasOccurred);
}
}
}
}

View file

@ -1,6 +1,7 @@
using Bit.Core.Abstractions;
using Bit.Core.Utilities;
using System;
using System.Threading.Tasks;
using Xamarin.Forms;
namespace Bit.App.Pages
@ -17,12 +18,11 @@ namespace Bit.App.Pages
InitializeComponent();
_vm = BindingContext as RegisterPageViewModel;
_vm.Page = this;
_vm.RegistrationSuccess = async () =>
_vm.RegistrationSuccess = () => Device.BeginInvokeOnMainThread(async () => await RegistrationSuccessAsync(homePage));
_vm.CloseAction = async () =>
{
if (homePage != null)
{
await homePage.DismissRegisterPageAndLogInAsync(_vm.Email);
}
_messagingService.Send("showStatusBar", false);
await Navigation.PopModalAsync();
};
MasterPasswordEntry = _masterPassword;
ConfirmMasterPasswordEntry = _confirmMasterPassword;
@ -55,13 +55,20 @@ namespace Bit.App.Pages
await _vm.SubmitAsync();
}
}
private async Task RegistrationSuccessAsync(HomePage homePage)
{
if (homePage != null)
{
await homePage.DismissRegisterPageAndLogInAsync(_vm.Email);
}
}
private async void Close_Clicked(object sender, EventArgs e)
private void Close_Clicked(object sender, EventArgs e)
{
if (DoOnce())
{
_messagingService.Send("showStatusBar", false);
await Navigation.PopModalAsync();
_vm.CloseAction();
}
}
}

View file

@ -52,6 +52,7 @@ namespace Bit.App.Pages
public string ConfirmMasterPassword { get; set; }
public string Hint { get; set; }
public Action RegistrationSuccess { get; set; }
public Action CloseAction { get; set; }
public async Task SubmitAsync()
{

View file

@ -0,0 +1,146 @@
<?xml version="1.0" encoding="utf-8" ?>
<pages:BaseContentPage
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Bit.App.Pages.SetPasswordPage"
xmlns:pages="clr-namespace:Bit.App.Pages"
xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:u="clr-namespace:Bit.App.Utilities"
x:DataType="pages:SetPasswordPageViewModel"
Title="{Binding PageTitle}">
<ContentPage.BindingContext>
<pages:SetPasswordPageViewModel />
</ContentPage.BindingContext>
<ContentPage.Resources>
<ResourceDictionary>
<u:InverseBoolConverter x:Key="inverseBool" />
</ResourceDictionary>
</ContentPage.Resources>
<ContentPage.ToolbarItems>
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
<ToolbarItem Text="{u:I18n Submit}" Clicked="Submit_Clicked" />
</ContentPage.ToolbarItems>
<ScrollView>
<StackLayout Spacing="20">
<StackLayout StyleClass="box">
<StackLayout StyleClass="box-row">
<Label Text="{u:I18n SetMasterPasswordSummary}"
StyleClass="text-md"
HorizontalTextAlignment="Start"></Label>
</StackLayout>
<Grid IsVisible="{Binding IsPolicyInEffect}"
RowSpacing="0"
ColumnSpacing="0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Frame Padding="10"
Margin="0"
HasShadow="False"
BackgroundColor="Transparent"
BorderColor="Accent">
<Label
Text="{Binding PolicySummary}"
StyleClass="text-muted, text-sm, text-bold"
HorizontalTextAlignment="Start" />
</Frame>
</Grid>
<Grid StyleClass="box-row">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Label
Text="{u:I18n MasterPassword}"
StyleClass="box-label"
Grid.Row="0"
Grid.Column="0" />
<controls:MonoEntry
x:Name="_masterPassword"
Text="{Binding MasterPassword}"
StyleClass="box-value"
IsSpellCheckEnabled="False"
IsTextPredictionEnabled="False"
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
Grid.Row="1"
Grid.Column="0" />
<controls:FaButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowPasswordIcon}"
Command="{Binding TogglePasswordCommand}"
Grid.Row="0"
Grid.Column="1"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
</Grid>
<Label
Text="{u:I18n MasterPasswordDescription}"
StyleClass="box-footer-label" />
</StackLayout>
<StackLayout StyleClass="box">
<Grid StyleClass="box-row">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Label
Text="{u:I18n RetypeMasterPassword}"
StyleClass="box-label"
Grid.Row="0"
Grid.Column="0" />
<controls:MonoEntry
x:Name="_confirmMasterPassword"
Text="{Binding ConfirmMasterPassword}"
StyleClass="box-value"
IsSpellCheckEnabled="False"
IsTextPredictionEnabled="False"
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
Grid.Row="1"
Grid.Column="0" />
<controls:FaButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowPasswordIcon}"
Command="{Binding ToggleConfirmPasswordCommand}"
Grid.Row="0"
Grid.Column="1"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
</Grid>
<StackLayout StyleClass="box-row">
<Label
Text="{u:I18n MasterPasswordHint}"
StyleClass="box-label" />
<Entry
x:Name="_hint"
Text="{Binding Hint}"
StyleClass="box-value"
ReturnType="Go"
ReturnCommand="{Binding SubmitCommand}" />
</StackLayout>
<Label
Text="{u:I18n MasterPasswordHintDescription}"
StyleClass="box-footer-label" />
</StackLayout>
</StackLayout>
</ScrollView>
</pages:BaseContentPage>

View file

@ -0,0 +1,82 @@
using Bit.Core.Abstractions;
using Bit.Core.Utilities;
using System;
using System.Threading.Tasks;
using Bit.App.Models;
using Bit.App.Utilities;
using Xamarin.Forms;
namespace Bit.App.Pages
{
public partial class SetPasswordPage : BaseContentPage
{
private readonly IMessagingService _messagingService;
private readonly SetPasswordPageViewModel _vm;
private readonly AppOptions _appOptions;
public SetPasswordPage(AppOptions appOptions = null)
{
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
_messagingService.Send("showStatusBar", true);
_appOptions = appOptions;
InitializeComponent();
_vm = BindingContext as SetPasswordPageViewModel;
_vm.Page = this;
_vm.SetPasswordSuccessAction =
() => Device.BeginInvokeOnMainThread(async () => await SetPasswordSuccessAsync());
_vm.CloseAction = async () =>
{
_messagingService.Send("showStatusBar", false);
await Navigation.PopModalAsync();
};
if (Device.RuntimePlatform == Device.Android)
{
ToolbarItems.RemoveAt(0);
}
MasterPasswordEntry = _masterPassword;
ConfirmMasterPasswordEntry = _confirmMasterPassword;
_masterPassword.ReturnType = ReturnType.Next;
_masterPassword.ReturnCommand = new Command(() => _confirmMasterPassword.Focus());
_confirmMasterPassword.ReturnType = ReturnType.Next;
_confirmMasterPassword.ReturnCommand = new Command(() => _hint.Focus());
}
public Entry MasterPasswordEntry { get; set; }
public Entry ConfirmMasterPasswordEntry { get; set; }
protected override async void OnAppearing()
{
base.OnAppearing();
await _vm.InitAsync();
RequestFocus(_masterPassword);
}
private async void Submit_Clicked(object sender, EventArgs e)
{
if (DoOnce())
{
await _vm.SubmitAsync();
}
}
private async void Close_Clicked(object sender, EventArgs e)
{
if (DoOnce())
{
_vm.CloseAction();
}
}
private async Task SetPasswordSuccessAsync()
{
if (AppHelpers.SetAlternateMainPage(_appOptions))
{
return;
}
var previousPage = await AppHelpers.ClearPreviousPage();
Application.Current.MainPage = new TabsPage(_appOptions, previousPage);
}
}
}

View file

@ -0,0 +1,259 @@
using Bit.App.Abstractions;
using Bit.App.Resources;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Request;
using Bit.Core.Utilities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Bit.Core.Models.Domain;
using Xamarin.Essentials;
using Xamarin.Forms;
namespace Bit.App.Pages
{
public class SetPasswordPageViewModel : BaseViewModel
{
private readonly IDeviceActionService _deviceActionService;
private readonly IApiService _apiService;
private readonly ICryptoService _cryptoService;
private readonly IPlatformUtilsService _platformUtilsService;
private readonly IUserService _userService;
private readonly IPolicyService _policyService;
private readonly IPasswordGenerationService _passwordGenerationService;
private readonly II18nService _i18nService;
private bool _showPassword;
private bool _isPolicyInEffect;
private string _policySummary;
private MasterPasswordPolicyOptions _policy;
public SetPasswordPageViewModel()
{
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
_apiService = ServiceContainer.Resolve<IApiService>("apiService");
_cryptoService = ServiceContainer.Resolve<ICryptoService>("cryptoService");
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
_userService = ServiceContainer.Resolve<IUserService>("userService");
_policyService = ServiceContainer.Resolve<IPolicyService>("policyService");
_passwordGenerationService =
ServiceContainer.Resolve<IPasswordGenerationService>("passwordGenerationService");
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
PageTitle = AppResources.SetMasterPassword;
TogglePasswordCommand = new Command(TogglePassword);
ToggleConfirmPasswordCommand = new Command(ToggleConfirmPassword);
SubmitCommand = new Command(async () => await SubmitAsync());
}
public bool ShowPassword
{
get => _showPassword;
set => SetProperty(ref _showPassword, value,
additionalPropertyNames: new[] { nameof(ShowPasswordIcon) });
}
public bool IsPolicyInEffect
{
get => _isPolicyInEffect;
set => SetProperty(ref _isPolicyInEffect, value);
}
public string PolicySummary
{
get => _policySummary;
set => SetProperty(ref _policySummary, value);
}
public MasterPasswordPolicyOptions Policy
{
get => _policy;
set => SetProperty(ref _policy, value);
}
public Command SubmitCommand { get; }
public Command TogglePasswordCommand { get; }
public Command ToggleConfirmPasswordCommand { get; }
public string ShowPasswordIcon => ShowPassword ? "" : "";
public string MasterPassword { get; set; }
public string ConfirmMasterPassword { get; set; }
public string Hint { get; set; }
public Action SetPasswordSuccessAction { get; set; }
public Action CloseAction { get; set; }
public async Task InitAsync()
{
await CheckPasswordPolicy();
}
public async Task SubmitAsync()
{
if (Connectivity.NetworkAccess == NetworkAccess.None)
{
await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
AppResources.InternetConnectionRequiredTitle);
return;
}
if (string.IsNullOrWhiteSpace(MasterPassword))
{
await Page.DisplayAlert(AppResources.AnErrorHasOccurred,
string.Format(AppResources.ValidationFieldRequired, AppResources.MasterPassword),
AppResources.Ok);
return;
}
if (IsPolicyInEffect)
{
var userInput = await GetPasswordStrengthUserInput();
var passwordStrength = _passwordGenerationService.PasswordStrength(MasterPassword, userInput);
if (!await _policyService.EvaluateMasterPassword(passwordStrength.Score, MasterPassword, Policy))
{
await Page.DisplayAlert(AppResources.MasterPasswordPolicyValidationTitle,
AppResources.MasterPasswordPolicyValidationMessage, AppResources.Ok);
return;
}
}
else
{
if (MasterPassword.Length < 8)
{
await Page.DisplayAlert(AppResources.MasterPasswordPolicyValidationTitle,
AppResources.MasterPasswordLengthValMessage, AppResources.Ok);
return;
}
}
if (MasterPassword != ConfirmMasterPassword)
{
await Page.DisplayAlert(AppResources.AnErrorHasOccurred,
AppResources.MasterPasswordConfirmationValMessage, AppResources.Ok);
return;
}
var kdf = KdfType.PBKDF2_SHA256;
var kdfIterations = 100000;
var email = await _userService.GetEmailAsync();
var key = await _cryptoService.MakeKeyAsync(MasterPassword, email, kdf, kdfIterations);
var masterPasswordHash = await _cryptoService.HashPasswordAsync(MasterPassword, key);
Tuple<SymmetricCryptoKey, CipherString> encKey;
var existingEncKey = await _cryptoService.GetEncKeyAsync();
if (existingEncKey == null)
{
encKey = await _cryptoService.MakeEncKeyAsync(key);
}
else
{
encKey = await _cryptoService.RemakeEncKeyAsync(key);
}
var keys = await _cryptoService.MakeKeyPairAsync(encKey.Item1);
var request = new SetPasswordRequest
{
MasterPasswordHash = masterPasswordHash,
Key = encKey.Item2.EncryptedString,
MasterPasswordHint = Hint,
Kdf = kdf,
KdfIterations = kdfIterations,
Keys = new KeysRequest
{
PublicKey = keys.Item1,
EncryptedPrivateKey = keys.Item2.EncryptedString
}
};
try
{
await _deviceActionService.ShowLoadingAsync(AppResources.CreatingAccount);
await _apiService.SetPasswordAsync(request);
await _userService.SetInformationAsync(await _userService.GetUserIdAsync(),
await _userService.GetEmailAsync(), kdf, kdfIterations);
await _cryptoService.SetKeyAsync(key);
await _cryptoService.SetKeyHashAsync(masterPasswordHash);
await _cryptoService.SetEncKeyAsync(encKey.Item2.EncryptedString);
await _cryptoService.SetEncPrivateKeyAsync(keys.Item2.EncryptedString);
await _deviceActionService.HideLoadingAsync();
SetPasswordSuccessAction?.Invoke();
}
catch (ApiException e)
{
await _deviceActionService.HideLoadingAsync();
if (e?.Error != null)
{
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
AppResources.AnErrorHasOccurred);
}
}
}
public void TogglePassword()
{
ShowPassword = !ShowPassword;
(Page as SetPasswordPage).MasterPasswordEntry.Focus();
}
public void ToggleConfirmPassword()
{
ShowPassword = !ShowPassword;
(Page as SetPasswordPage).ConfirmMasterPasswordEntry.Focus();
}
private async Task CheckPasswordPolicy()
{
Policy = await _policyService.GetMasterPasswordPolicyOptions();
IsPolicyInEffect = Policy?.InEffect() ?? false;
if (!IsPolicyInEffect)
{
return;
}
var bullet = "\n" + "".PadLeft(4) + "\u2022 ";
var sb = new StringBuilder();
sb.Append(_i18nService.T("MasterPasswordPolicyInEffect"));
if (Policy.MinComplexity > 0)
{
sb.Append(bullet)
.Append(string.Format(_i18nService.T("PolicyInEffectMinComplexity"), Policy.MinComplexity));
}
if (Policy.MinLength > 0)
{
sb.Append(bullet).Append(string.Format(_i18nService.T("PolicyInEffectMinLength"), Policy.MinLength));
}
if (Policy.RequireUpper)
{
sb.Append(bullet).Append(_i18nService.T("PolicyInEffectUppercase"));
}
if (Policy.RequireLower)
{
sb.Append(bullet).Append(_i18nService.T("PolicyInEffectLowercase"));
}
if (Policy.RequireNumbers)
{
sb.Append(bullet).Append(_i18nService.T("PolicyInEffectNumbers"));
}
if (Policy.RequireSpecial)
{
sb.Append(bullet).Append(string.Format(_i18nService.T("PolicyInEffectSpecial"), "!@#$%^&*"));
}
PolicySummary = sb.ToString();
}
private async Task<List<string>> GetPasswordStrengthUserInput()
{
var email = await _userService.GetEmailAsync();
List<string> userInput = null;
var atPosition = email.IndexOf('@');
if (atPosition > -1)
{
var rx = new Regex("/[^A-Za-z0-9]/", RegexOptions.Compiled);
var data = rx.Split(email.Substring(0, atPosition).Trim().ToLower());
userInput = new List<string>(data);
}
return userInput;
}
}
}

View file

@ -1,10 +1,10 @@
using Bit.App.Controls;
using Bit.App.Models;
using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Core.Utilities;
using System;
using System.Threading.Tasks;
using Bit.App.Utilities;
using Xamarin.Forms;
namespace Bit.App.Pages
@ -14,22 +14,29 @@ namespace Bit.App.Pages
private readonly IBroadcasterService _broadcasterService;
private readonly IMessagingService _messagingService;
private readonly IStorageService _storageService;
private readonly IVaultTimeoutService _vaultTimeoutService;
private readonly AppOptions _appOptions;
private TwoFactorPageViewModel _vm;
private bool _inited;
private bool _authingWithSso;
public TwoFactorPage(AppOptions appOptions = null)
public TwoFactorPage(bool? authingWithSso = false, AppOptions appOptions = null)
{
InitializeComponent();
SetActivityIndicator();
_authingWithSso = authingWithSso ?? false;
_appOptions = appOptions;
_storageService = ServiceContainer.Resolve<IStorageService>("storageService");
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
_vm = BindingContext as TwoFactorPageViewModel;
_vm.Page = this;
_vm.TwoFactorAction = () => Device.BeginInvokeOnMainThread(async () => await TwoFactorAuthAsync());
_vm.StartSetPasswordAction = () =>
Device.BeginInvokeOnMainThread(async () => await StartSetPasswordAsync());
_vm.TwoFactorAuthSuccessAction = () =>
Device.BeginInvokeOnMainThread(async () => await TwoFactorAuthSuccessAsync());
_vm.CloseAction = async () => await Navigation.PopModalAsync();
DuoWebView = _duoWebView;
if (Device.RuntimePlatform == Device.Android)
@ -141,7 +148,7 @@ namespace Bit.App.Pages
}
}
private async void Close_Clicked(object sender, System.EventArgs e)
private void Close_Clicked(object sender, System.EventArgs e)
{
if (DoOnce())
{
@ -159,28 +166,29 @@ namespace Bit.App.Pages
}
}
}
private async Task TwoFactorAuthAsync()
private async Task StartSetPasswordAsync()
{
if (_appOptions != null)
_vm.CloseAction();
var page = new SetPasswordPage(_appOptions);
await Navigation.PushModalAsync(new NavigationPage(page));
}
private async Task TwoFactorAuthSuccessAsync()
{
if (_authingWithSso)
{
if (_appOptions.FromAutofillFramework && _appOptions.SaveType.HasValue)
Application.Current.MainPage = new NavigationPage(new LockPage(_appOptions));
}
else
{
if (AppHelpers.SetAlternateMainPage(_appOptions))
{
Application.Current.MainPage = new NavigationPage(new AddEditPage(appOptions: _appOptions));
return;
}
if (_appOptions.Uri != null)
{
Application.Current.MainPage = new NavigationPage(new AutofillCiphersPage(_appOptions));
return;
}
var previousPage = await AppHelpers.ClearPreviousPage();
Application.Current.MainPage = new TabsPage(_appOptions, previousPage);
}
var previousPage = await _storageService.GetAsync<PreviousPageInfo>(Constants.PreviousPageKey);
if (previousPage != null)
{
await _storageService.RemoveAsync(Constants.PreviousPageKey);
}
Application.Current.MainPage = new TabsPage(_appOptions, previousPage);
}
}
}

View file

@ -31,6 +31,7 @@ namespace Bit.App.Pages
private TwoFactorProviderType? _selectedProviderType;
private string _totpInstruction;
private string _webVaultUrl = "https://vault.bitwarden.com";
private bool _authingWithSso = false;
public TwoFactorPageViewModel()
{
@ -89,19 +90,21 @@ namespace Bit.App.Pages
});
}
public Command SubmitCommand { get; }
public Action TwoFactorAction { get; set; }
public Action TwoFactorAuthSuccessAction { get; set; }
public Action StartSetPasswordAction { get; set; }
public Action CloseAction { get; set; }
public void Init()
{
if (string.IsNullOrWhiteSpace(_authService.Email) ||
string.IsNullOrWhiteSpace(_authService.MasterPasswordHash) ||
if ((!_authService.AuthingWithSso() && !_authService.AuthingWithPassword()) ||
_authService.TwoFactorProvidersData == null)
{
// TODO: dismiss modal?
return;
}
_authingWithSso = _authService.AuthingWithSso();
if (!string.IsNullOrWhiteSpace(_environmentService.BaseUrl))
{
_webVaultUrl = _environmentService.BaseUrl;
@ -204,14 +207,21 @@ namespace Bit.App.Pages
try
{
await _deviceActionService.ShowLoadingAsync(AppResources.Validating);
await _authService.LogInTwoFactorAsync(SelectedProviderType.Value, Token, Remember);
await _deviceActionService.HideLoadingAsync();
var result = await _authService.LogInTwoFactorAsync(SelectedProviderType.Value, Token, Remember);
var task = Task.Run(() => _syncService.FullSyncAsync(true));
await _deviceActionService.HideLoadingAsync();
_messagingService.Send("listenYubiKeyOTP", false);
_broadcasterService.Unsubscribe(nameof(TwoFactorPage));
var disableFavicon = await _storageService.GetAsync<bool?>(Constants.DisableFaviconKey);
await _stateService.SaveAsync(Constants.DisableFaviconKey, disableFavicon.GetValueOrDefault());
TwoFactorAction?.Invoke();
if (_authingWithSso && result.ResetMasterPassword)
{
StartSetPasswordAction?.Invoke();
}
else
{
var disableFavicon = await _storageService.GetAsync<bool?>(Constants.DisableFaviconKey);
await _stateService.SaveAsync(Constants.DisableFaviconKey, disableFavicon.GetValueOrDefault());
TwoFactorAuthSuccessAction?.Invoke();
}
}
catch (ApiException e)
{

File diff suppressed because it is too large Load diff

View file

@ -1683,4 +1683,52 @@
<data name="EnableSyncOnRefreshDescription" xml:space="preserve">
<value>Syncing vault with pull down gesture.</value>
</data>
<data name="LogInSso" xml:space="preserve">
<value>Enterprise Single Sign-On</value>
</data>
<data name="LogInSsoSummary" xml:space="preserve">
<value>Quickly log in using your organization's single sign-on portal. Please enter your organization's identifier to begin.</value>
</data>
<data name="OrgIdentifier" xml:space="preserve">
<value>Organization Identifier</value>
</data>
<data name="LoginSsoError" xml:space="preserve">
<value>Currently unable to login with SSO</value>
</data>
<data name="SetMasterPassword" xml:space="preserve">
<value>Set Master Password</value>
</data>
<data name="SetMasterPasswordSummary" xml:space="preserve">
<value>In order to complete logging in with SSO, please set a master password to access and protect your vault.</value>
</data>
<data name="MasterPasswordPolicyInEffect" xml:space="preserve">
<value>One or more organization policies require your master password to meet the following requirements:</value>
</data>
<data name="PolicyInEffectMinComplexity" xml:space="preserve">
<value>Minimum complexity score of {0}</value>
</data>
<data name="PolicyInEffectMinLength" xml:space="preserve">
<value>Minimum length of {0}</value>
</data>
<data name="PolicyInEffectUppercase" xml:space="preserve">
<value>Contain one or more uppercase characters</value>
</data>
<data name="PolicyInEffectLowercase" xml:space="preserve">
<value>Contain one or more lowercase characters</value>
</data>
<data name="PolicyInEffectNumbers" xml:space="preserve">
<value>Contain one or more numbers</value>
</data>
<data name="PolicyInEffectSpecial" xml:space="preserve">
<value>Contain one or more of the following special characters: {0}</value>
</data>
<data name="MasterPasswordPolicyValidationTitle" xml:space="preserve">
<value>Invalid Password</value>
</data>
<data name="MasterPasswordPolicyValidationMessage" xml:space="preserve">
<value>Password does not meet organization requirements. Please check the policy information and try again.</value>
</data>
<data name="Loading" xml:space="preserve">
<value>Loading</value>
</data>
</root>

View file

@ -8,6 +8,7 @@ using Bit.Core.Utilities;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Bit.App.Models;
using Xamarin.Forms;
namespace Bit.App.Utilities
@ -192,5 +193,34 @@ namespace Bit.App.Utilities
}
}
}
public static bool SetAlternateMainPage(AppOptions appOptions)
{
if (appOptions != null)
{
if (appOptions.FromAutofillFramework && appOptions.SaveType.HasValue)
{
Application.Current.MainPage = new NavigationPage(new AddEditPage(appOptions: appOptions));
return true;
}
if (appOptions.Uri != null)
{
Application.Current.MainPage = new NavigationPage(new AutofillCiphersPage(appOptions));
return true;
}
}
return false;
}
public static async Task<PreviousPageInfo> ClearPreviousPage()
{
var storageService = ServiceContainer.Resolve<IStorageService>("storageService");
var previousPage = await storageService.GetAsync<PreviousPageInfo>(Constants.PreviousPageKey);
if (previousPage != null)
{
await storageService.RemoveAsync(Constants.PreviousPageKey);
}
return previousPage;
}
}
}

View file

@ -26,11 +26,13 @@ namespace Bit.Core.Abstractions
Task<ProfileResponse> GetProfileAsync();
Task<SyncResponse> GetSyncAsync();
Task PostAccountKeysAsync(KeysRequest request);
Task PostAccountVerifyPasswordAsync(PasswordVerificationRequest request);
Task<CipherResponse> PostCipherAsync(CipherRequest request);
Task<CipherResponse> PostCipherCreateAsync(CipherCreateRequest request);
Task<FolderResponse> PostFolderAsync(FolderRequest request);
Task<Tuple<IdentityTokenResponse, IdentityTwoFactorResponse>> PostIdentityTokenAsync(TokenRequest request);
Task PostPasswordHintAsync(PasswordHintRequest request);
Task SetPasswordAsync(SetPasswordRequest request);
Task<PreloginResponse> PostPreloginAsync(PreloginRequest request);
Task PostRegisterAsync(RegisterRequest request);
Task<CipherResponse> PutCipherAsync(string id, CipherRequest request);
@ -40,6 +42,7 @@ namespace Bit.Core.Abstractions
Task PutDeleteCipherAsync(string id);
Task PutRestoreCipherAsync(string id);
Task RefreshIdentityTokenAsync();
Task<object> PreValidateSso(string identifier);
Task<TResponse> SendAsync<TRequest, TResponse>(HttpMethod method, string path,
TRequest body, bool authed, bool hasResponse);
void SetUrls(EnvironmentUrls urls);

View file

@ -10,16 +10,22 @@ namespace Bit.Core.Abstractions
{
string Email { get; set; }
string MasterPasswordHash { get; set; }
string Code { get; set; }
string CodeVerifier { get; set; }
string SsoRedirectUrl { get; set; }
TwoFactorProviderType? SelectedTwoFactorProviderType { get; set; }
Dictionary<TwoFactorProviderType, TwoFactorProvider> TwoFactorProviders { get; set; }
Dictionary<TwoFactorProviderType, Dictionary<string, object>> TwoFactorProvidersData { get; set; }
TwoFactorProviderType? GetDefaultTwoFactorProvider(bool u2fSupported);
bool AuthingWithSso();
bool AuthingWithPassword();
List<TwoFactorProvider> GetSupportedTwoFactorProviders();
Task<AuthResult> LogInAsync(string email, string masterPassword);
Task<AuthResult> LogInSsoAsync(string code, string codeVerifier, string redirectUrl);
Task<AuthResult> LogInCompleteAsync(string email, string masterPassword, TwoFactorProviderType twoFactorProvider, string twoFactorToken, bool? remember = null);
Task<AuthResult> LogInTwoFactorAsync(TwoFactorProviderType twoFactorProvider, string twoFactorToken, bool? remember = null);
void LogOut(Action callback);
void Init();
}
}
}

View file

@ -15,7 +15,7 @@ namespace Bit.Core.Abstractions
Task<(PasswordGenerationOptions, PasswordGeneratorPolicyOptions)> GetOptionsAsync();
Task<(PasswordGenerationOptions, PasswordGeneratorPolicyOptions)>
EnforcePasswordGeneratorPoliciesOnOptionsAsync(PasswordGenerationOptions options);
Task<object> PasswordStrength(string password, List<string> userInputs = null);
Zxcvbn.Result PasswordStrength(string password, List<string> userInputs = null);
Task SaveOptionsAsync(PasswordGenerationOptions options);
void NormalizeOptions(PasswordGenerationOptions options, PasswordGeneratorPolicyOptions enforcedPolicyOptions);
}

View file

@ -12,5 +12,8 @@ namespace Bit.Core.Abstractions
Task<IEnumerable<Policy>> GetAll(PolicyType? type);
Task Replace(Dictionary<string, PolicyData> policies);
Task Clear(string userId);
Task<MasterPasswordPolicyOptions> GetMasterPasswordPolicyOptions(IEnumerable<Policy> policies = null);
Task<bool> EvaluateMasterPassword(int passwordStrength, string newPassword,
MasterPasswordPolicyOptions enforcedPolicyOptions);
}
}

View file

@ -27,6 +27,7 @@
<PackageReference Include="LiteDB" Version="5.0.8" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="PCLCrypto" Version="2.0.147" />
<PackageReference Include="zxcvbn-core" Version="2.1.44" />
</ItemGroup>
</Project>

View file

@ -6,6 +6,7 @@ namespace Bit.Core.Models.Domain
public class AuthResult
{
public bool TwoFactor { get; set; }
public bool ResetMasterPassword { get; set; }
public Dictionary<TwoFactorProviderType, Dictionary<string, object>> TwoFactorProviders { get; set; }
}
}

View file

@ -0,0 +1,22 @@
namespace Bit.Core.Models.Domain
{
public class MasterPasswordPolicyOptions
{
public int MinComplexity { get; set; }
public int MinLength { get; set; }
public bool RequireUpper { get; set; }
public bool RequireLower { get; set; }
public bool RequireNumbers { get; set; }
public bool RequireSpecial { get; set; }
public bool InEffect()
{
return MinComplexity > 0 ||
MinLength > 0 ||
RequireUpper ||
RequireLower ||
RequireNumbers ||
RequireSpecial;
}
}
}

View file

@ -0,0 +1,7 @@
namespace Bit.Core.Models.Request
{
public class PasswordVerificationRequest
{
public string MasterPasswordHash { get; set; }
}
}

View file

@ -0,0 +1,14 @@
using Bit.Core.Enums;
namespace Bit.Core.Models.Request
{
public class SetPasswordRequest
{
public string MasterPasswordHash { get; set; }
public string Key { get; set; }
public string MasterPasswordHint { get; set; }
public KeysRequest Keys { get; set; }
public KdfType Kdf { get; set; }
public int KdfIterations { get; set; }
}
}

View file

@ -9,21 +9,60 @@ namespace Bit.Core.Models.Request
{
public string Email { get; set; }
public string MasterPasswordHash { get; set; }
public string Code { get; set; }
public string CodeVerifier { get; set; }
public string RedirectUri { get; set; }
public string Token { get; set; }
public TwoFactorProviderType? Provider { get; set; }
public bool Remember { get; set; }
public bool? Remember { get; set; }
public DeviceRequest Device { get; set; }
public TokenRequest(string[] credentials, string[] codes, TwoFactorProviderType? provider, string token,
bool? remember, DeviceRequest device = null)
{
if (credentials != null && credentials.Length > 1)
{
Email = credentials[0];
MasterPasswordHash = credentials[1];
}
else if (codes != null && codes.Length > 2)
{
Code = codes[0];
CodeVerifier = codes[1];
RedirectUri = codes[2];
}
Token = token;
Provider = provider;
Remember = remember;
Device = device;
}
public Dictionary<string, string> ToIdentityToken(string clientId)
{
var obj = new Dictionary<string, string>
{
["grant_type"] = "password",
["username"] = Email,
["password"] = MasterPasswordHash,
["scope"] = "api offline_access",
["client_id"] = clientId
};
if (MasterPasswordHash != null && Email != null)
{
obj.Add("grant_type", "password");
obj.Add("username", Email);
obj.Add("password", MasterPasswordHash);
}
else if (Code != null && CodeVerifier != null && RedirectUri != null)
{
obj.Add("grant_type", "authorization_code");
obj.Add("code", Code);
obj.Add("code_verifier", CodeVerifier);
obj.Add("redirect_uri", RedirectUri);
}
else
{
throw new Exception("must provide credentials or codes");
}
if (Device != null)
{
obj.Add("deviceType", ((int)Device.Type).ToString());
@ -31,11 +70,11 @@ namespace Bit.Core.Models.Request
obj.Add("deviceName", Device.Name);
obj.Add("devicePushToken", Device.PushToken);
}
if (!string.IsNullOrWhiteSpace(Token) && Provider != null)
if (!string.IsNullOrWhiteSpace(Token) && Provider != null && Remember.HasValue)
{
obj.Add("twoFactorToken", Token);
obj.Add("twoFactorProvider", ((int)Provider.Value).ToString());
obj.Add("twoFactorRemember", Remember ? "1" : "0");
obj.Add("twoFactorRemember", Remember.GetValueOrDefault() ? "1" : "0");
}
return obj;
}

View file

@ -1,4 +1,5 @@
using Newtonsoft.Json;
using Bit.Core.Enums;
using Newtonsoft.Json;
namespace Bit.Core.Models.Response
{
@ -12,8 +13,12 @@ namespace Bit.Core.Models.Response
public string RefreshToken { get; set; }
[JsonProperty("token_type")]
public string TokenType { get; set; }
public bool ResetMasterPassword { get; set; }
public string PrivateKey { get; set; }
public string Key { get; set; }
public string TwoFactorToken { get; set; }
public KdfType Kdf { get; set; }
public int KdfIterations { get; set; }
}
}

View file

@ -165,6 +165,12 @@ namespace Bit.Core.Services
request, false, false);
}
public Task SetPasswordAsync(SetPasswordRequest request)
{
return SendAsync<SetPasswordRequest, object>(HttpMethod.Post, "/accounts/set-password", request, true,
false);
}
public Task PostRegisterAsync(RegisterRequest request)
{
return SendAsync<RegisterRequest, object>(HttpMethod.Post, "/accounts/register", request, false, false);
@ -175,6 +181,12 @@ namespace Bit.Core.Services
return SendAsync<KeysRequest, object>(HttpMethod.Post, "/accounts/keys", request, true, false);
}
public Task PostAccountVerifyPasswordAsync(PasswordVerificationRequest request)
{
return SendAsync<PasswordVerificationRequest, object>(HttpMethod.Post, "/accounts/verify-password", request,
true, false);
}
#endregion
#region Folder APIs
@ -365,6 +377,34 @@ namespace Bit.Core.Services
return accessToken;
}
public async Task<object> PreValidateSso(string identifier)
{
var path = "/account/prevalidate?domainHint=" + WebUtility.UrlEncode(identifier);
using (var requestMessage = new HttpRequestMessage())
{
requestMessage.Version = new Version(1, 0);
requestMessage.Method = HttpMethod.Get;
requestMessage.RequestUri = new Uri(string.Concat(IdentityBaseUrl, path));
requestMessage.Headers.Add("Accept", "application/json");
HttpResponseMessage response;
try
{
response = await _httpClient.SendAsync(requestMessage);
}
catch (Exception e)
{
throw new ApiException(HandleWebError(e));
}
if (!response.IsSuccessStatusCode)
{
var error = await HandleErrorAsync(response, false);
throw new ApiException(error);
}
return null;
}
}
public async Task<TResponse> SendAsync<TRequest, TResponse>(HttpMethod method, string path, TRequest body,
bool authed, bool hasResponse)
{
@ -488,13 +528,20 @@ namespace Bit.Core.Services
await _logoutCallbackAsync(true);
return null;
}
JObject responseJObject = null;
if (IsJsonResponse(response))
try
{
var responseJsonString = await response.Content.ReadAsStringAsync();
responseJObject = JObject.Parse(responseJsonString);
JObject responseJObject = null;
if (IsJsonResponse(response))
{
var responseJsonString = await response.Content.ReadAsStringAsync();
responseJObject = JObject.Parse(responseJsonString);
}
return new ErrorResponse(responseJObject, response.StatusCode, tokenError);
}
catch
{
return null;
}
return new ErrorResponse(responseJObject, response.StatusCode, tokenError);
}
private bool IsJsonResponse(HttpResponseMessage response)

View file

@ -23,8 +23,6 @@ namespace Bit.Core.Services
private readonly bool _setCryptoKeys;
private SymmetricCryptoKey _key;
private KdfType? _kdf;
private int? _kdfIterations;
public AuthService(
ICryptoService cryptoService,
@ -95,6 +93,9 @@ namespace Bit.Core.Services
public string Email { get; set; }
public string MasterPasswordHash { get; set; }
public string Code { get; set; }
public string CodeVerifier { get; set; }
public string SsoRedirectUrl { get; set; }
public Dictionary<TwoFactorProviderType, TwoFactorProvider> TwoFactorProviders { get; set; }
public Dictionary<TwoFactorProviderType, Dictionary<string, object>> TwoFactorProvidersData { get; set; }
public TwoFactorProviderType? SelectedTwoFactorProviderType { get; set; }
@ -122,13 +123,20 @@ namespace Bit.Core.Services
SelectedTwoFactorProviderType = null;
var key = await MakePreloginKeyAsync(masterPassword, email);
var hashedPassword = await _cryptoService.HashPasswordAsync(masterPassword, key);
return await LogInHelperAsync(email, hashedPassword, key);
return await LogInHelperAsync(email, hashedPassword, null, null, null, key, null, null, null);
}
public async Task<AuthResult> LogInSsoAsync(string code, string codeVerifier, string redirectUrl)
{
SelectedTwoFactorProviderType = null;
return await LogInHelperAsync(null, null, code, codeVerifier, redirectUrl, null, null, null, null);
}
public Task<AuthResult> LogInTwoFactorAsync(TwoFactorProviderType twoFactorProvider, string twoFactorToken,
bool? remember = null)
{
return LogInHelperAsync(Email, MasterPasswordHash, _key, twoFactorProvider, twoFactorToken, remember);
return LogInHelperAsync(Email, MasterPasswordHash, Code, CodeVerifier, SsoRedirectUrl, _key,
twoFactorProvider, twoFactorToken, remember);
}
public async Task<AuthResult> LogInCompleteAsync(string email, string masterPassword,
@ -137,7 +145,16 @@ namespace Bit.Core.Services
SelectedTwoFactorProviderType = null;
var key = await MakePreloginKeyAsync(masterPassword, email);
var hashedPassword = await _cryptoService.HashPasswordAsync(masterPassword, key);
return await LogInHelperAsync(email, hashedPassword, key, twoFactorProvider, twoFactorToken, remember);
return await LogInHelperAsync(email, hashedPassword, null, null, null, key, twoFactorProvider,
twoFactorToken, remember);
}
public async Task<AuthResult> LogInSsoCompleteAsync(string code, string codeVerifier, string redirectUrl,
TwoFactorProviderType twoFactorProvider, string twoFactorToken, bool? remember = null)
{
SelectedTwoFactorProviderType = null;
return await LogInHelperAsync(null, null, code, codeVerifier, redirectUrl, null, twoFactorProvider,
twoFactorToken, remember);
}
public void LogOut(Action callback)
@ -213,20 +230,30 @@ namespace Bit.Core.Services
return providerType;
}
public bool AuthingWithSso()
{
return Code != null && CodeVerifier != null && SsoRedirectUrl != null;
}
public bool AuthingWithPassword()
{
return Email != null && MasterPasswordHash != null;
}
// Helpers
private async Task<SymmetricCryptoKey> MakePreloginKeyAsync(string masterPassword, string email)
{
email = email.Trim().ToLower();
_kdf = null;
_kdfIterations = null;
KdfType? kdf = null;
int? kdfIterations = null;
try
{
var preloginResponse = await _apiService.PostPreloginAsync(new PreloginRequest { Email = email });
if (preloginResponse != null)
{
_kdf = preloginResponse.Kdf;
_kdfIterations = preloginResponse.KdfIterations;
kdf = preloginResponse.Kdf;
kdfIterations = preloginResponse.KdfIterations;
}
}
catch (ApiException e)
@ -236,46 +263,64 @@ namespace Bit.Core.Services
throw e;
}
}
return await _cryptoService.MakeKeyAsync(masterPassword, email, _kdf, _kdfIterations);
return await _cryptoService.MakeKeyAsync(masterPassword, email, kdf, kdfIterations);
}
private async Task<AuthResult> LogInHelperAsync(string email, string hashedPassword, SymmetricCryptoKey key,
private async Task<AuthResult> LogInHelperAsync(string email, string hashedPassword, string code,
string codeVerifier, string redirectUrl, SymmetricCryptoKey key,
TwoFactorProviderType? twoFactorProvider = null, string twoFactorToken = null, bool? remember = null)
{
var storedTwoFactorToken = await _tokenService.GetTwoFactorTokenAsync(email);
var appId = await _appIdService.GetAppIdAsync();
var deviceRequest = new DeviceRequest(appId, _platformUtilsService);
var request = new TokenRequest
string[] emailPassword;
string[] codeCodeVerifier;
if (email != null && hashedPassword != null)
{
Email = email,
MasterPasswordHash = hashedPassword,
Device = deviceRequest,
Remember = false
};
emailPassword = new[] { email, hashedPassword };
}
else
{
emailPassword = null;
}
if (code != null && codeVerifier != null && redirectUrl != null)
{
codeCodeVerifier = new[] { code, codeVerifier, redirectUrl };
}
else
{
codeCodeVerifier = null;
}
TokenRequest request;
if (twoFactorToken != null && twoFactorProvider != null)
{
request.Provider = twoFactorProvider;
request.Token = twoFactorToken;
request.Remember = remember.GetValueOrDefault();
request = new TokenRequest(emailPassword, codeCodeVerifier, twoFactorProvider, twoFactorToken, remember,
deviceRequest);
}
else if (storedTwoFactorToken != null)
{
request.Provider = TwoFactorProviderType.Remember;
request.Token = storedTwoFactorToken;
request = new TokenRequest(emailPassword, codeCodeVerifier, TwoFactorProviderType.Remember,
storedTwoFactorToken, false, deviceRequest);
}
else
{
request = new TokenRequest(emailPassword, codeCodeVerifier, null, null, false, deviceRequest);
}
var response = await _apiService.PostIdentityTokenAsync(request);
ClearState();
var result = new AuthResult
{
TwoFactor = response.Item2 != null
};
var result = new AuthResult { TwoFactor = response.Item2 != null };
if (result.TwoFactor)
{
// Two factor required.
var twoFactorResponse = response.Item2;
Email = email;
MasterPasswordHash = hashedPassword;
Code = code;
CodeVerifier = codeVerifier;
SsoRedirectUrl = redirectUrl;
_key = _setCryptoKeys ? key : null;
TwoFactorProvidersData = twoFactorResponse.TwoFactorProviders2;
result.TwoFactorProviders = twoFactorResponse.TwoFactorProviders2;
@ -283,13 +328,14 @@ namespace Bit.Core.Services
}
var tokenResponse = response.Item1;
result.ResetMasterPassword = tokenResponse.ResetMasterPassword;
if (tokenResponse.TwoFactorToken != null)
{
await _tokenService.SetTwoFactorTokenAsync(tokenResponse.TwoFactorToken, email);
}
await _tokenService.SetTokensAsync(tokenResponse.AccessToken, tokenResponse.RefreshToken);
await _userService.SetInformationAsync(_tokenService.GetUserId(), _tokenService.GetEmail(),
_kdf.Value, _kdfIterations.Value);
tokenResponse.Kdf, tokenResponse.KdfIterations);
if (_setCryptoKeys)
{
await _cryptoService.SetKeyAsync(key);
@ -322,8 +368,12 @@ namespace Bit.Core.Services
private void ClearState()
{
_key = null;
Email = null;
MasterPasswordHash = null;
Code = null;
CodeVerifier = null;
SsoRedirectUrl = null;
TwoFactorProvidersData = null;
SelectedTwoFactorProviderType = null;
}

View file

@ -8,6 +8,7 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Bit.Core.Enums;
using Zxcvbn;
namespace Bit.Core.Services
{
@ -481,9 +482,28 @@ namespace Bit.Core.Services
await _storageService.RemoveAsync(Keys_History);
}
public Task<object> PasswordStrength(string password, List<string> userInputs = null)
public Result PasswordStrength(string password, List<string> userInputs = null)
{
throw new NotImplementedException();
if (string.IsNullOrEmpty(password))
{
return null;
}
var globalUserInputs = new List<string>
{
"bitwarden",
"bit",
"warden"
};
if (userInputs != null && userInputs.Any())
{
globalUserInputs.AddRange(userInputs);
}
// Use a hash set to get rid of any duplicate user inputs
var hashSet = new HashSet<string>(globalUserInputs);
var finalUserInputs = new string[hashSet.Count];
hashSet.CopyTo(finalUserInputs);
var result = Zxcvbn.Zxcvbn.MatchPassword(password, finalUserInputs);
return result;
}
public void NormalizeOptions(PasswordGenerationOptions options,

View file

@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
@ -66,5 +67,156 @@ namespace Bit.Core.Services
await _storageService.RemoveAsync(string.Format(Keys_PoliciesPrefix, userId));
_policyCache = null;
}
public async Task<MasterPasswordPolicyOptions> GetMasterPasswordPolicyOptions(
IEnumerable<Policy> policies = null)
{
MasterPasswordPolicyOptions enforcedOptions = null;
if (policies == null)
{
policies = await GetAll(PolicyType.MasterPassword);
}
else
{
policies = policies.Where(p => p.Type == PolicyType.MasterPassword);
}
if (policies == null || !policies.Any())
{
return enforcedOptions;
}
foreach (var currentPolicy in policies)
{
if (!currentPolicy.Enabled || currentPolicy.Data == null)
{
continue;
}
if (enforcedOptions == null)
{
enforcedOptions = new MasterPasswordPolicyOptions();
}
var minComplexity = GetPolicyInt(currentPolicy, "minComplexity");
if (minComplexity != null && (int)(long)minComplexity > enforcedOptions.MinComplexity)
{
enforcedOptions.MinComplexity = (int)(long)minComplexity;
}
var minLength = GetPolicyInt(currentPolicy, "minLength");
if (minLength != null && (int)(long)minLength > enforcedOptions.MinLength)
{
enforcedOptions.MinLength = (int)(long)minLength;
}
var requireUpper = GetPolicyBool(currentPolicy, "requireUpper");
if (requireUpper != null && (bool)requireUpper)
{
enforcedOptions.RequireUpper = true;
}
var requireLower = GetPolicyBool(currentPolicy, "requireLower");
if (requireLower != null && (bool)requireLower)
{
enforcedOptions.RequireLower = true;
}
var requireNumbers = GetPolicyBool(currentPolicy, "requireNumbers");
if (requireNumbers != null && (bool)requireNumbers)
{
enforcedOptions.RequireNumbers = true;
}
var requireSpecial = GetPolicyBool(currentPolicy, "requireSpecial");
if (requireSpecial != null && (bool)requireSpecial)
{
enforcedOptions.RequireSpecial = true;
}
}
return enforcedOptions;
}
public async Task<bool> EvaluateMasterPassword(int passwordStrength, string newPassword,
MasterPasswordPolicyOptions enforcedPolicyOptions)
{
if (enforcedPolicyOptions == null)
{
return true;
}
if (enforcedPolicyOptions.MinComplexity > 0 && enforcedPolicyOptions.MinComplexity > passwordStrength)
{
return false;
}
if (enforcedPolicyOptions.MinLength > 0 && enforcedPolicyOptions.MinLength > newPassword.Length)
{
return false;
}
if (enforcedPolicyOptions.RequireUpper && newPassword.ToLower() == newPassword)
{
return false;
}
if (enforcedPolicyOptions.RequireLower && newPassword.ToUpper() == newPassword)
{
return false;
}
if (enforcedPolicyOptions.RequireNumbers && !newPassword.Any(char.IsDigit))
{
return false;
}
if (enforcedPolicyOptions.RequireSpecial && !Regex.IsMatch(newPassword, "^.*[!@#$%\\^&*].*$"))
{
return false;
}
return true;
}
private int? GetPolicyInt(Policy policy, string key)
{
if (policy.Data.ContainsKey(key))
{
var value = policy.Data[key];
if (value != null)
{
return (int)(long)value;
}
}
return null;
}
private bool? GetPolicyBool(Policy policy, string key)
{
if (policy.Data.ContainsKey(key))
{
var value = policy.Data[key];
if (value != null)
{
return (bool)value;
}
}
return null;
}
private string GetPolicyString(Policy policy, string key)
{
if (policy.Data.ContainsKey(key))
{
var value = policy.Data[key];
if (value != null)
{
return (string)value;
}
}
return null;
}
}
}

View file

@ -56,7 +56,7 @@ namespace Bit.iOS.Autofill
}
if (!await IsAuthed())
{
LaunchLoginFlow();
LaunchHomePage();
}
else if (await IsLocked())
{
@ -94,7 +94,7 @@ namespace Bit.iOS.Autofill
InitAppIfNeeded();
if (!await IsAuthed())
{
LaunchLoginFlow();
LaunchHomePage();
return;
}
_context.CredentialIdentity = credentialIdentity;
@ -107,7 +107,7 @@ namespace Bit.iOS.Autofill
_context.Configuring = true;
if (!await IsAuthed())
{
LaunchLoginFlow();
LaunchHomePage();
return;
}
CheckLock(() => PerformSegue("setupSegue", this));
@ -294,17 +294,74 @@ namespace Bit.iOS.Autofill
}
}
private void LaunchLoginFlow()
private void LaunchHomePage()
{
var loginPage = new LoginPage();
var homePage = new HomePage();
var app = new App.App(new AppOptions { IosExtension = true });
ThemeManager.SetTheme(false, app.Resources);
ThemeManager.ApplyResourcesToPage(homePage);
if (homePage.BindingContext is HomeViewModel vm)
{
vm.StartLoginAction = () => DismissViewController(false, () => LaunchLoginFlow());
vm.StartRegisterAction = () => DismissViewController(false, () => LaunchRegisterFlow());
vm.StartSsoLoginAction = () => DismissViewController(false, () => LaunchLoginSsoFlow());
vm.StartEnvironmentAction = () => DismissViewController(false, () => LaunchEnvironmentFlow());
vm.CloseAction = () => CompleteRequest();
}
var navigationPage = new NavigationPage(homePage);
var loginController = navigationPage.CreateViewController();
loginController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
PresentViewController(loginController, true, null);
}
private void LaunchEnvironmentFlow()
{
var environmentPage = new EnvironmentPage();
var app = new App.App(new AppOptions { IosExtension = true });
ThemeManager.SetTheme(false, app.Resources);
ThemeManager.ApplyResourcesToPage(environmentPage);
if (environmentPage.BindingContext is EnvironmentPageViewModel vm)
{
vm.SubmitSuccessAction = () => DismissViewController(false, () => LaunchHomePage());
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage());
}
var navigationPage = new NavigationPage(environmentPage);
var loginController = navigationPage.CreateViewController();
loginController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
PresentViewController(loginController, true, null);
}
private void LaunchRegisterFlow()
{
var registerPage = new RegisterPage(null);
var app = new App.App(new AppOptions { IosExtension = true });
ThemeManager.SetTheme(false, app.Resources);
ThemeManager.ApplyResourcesToPage(registerPage);
if (registerPage.BindingContext is RegisterPageViewModel vm)
{
vm.RegistrationSuccess = () => DismissViewController(false, () => LaunchLoginFlow(vm.Email));
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage());
}
var navigationPage = new NavigationPage(registerPage);
var loginController = navigationPage.CreateViewController();
loginController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
PresentViewController(loginController, true, null);
}
private void LaunchLoginFlow(string email = null)
{
var loginPage = new LoginPage(email);
var app = new App.App(new AppOptions { IosExtension = true });
ThemeManager.SetTheme(false, app.Resources);
ThemeManager.ApplyResourcesToPage(loginPage);
if (loginPage.BindingContext is LoginPageViewModel vm)
{
vm.StartTwoFactorAction = () => DismissViewController(false, () => LaunchTwoFactorFlow());
vm.LoggedInAction = () => DismissLockAndContinue();
vm.CloseAction = () => CompleteRequest();
vm.StartTwoFactorAction = () => DismissViewController(false, () => LaunchTwoFactorFlow(false));
vm.LogInSuccessAction = () => DismissLockAndContinue();
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage());
vm.HideHintButton = true;
}
@ -314,16 +371,44 @@ namespace Bit.iOS.Autofill
PresentViewController(loginController, true, null);
}
private void LaunchTwoFactorFlow()
private void LaunchLoginSsoFlow()
{
var twoFactorPage = new TwoFactorPage();
var loginPage = new LoginSsoPage();
var app = new App.App(new AppOptions { IosExtension = true });
ThemeManager.SetTheme(false, app.Resources);
ThemeManager.ApplyResourcesToPage(loginPage);
if (loginPage.BindingContext is LoginSsoPageViewModel vm)
{
vm.StartTwoFactorAction = () => DismissViewController(false, () => LaunchTwoFactorFlow(true));
vm.StartSetPasswordAction = () => DismissViewController(false, () => LaunchSetPasswordFlow());
vm.SsoAuthSuccessAction = () => DismissLockAndContinue();
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage());
}
var navigationPage = new NavigationPage(loginPage);
var loginController = navigationPage.CreateViewController();
loginController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
PresentViewController(loginController, true, null);
}
private void LaunchTwoFactorFlow(bool authingWithSso)
{
var twoFactorPage = new TwoFactorPage(authingWithSso);
var app = new App.App(new AppOptions { IosExtension = true });
ThemeManager.SetTheme(false, app.Resources);
ThemeManager.ApplyResourcesToPage(twoFactorPage);
if (twoFactorPage.BindingContext is TwoFactorPageViewModel vm)
{
vm.TwoFactorAction = () => DismissLockAndContinue();
vm.CloseAction = () => DismissViewController(false, () => LaunchLoginFlow());
vm.TwoFactorAuthSuccessAction = () => DismissLockAndContinue();
vm.StartSetPasswordAction = () => DismissViewController(false, () => LaunchSetPasswordFlow());
if (authingWithSso)
{
vm.CloseAction = () => DismissViewController(false, () => LaunchLoginSsoFlow());
}
else
{
vm.CloseAction = () => DismissViewController(false, () => LaunchLoginFlow());
}
}
var navigationPage = new NavigationPage(twoFactorPage);
@ -331,5 +416,23 @@ namespace Bit.iOS.Autofill
twoFactorController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
PresentViewController(twoFactorController, true, null);
}
private void LaunchSetPasswordFlow()
{
var setPasswordPage = new SetPasswordPage();
var app = new App.App(new AppOptions { IosExtension = true });
ThemeManager.SetTheme(false, app.Resources);
ThemeManager.ApplyResourcesToPage(setPasswordPage);
if (setPasswordPage.BindingContext is SetPasswordPageViewModel vm)
{
vm.SetPasswordSuccessAction = () => DismissLockAndContinue();
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage());
}
var navigationPage = new NavigationPage(setPasswordPage);
var setPasswordController = navigationPage.CreateViewController();
setPasswordController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
PresentViewController(setPasswordController, true, null);
}
}
}

View file

@ -73,7 +73,7 @@ namespace Bit.iOS.Extension
}
if (!await IsAuthed())
{
LaunchLoginFlow();
LaunchHomePage();
return;
}
else if (await IsLocked())
@ -420,16 +420,73 @@ namespace Bit.iOS.Extension
return userService.IsAuthenticatedAsync();
}
private void LaunchLoginFlow()
private void LaunchHomePage()
{
var loginPage = new LoginPage();
var homePage = new HomePage();
var app = new App.App(new AppOptions { IosExtension = true });
ThemeManager.SetTheme(false, app.Resources);
ThemeManager.ApplyResourcesToPage(homePage);
if (homePage.BindingContext is HomeViewModel vm)
{
vm.StartLoginAction = () => DismissViewController(false, () => LaunchLoginFlow());
vm.StartRegisterAction = () => DismissViewController(false, () => LaunchRegisterFlow());
vm.StartSsoLoginAction = () => DismissViewController(false, () => LaunchLoginSsoFlow());
vm.StartEnvironmentAction = () => DismissViewController(false, () => LaunchEnvironmentFlow());
vm.CloseAction = () => CompleteRequest(null, null);
}
var navigationPage = new NavigationPage(homePage);
var loginController = navigationPage.CreateViewController();
loginController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
PresentViewController(loginController, true, null);
}
private void LaunchEnvironmentFlow()
{
var environmentPage = new EnvironmentPage();
var app = new App.App(new AppOptions { IosExtension = true });
ThemeManager.SetTheme(false, app.Resources);
ThemeManager.ApplyResourcesToPage(environmentPage);
if (environmentPage.BindingContext is EnvironmentPageViewModel vm)
{
vm.SubmitSuccessAction = () => DismissViewController(false, () => LaunchHomePage());
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage());
}
var navigationPage = new NavigationPage(environmentPage);
var loginController = navigationPage.CreateViewController();
loginController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
PresentViewController(loginController, true, null);
}
private void LaunchRegisterFlow()
{
var registerPage = new RegisterPage(null);
var app = new App.App(new AppOptions { IosExtension = true });
ThemeManager.SetTheme(false, app.Resources);
ThemeManager.ApplyResourcesToPage(registerPage);
if (registerPage.BindingContext is RegisterPageViewModel vm)
{
vm.RegistrationSuccess = () => DismissViewController(false, () => LaunchLoginFlow(vm.Email));
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage());
}
var navigationPage = new NavigationPage(registerPage);
var loginController = navigationPage.CreateViewController();
loginController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
PresentViewController(loginController, true, null);
}
private void LaunchLoginFlow(string email = null)
{
var loginPage = new LoginPage(email);
var app = new App.App(new AppOptions { IosExtension = true });
ThemeManager.SetTheme(false, app.Resources);
ThemeManager.ApplyResourcesToPage(loginPage);
if (loginPage.BindingContext is LoginPageViewModel vm)
{
vm.StartTwoFactorAction = () => DismissViewController(false, () => LaunchTwoFactorFlow());
vm.LoggedInAction = () => DismissLockAndContinue();
vm.StartTwoFactorAction = () => DismissViewController(false, () => LaunchTwoFactorFlow(false));
vm.LogInSuccessAction = () => DismissLockAndContinue();
vm.CloseAction = () => CompleteRequest(null, null);
vm.HideHintButton = true;
}
@ -440,7 +497,27 @@ namespace Bit.iOS.Extension
PresentViewController(loginController, true, null);
}
private void LaunchTwoFactorFlow()
private void LaunchLoginSsoFlow()
{
var loginPage = new LoginSsoPage();
var app = new App.App(new AppOptions { IosExtension = true });
ThemeManager.SetTheme(false, app.Resources);
ThemeManager.ApplyResourcesToPage(loginPage);
if (loginPage.BindingContext is LoginSsoPageViewModel vm)
{
vm.StartTwoFactorAction = () => DismissViewController(false, () => LaunchTwoFactorFlow(true));
vm.StartSetPasswordAction = () => DismissViewController(false, () => LaunchSetPasswordFlow());
vm.SsoAuthSuccessAction = () => DismissLockAndContinue();
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage());
}
var navigationPage = new NavigationPage(loginPage);
var loginController = navigationPage.CreateViewController();
loginController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
PresentViewController(loginController, true, null);
}
private void LaunchTwoFactorFlow(bool authingWithSso)
{
var twoFactorPage = new TwoFactorPage();
var app = new App.App(new AppOptions { IosExtension = true });
@ -448,8 +525,16 @@ namespace Bit.iOS.Extension
ThemeManager.ApplyResourcesToPage(twoFactorPage);
if (twoFactorPage.BindingContext is TwoFactorPageViewModel vm)
{
vm.TwoFactorAction = () => DismissLockAndContinue();
vm.CloseAction = () => DismissViewController(false, () => LaunchLoginFlow());
vm.TwoFactorAuthSuccessAction = () => DismissLockAndContinue();
vm.StartSetPasswordAction = () => DismissViewController(false, () => LaunchSetPasswordFlow());
if (authingWithSso)
{
vm.CloseAction = () => DismissViewController(false, () => LaunchLoginSsoFlow());
}
else
{
vm.CloseAction = () => DismissViewController(false, () => LaunchLoginFlow());
}
}
var navigationPage = new NavigationPage(twoFactorPage);
@ -457,5 +542,23 @@ namespace Bit.iOS.Extension
twoFactorController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
PresentViewController(twoFactorController, true, null);
}
private void LaunchSetPasswordFlow()
{
var setPasswordPage = new SetPasswordPage();
var app = new App.App(new AppOptions { IosExtension = true });
ThemeManager.SetTheme(false, app.Resources);
ThemeManager.ApplyResourcesToPage(setPasswordPage);
if (setPasswordPage.BindingContext is SetPasswordPageViewModel vm)
{
vm.SetPasswordSuccessAction = () => DismissLockAndContinue();
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage());
}
var navigationPage = new NavigationPage(setPasswordPage);
var setPasswordController = navigationPage.CreateViewController();
setPasswordController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
PresentViewController(setPasswordController, true, null);
}
}
}

View file

@ -241,6 +241,15 @@ namespace Bit.iOS
return true;
}
public override bool OpenUrl(UIApplication app, NSUrl url, NSDictionary options)
{
if (Xamarin.Essentials.Platform.OpenUrl(app, url, options))
{
return true;
}
return base.OpenUrl(app, url, options);
}
public override void FailedToRegisterForRemoteNotifications(UIApplication application, NSError error)
{
_pushHandler?.OnErrorReceived(error);

View file

@ -156,7 +156,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Xamarin.Essentials">
<Version>1.5.3.1</Version>
<Version>1.5.3.2</Version>
</PackageReference>
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath)\Xamarin\iOS\Xamarin.iOS.CSharp.targets" />