From 6edac4f296d9c6c6cb8049703094d7f8857415df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Bispo?= Date: Thu, 27 Jul 2023 18:17:04 +0100 Subject: [PATCH] [PM-2408] Pending admin approval requests (#2643) * [PM-2408] Save PendingAdminAuthRequest to state * [PM-2408] Add logic to check pending auth requests before creating a new one. * [PM-2408] Remove active user params * [PM-2408] Fix bug where TDE MP unlock wasn't syncing and vault appeared empty * [PM-2408] Add reconciledOptions * [PM-2408] Set user key when login using master password * [PM-2408] Generate Fingerprint phrase and PubKey locally to avoid MITM attacks * [PM-2408] inheritdoc * [PM-1280] TDE 2FA - approval options navigation (#2646) * [PM-1208] Add navigation to device approval options after 2FA auth when TDE is enabled * [PM-1208] Add navigations to iOS extensions Co-authored-by: Federico Maccaroni --- src/App/Pages/Accounts/LockPageViewModel.cs | 3 + .../LoginPasswordlessRequestViewModel.cs | 88 ++++++++++++++----- src/App/Pages/Accounts/TwoFactorPage.xaml.cs | 8 ++ .../Pages/Accounts/TwoFactorPageViewModel.cs | 27 +++++- src/Core/Abstractions/IAuthService.cs | 5 +- src/Core/Abstractions/IStateService.cs | 2 + src/Core/Constants.cs | 1 + .../Models/Domain/PendingAdminAuthRequest.cs | 10 +++ src/Core/Services/AuthService.cs | 8 +- src/Core/Services/StateService.cs | 22 ++++- .../CredentialProviderViewController.cs | 1 + src/iOS.Extension/LoadingViewController.cs | 1 + .../LoadingViewController.cs | 1 + 13 files changed, 149 insertions(+), 28 deletions(-) create mode 100644 src/Core/Models/Domain/PendingAdminAuthRequest.cs diff --git a/src/App/Pages/Accounts/LockPageViewModel.cs b/src/App/Pages/Accounts/LockPageViewModel.cs index dc6221e2e..6d097f888 100644 --- a/src/App/Pages/Accounts/LockPageViewModel.cs +++ b/src/App/Pages/Accounts/LockPageViewModel.cs @@ -34,6 +34,7 @@ namespace Bit.App.Pages private readonly IPolicyService _policyService; private readonly IPasswordGenerationService _passwordGenerationService; private IDeviceTrustCryptoService _deviceTrustCryptoService; + private readonly ISyncService _syncService; private string _email; private string _masterPassword; private string _pin; @@ -65,6 +66,7 @@ namespace Bit.App.Pages _policyService = ServiceContainer.Resolve(); _passwordGenerationService = ServiceContainer.Resolve(); _deviceTrustCryptoService = ServiceContainer.Resolve(); + _syncService = ServiceContainer.Resolve(); PageTitle = AppResources.VerifyMasterPassword; TogglePasswordCommand = new Command(TogglePassword); @@ -480,6 +482,7 @@ namespace Bit.App.Pages private async Task DoContinueAsync() { + _syncService.FullSyncAsync(false).FireAndForget(); await _stateService.SetBiometricLockedAsync(false); _watchDeviceService.SyncDataToWatchAsync().FireAndForget(); _messagingService.Send("unlocked"); diff --git a/src/App/Pages/Accounts/LoginPasswordlessRequestViewModel.cs b/src/App/Pages/Accounts/LoginPasswordlessRequestViewModel.cs index 9cd7dfa0a..bbb6a6085 100644 --- a/src/App/Pages/Accounts/LoginPasswordlessRequestViewModel.cs +++ b/src/App/Pages/Accounts/LoginPasswordlessRequestViewModel.cs @@ -13,6 +13,7 @@ using Bit.Core; using Bit.Core.Abstractions; using Bit.Core.Enums; using Bit.Core.Models.Domain; +using Bit.Core.Models.Response; using Bit.Core.Services; using Bit.Core.Utilities; using Xamarin.CommunityToolkit.ObjectModel; @@ -34,6 +35,8 @@ namespace Bit.App.Pages private IEnvironmentService _environmentService; private ILogger _logger; private IDeviceTrustCryptoService _deviceTrustCryptoService; + private readonly ICryptoFunctionService _cryptoFunctionService; + private readonly ICryptoService _cryptoService; protected override II18nService i18nService => _i18nService; protected override IEnvironmentService environmentService => _environmentService; @@ -61,6 +64,8 @@ namespace Bit.App.Pages _stateService = ServiceContainer.Resolve(); _logger = ServiceContainer.Resolve(); _deviceTrustCryptoService = ServiceContainer.Resolve(); + _cryptoFunctionService = ServiceContainer.Resolve(); + _cryptoService = ServiceContainer.Resolve(); PageTitle = AppResources.LogInWithAnotherDevice; @@ -202,14 +207,22 @@ namespace Bit.App.Pages private async Task CheckLoginRequestStatus() { - if (string.IsNullOrEmpty(_requestId) || string.IsNullOrEmpty(_requestAccessCode)) + if (string.IsNullOrEmpty(_requestId)) { return; } try { - var response = await _authService.GetPasswordlessLoginResponseAsync(_requestId, _requestAccessCode); + PasswordlessLoginResponse response = null; + if (await _stateService.IsAuthenticatedAsync()) + { + response = await _authService.GetPasswordlessLoginRequestByIdAsync(_requestId); + } + else + { + response = await _authService.GetPasswordlessLoginResquestAsync(_requestId, _requestAccessCode); + } if (response.RequestApproved == null || !response.RequestApproved.Value) { @@ -223,8 +236,7 @@ namespace Bit.App.Pages if (authResult == null && await _stateService.IsAuthenticatedAsync()) { - _syncService.FullSyncAsync(true).FireAndForget(); - LogInSuccessAction?.Invoke(); + await HandleLoginCompleteAsync(); return; } @@ -243,8 +255,7 @@ namespace Bit.App.Pages } else { - _syncService.FullSyncAsync(true).FireAndForget(); - LogInSuccessAction?.Invoke(); + await HandleLoginCompleteAsync(); } } catch (Exception ex) @@ -254,31 +265,66 @@ namespace Bit.App.Pages } } + private async Task HandleLoginCompleteAsync() + { + await _stateService.SetPendingAdminAuthRequestAsync(null); + _syncService.FullSyncAsync(true).FireAndForget(); + LogInSuccessAction?.Invoke(); + } + private async Task CreatePasswordlessLoginAsync() { await Device.InvokeOnMainThreadAsync(() => _deviceActionService.ShowLoadingAsync(AppResources.Loading)); - var response = await _authService.PasswordlessCreateLoginRequestAsync(_email, AuthRequestType); - if (response != null) + PasswordlessLoginResponse response = null; + var pendingRequest = await _stateService.GetPendingAdminAuthRequestAsync(); + if (pendingRequest != null && _authRequestType == AuthRequestType.AdminApproval) { - //TODO TDE if is admin type save to memory to later see if it was approved - /* - const adminAuthReqStorable = new AdminAuthRequestStorable({ - id: reqResponse.id, - privateKey: this.authRequestKeyPair.privateKey, - }); - - await this.stateService.setAdminAuthRequest(adminAuthReqStorable); - */ - FingerprintPhrase = response.FingerprintPhrase; - _requestId = response.Id; - _requestAccessCode = response.RequestAccessCode; - _requestKeyPair = response.RequestKeyPair; + response = await _authService.GetPasswordlessLoginRequestByIdAsync(pendingRequest.Id); + if (response == null || (response.IsAnswered && !response.RequestApproved.Value)) + { + // handle pending auth request not valid remove it from state + await _stateService.SetPendingAdminAuthRequestAsync(null); + pendingRequest = null; + } + else + { + // Derive pubKey from privKey in state to avoid MITM attacks + // Also generate FingerprintPhrase locally for the same reason + var derivedPublicKey = await _cryptoFunctionService.RsaExtractPublicKeyAsync(pendingRequest.PrivateKey); + response.FingerprintPhrase = string.Join("-", await _cryptoService.GetFingerprintAsync(Email, derivedPublicKey)); + response.RequestKeyPair = new Tuple(derivedPublicKey, pendingRequest.PrivateKey); + } } + if (response == null) + { + response = await _authService.PasswordlessCreateLoginRequestAsync(_email, AuthRequestType); + } + + await HandlePasswordlessLoginAsync(response, pendingRequest == null && _authRequestType == AuthRequestType.AdminApproval); await _deviceActionService.HideLoadingAsync(); } + private async Task HandlePasswordlessLoginAsync(PasswordlessLoginResponse response, bool createPendingAdminRequest) + { + if (response == null) + { + throw new ArgumentNullException(AppResources.GenericErrorMessage); + } + + if (createPendingAdminRequest) + { + var pendingAuthRequest = new PendingAdminAuthRequest { Id = response.Id, PrivateKey = response.RequestKeyPair.Item2 }; + await _stateService.SetPendingAdminAuthRequestAsync(pendingAuthRequest); + } + + FingerprintPhrase = response.FingerprintPhrase; + _requestId = response.Id; + _requestAccessCode = response.RequestAccessCode; + _requestKeyPair = response.RequestKeyPair; + } + private void HandleException(Exception ex) { Xamarin.Essentials.MainThread.InvokeOnMainThreadAsync(async () => diff --git a/src/App/Pages/Accounts/TwoFactorPage.xaml.cs b/src/App/Pages/Accounts/TwoFactorPage.xaml.cs index 29f4b7c14..e3c1ec755 100644 --- a/src/App/Pages/Accounts/TwoFactorPage.xaml.cs +++ b/src/App/Pages/Accounts/TwoFactorPage.xaml.cs @@ -37,6 +37,8 @@ namespace Bit.App.Pages Device.BeginInvokeOnMainThread(async () => await TwoFactorAuthSuccessAsync()); _vm.UpdateTempPasswordAction = () => Device.BeginInvokeOnMainThread(async () => await UpdateTempPasswordAsync()); + _vm.StartDeviceApprovalOptionsAction = + () => Device.BeginInvokeOnMainThread(async () => await StartDeviceApprovalOptionsAsync()); _vm.CloseAction = async () => await Navigation.PopModalAsync(); DuoWebView = _duoWebView; if (Device.RuntimePlatform == Device.Android) @@ -180,6 +182,12 @@ namespace Bit.App.Pages await Navigation.PushModalAsync(new NavigationPage(page)); } + private async Task StartDeviceApprovalOptionsAsync() + { + var page = new LoginApproveDevicePage(); + await Navigation.PushModalAsync(new NavigationPage(page)); + } + private async Task TwoFactorAuthSuccessAsync() { if (_authingWithSso) diff --git a/src/App/Pages/Accounts/TwoFactorPageViewModel.cs b/src/App/Pages/Accounts/TwoFactorPageViewModel.cs index 486b54f80..54bd90983 100644 --- a/src/App/Pages/Accounts/TwoFactorPageViewModel.cs +++ b/src/App/Pages/Accounts/TwoFactorPageViewModel.cs @@ -11,6 +11,7 @@ using Bit.Core.Abstractions; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Request; +using Bit.Core.Services; using Bit.Core.Utilities; using Newtonsoft.Json; using Xamarin.CommunityToolkit.ObjectModel; @@ -33,7 +34,7 @@ namespace Bit.App.Pages private readonly II18nService _i18nService; private readonly IAppIdService _appIdService; private readonly ILogger _logger; - + private readonly IDeviceTrustCryptoService _deviceTrustCryptoService; private TwoFactorProviderType? _selectedProviderType; private string _totpInstruction; private string _webVaultUrl = "https://vault.bitwarden.com"; @@ -55,6 +56,7 @@ namespace Bit.App.Pages _i18nService = ServiceContainer.Resolve("i18nService"); _appIdService = ServiceContainer.Resolve("appIdService"); _logger = ServiceContainer.Resolve(); + _deviceTrustCryptoService = ServiceContainer.Resolve(); PageTitle = AppResources.TwoStepLogin; SubmitCommand = new Command(async () => await SubmitAsync()); @@ -118,6 +120,7 @@ namespace Bit.App.Pages public Command SubmitCommand { get; } public ICommand MoreCommand { get; } public Action TwoFactorAuthSuccessAction { get; set; } + public Action StartDeviceApprovalOptionsAction { get; set; } public Action StartSetPasswordAction { get; set; } public Action CloseAction { get; set; } public Action UpdateTempPasswordAction { get; set; } @@ -315,6 +318,7 @@ namespace Bit.App.Pages var task = Task.Run(() => _syncService.FullSyncAsync(true)); await _deviceActionService.HideLoadingAsync(); + var decryptOptions = await _stateService.GetAccountDecryptionOptions(); _messagingService.Send("listenYubiKeyOTP", false); _broadcasterService.Unsubscribe(nameof(TwoFactorPage)); @@ -326,6 +330,27 @@ namespace Bit.App.Pages { UpdateTempPasswordAction?.Invoke(); } + else if (decryptOptions?.TrustedDeviceOption != null) + { + // If user doesn't have a MP, but has reset password permission, they must set a MP + if (!decryptOptions.HasMasterPassword && + decryptOptions.TrustedDeviceOption.HasManageResetPasswordPermission) + { + StartSetPasswordAction?.Invoke(); + } + else if (result.ForcePasswordReset) + { + UpdateTempPasswordAction?.Invoke(); + } + else if (await _deviceTrustCryptoService.IsDeviceTrustedAsync()) + { + TwoFactorAuthSuccessAction?.Invoke(); + } + else + { + StartDeviceApprovalOptionsAction?.Invoke(); + } + } else { TwoFactorAuthSuccessAction?.Invoke(); diff --git a/src/Core/Abstractions/IAuthService.cs b/src/Core/Abstractions/IAuthService.cs index d463b83a9..aa5ec47f0 100644 --- a/src/Core/Abstractions/IAuthService.cs +++ b/src/Core/Abstractions/IAuthService.cs @@ -32,7 +32,10 @@ namespace Bit.Core.Abstractions Task> GetPasswordlessLoginRequestsAsync(); Task> GetActivePasswordlessLoginRequestsAsync(); Task GetPasswordlessLoginRequestByIdAsync(string id); - Task GetPasswordlessLoginResponseAsync(string id, string accessCode); + /// + /// Gets a passwordless login request by and . No authentication required. + /// + Task GetPasswordlessLoginResquestAsync(string id, string accessCode); Task PasswordlessLoginAsync(string id, string pubKey, bool requestApproved); Task PasswordlessCreateLoginRequestAsync(string email, AuthRequestType authRequestType); diff --git a/src/Core/Abstractions/IStateService.cs b/src/Core/Abstractions/IStateService.cs index 7a7d5a8e6..12a2f80d3 100644 --- a/src/Core/Abstractions/IStateService.cs +++ b/src/Core/Abstractions/IStateService.cs @@ -183,6 +183,8 @@ namespace Bit.Core.Abstractions Task GetPreLoginEmailAsync(); Task SetPreLoginEmailAsync(string value); Task GetAccountDecryptionOptions(string userId = null); + Task GetPendingAdminAuthRequestAsync(string userId = null); + Task SetPendingAdminAuthRequestAsync(PendingAdminAuthRequest value, string userId = null); string GetLocale(); void SetLocale(string locale); ConfigResponse GetConfigs(); diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 7bfd1eb30..38fad57d6 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -124,6 +124,7 @@ namespace Bit.Core public static string PushCurrentTokenKey(string userId) => $"pushCurrentToken_{userId}"; public static string ShouldConnectToWatchKey(string userId) => $"shouldConnectToWatch_{userId}"; public static string ScreenCaptureAllowedKey(string userId) => $"screenCaptureAllowed_{userId}"; + public static string PendingAdminAuthRequest(string userId) => $"pendingAdminAuthRequest_{userId}"; [Obsolete] public static string KeyKey(string userId) => $"key_{userId}"; [Obsolete] diff --git a/src/Core/Models/Domain/PendingAdminAuthRequest.cs b/src/Core/Models/Domain/PendingAdminAuthRequest.cs new file mode 100644 index 000000000..15727e368 --- /dev/null +++ b/src/Core/Models/Domain/PendingAdminAuthRequest.cs @@ -0,0 +1,10 @@ +using System; +namespace Bit.Core.Models.Domain +{ + public class PendingAdminAuthRequest + { + public string Id { get; set; } + public byte[] PrivateKey { get; set; } + } +} + diff --git a/src/Core/Services/AuthService.cs b/src/Core/Services/AuthService.cs index 13622942d..c3445e8f8 100644 --- a/src/Core/Services/AuthService.cs +++ b/src/Core/Services/AuthService.cs @@ -504,6 +504,9 @@ namespace Bit.Core.Services if (localHashedPassword != null) { await _cryptoService.SetPasswordHashAsync(localHashedPassword); + await _cryptoService.SetMasterKeyAsync(masterKey); + var userKey = await _cryptoService.DecryptUserKeyWithMasterKeyAsync(masterKey); + await _cryptoService.SetUserKeyAsync(userKey); } if (code == null || tokenResponse.Key != null) @@ -591,7 +594,6 @@ namespace Bit.Core.Services ); await _apiService.PostSetKeyConnectorKey(setPasswordRequest); } - } _authedUserId = _tokenService.GetUserId(); @@ -628,14 +630,14 @@ namespace Bit.Core.Services var activeRequests = requests.Where(r => !r.IsAnswered && !r.IsExpired).OrderByDescending(r => r.CreationDate).ToList(); return await PopulateFingerprintPhrasesAsync(activeRequests); } - public async Task GetPasswordlessLoginRequestByIdAsync(string id) { var response = await _apiService.GetAuthRequestAsync(id); return await PopulateFingerprintPhraseAsync(response, await _stateService.GetEmailAsync()); } - public async Task GetPasswordlessLoginResponseAsync(string id, string accessCode) + /// + public async Task GetPasswordlessLoginResquestAsync(string id, string accessCode) { return await _apiService.GetAuthResponseAsync(id, accessCode); } diff --git a/src/Core/Services/StateService.cs b/src/Core/Services/StateService.cs index 480bacaaa..738b5346c 100644 --- a/src/Core/Services/StateService.cs +++ b/src/Core/Services/StateService.cs @@ -336,12 +336,16 @@ namespace Bit.Core.Services public async Task GetUserKeyMasterKeyAsync(string userId = null) { - return await _storageMediatorService.GetAsync(Constants.UserKeyKey(userId), false); + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultInMemoryOptionsAsync()); + return await _storageMediatorService.GetAsync(Constants.UserKeyKey(reconciledOptions.UserId), false); } public async Task SetUserKeyMasterKeyAsync(string value, string userId = null) { - await _storageMediatorService.SaveAsync(Constants.UserKeyKey(userId), value, false); + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultInMemoryOptionsAsync()); + await _storageMediatorService.SaveAsync(Constants.UserKeyKey(reconciledOptions.UserId), value, false); } public async Task CanAccessPremiumAsync(string userId = null) @@ -1347,6 +1351,20 @@ namespace Bit.Core.Services await _storageMediatorService.SaveAsync(Constants.ShouldTrustDevice, value); } + public async Task GetPendingAdminAuthRequestAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + return await _storageMediatorService.GetAsync(Constants.PendingAdminAuthRequest(reconciledOptions.UserId), true); + } + + public async Task SetPendingAdminAuthRequestAsync(PendingAdminAuthRequest value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + await _storageMediatorService.SaveAsync(Constants.PendingAdminAuthRequest(reconciledOptions.UserId), value, true); + } + public ConfigResponse GetConfigs() { return _storageMediatorService.Get(Constants.ConfigsKey); diff --git a/src/iOS.Autofill/CredentialProviderViewController.cs b/src/iOS.Autofill/CredentialProviderViewController.cs index ed0ae3ec4..0ba9ba458 100644 --- a/src/iOS.Autofill/CredentialProviderViewController.cs +++ b/src/iOS.Autofill/CredentialProviderViewController.cs @@ -568,6 +568,7 @@ namespace Bit.iOS.Autofill { vm.TwoFactorAuthSuccessAction = () => DismissLockAndContinue(); vm.StartSetPasswordAction = () => DismissViewController(false, () => LaunchSetPasswordFlow()); + vm.StartDeviceApprovalOptionsAction = () => DismissViewController(false, () => LaunchDeviceApprovalOptionsFlow()); if (authingWithSso) { vm.CloseAction = () => DismissViewController(false, () => LaunchLoginSsoFlow()); diff --git a/src/iOS.Extension/LoadingViewController.cs b/src/iOS.Extension/LoadingViewController.cs index 597ee90ff..7f9d451c2 100644 --- a/src/iOS.Extension/LoadingViewController.cs +++ b/src/iOS.Extension/LoadingViewController.cs @@ -590,6 +590,7 @@ namespace Bit.iOS.Extension { vm.TwoFactorAuthSuccessAction = () => DismissLockAndContinue(); vm.StartSetPasswordAction = () => DismissViewController(false, () => LaunchSetPasswordFlow()); + vm.StartDeviceApprovalOptionsAction = () => DismissViewController(false, () => LaunchDeviceApprovalOptionsFlow()); if (authingWithSso) { vm.CloseAction = () => DismissViewController(false, () => LaunchLoginSsoFlow()); diff --git a/src/iOS.ShareExtension/LoadingViewController.cs b/src/iOS.ShareExtension/LoadingViewController.cs index 95b753b08..200946cfa 100644 --- a/src/iOS.ShareExtension/LoadingViewController.cs +++ b/src/iOS.ShareExtension/LoadingViewController.cs @@ -390,6 +390,7 @@ namespace Bit.iOS.ShareExtension { vm.TwoFactorAuthSuccessAction = () => DismissLockAndContinue(); vm.StartSetPasswordAction = () => DismissAndLaunch(() => LaunchSetPasswordFlow()); + vm.StartDeviceApprovalOptionsAction = () => DismissViewController(false, () => LaunchDeviceApprovalOptionsFlow()); if (authingWithSso) { vm.CloseAction = () => DismissAndLaunch(() => LaunchLoginSsoFlow());