[KeyConnector] Add support for key connector OTP (#1633)

* initial commit
- add UsesKeyConnector to UserService
- add models
- begin work on authentication

* finish auth workflow for key connector sso login
- finish api call for get user key
- start api calls for posts to key connector

* Bypass lock page if already unlocked

* Move logic to KeyConnectorService, log out if no pin or biometric is set

* Disable password reprompt when using key connector

* hide password reprompt checkbox when editing or adding cipher

* add PostUserKey and PostSetKeyConnector calls

* add ConvertMasterPasswordPage

* add functionality to RemoveMasterPasswordPage
- rename Convert to Remove

* Hide Change Master Password button if using key connector

* Add OTP verification for export component

* Update src/App/Pages/Vault/AddEditPage.xaml.cs

Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>

* remove toolbar item "close"

* Update src/Core/Models/Request/KeyConnectorUserKeyRequest.cs

Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>

* remove new line in resource string
- format warning as two labels
- set label in code behind for loading simultaneously

* implement GetAndSetKey in KeyConnectorService
- ignore EnvironmentService call

* remove unnecesary orgIdentifier

* move RemoveMasterPasswordPage call to LockPage

* add spacing to export vault page

* log out if no PIN or bio on lock page with key connector

* Delete excessive whitespace

* Delete excessive whitespace

* Change capitalisation of OTP

* add default value to models for backwards compatibility

* remove this keyword

* actually handle exceptions

* move RemoveMasterPasswordPage to TabPage using messaging service

* add minor improvements

* remove 'this.'

Co-authored-by: Hinton <oscar@oscarhinton.com>
Co-authored-by: Thomas Rittson <trittson@bitwarden.com>
Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
This commit is contained in:
Jake Fink 2021-11-10 20:46:48 -05:00 committed by GitHub
parent 90b62d61ae
commit 13869b5a1b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 869 additions and 74 deletions

View file

@ -7,5 +7,7 @@ namespace Bit.App.Abstractions
string[] ProtectedFields { get; }
Task<bool> ShowPasswordPromptAsync();
Task<bool> Enabled();
}
}

View file

@ -140,6 +140,15 @@ namespace Bit.App
}
});
}
else if (message.Command == "convertAccountToKeyConnector")
{
Device.BeginInvokeOnMainThread(async () =>
{
await Application.Current.MainPage.Navigation.PushModalAsync(
new NavigationPage(new RemoveMasterPasswordPage()));
});
}
});
}

View file

@ -11,7 +11,6 @@ namespace Bit.App.Pages
{
public partial class LockPage : BaseContentPage
{
private readonly IStorageService _storageService;
private readonly AppOptions _appOptions;
private readonly bool _autoPromptBiometric;
private readonly LockPageViewModel _vm;
@ -21,7 +20,6 @@ namespace Bit.App.Pages
public LockPage(AppOptions appOptions = null, bool autoPromptBiometric = true)
{
_storageService = ServiceContainer.Resolve<IStorageService>("storageService");
_appOptions = appOptions;
_autoPromptBiometric = autoPromptBiometric;
InitializeComponent();
@ -130,6 +128,7 @@ namespace Bit.App.Pages
return;
}
var previousPage = await AppHelpers.ClearPreviousPage();
Application.Current.MainPage = new TabsPage(_appOptions, previousPage);
}
}

View file

@ -28,6 +28,7 @@ namespace Bit.App.Pages
private readonly IEnvironmentService _environmentService;
private readonly IStateService _stateService;
private readonly IBiometricService _biometricService;
private readonly IKeyConnectorService _keyConnectorService;
private string _email;
private bool _showPassword;
@ -54,6 +55,7 @@ namespace Bit.App.Pages
_environmentService = ServiceContainer.Resolve<IEnvironmentService>("environmentService");
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
_biometricService = ServiceContainer.Resolve<IBiometricService>("biometricService");
_keyConnectorService = ServiceContainer.Resolve<IKeyConnectorService>("keyConnectorService");
PageTitle = AppResources.VerifyMasterPassword;
TogglePasswordCommand = new Command(TogglePassword);
@ -124,6 +126,12 @@ namespace Bit.App.Pages
_pinSet = await _vaultTimeoutService.IsPinLockSetAsync();
PinLock = (_pinSet.Item1 && _vaultTimeoutService.PinProtectedKey != null) || _pinSet.Item2;
BiometricLock = await _vaultTimeoutService.IsBiometricLockSetAsync() && await _cryptoService.HasKeyAsync();
// Users with key connector and without biometric or pin has no MP to unlock with
if (await _keyConnectorService.GetUsesKeyConnector() && !(BiometricLock || PinLock))
{
await _vaultTimeoutService.LogOutAsync();
}
_email = await _userService.GetEmailAsync();
var webVault = _environmentService.GetWebVaultUrl();
if (string.IsNullOrWhiteSpace(webVault))

View file

@ -116,7 +116,14 @@ namespace Bit.App.Pages
{
RestoreAppOptionsFromCopy();
await AppHelpers.ClearPreviousPage();
Application.Current.MainPage = new NavigationPage(new LockPage(_appOptions));
if (await _vaultTimeoutService.IsLockedAsync())
{
Application.Current.MainPage = new NavigationPage(new LockPage(_appOptions));
}
else
{
Application.Current.MainPage = new TabsPage(_appOptions, null);
}
}
}
}

View file

@ -0,0 +1,33 @@
<?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.RemoveMasterPasswordPage"
xmlns:pages="clr-namespace:Bit.App.Pages"
xmlns:u="clr-namespace:Bit.App.Utilities"
x:DataType="pages:RemoveMasterPasswordPageViewModel"
Title="{Binding PageTitle}">
<ContentPage.BindingContext>
<pages:RemoveMasterPasswordPageViewModel />
</ContentPage.BindingContext>
<StackLayout Spacing="20"
Padding="10, 5">
<StackLayout Spacing="18"
Padding="30">
<Label x:Name="_warningLabel"
HorizontalTextAlignment="Center"/>
<Label x:Name="_warningLabel2"
HorizontalTextAlignment="Center"/>
</StackLayout>
<StackLayout Spacing="5">
<Button Text="{u:I18n Continue}"
StyleClass="btn-primary"
Clicked="Continue_Clicked" />
<Button Text="{u:I18n LeaveOrganization}"
Clicked="LeaveOrg_Clicked" />
</StackLayout>
</StackLayout>
</pages:BaseContentPage>

View file

