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

View file

@ -28,6 +28,7 @@ namespace Bit.App.Pages
private readonly IEnvironmentService _environmentService; private readonly IEnvironmentService _environmentService;
private readonly IStateService _stateService; private readonly IStateService _stateService;
private readonly IBiometricService _biometricService; private readonly IBiometricService _biometricService;
private readonly IKeyConnectorService _keyConnectorService;
private string _email; private string _email;
private bool _showPassword; private bool _showPassword;
@ -54,6 +55,7 @@ namespace Bit.App.Pages
_environmentService = ServiceContainer.Resolve<IEnvironmentService>("environmentService"); _environmentService = ServiceContainer.Resolve<IEnvironmentService>("environmentService");
_stateService = ServiceContainer.Resolve<IStateService>("stateService"); _stateService = ServiceContainer.Resolve<IStateService>("stateService");
_biometricService = ServiceContainer.Resolve<IBiometricService>("biometricService"); _biometricService = ServiceContainer.Resolve<IBiometricService>("biometricService");
_keyConnectorService = ServiceContainer.Resolve<IKeyConnectorService>("keyConnectorService");
PageTitle = AppResources.VerifyMasterPassword; PageTitle = AppResources.VerifyMasterPassword;
TogglePasswordCommand = new Command(TogglePassword); TogglePasswordCommand = new Command(TogglePassword);
@ -124,6 +126,12 @@ namespace Bit.App.Pages
_pinSet = await _vaultTimeoutService.IsPinLockSetAsync(); _pinSet = await _vaultTimeoutService.IsPinLockSetAsync();
PinLock = (_pinSet.Item1 && _vaultTimeoutService.PinProtectedKey != null) || _pinSet.Item2; PinLock = (_pinSet.Item1 && _vaultTimeoutService.PinProtectedKey != null) || _pinSet.Item2;
BiometricLock = await _vaultTimeoutService.IsBiometricLockSetAsync() && await _cryptoService.HasKeyAsync(); 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(); _email = await _userService.GetEmailAsync();
var webVault = _environmentService.GetWebVaultUrl(); var webVault = _environmentService.GetWebVaultUrl();
if (string.IsNullOrWhiteSpace(webVault)) if (string.IsNullOrWhiteSpace(webVault))

View file

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

View file