@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using Bit.App.Resources;
using Xamarin.Forms;
namespace Bit.App.Pages
{
public partial class RemoveMasterPasswordPage : BaseContentPage
{
private readonly RemoveMasterPasswordPageViewModel _vm;
public Action NavigateAction { get; set; }
public RemoveMasterPasswordPage()
{
InitializeComponent();
_vm = BindingContext as RemoveMasterPasswordPageViewModel;
}
protected override async void OnAppearing()
{
await _vm.Init();
_warningLabel.Text = string.Format(AppResources.RemoveMasterPasswordWarning,
_vm.Organization.Name);
_warningLabel2.Text = AppResources.RemoveMasterPasswordWarning2;
}
private async void Continue_Clicked(object sender, System.EventArgs e)
{
if (DoOnce())
{
await _vm.MigrateAccount();
await Navigation.PopModalAsync();
}
}
private async void LeaveOrg_Clicked(object sender, System.EventArgs e)
{
if (DoOnce())
{
var confirm = await DisplayAlert(AppResources.LeaveOrganization,
string.Format(AppResources.LeaveOrganizationName, _vm.Organization.Name),
AppResources.Yes, AppResources.No);
if (confirm)
{
await _vm.LeaveOrganization();
await Navigation.PopModalAsync();
}
}
}
protected override async void OnDisappearing()
{
NavigateAction?.Invoke();
}
}
}

View file

@ -0,0 +1,56 @@
using System;
using System.Threading.Tasks;
using Bit.App.Abstractions;
using Bit.App.Resources;
using Bit.Core.Abstractions;
using Bit.Core.Models.Domain;
using Bit.Core.Utilities;
namespace Bit.App.Pages
{
public class RemoveMasterPasswordPageViewModel : BaseViewModel
{
private readonly IKeyConnectorService _keyConnectorService;
private readonly IDeviceActionService _deviceActionService;
private readonly IApiService _apiService;
private readonly ISyncService _syncService;
public Organization Organization;
public RemoveMasterPasswordPageViewModel()
{
PageTitle = AppResources.RemoveMasterPassword;
_keyConnectorService = ServiceContainer.Resolve<IKeyConnectorService>("keyConnectorService");
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
_apiService = ServiceContainer.Resolve<IApiService>("apiService");
_syncService = ServiceContainer.Resolve<ISyncService>("syncService");
}
public async Task Init()
{
Organization = await _keyConnectorService.GetManagingOrganization();
}
public async Task MigrateAccount()
{
await _deviceActionService.ShowLoadingAsync(AppResources.Loading);
await _keyConnectorService.MigrateUser();
await _syncService.FullSyncAsync(true);
await _deviceActionService.HideLoadingAsync();
}
public async Task LeaveOrganization()
{
await _deviceActionService.ShowLoadingAsync(AppResources.Loading);
await _apiService.PostLeaveOrganization(Organization.Id);
await _syncService.FullSyncAsync(true);
await _deviceActionService.HideLoadingAsync();
}
}
}

View file

@ -25,8 +25,9 @@
</ContentPage.ToolbarItems>
<ScrollView>
<StackLayout Spacing="20">
<StackLayout StyleClass="box">
<StackLayout>
<StackLayout StyleClass="box"
Spacing="20">
<Frame
IsVisible="{Binding DisablePrivateVaultPolicyEnabled}"
Padding="10"
@ -39,7 +40,7 @@
StyleClass="text-muted, text-sm, text-bold"
HorizontalTextAlignment="Center" />
</Frame>
<StackLayout StyleClass="box-row, box-row-input, box-row-input-options-platform">
<StackLayout StyleClass="box-row">
<Label
Text="{u:I18n FileFormat}"
StyleClass="box-label" />
@ -53,6 +54,7 @@
</StackLayout>
<Grid StyleClass="box-row">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
@ -60,20 +62,30 @@
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Label
Text="{u:I18n MasterPassword}"
StyleClass="box-label"
<Button x:Name="_requestOTP"
Text="{u:I18n RequestOTP}"
Clicked="RequestOTP_Clicked"
HorizontalOptions="Fill"
VerticalOptions="End"
IsEnabled="{Binding DisablePrivateVaultPolicyEnabled, Converter={StaticResource inverseBool}}"
IsVisible="{Binding UseOTPVerification}"
Grid.Row="0"
Grid.ColumnSpan="2"
Margin="0,0,0,10"/>
<Label
Text="{Binding SecretName}"
StyleClass="box-label"
Grid.Row="1"
Grid.Column="0" />
<controls:MonoEntry
x:Name="_masterPassword"
Text="{Binding MasterPassword}"
x:Name="_secret"
Text="{Binding Secret}"
StyleClass="box-value"
IsSpellCheckEnabled="False"
IsTextPredictionEnabled="False"
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
IsEnabled="{Binding DisablePrivateVaultPolicyEnabled, Converter={StaticResource inverseBool}}"
Grid.Row="1"
Grid.Row="2"
Grid.Column="0"
ReturnType="Go"
ReturnCommand="{Binding ExportVaultCommand}" />
@ -81,16 +93,16 @@
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowPasswordIcon}"
Command="{Binding TogglePasswordCommand}"
Grid.Row="0"
Grid.Row="2"
Grid.Column="1"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
AutomationProperties.Name="{u:I18n ToggleVisibility}"
IsVisible="{Binding UseOTPVerification, Converter={StaticResource inverseBool}}"/>
</Grid>
<Label
Text="{u:I18n ExportVaultMasterPasswordDescription}"
StyleClass="box-footer-label, box-footer-label-switch" />
<StackLayout Spacing="20">
<StackLayout StyleClass="box-row">
<Label
Text="{Binding InstructionText}"
StyleClass="box-footer-label, box-footer-label-switch" />
<Button Text="{u:I18n ExportVault}"
Clicked="ExportVault_Clicked"
HorizontalOptions="Fill"

View file

@ -17,7 +17,7 @@ namespace Bit.App.Pages
_vm = BindingContext as ExportVaultPageViewModel;
_vm.Page = this;
_fileFormatPicker.ItemDisplayBinding = new Binding("Value");
MasterPasswordEntry = _masterPassword;
SecretEntry = _secret;
}
protected async override void OnAppearing()
@ -39,7 +39,7 @@ namespace Bit.App.Pages
});
}
});
RequestFocus(_masterPassword);
RequestFocus(_secret);
}
protected async override void OnDisappearing()
@ -48,7 +48,7 @@ namespace Bit.App.Pages
_broadcasterService.Unsubscribe(nameof(ExportVaultPage));
}
public Entry MasterPasswordEntry { get; set; }
public Entry SecretEntry { get; set; }
private async void Close_Clicked(object sender, System.EventArgs e)
{
@ -66,6 +66,15 @@ namespace Bit.App.Pages
}
}
private async void RequestOTP_Clicked(object sender, EventArgs e)
{
if (DoOnce())
{
await _vm.RequestOTP();
_requestOTP.IsEnabled = false;
}
}
void FileFormat_Changed(object sender, EventArgs e)
{
_vm?.UpdateWarning();

View file

@ -4,10 +4,8 @@ using Bit.App.Resources;
using Bit.Core.Abstractions;
using Bit.Core.Utilities;
using System.Collections.Generic;
using System.ComponentModel;
using System.Text;
using System.Threading.Tasks;
using Bit.Core;
using Bit.Core.Enums;
#if !FDROID
using Microsoft.AppCenter.Crashes;
@ -21,26 +19,33 @@ namespace Bit.App.Pages
private readonly IDeviceActionService _deviceActionService;
private readonly IPlatformUtilsService _platformUtilsService;
private readonly II18nService _i18nService;
private readonly ICryptoService _cryptoService;
private readonly IExportService _exportService;
private readonly IPolicyService _policyService;
private readonly IKeyConnectorService _keyConnectorService;
private readonly IUserVerificationService _userVerificationService;
private readonly IApiService _apiService;
private int _fileFormatSelectedIndex;
private string _exportWarningMessage;
private bool _showPassword;
private string _masterPassword;
private string _secret;
private byte[] _exportResult;
private string _defaultFilename;
private bool _initialized = false;
private bool _useOTPVerification = false;
private string _secretName;
private string _instructionText;
public ExportVaultPageViewModel()
{
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
_cryptoService = ServiceContainer.Resolve<ICryptoService>("cryptoService");
_exportService = ServiceContainer.Resolve<IExportService>("exportService");
_policyService = ServiceContainer.Resolve<IPolicyService>("policyService");
_keyConnectorService = ServiceContainer.Resolve<IKeyConnectorService>("keyConnectorService");
_userVerificationService = ServiceContainer.Resolve<IUserVerificationService>("userVerificationService");
_apiService = ServiceContainer.Resolve<IApiService>("apiService");
PageTitle = AppResources.ExportVault;
TogglePasswordCommand = new Command(TogglePassword);
@ -59,7 +64,19 @@ namespace Bit.App.Pages
_initialized = true;
FileFormatSelectedIndex = FileFormatOptions.FindIndex(k => k.Key == "json");
DisablePrivateVaultPolicyEnabled = await _policyService.PolicyAppliesToUser(PolicyType.DisablePersonalVaultExport);
UseOTPVerification = await _keyConnectorService.GetUsesKeyConnector();
if (UseOTPVerification)
{
InstructionText = _i18nService.T("ExportVaultOTPDescription");
SecretName = _i18nService.T("VerificationCode");
}
else
{
InstructionText = _i18nService.T("ExportVaultMasterPasswordDescription");
SecretName = _i18nService.T("MasterPassword");
}
UpdateWarning();
}
@ -94,10 +111,28 @@ namespace Bit.App.Pages
additionalPropertyNames: new string[] {nameof(ShowPasswordIcon)});
}
public string MasterPassword
public bool UseOTPVerification
{
get => _masterPassword;
set => SetProperty(ref _masterPassword, value);
get => _useOTPVerification;
set => SetProperty(ref _useOTPVerification, value);
}
public string Secret
{
get => _secret;
set => SetProperty(ref _secret, value);
}
public string SecretName
{
get => _secretName;
set => SetProperty(ref _secretName, value);
}
public string InstructionText
{
get => _instructionText;
set => SetProperty(ref _instructionText, value);
}
public Command TogglePasswordCommand { get; }
@ -107,27 +142,13 @@ namespace Bit.App.Pages
public void TogglePassword()
{
ShowPassword = !ShowPassword;
(Page as ExportVaultPage).MasterPasswordEntry.Focus();
(Page as ExportVaultPage).SecretEntry.Focus();
}
public Command ExportVaultCommand { get; }
public async Task ExportVaultAsync()
{
if (string.IsNullOrEmpty(_masterPassword))
{
await _platformUtilsService.ShowDialogAsync(_i18nService.T("InvalidMasterPassword"));
return;
}
var passwordValid = await _cryptoService.CompareAndUpdateKeyHashAsync(_masterPassword, null);
MasterPassword = string.Empty;
if (!passwordValid)
{
await _platformUtilsService.ShowDialogAsync(_i18nService.T("InvalidMasterPassword"));
return;
}
bool userConfirmedExport = await _platformUtilsService.ShowDialogAsync(ExportWarningMessage,
_i18nService.T("ExportVaultConfirmationTitle"), _i18nService.T("ExportVault"), _i18nService.T("Cancel"));
@ -136,6 +157,16 @@ namespace Bit.App.Pages
return;
}
var verificationType = await _keyConnectorService.GetUsesKeyConnector()
? VerificationType.OTP
: VerificationType.MasterPassword;
if (!await _userVerificationService.VerifyUser(Secret, verificationType))
{
return;
}
Secret = string.Empty;
try
{
var data = await _exportService.GetExport(FileFormatOptions[FileFormatSelectedIndex].Key);
@ -162,6 +193,11 @@ namespace Bit.App.Pages
}
}
public async Task RequestOTP()
{
await _apiService.PostAccountRequestOTP();
}
public async void SaveFileSelected(string contentUri, string filename)
{
if (_deviceActionService.SaveFile(_exportResult, null, filename ?? _defaultFilename, contentUri))

View file

@ -27,6 +27,7 @@ namespace Bit.App.Pages
private readonly IBiometricService _biometricService;
private readonly IPolicyService _policyService;
private readonly ILocalizeService _localizeService;
private readonly IKeyConnectorService _keyConnectorService;
private const int CustomVaultTimeoutValue = -100;
@ -36,6 +37,8 @@ namespace Bit.App.Pages
private string _lastSyncDate;
private string _vaultTimeoutDisplayValue;
private string _vaultTimeoutActionDisplayValue;
private bool _showChangeMasterPassword;
private List<KeyValuePair<string, int?>> _vaultTimeouts =
new List<KeyValuePair<string, int?>>
{
@ -74,6 +77,7 @@ namespace Bit.App.Pages
_biometricService = ServiceContainer.Resolve<IBiometricService>("biometricService");
_policyService = ServiceContainer.Resolve<IPolicyService>("policyService");
_localizeService = ServiceContainer.Resolve<ILocalizeService>("localizeService");
_keyConnectorService = ServiceContainer.Resolve<IKeyConnectorService>("keyConnectorService");
GroupedItems = new ExtendedObservableCollection<SettingsPageListGroup>();
PageTitle = AppResources.Settings;
@ -116,6 +120,9 @@ namespace Bit.App.Pages
_vaultTimeoutDisplayValue = AppResources.Custom;
}
_showChangeMasterPassword = IncludeLinksWithSubscriptionInfo() &&
!await _keyConnectorService.GetUsesKeyConnector();
BuildList();
}
@ -460,7 +467,7 @@ namespace Bit.App.Pages
new SettingsPageListItem { Name = AppResources.FingerprintPhrase },
new SettingsPageListItem { Name = AppResources.LogOut }
};
if (IncludeLinksWithSubscriptionInfo())
if (_showChangeMasterPassword)
{
accountItems.Insert(0, new SettingsPageListItem { Name = AppResources.ChangeMasterPassword });
}