@ -17,7 +17,7 @@ namespace Bit.App.Pages
_vm = BindingContext as ExportVaultPageViewModel; _vm = BindingContext as ExportVaultPageViewModel;
_vm.Page = this; _vm.Page = this;
_fileFormatPicker.ItemDisplayBinding = new Binding("Value"); _fileFormatPicker.ItemDisplayBinding = new Binding("Value");
MasterPasswordEntry = _masterPassword; SecretEntry = _secret;
} }
protected async override void OnAppearing() protected async override void OnAppearing()
@ -39,7 +39,7 @@ namespace Bit.App.Pages
}); });
} }
}); });
RequestFocus(_masterPassword); RequestFocus(_secret);
} }
protected async override void OnDisappearing() protected async override void OnDisappearing()
@ -48,7 +48,7 @@ namespace Bit.App.Pages
_broadcasterService.Unsubscribe(nameof(ExportVaultPage)); _broadcasterService.Unsubscribe(nameof(ExportVaultPage));
} }
public Entry MasterPasswordEntry { get; set; } public Entry SecretEntry { get; set; }
private async void Close_Clicked(object sender, System.EventArgs e) 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) void FileFormat_Changed(object sender, EventArgs e)
{ {
_vm?.UpdateWarning(); _vm?.UpdateWarning();

View file

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

View file

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

View file

@ -10,6 +10,7 @@ namespace Bit.App.Pages
public class TabsPage : TabbedPage public class TabsPage : TabbedPage
{ {
private readonly IMessagingService _messagingService; private readonly IMessagingService _messagingService;
private readonly IKeyConnectorService _keyConnectorService;
private NavigationPage _groupingsPage; private NavigationPage _groupingsPage;
private NavigationPage _sendGroupingsPage; private NavigationPage _sendGroupingsPage;
@ -18,6 +19,7 @@ namespace Bit.App.Pages
public TabsPage(AppOptions appOptions = null, PreviousPageInfo previousPage = null) public TabsPage(AppOptions appOptions = null, PreviousPageInfo previousPage = null)
{ {
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService"); _messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
_keyConnectorService = ServiceContainer.Resolve<IKeyConnectorService>("keyConnectorService");
_groupingsPage = new NavigationPage(new GroupingsPage(true, previousPage: previousPage)) _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() public void ResetToVaultPage()
{ {
CurrentPage = _groupingsPage; CurrentPage = _groupingsPage;

View file

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

View file

@ -21,6 +21,7 @@ namespace Bit.App.Pages
private readonly IStorageService _storageService; private readonly IStorageService _storageService;
private readonly IDeviceActionService _deviceActionService; private readonly IDeviceActionService _deviceActionService;
private readonly IVaultTimeoutService _vaultTimeoutService; private readonly IVaultTimeoutService _vaultTimeoutService;
private readonly IKeyConnectorService _keyConnectorService;
private AddEditPageViewModel _vm; private AddEditPageViewModel _vm;
private bool _fromAutofill; private bool _fromAutofill;
@ -40,6 +41,8 @@ namespace Bit.App.Pages
_storageService = ServiceContainer.Resolve<IStorageService>("storageService"); _storageService = ServiceContainer.Resolve<IStorageService>("storageService");
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService"); _deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService"); _vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
_keyConnectorService = ServiceContainer.Resolve<IKeyConnectorService>("keyConnectorService");
_appOptions = appOptions; _appOptions = appOptions;
_fromAutofill = fromAutofill; _fromAutofill = fromAutofill;
FromAutofillFramework = _appOptions?.FromAutofillFramework ?? false; FromAutofillFramework = _appOptions?.FromAutofillFramework ?? false;
@ -171,6 +174,8 @@ namespace Bit.App.Pages
} }
_scrollView.Scrolled += (sender, args) => _vm.HandleScroll(); _scrollView.Scrolled += (sender, args) => _vm.HandleScroll();
}); });
// Hide password reprompt option if using key connector
_passwordPrompt.IsVisible = !await _keyConnectorService.GetUsesKeyConnector();
} }
protected override void OnDisappearing() 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 { public static string ExportVaultWarning {
get { get {
return ResourceManager.GetString("ExportVaultWarning", resourceCulture); 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 { public static string Fido2Title {
get { get {
return ResourceManager.GetString("Fido2Title", resourceCulture); return ResourceManager.GetString("Fido2Title", resourceCulture);
@ -3658,5 +3694,17 @@ namespace Bit.App.Resources {
return ResourceManager.GetString("DisablePersonalVaultExportPolicyInEffect", resourceCulture); 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>
<data name="ExportVaultMasterPasswordDescription" xml:space="preserve"> <data name="ExportVaultMasterPasswordDescription" xml:space="preserve">
<value>Enter your master password to export your vault data.</value> <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>
<data name="ExportVaultWarning" xml:space="preserve"> <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> <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"> <data name="UpdatePasswordError" xml:space="preserve">
<value>Currently unable to update password</value> <value>Currently unable to update password</value>
</data> </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"> <data name="Fido2Title" xml:space="preserve">
<value>FIDO2 WebAuthn</value> <value>FIDO2 WebAuthn</value>
</data> </data>
@ -2063,4 +2081,10 @@
<data name="DisablePersonalVaultExportPolicyInEffect"> <data name="DisablePersonalVaultExportPolicyInEffect">
<value>One or more organization policies prevents your from exporting your personal vault.</value> <value>One or more organization policies prevents your from exporting your personal vault.</value>
</data> </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> </root>

View file

@ -3,6 +3,7 @@ using Bit.Core.Abstractions;
using Bit.App.Abstractions; using Bit.App.Abstractions;
using Bit.App.Resources; using Bit.App.Resources;
using System; using System;
using Bit.Core.Utilities;
namespace Bit.App.Services namespace Bit.App.Services
{ {
@ -21,6 +22,11 @@ namespace Bit.App.Services
public async Task<bool> ShowPasswordPromptAsync() public async Task<bool> ShowPasswordPromptAsync()
{ {
if (!await Enabled())
{
return true;
}
Func<string, Task<bool>> validator = async (string password) => Func<string, Task<bool>> validator = async (string password) =>
{ {
// Assume user has canceled. // Assume user has canceled.
@ -34,5 +40,11 @@ namespace Bit.App.Services
return await _platformUtilsService.ShowPasswordDialogAsync(AppResources.PasswordConfirmation, AppResources.PasswordConfirmationDesc, validator); 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<SyncResponse> GetSyncAsync();
Task PostAccountKeysAsync(KeysRequest request); Task PostAccountKeysAsync(KeysRequest request);
Task PostAccountVerifyPasswordAsync(PasswordVerificationRequest request); Task PostAccountVerifyPasswordAsync(PasswordVerificationRequest request);
Task PostAccountRequestOTP();
Task PostAccountVerifyOTPAsync(VerifyOTPRequest request);
Task<CipherResponse> PostCipherAsync(CipherRequest request); Task<CipherResponse> PostCipherAsync(CipherRequest request);
Task<CipherResponse> PostCipherCreateAsync(CipherCreateRequest request); Task<CipherResponse> PostCipherCreateAsync(CipherCreateRequest request);
Task<FolderResponse> PostFolderAsync(FolderRequest request); Task<FolderResponse> PostFolderAsync(FolderRequest request);
@ -63,6 +65,11 @@ namespace Bit.Core.Abstractions
Task<OrganizationAutoEnrollStatusResponse> GetOrganizationAutoEnrollStatusAsync(string identifier); Task<OrganizationAutoEnrollStatusResponse> GetOrganizationAutoEnrollStatusAsync(string identifier);
Task PutOrganizationUserResetPasswordEnrollmentAsync(string orgId, string userId, Task PutOrganizationUserResetPasswordEnrollmentAsync(string orgId, string userId,
OrganizationUserResetPasswordEnrollmentRequest request); 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> GetSendAsync(string id);
Task<SendResponse> PostSendAsync(SendRequest request); 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 GetIssuer();
string GetName(); string GetName();
bool GetPremium(); bool GetPremium();
bool GetIsExternal();
Task<string> GetRefreshTokenAsync(); Task<string> GetRefreshTokenAsync();
Task<string> GetTokenAsync(); Task<string> GetTokenAsync();
Task ToggleTokensAsync(); 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; MaxStorageGb = response.MaxStorageGb;
Permissions = response.Permissions ?? new Permissions(); Permissions = response.Permissions ?? new Permissions();
Identifier = response.Identifier; Identifier = response.Identifier;
UsesKeyConnector = response.UsesKeyConnector;
KeyConnectorUrl = response.KeyConnectorUrl;
} }
public string Id { get; set; } public string Id { get; set; }
@ -50,5 +52,7 @@ namespace Bit.Core.Models.Data
public short? MaxStorageGb { get; set; } public short? MaxStorageGb { get; set; }
public Permissions Permissions { get; set; } = new Permissions(); public Permissions Permissions { get; set; } = new Permissions();
public string Identifier { get; set; } 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; MaxStorageGb = obj.MaxStorageGb;
Permissions = obj.Permissions ?? new Permissions(); Permissions = obj.Permissions ?? new Permissions();
Identifier = obj.Identifier; Identifier = obj.Identifier;
UsesKeyConnector = obj.UsesKeyConnector;
KeyConnectorUrl = obj.KeyConnectorUrl;
} }
public string Id { get; set; } public string Id { get; set; }
@ -50,6 +52,8 @@ namespace Bit.Core.Models.Domain
public short? MaxStorageGb { get; set; } public short? MaxStorageGb { get; set; }
public Permissions Permissions { get; set; } = new Permissions(); public Permissions Permissions { get; set; } = new Permissions();
public string Identifier { get; set; } public string Identifier { get; set; }
public bool UsesKeyConnector { get; set; }
public string KeyConnectorUrl { get; set; }
public bool CanAccess 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 KdfType Kdf { get; set; }
public int? KdfIterations { get; set; } public int? KdfIterations { get; set; }
public bool ForcePasswordReset { 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 bool Enabled { get; set; }
public Permissions Permissions { get; set; } = new Permissions(); public Permissions Permissions { get; set; } = new Permissions();
public string Identifier { get; set; } 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 string SecurityStamp { get; set; }
public bool ForcePasswordReset { get; set; } public bool ForcePasswordReset { get; set; }
public List<ProfileOrganizationResponse> Organizations { 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); 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) public Task PutUpdateTempPasswordAsync(UpdateTempPasswordRequest request)
{ {
return SendAsync<UpdateTempPasswordRequest, object>(HttpMethod.Put, "/accounts/update-temp-password", return SendAsync<UpdateTempPasswordRequest, object>(HttpMethod.Put, "/accounts/update-temp-password",
request, true, false); 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 #endregion
#region Folder APIs #region Folder APIs
@ -423,6 +444,11 @@ namespace Bit.Core.Services
$"/organizations/{identifier}/auto-enroll-status", null, true, true); $"/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 #endregion
#region Organization User APIs #region Organization User APIs
@ -436,6 +462,70 @@ namespace Bit.Core.Services
#endregion #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 #region Helpers
public async Task<string> GetActiveBearerTokenAsync() public async Task<string> GetActiveBearerTokenAsync()

View file

@ -12,6 +12,7 @@ namespace Bit.Core.Services
public class AuthService : IAuthService public class AuthService : IAuthService
{ {
private readonly ICryptoService _cryptoService; private readonly ICryptoService _cryptoService;
private readonly ICryptoFunctionService _cryptoFunctionService;
private readonly IApiService _apiService; private readonly IApiService _apiService;
private readonly IUserService _userService; private readonly IUserService _userService;
private readonly ITokenService _tokenService; private readonly ITokenService _tokenService;
@ -20,12 +21,14 @@ namespace Bit.Core.Services
private readonly IPlatformUtilsService _platformUtilsService; private readonly IPlatformUtilsService _platformUtilsService;
private readonly IMessagingService _messagingService; private readonly IMessagingService _messagingService;
private readonly IVaultTimeoutService _vaultTimeoutService; private readonly IVaultTimeoutService _vaultTimeoutService;
private readonly IKeyConnectorService _keyConnectorService;
private readonly bool _setCryptoKeys; private readonly bool _setCryptoKeys;
private SymmetricCryptoKey _key; private SymmetricCryptoKey _key;
public AuthService( public AuthService(
ICryptoService cryptoService, ICryptoService cryptoService,
ICryptoFunctionService cryptoFunctionService,
IApiService apiService, IApiService apiService,
IUserService userService, IUserService userService,
ITokenService tokenService, ITokenService tokenService,
@ -34,9 +37,11 @@ namespace Bit.Core.Services
IPlatformUtilsService platformUtilsService, IPlatformUtilsService platformUtilsService,
IMessagingService messagingService, IMessagingService messagingService,
IVaultTimeoutService vaultTimeoutService, IVaultTimeoutService vaultTimeoutService,
IKeyConnectorService keyConnectorService,
bool setCryptoKeys = true) bool setCryptoKeys = true)
{ {
_cryptoService = cryptoService; _cryptoService = cryptoService;
_cryptoFunctionService = cryptoFunctionService;
_apiService = apiService; _apiService = apiService;
_userService = userService; _userService = userService;
_tokenService = tokenService; _tokenService = tokenService;
@ -45,6 +50,7 @@ namespace Bit.Core.Services
_platformUtilsService = platformUtilsService; _platformUtilsService = platformUtilsService;
_messagingService = messagingService; _messagingService = messagingService;
_vaultTimeoutService = vaultTimeoutService; _vaultTimeoutService = vaultTimeoutService;
_keyConnectorService = keyConnectorService;
_setCryptoKeys = setCryptoKeys; _setCryptoKeys = setCryptoKeys;
TwoFactorProviders = new Dictionary<TwoFactorProviderType, TwoFactorProvider>(); TwoFactorProviders = new Dictionary<TwoFactorProviderType, TwoFactorProvider>();
@ -275,7 +281,7 @@ namespace Bit.Core.Services
private async Task<AuthResult> LogInHelperAsync(string email, string hashedPassword, string localHashedPassword, private async Task<AuthResult> LogInHelperAsync(string email, string hashedPassword, string localHashedPassword,
string code, string codeVerifier, string redirectUrl, SymmetricCryptoKey key, string code, string codeVerifier, string redirectUrl, SymmetricCryptoKey key,
TwoFactorProviderType? twoFactorProvider = null, string twoFactorToken = null, bool? remember = null, 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 storedTwoFactorToken = await _tokenService.GetTwoFactorTokenAsync(email);
var appId = await _appIdService.GetAppIdAsync(); var appId = await _appIdService.GetAppIdAsync();
@ -353,27 +359,75 @@ namespace Bit.Core.Services
tokenResponse.Kdf, tokenResponse.KdfIterations); tokenResponse.Kdf, tokenResponse.KdfIterations);
if (_setCryptoKeys) if (_setCryptoKeys)
{ {
await _cryptoService.SetKeyAsync(key); if (key != null)
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)
{ {
try await _cryptoService.SetKeyAsync(key);
{ }
var keyPair = await _cryptoService.MakeKeyPairAsync();
await _apiService.PostAccountKeysAsync(new KeysRequest if (localHashedPassword != null)
{ {
PublicKey = keyPair.Item1, await _cryptoService.SetKeyHashAsync(localHashedPassword);
EncryptedPrivateKey = keyPair.Item2.EncryptedString }
});
tokenResponse.PrivateKey = keyPair.Item2.EncryptedString; if (code == null || tokenResponse.Key != null)
} {
catch { } 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; _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 IMessagingService _messagingService;
private readonly IPolicyService _policyService; private readonly IPolicyService _policyService;
private readonly ISendService _sendService; private readonly ISendService _sendService;
private readonly IKeyConnectorService _keyConnectorService;
private readonly Func<bool, Task> _logoutCallbackAsync; private readonly Func<bool, Task> _logoutCallbackAsync;
public SyncService( public SyncService(
@ -39,6 +40,7 @@ namespace Bit.Core.Services
IMessagingService messagingService, IMessagingService messagingService,
IPolicyService policyService, IPolicyService policyService,
ISendService sendService, ISendService sendService,
IKeyConnectorService keyConnectorService,
Func<bool, Task> logoutCallbackAsync) Func<bool, Task> logoutCallbackAsync)
{ {
_userService = userService; _userService = userService;
@ -52,6 +54,7 @@ namespace Bit.Core.Services
_messagingService = messagingService; _messagingService = messagingService;
_policyService = policyService; _policyService = policyService;
_sendService = sendService; _sendService = sendService;
_keyConnectorService = keyConnectorService;
_logoutCallbackAsync = logoutCallbackAsync; _logoutCallbackAsync = logoutCallbackAsync;
} }
@ -329,6 +332,7 @@ namespace Bit.Core.Services
await _userService.ReplaceOrganizationsAsync(organizations); await _userService.ReplaceOrganizationsAsync(organizations);
await _userService.SetEmailVerifiedAsync(response.EmailVerified); await _userService.SetEmailVerifiedAsync(response.EmailVerified);
await _userService.SetForcePasswordReset(response.ForcePasswordReset); await _userService.SetForcePasswordReset(response.ForcePasswordReset);
await _keyConnectorService.SetUsesKeyConnector(response.UsesKeyConnector);
} }
private async Task SyncFoldersAsync(string userId, List<FolderResponse> response) private async Task SyncFoldersAsync(string userId, List<FolderResponse> response)

View file

@ -4,6 +4,7 @@ using Newtonsoft.Json.Linq;
using System; using System;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Linq;
namespace Bit.Core.Services namespace Bit.Core.Services
{ {
@ -230,6 +231,16 @@ namespace Bit.Core.Services
return decoded["iss"].Value<string>(); 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() private async Task<bool> SkipTokenStorage()
{ {
var timeout = await _storageService.GetAsync<int?>(Constants.VaultTimeoutKey); 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 IMessagingService _messagingService;
private readonly ITokenService _tokenService; private readonly ITokenService _tokenService;
private readonly IPolicyService _policyService; private readonly IPolicyService _policyService;
private readonly IKeyConnectorService _keyConnectorService;
private readonly Action<bool> _lockedCallback; private readonly Action<bool> _lockedCallback;
private readonly Func<bool, Task> _loggedOutCallback; private readonly Func<bool, Task> _loggedOutCallback;
@ -35,6 +36,7 @@ namespace Bit.Core.Services
IMessagingService messagingService, IMessagingService messagingService,
ITokenService tokenService, ITokenService tokenService,
IPolicyService policyService, IPolicyService policyService,
IKeyConnectorService keyConnectorService,
Action<bool> lockedCallback, Action<bool> lockedCallback,
Func<bool, Task> loggedOutCallback) Func<bool, Task> loggedOutCallback)
{ {
@ -49,6 +51,7 @@ namespace Bit.Core.Services
_messagingService = messagingService; _messagingService = messagingService;
_tokenService = tokenService; _tokenService = tokenService;
_policyService = policyService; _policyService = policyService;
_keyConnectorService = keyConnectorService;
_lockedCallback = lockedCallback; _lockedCallback = lockedCallback;
_loggedOutCallback = loggedOutCallback; _loggedOutCallback = loggedOutCallback;
} }
@ -119,6 +122,18 @@ namespace Bit.Core.Services
{ {
return; 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) if (allowSoftLock)
{ {
BiometricLocked = await IsBiometricLockSetAsync(); BiometricLocked = await IsBiometricLockSetAsync();

View file

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