View file

@ -10,6 +10,7 @@ namespace Bit.App.Pages
public class TabsPage : TabbedPage
{
private readonly IMessagingService _messagingService;
private readonly IKeyConnectorService _keyConnectorService;
private NavigationPage _groupingsPage;
private NavigationPage _sendGroupingsPage;
@ -18,6 +19,7 @@ namespace Bit.App.Pages
public TabsPage(AppOptions appOptions = null, PreviousPageInfo previousPage = null)
{
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
_keyConnectorService = ServiceContainer.Resolve<IKeyConnectorService>("keyConnectorService");
_groupingsPage = new NavigationPage(new GroupingsPage(true, previousPage: previousPage))
{
@ -72,6 +74,15 @@ namespace Bit.App.Pages
}
}
protected override async void OnAppearing()
{
base.OnAppearing();
if (await _keyConnectorService.UserNeedsMigration())
{
_messagingService.Send("convertAccountToKeyConnector");
}
}
public void ResetToVaultPage()
{
CurrentPage = _groupingsPage;

View file

@ -556,7 +556,7 @@
StyleClass="box-value"
HorizontalOptions="End" />
</StackLayout>
<StackLayout StyleClass="box-row, box-row-switch">
<StackLayout x:Name="_passwordPrompt" StyleClass="box-row, box-row-switch">
<Label
Text="{u:I18n PasswordPrompt}"
StyleClass="box-label-regular" />

View file

@ -21,6 +21,7 @@ namespace Bit.App.Pages
private readonly IStorageService _storageService;
private readonly IDeviceActionService _deviceActionService;
private readonly IVaultTimeoutService _vaultTimeoutService;
private readonly IKeyConnectorService _keyConnectorService;
private AddEditPageViewModel _vm;
private bool _fromAutofill;
@ -40,6 +41,8 @@ namespace Bit.App.Pages
_storageService = ServiceContainer.Resolve<IStorageService>("storageService");
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
_keyConnectorService = ServiceContainer.Resolve<IKeyConnectorService>("keyConnectorService");
_appOptions = appOptions;
_fromAutofill = fromAutofill;
FromAutofillFramework = _appOptions?.FromAutofillFramework ?? false;
@ -171,6 +174,8 @@ namespace Bit.App.Pages
}
_scrollView.Scrolled += (sender, args) => _vm.HandleScroll();
});
// Hide password reprompt option if using key connector
_passwordPrompt.IsVisible = !await _keyConnectorService.GetUsesKeyConnector();
}
protected override void OnDisappearing()

View file

@ -2837,6 +2837,12 @@ namespace Bit.App.Resources {
}
}
public static string ExportVaultOTPDescription {
get {
return ResourceManager.GetString("ExportVaultOTPDescription", resourceCulture);
}
}
public static string ExportVaultWarning {
get {
return ResourceManager.GetString("ExportVaultWarning", resourceCulture);
@ -3599,6 +3605,36 @@ namespace Bit.App.Resources {
}
}
public static string RemoveMasterPassword {
get {
return ResourceManager.GetString("RemoveMasterPassword", resourceCulture);
}
}
public static string RemoveMasterPasswordWarning {
get {
return ResourceManager.GetString("RemoveMasterPasswordWarning", resourceCulture);
}
}
public static string RemoveMasterPasswordWarning2 {
get {
return ResourceManager.GetString("RemoveMasterPasswordWarning2", resourceCulture);
}
}
public static string LeaveOrganization {
get {
return ResourceManager.GetString("LeaveOrganization", resourceCulture);
}
}
public static string LeaveOrganizationName {
get {
return ResourceManager.GetString("LeaveOrganizationName", resourceCulture);
}
}
public static string Fido2Title {
get {
return ResourceManager.GetString("Fido2Title", resourceCulture);
@ -3658,5 +3694,17 @@ namespace Bit.App.Resources {
return ResourceManager.GetString("DisablePersonalVaultExportPolicyInEffect", resourceCulture);
}
}
public static string InvalidVerificationCode {
get {
return ResourceManager.GetString("InvalidVerificationCode", resourceCulture);
}
}
public static string RequestOTP {
get {
return ResourceManager.GetString("RequestOTP", resourceCulture);
}
}
}
}

View file

@ -1610,6 +1610,9 @@
</data>
<data name="ExportVaultMasterPasswordDescription" xml:space="preserve">
<value>Enter your master password to export your vault data.</value>
</data>
<data name="ExportVaultOTPDescription" xml:space="preserve">
<value>Enter the verification code to export your vault data.</value>
</data>
<data name="ExportVaultWarning" xml:space="preserve">
<value>This export contains your vault data in an unencrypted format. You should not store or send the exported file over unsecure channels (such as email). Delete it immediately after you are done using it.</value>
@ -2033,6 +2036,21 @@
<data name="UpdatePasswordError" xml:space="preserve">
<value>Currently unable to update password</value>
</data>
<data name="RemoveMasterPassword" xml:space="preserve">
<value>Remove Master Password</value>
</data>
<data name="RemoveMasterPasswordWarning" xml:space="preserve">
<value>{0} is using SSO with customer-managed encryption. Continuing will remove your Master Password from your account and require SSO to login.</value>
</data>
<data name="RemoveMasterPasswordWarning2" xml:space="preserve">
<value>If you do not want to remove your Master Password, you may leave this organization.</value>
</data>
<data name="LeaveOrganization" xml:space="preserve">
<value>Leave Organization</value>
</data>
<data name="LeaveOrganizationName" xml:space="preserve">
<value>Leave {0}?</value>
</data>
<data name="Fido2Title" xml:space="preserve">
<value>FIDO2 WebAuthn</value>
</data>
@ -2063,4 +2081,10 @@
<data name="DisablePersonalVaultExportPolicyInEffect">
<value>One or more organization policies prevents your from exporting your personal vault.</value>
</data>
<data name="InvalidVerificationCode" xml:space="preserve">
<value>Invalid Verification Code.</value>
</data>
<data name="RequestOTP" xml:space="preserve">
<value>Request one-time password</value>
</data>
</root>

View file

@ -3,6 +3,7 @@ using Bit.Core.Abstractions;
using Bit.App.Abstractions;
using Bit.App.Resources;
using System;
using Bit.Core.Utilities;
namespace Bit.App.Services
{
@ -21,6 +22,11 @@ namespace Bit.App.Services
public async Task<bool> ShowPasswordPromptAsync()
{
if (!await Enabled())
{
return true;
}
Func<string, Task<bool>> validator = async (string password) =>
{
// Assume user has canceled.
@ -34,5 +40,11 @@ namespace Bit.App.Services
return await _platformUtilsService.ShowPasswordDialogAsync(AppResources.PasswordConfirmation, AppResources.PasswordConfirmationDesc, validator);
}
public async Task<bool> Enabled()
{
var keyConnectorService = ServiceContainer.Resolve<IKeyConnectorService>("keyConnectorService");
return !await keyConnectorService.GetUsesKeyConnector();
}
}
}

View file

@ -27,6 +27,8 @@ namespace Bit.Core.Abstractions
Task<SyncResponse> GetSyncAsync();
Task PostAccountKeysAsync(KeysRequest request);
Task PostAccountVerifyPasswordAsync(PasswordVerificationRequest request);
Task PostAccountRequestOTP();
Task PostAccountVerifyOTPAsync(VerifyOTPRequest request);
Task<CipherResponse> PostCipherAsync(CipherRequest request);
Task<CipherResponse> PostCipherCreateAsync(CipherCreateRequest request);
Task<FolderResponse> PostFolderAsync(FolderRequest request);
@ -63,6 +65,11 @@ namespace Bit.Core.Abstractions
Task<OrganizationAutoEnrollStatusResponse> GetOrganizationAutoEnrollStatusAsync(string identifier);
Task PutOrganizationUserResetPasswordEnrollmentAsync(string orgId, string userId,
OrganizationUserResetPasswordEnrollmentRequest request);
Task<KeyConnectorUserKeyResponse> GetUserKeyFromKeyConnector(string keyConnectorUrl);
Task PostUserKeyToKeyConnector(string keyConnectorUrl, KeyConnectorUserKeyRequest request);
Task PostSetKeyConnectorKey(SetKeyConnectorKeyRequest request);
Task PostConvertToKeyConnector();
Task PostLeaveOrganization(string id);
Task<SendResponse> GetSendAsync(string id);
Task<SendResponse> PostSendAsync(SendRequest request);

View file

@ -0,0 +1,16 @@
using System;
using System.Threading.Tasks;
using Bit.Core.Models.Domain;
namespace Bit.Core.Abstractions
{
public interface IKeyConnectorService
{
Task SetUsesKeyConnector(bool usesKeyConnector);
Task<bool> GetUsesKeyConnector();
Task<bool> UserNeedsMigration();
Task MigrateUser();
Task GetAndSetKey(string url);
Task<Organization> GetManagingOrganization();
}
}

View file

@ -14,6 +14,7 @@ namespace Bit.Core.Abstractions
string GetIssuer();
string GetName();
bool GetPremium();
bool GetIsExternal();
Task<string> GetRefreshTokenAsync();
Task<string> GetTokenAsync();
Task ToggleTokensAsync();

View file

@ -0,0 +1,10 @@
using System.Threading.Tasks;
using Bit.Core.Enums;
namespace Bit.Core.Abstractions
{
public interface IUserVerificationService
{
Task<bool> VerifyUser(string secret, VerificationType verificationType);
}
}

View file

@ -0,0 +1,8 @@
namespace Bit.Core.Enums
{
public enum VerificationType
{
MasterPassword = 0,
OTP = 1,
}
}

View file

@ -29,6 +29,8 @@ namespace Bit.Core.Models.Data
MaxStorageGb = response.MaxStorageGb;
Permissions = response.Permissions ?? new Permissions();
Identifier = response.Identifier;
UsesKeyConnector = response.UsesKeyConnector;
KeyConnectorUrl = response.KeyConnectorUrl;
}
public string Id { get; set; }
@ -50,5 +52,7 @@ namespace Bit.Core.Models.Data
public short? MaxStorageGb { get; set; }
public Permissions Permissions { get; set; } = new Permissions();
public string Identifier { get; set; }
public bool UsesKeyConnector { get; set; }
public string KeyConnectorUrl { get; set; }
}
}

View file

@ -29,6 +29,8 @@ namespace Bit.Core.Models.Domain
MaxStorageGb = obj.MaxStorageGb;
Permissions = obj.Permissions ?? new Permissions();
Identifier = obj.Identifier;
UsesKeyConnector = obj.UsesKeyConnector;
KeyConnectorUrl = obj.KeyConnectorUrl;
}
public string Id { get; set; }
@ -50,6 +52,8 @@ namespace Bit.Core.Models.Domain
public short? MaxStorageGb { get; set; }
public Permissions Permissions { get; set; } = new Permissions();
public string Identifier { get; set; }
public bool UsesKeyConnector { get; set; }
public string KeyConnectorUrl { get; set; }
public bool CanAccess
{

View file

@ -0,0 +1,13 @@
using System;
namespace Bit.Core.Models.Request
{
public class KeyConnectorUserKeyRequest
{
public string Key { get; set; }
public KeyConnectorUserKeyRequest(string key)
{
Key = key;
}
}
}

View file

@ -0,0 +1,24 @@
using System;
using Bit.Core.Enums;
namespace Bit.Core.Models.Request
{
public class SetKeyConnectorKeyRequest
{
public string Key { get; set; }
public KeysRequest Keys { get; set; }
public KdfType Kdf { get; set; }
public int? KdfIterations { get; set; }
public string OrgIdentifier { get; set; }
public SetKeyConnectorKeyRequest(string key, KeysRequest keys,
KdfType kdf, int? kdfIterations, string orgIdentifier)
{
this.Key = key;
this.Keys = keys;
this.Kdf = kdf;
this.KdfIterations = kdfIterations;
this.OrgIdentifier = orgIdentifier;
}
}
}

View file

@ -0,0 +1,13 @@
using System;
namespace Bit.Core.Models.Request
{
public class VerifyOTPRequest
{
public string OTP;
public VerifyOTPRequest(string otp)
{
OTP = otp;
}
}
}

View file

@ -21,5 +21,6 @@ namespace Bit.Core.Models.Response
public KdfType Kdf { get; set; }
public int? KdfIterations { get; set; }
public bool ForcePasswordReset { get; set; }
public string KeyConnectorUrl { get; set; }
}
}

View file

@ -0,0 +1,9 @@
using System;
namespace Bit.Core.Models.Response
{
public class KeyConnectorUserKeyResponse
{
public string Key { get; set; }
}
}

View file

@ -25,5 +25,8 @@ namespace Bit.Core.Models.Response
public bool Enabled { get; set; }
public Permissions Permissions { get; set; } = new Permissions();
public string Identifier { get; set; }
public bool UsesKeyConnector { get; set; }
public string KeyConnectorUrl { get; set; }
}
}

View file

@ -18,5 +18,6 @@ namespace Bit.Core.Models.Response
public string SecurityStamp { get; set; }
public bool ForcePasswordReset { get; set; }
public List<ProfileOrganizationResponse> Organizations { get; set; }
public bool UsesKeyConnector { get; set; }
}
}

View file

@ -178,12 +178,33 @@ namespace Bit.Core.Services
true, false);
}
public Task PostAccountRequestOTP()
{
return SendAsync<object, object>(HttpMethod.Post, "/accounts/request-otp", null, true, false);
}
public Task PostAccountVerifyOTPAsync(VerifyOTPRequest request)
{
return SendAsync<VerifyOTPRequest, object>(HttpMethod.Post, "/accounts/verify-otp", request,
true, false);
}
public Task PutUpdateTempPasswordAsync(UpdateTempPasswordRequest request)
{
return SendAsync<UpdateTempPasswordRequest, object>(HttpMethod.Put, "/accounts/update-temp-password",
request, true, false);
}
public Task PostConvertToKeyConnector()
{
return SendAsync<object, object>(HttpMethod.Post, "/accounts/convert-to-key-connector", null, true, false);
}
public Task PostSetKeyConnectorKey(SetKeyConnectorKeyRequest request)
{
return SendAsync<SetKeyConnectorKeyRequest>(HttpMethod.Post, "/accounts/set-key-connector-key", request, true);
}
#endregion
#region Folder APIs
@ -422,9 +443,14 @@ namespace Bit.Core.Services
return SendAsync<object, OrganizationAutoEnrollStatusResponse>(HttpMethod.Get,
$"/organizations/{identifier}/auto-enroll-status", null, true, true);
}
public Task PostLeaveOrganization(string id)
{
return SendAsync<object, object>(HttpMethod.Post, $"/organizations/{id}/leave", null, true, false);
}
#endregion
#region Organization User APIs
public Task PutOrganizationUserResetPasswordEnrollmentAsync(string orgId, string userId,
@ -433,7 +459,71 @@ namespace Bit.Core.Services
return SendAsync<OrganizationUserResetPasswordEnrollmentRequest, object>(HttpMethod.Put,
$"/organizations/{orgId}/users/{userId}/reset-password-enrollment", request, true, false);
}
#endregion
#region Key Connector
public async Task<KeyConnectorUserKeyResponse> GetUserKeyFromKeyConnector(string keyConnectorUrl)
{
using (var requestMessage = new HttpRequestMessage())
{
var authHeader = await GetActiveBearerTokenAsync();
requestMessage.Version = new Version(1, 0);
requestMessage.Method = HttpMethod.Get;
requestMessage.RequestUri = new Uri(string.Concat(keyConnectorUrl, "/user-keys"));
requestMessage.Headers.Add("Authorization", string.Concat("Bearer ", authHeader));
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, true);
throw new ApiException(error);
}
var responseJsonString = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<KeyConnectorUserKeyResponse>(responseJsonString);
}
}
public async Task PostUserKeyToKeyConnector(string keyConnectorUrl, KeyConnectorUserKeyRequest request)
{
using (var requestMessage = new HttpRequestMessage())
{
var authHeader = await GetActiveBearerTokenAsync();
requestMessage.Version = new Version(1, 0);
requestMessage.Method = HttpMethod.Post;
requestMessage.RequestUri = new Uri(string.Concat(keyConnectorUrl, "/user-keys"));
requestMessage.Headers.Add("Authorization", string.Concat("Bearer ", authHeader));
requestMessage.Content = new StringContent(JsonConvert.SerializeObject(request, _jsonSettings),
Encoding.UTF8, "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, true);
throw new ApiException(error);
}
}
}
#endregion
#region Helpers

View file

@ -12,6 +12,7 @@ namespace Bit.Core.Services
public class AuthService : IAuthService
{
private readonly ICryptoService _cryptoService;
private readonly ICryptoFunctionService _cryptoFunctionService;
private readonly IApiService _apiService;
private readonly IUserService _userService;
private readonly ITokenService _tokenService;
@ -20,12 +21,14 @@ namespace Bit.Core.Services
private readonly IPlatformUtilsService _platformUtilsService;
private readonly IMessagingService _messagingService;
private readonly IVaultTimeoutService _vaultTimeoutService;
private readonly IKeyConnectorService _keyConnectorService;
private readonly bool _setCryptoKeys;
private SymmetricCryptoKey _key;
public AuthService(
ICryptoService cryptoService,
ICryptoFunctionService cryptoFunctionService,
IApiService apiService,
IUserService userService,
ITokenService tokenService,
@ -34,9 +37,11 @@ namespace Bit.Core.Services
IPlatformUtilsService platformUtilsService,
IMessagingService messagingService,
IVaultTimeoutService vaultTimeoutService,
IKeyConnectorService keyConnectorService,
bool setCryptoKeys = true)
{
_cryptoService = cryptoService;
_cryptoFunctionService = cryptoFunctionService;
_apiService = apiService;
_userService = userService;
_tokenService = tokenService;
@ -45,6 +50,7 @@ namespace Bit.Core.Services
_platformUtilsService = platformUtilsService;
_messagingService = messagingService;
_vaultTimeoutService = vaultTimeoutService;
_keyConnectorService = keyConnectorService;
_setCryptoKeys = setCryptoKeys;
TwoFactorProviders = new Dictionary<TwoFactorProviderType, TwoFactorProvider>();
@ -275,7 +281,7 @@ namespace Bit.Core.Services
private async Task<AuthResult> LogInHelperAsync(string email, string hashedPassword, string localHashedPassword,
string code, string codeVerifier, string redirectUrl, SymmetricCryptoKey key,
TwoFactorProviderType? twoFactorProvider = null, string twoFactorToken = null, bool? remember = null,
string captchaToken = null)
string captchaToken = null, string orgId = null)
{
var storedTwoFactorToken = await _tokenService.GetTwoFactorTokenAsync(email);
var appId = await _appIdService.GetAppIdAsync();
@ -353,27 +359,75 @@ namespace Bit.Core.Services
tokenResponse.Kdf, tokenResponse.KdfIterations);
if (_setCryptoKeys)
{
await _cryptoService.SetKeyAsync(key);
await _cryptoService.SetKeyHashAsync(localHashedPassword);
await _cryptoService.SetEncKeyAsync(tokenResponse.Key);
// User doesn't have a key pair yet (old account), let's generate one for them.
if (tokenResponse.PrivateKey == null)
if (key != null)
{
try
{
var keyPair = await _cryptoService.MakeKeyPairAsync();
await _apiService.PostAccountKeysAsync(new KeysRequest
{
PublicKey = keyPair.Item1,
EncryptedPrivateKey = keyPair.Item2.EncryptedString
});
tokenResponse.PrivateKey = keyPair.Item2.EncryptedString;
}
catch { }
await _cryptoService.SetKeyAsync(key);
}
if (localHashedPassword != null)
{
await _cryptoService.SetKeyHashAsync(localHashedPassword);
}
if (code == null || tokenResponse.Key != null)
{
if (tokenResponse.KeyConnectorUrl != null)
{
await _keyConnectorService.GetAndSetKey(tokenResponse.KeyConnectorUrl);
}
await _cryptoService.SetEncKeyAsync(tokenResponse.Key);
// User doesn't have a key pair yet (old account), let's generate one for them.
if (tokenResponse.PrivateKey == null)
{
try
{
var keyPair = await _cryptoService.MakeKeyPairAsync();
await _apiService.PostAccountKeysAsync(new KeysRequest
{
PublicKey = keyPair.Item1,
EncryptedPrivateKey = keyPair.Item2.EncryptedString
});
tokenResponse.PrivateKey = keyPair.Item2.EncryptedString;
}
catch { }
}
await _cryptoService.SetEncPrivateKeyAsync(tokenResponse.PrivateKey);
}
else if (tokenResponse.KeyConnectorUrl != null)
{
// SSO Key Connector Onboarding
var password = await _cryptoFunctionService.RandomBytesAsync(64);
var k = await _cryptoService.MakeKeyAsync(Convert.ToBase64String(password), _tokenService.GetEmail(), tokenResponse.Kdf, tokenResponse.KdfIterations);
var keyConnectorRequest = new KeyConnectorUserKeyRequest(k.EncKeyB64);
await _cryptoService.SetKeyAsync(k);
var encKey = await _cryptoService.MakeEncKeyAsync(k);
await _cryptoService.SetEncKeyAsync(encKey.Item2.EncryptedString);
var keyPair = await _cryptoService.MakeKeyPairAsync();
try
{
await _apiService.PostUserKeyToKeyConnector(tokenResponse.KeyConnectorUrl, keyConnectorRequest);
}
catch (Exception e)
{
throw new Exception("Unable to reach Key Connector", e);
}
var keys = new KeysRequest
{
PublicKey = keyPair.Item1,
EncryptedPrivateKey = keyPair.Item2.EncryptedString
};
var setPasswordRequest = new SetKeyConnectorKeyRequest(
encKey.Item2.EncryptedString, keys, tokenResponse.Kdf, tokenResponse.KdfIterations, orgId
);
await _apiService.PostSetKeyConnectorKey(setPasswordRequest);
}
await _cryptoService.SetEncPrivateKeyAsync(tokenResponse.PrivateKey);
}
_vaultTimeoutService.BiometricLocked = false;

View file

@ -0,0 +1,98 @@
using System;
using System.Threading.Tasks;
using Bit.Core.Abstractions;
using Bit.Core.Exceptions;
using Bit.Core.Models.Domain;
using Bit.Core.Models.Request;
namespace Bit.Core.Services
{
public class KeyConnectorService : IKeyConnectorService
{
private const string Keys_UsesKeyConnector = "usesKeyConnector";
private readonly IUserService _userService;
private readonly ICryptoService _cryptoService;
private readonly IStorageService _storageService;
private readonly ITokenService _tokenService;
private readonly IApiService _apiService;
private bool? _usesKeyConnector;
public KeyConnectorService(IUserService userService, ICryptoService cryptoService,
IStorageService storageService, ITokenService tokenService, IApiService apiService)
{
_userService = userService;
_cryptoService = cryptoService;
_storageService = storageService;
_tokenService = tokenService;
_apiService = apiService;
}
public async Task GetAndSetKey(string url)
{
try
{
var userKeyResponse = await _apiService.GetUserKeyFromKeyConnector(url);
var keyArr = Convert.FromBase64String(userKeyResponse.Key);
var k = new SymmetricCryptoKey(keyArr);
await _cryptoService.SetKeyAsync(k);
}
catch (Exception e)
{
throw new Exception("Unable to reach Key Connector", e);
}
}
public async Task SetUsesKeyConnector(bool usesKeyConnector)
{
_usesKeyConnector = usesKeyConnector;
await _storageService.SaveAsync(Keys_UsesKeyConnector, usesKeyConnector);
}
public async Task<bool> GetUsesKeyConnector()
{
if (!_usesKeyConnector.HasValue)
{
_usesKeyConnector = await _storageService.GetAsync<bool>(Keys_UsesKeyConnector);
}
return _usesKeyConnector.Value;
}
public async Task<Organization> GetManagingOrganization()
{
var orgs = await _userService.GetAllOrganizationAsync();
return orgs.Find(o =>
o.UsesKeyConnector &&
!o.IsAdmin);
}
public async Task MigrateUser()
{
var organization = await GetManagingOrganization();
var key = await _cryptoService.GetKeyAsync();
try
{
var keyConnectorRequest = new KeyConnectorUserKeyRequest(key.EncKeyB64);
await _apiService.PostUserKeyToKeyConnector(organization.KeyConnectorUrl, keyConnectorRequest);
}
catch (Exception e)
{
throw new Exception("Unable to reach Key Connector", e);
}
await _apiService.PostConvertToKeyConnector();
}
public async Task<bool> UserNeedsMigration()
{
var loggedInUsingSso = _tokenService.GetIsExternal();
var requiredByOrganization = await GetManagingOrganization() != null;
var userIsNotUsingKeyConnector = !await GetUsesKeyConnector();
return loggedInUsingSso && requiredByOrganization && userIsNotUsingKeyConnector;
}
}
}

View file

@ -25,6 +25,7 @@ namespace Bit.Core.Services
private readonly IMessagingService _messagingService;
private readonly IPolicyService _policyService;
private readonly ISendService _sendService;
private readonly IKeyConnectorService _keyConnectorService;
private readonly Func<bool, Task> _logoutCallbackAsync;
public SyncService(
@ -39,6 +40,7 @@ namespace Bit.Core.Services
IMessagingService messagingService,
IPolicyService policyService,
ISendService sendService,
IKeyConnectorService keyConnectorService,
Func<bool, Task> logoutCallbackAsync)
{
_userService = userService;
@ -52,6 +54,7 @@ namespace Bit.Core.Services
_messagingService = messagingService;
_policyService = policyService;
_sendService = sendService;
_keyConnectorService = keyConnectorService;
_logoutCallbackAsync = logoutCallbackAsync;
}
@ -329,6 +332,7 @@ namespace Bit.Core.Services
await _userService.ReplaceOrganizationsAsync(organizations);
await _userService.SetEmailVerifiedAsync(response.EmailVerified);
await _userService.SetForcePasswordReset(response.ForcePasswordReset);
await _keyConnectorService.SetUsesKeyConnector(response.UsesKeyConnector);
}
private async Task SyncFoldersAsync(string userId, List<FolderResponse> response)

View file

@ -4,6 +4,7 @@ using Newtonsoft.Json.Linq;
using System;
using System.Text;
using System.Threading.Tasks;
using System.Linq;
namespace Bit.Core.Services
{
@ -230,6 +231,16 @@ namespace Bit.Core.Services
return decoded["iss"].Value<string>();
}
public bool GetIsExternal()
{
var decoded = DecodeToken();
if (decoded?["amr"] == null)
{
return false;
}
return decoded["amr"].Value<JArray>().Any(t => t.Value<string>() == "external");
}
private async Task<bool> SkipTokenStorage()
{
var timeout = await _storageService.GetAsync<int?>(Constants.VaultTimeoutKey);

View file

@ -0,0 +1,69 @@
using System;
using Bit.Core.Enums;
using Bit.Core.Models.Request;
using Bit.Core.Services;
using Bit.Core.Abstractions;
using System.Threading.Tasks;
namespace Bit.Core.Services
{
public class UserVerificationService : IUserVerificationService
{
private readonly IApiService _apiService;
private readonly IPlatformUtilsService _platformUtilsService;
private readonly II18nService _i18nService;
private readonly ICryptoService _cryptoService;
public UserVerificationService(IApiService apiService, IPlatformUtilsService platformUtilsService,
II18nService i18nService, ICryptoService cryptoService)
{
_apiService = apiService;
_platformUtilsService = platformUtilsService;
_i18nService = i18nService;
_cryptoService = cryptoService;
}
async public Task<bool> VerifyUser(string secret, VerificationType verificationType)
{
if (string.IsNullOrEmpty(secret))
{
await InvalidSecretErrorAsync(verificationType);
return false;
}
if (verificationType == VerificationType.OTP)
{
var request = new VerifyOTPRequest(secret);
try
{
await _apiService.PostAccountVerifyOTPAsync(request);
}
catch
{
await InvalidSecretErrorAsync(verificationType);
return false;
}
}
else
{
var passwordValid = await _cryptoService.CompareAndUpdateKeyHashAsync(secret, null);
if (!passwordValid)
{
await InvalidSecretErrorAsync(verificationType);
return false;
}
}
return true;
}
async private Task InvalidSecretErrorAsync(VerificationType verificationType)
{
var errorMessage = verificationType == VerificationType.OTP
? _i18nService.T("InvalidVerificationCode")
: _i18nService.T("InvalidMasterPassword");
await _platformUtilsService.ShowDialogAsync(errorMessage);
}
}
}

View file

@ -20,6 +20,7 @@ namespace Bit.Core.Services
private readonly IMessagingService _messagingService;
private readonly ITokenService _tokenService;
private readonly IPolicyService _policyService;
private readonly IKeyConnectorService _keyConnectorService;
private readonly Action<bool> _lockedCallback;
private readonly Func<bool, Task> _loggedOutCallback;
@ -35,6 +36,7 @@ namespace Bit.Core.Services
IMessagingService messagingService,
ITokenService tokenService,
IPolicyService policyService,
IKeyConnectorService keyConnectorService,
Action<bool> lockedCallback,
Func<bool, Task> loggedOutCallback)
{
@ -49,6 +51,7 @@ namespace Bit.Core.Services
_messagingService = messagingService;
_tokenService = tokenService;
_policyService = policyService;
_keyConnectorService = keyConnectorService;
_lockedCallback = lockedCallback;
_loggedOutCallback = loggedOutCallback;
}
@ -119,6 +122,18 @@ namespace Bit.Core.Services
{
return;
}
if (await _keyConnectorService.GetUsesKeyConnector()) {
var pinSet = await IsPinLockSetAsync();
var pinLock = (pinSet.Item1 && PinProtectedKey != null) || pinSet.Item2;
if (!pinLock && !await IsBiometricLockSetAsync())
{
await LogOutAsync();
return;
}
}
if (allowSoftLock)
{
BiometricLocked = await IsBiometricLockSetAsync();

View file

@ -49,16 +49,17 @@ namespace Bit.Core.Utilities
i18nService, cryptoFunctionService);
searchService = new SearchService(cipherService, sendService);
var policyService = new PolicyService(storageService, userService);
var keyConnectorService = new KeyConnectorService(userService, cryptoService, storageService, tokenService, apiService);
var vaultTimeoutService = new VaultTimeoutService(cryptoService, userService, platformUtilsService,
storageService, folderService, cipherService, collectionService, searchService, messagingService, tokenService,
policyService, null, (expired) =>
policyService, keyConnectorService, null, (expired) =>
{
messagingService.Send("logout", expired);
return Task.FromResult(0);
});
var syncService = new SyncService(userService, apiService, settingsService, folderService,
cipherService, cryptoService, collectionService, storageService, messagingService, policyService, sendService,
(bool expired) =>
keyConnectorService, (bool expired) =>
{
messagingService.Send("logout", expired);
return Task.FromResult(0);
@ -66,12 +67,13 @@ namespace Bit.Core.Utilities
var passwordGenerationService = new PasswordGenerationService(cryptoService, storageService,
cryptoFunctionService, policyService);
var totpService = new TotpService(storageService, cryptoFunctionService);
var authService = new AuthService(cryptoService, apiService, userService, tokenService, appIdService,
i18nService, platformUtilsService, messagingService, vaultTimeoutService);
var authService = new AuthService(cryptoService, cryptoFunctionService, apiService, userService, tokenService, appIdService,
i18nService, platformUtilsService, messagingService, vaultTimeoutService, keyConnectorService);
var exportService = new ExportService(folderService, cipherService, cryptoService);
var auditService = new AuditService(cryptoFunctionService, apiService);
var environmentService = new EnvironmentService(apiService, storageService);
var eventService = new EventService(storageService, apiService, userService, cipherService);
var userVerificationService = new UserVerificationService(apiService, platformUtilsService, i18nService, cryptoService);
Register<IStateService>("stateService", stateService);
Register<ITokenService>("tokenService", tokenService);
@ -94,6 +96,8 @@ namespace Bit.Core.Utilities
Register<IAuditService>("auditService", auditService);
Register<IEnvironmentService>("environmentService", environmentService);
Register<IEventService>("eventService", eventService);
Register<IKeyConnectorService>("keyConnectorService", keyConnectorService);
Register<IUserVerificationService>("userVerificationService", userVerificationService);
}
public static void Register<T>(string serviceName, T obj)