diff --git a/src/App/Pages/Accounts/LoginApproveDevicePage.xaml.cs b/src/App/Pages/Accounts/LoginApproveDevicePage.xaml.cs index 03b815253..95c088a4e 100644 --- a/src/App/Pages/Accounts/LoginApproveDevicePage.xaml.cs +++ b/src/App/Pages/Accounts/LoginApproveDevicePage.xaml.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; - +using Bit.App.Models; using Xamarin.Forms; namespace Bit.App.Pages @@ -9,7 +9,7 @@ namespace Bit.App.Pages { private readonly LoginApproveDeviceViewModel _vm; - public LoginApproveDevicePage() + public LoginApproveDevicePage(AppOptions appOptions = null) { InitializeComponent(); _vm = BindingContext as LoginApproveDeviceViewModel; diff --git a/src/App/Pages/Accounts/LoginApproveDeviceViewModel.cs b/src/App/Pages/Accounts/LoginApproveDeviceViewModel.cs index b82343529..8044dab48 100644 --- a/src/App/Pages/Accounts/LoginApproveDeviceViewModel.cs +++ b/src/App/Pages/Accounts/LoginApproveDeviceViewModel.cs @@ -1,7 +1,13 @@ using System; using System.Threading.Tasks; using System.Windows.Input; +using Bit.App.Abstractions; using Bit.App.Resources; +using Bit.App.Utilities.AccountManagement; +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Bit.Core.Models.Request; +using Bit.Core.Utilities; using Xamarin.CommunityToolkit.ObjectModel; namespace Bit.App.Pages @@ -12,6 +18,8 @@ namespace Bit.App.Pages private bool _approveWithMyOtherDeviceEnabled; private bool _requestAdminApprovalEnabled; private bool _approveWithMasterPasswordEnabled; + private readonly IStateService _stateService; + private readonly IApiService _apiService; public ICommand ApproveWithMyOtherDeviceCommand { get; } public ICommand RequestAdminApprovalCommand { get; } @@ -19,7 +27,11 @@ namespace Bit.App.Pages public LoginApproveDeviceViewModel() { + _stateService = ServiceContainer.Resolve(); + _apiService = ServiceContainer.Resolve(); + PageTitle = AppResources.LoggedIn; + ApproveWithMyOtherDeviceCommand = new AsyncCommand(InitAsync, onException: ex => HandleException(ex), allowsMultipleExecutions: false); @@ -59,9 +71,25 @@ namespace Bit.App.Pages public async Task InitAsync() { - ApproveWithMyOtherDeviceEnabled = true; - RequestAdminApprovalEnabled = true; - ApproveWithMasterPasswordEnabled = true; + try + { + var decryptOptions = await _stateService.GetAccountDecryptionOptions(); + RequestAdminApprovalEnabled = decryptOptions.TrustedDeviceOption.HasAdminApproval; + ApproveWithMasterPasswordEnabled = decryptOptions.HasMasterPassword; + } + catch (Exception ex) + { + HandleException(ex); + } + + try + { + ApproveWithMyOtherDeviceEnabled = await _apiService.GetDevicesExistenceByTypes(DeviceTypeExtensions.GetDesktopAndMobileTypes().ToArray()); + } + catch (Exception ex) + { + HandleException(ex); + } } } } diff --git a/src/App/Pages/Accounts/LoginSsoPage.xaml.cs b/src/App/Pages/Accounts/LoginSsoPage.xaml.cs index 3871dc8e5..a910c9454 100644 --- a/src/App/Pages/Accounts/LoginSsoPage.xaml.cs +++ b/src/App/Pages/Accounts/LoginSsoPage.xaml.cs @@ -110,6 +110,11 @@ namespace Bit.App.Pages { RestoreAppOptionsFromCopy(); await AppHelpers.ClearPreviousPage(); + + // Just for testing the screen + Application.Current.MainPage = new NavigationPage(new LoginApproveDevicePage(_appOptions)); + return; + if (await _vaultTimeoutService.IsLockedAsync()) { Application.Current.MainPage = new NavigationPage(new LockPage(_appOptions)); diff --git a/src/Core/Abstractions/IApiService.cs b/src/Core/Abstractions/IApiService.cs index 19930ce55..7a5ada431 100644 --- a/src/Core/Abstractions/IApiService.cs +++ b/src/Core/Abstractions/IApiService.cs @@ -92,5 +92,7 @@ namespace Bit.Core.Abstractions Task GetUsernameFromAsync(ForwardedEmailServiceType service, UsernameGeneratorConfig config); Task GetKnownDeviceAsync(string email, string deviceIdentifier); Task GetOrgDomainSsoDetailsAsync(string email); + Task GetDevicesExistenceByTypes(DeviceType[] deviceTypes); + Task PutUpdateTrustedDeviceKeys(UpdateTrustedDeviceKeysRequest request); } } diff --git a/src/Core/Abstractions/IStateService.cs b/src/Core/Abstractions/IStateService.cs index 4062599af..1e1044ee0 100644 --- a/src/Core/Abstractions/IStateService.cs +++ b/src/Core/Abstractions/IStateService.cs @@ -172,6 +172,7 @@ namespace Bit.Core.Abstractions Task GetAvatarColorAsync(string userId = null); Task GetPreLoginEmailAsync(); Task SetPreLoginEmailAsync(string value); + Task GetAccountDecryptionOptions(string userId = null); string GetLocale(); void SetLocale(string locale); } diff --git a/src/Core/Enums/DeviceType.cs b/src/Core/Enums/DeviceType.cs index 69b740756..7c4b7065a 100644 --- a/src/Core/Enums/DeviceType.cs +++ b/src/Core/Enums/DeviceType.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Enums +using System.Collections.Generic; +using System.Linq; + +namespace Bit.Core.Enums { public enum DeviceType : byte { @@ -24,4 +27,24 @@ VivaldiExtension = 19, SafariExtension = 20 } + + public static class DeviceTypeExtensions + { + public static List GetMobileTypes() => new List + { + DeviceType.Android, + DeviceType.AndroidAmazon, + DeviceType.iOS + }; + + public static List GetDesktopTypes() => new List + { + DeviceType.WindowsDesktop, + DeviceType.MacOsDesktop, + DeviceType.LinuxDesktop, + DeviceType.UWP, + }; + + public static List GetDesktopAndMobileTypes() => GetMobileTypes().Concat(GetDesktopTypes()).ToList(); + } } diff --git a/src/Core/Models/Domain/Account.cs b/src/Core/Models/Domain/Account.cs index 6267d5815..f28a9867e 100644 --- a/src/Core/Models/Domain/Account.cs +++ b/src/Core/Models/Domain/Account.cs @@ -53,6 +53,7 @@ namespace Bit.Core.Models.Domain HasPremiumPersonally = copy.HasPremiumPersonally; AvatarColor = copy.AvatarColor; ForcePasswordResetReason = copy.ForcePasswordResetReason; + UserDecryptionOptions = copy.UserDecryptionOptions; } public string UserId; @@ -68,6 +69,7 @@ namespace Bit.Core.Models.Domain public bool? EmailVerified; public bool? HasPremiumPersonally; public ForcePasswordResetReason? ForcePasswordResetReason; + public AccountDecryptionOptions UserDecryptionOptions; } public class AccountTokens diff --git a/src/Core/Models/Domain/AccountDecryptionOptions.cs b/src/Core/Models/Domain/AccountDecryptionOptions.cs new file mode 100644 index 000000000..8101bd9f3 --- /dev/null +++ b/src/Core/Models/Domain/AccountDecryptionOptions.cs @@ -0,0 +1,21 @@ +using System; +namespace Bit.Core.Models.Domain +{ + public class AccountDecryptionOptions + { + public bool HasMasterPassword { get; set; } + public TrustedDeviceOption TrustedDeviceOption { get; set; } + public KeyConnectorOption KeyConnectorOption { get; set; } + } + + public class TrustedDeviceOption + { + public bool HasAdminApproval { get; set; } + } + + public class KeyConnectorOption + { + public bool KeyConnectorUrl { get; set; } + } +} + diff --git a/src/Core/Models/Request/UpdateTrustedDeviceKeysRequest.cs b/src/Core/Models/Request/UpdateTrustedDeviceKeysRequest.cs new file mode 100644 index 000000000..90ae4098e --- /dev/null +++ b/src/Core/Models/Request/UpdateTrustedDeviceKeysRequest.cs @@ -0,0 +1,12 @@ +using System; +namespace Bit.Core.Models.Request +{ + public class UpdateTrustedDeviceKeysRequest + { + public string DeviceIdentifier { get; set; } + public string DevicePublicKeyEncryptedUserKey { get; set; } + public string UserKeyEncryptedDevicePublicKey { get; set; } + public string DeviceKeyEncryptedDevicePrivateKey { get; set; } + } +} + diff --git a/src/Core/Models/Response/DeviceResponse.cs b/src/Core/Models/Response/DeviceResponse.cs new file mode 100644 index 000000000..39c6de52e --- /dev/null +++ b/src/Core/Models/Response/DeviceResponse.cs @@ -0,0 +1,17 @@ +using System; +using Bit.Core.Enums; + +namespace Bit.Core.Models.Response +{ + public class DeviceResponse + { + public string Id { get; set; } + public string UserId { get; set; } + public string Name { get; set; } + public string Identifier { get; set; } + public DeviceType Type { get; set; } + public string CreationDate { get; set; } + public string RevisionDate { get; set; } + } +} + diff --git a/src/Core/Models/Response/IdentityTokenResponse.cs b/src/Core/Models/Response/IdentityTokenResponse.cs index 55d49bd24..1385f2c46 100644 --- a/src/Core/Models/Response/IdentityTokenResponse.cs +++ b/src/Core/Models/Response/IdentityTokenResponse.cs @@ -27,6 +27,7 @@ namespace Bit.Core.Models.Response public bool ForcePasswordReset { get; set; } public string KeyConnectorUrl { get; set; } public MasterPasswordPolicyOptions MasterPasswordPolicy { get; set; } + public AccountDecryptionOptions UserDecryptionOptions { get; set; } [JsonIgnore] public KdfConfig KdfConfig => new KdfConfig(Kdf, KdfIterations, KdfMemory, KdfParallelism); } diff --git a/src/Core/Services/ApiService.cs b/src/Core/Services/ApiService.cs index d486ac2c4..6bfe1dd32 100644 --- a/src/Core/Services/ApiService.cs +++ b/src/Core/Services/ApiService.cs @@ -395,12 +395,34 @@ namespace Bit.Core.Services #region Device APIs + + public Task GetKnownDeviceAsync(string email, string deviceIdentifier) + { + return SendAsync(HttpMethod.Get, "/devices/knowndevice", null, false, true, (message) => + { + message.Headers.Add("X-Device-Identifier", deviceIdentifier); + message.Headers.Add("X-Request-Email", CoreHelpers.Base64UrlEncode(Encoding.UTF8.GetBytes(email))); + }); + } + public Task PutDeviceTokenAsync(string identifier, DeviceTokenRequest request) { return SendAsync( HttpMethod.Put, $"/devices/identifier/{identifier}/token", request, true, false); } + public Task GetDevicesExistenceByTypes(DeviceType[] deviceTypes) + { + return SendAsync( + HttpMethod.Post, "/devices/exist-by-types", deviceTypes, true, true); + } + + public Task PutUpdateTrustedDeviceKeys(UpdateTrustedDeviceKeysRequest request) + { + return SendAsync( + HttpMethod.Put, $"/devices/${request.DeviceIdentifier}/keys", request, true, true); + } + #endregion #region Event APIs @@ -574,15 +596,6 @@ namespace Bit.Core.Services return SendAsync(HttpMethod.Put, $"/auth-requests/{id}", request, true, true); } - public Task GetKnownDeviceAsync(string email, string deviceIdentifier) - { - return SendAsync(HttpMethod.Get, "/devices/knowndevice", null, false, true, (message) => - { - message.Headers.Add("X-Device-Identifier", deviceIdentifier); - message.Headers.Add("X-Request-Email", CoreHelpers.Base64UrlEncode(Encoding.UTF8.GetBytes(email))); - }); - } - #endregion #region Helpers diff --git a/src/Core/Services/AuthService.cs b/src/Core/Services/AuthService.cs index 92cbadd3b..b0ce4effd 100644 --- a/src/Core/Services/AuthService.cs +++ b/src/Core/Services/AuthService.cs @@ -459,6 +459,7 @@ namespace Bit.Core.Services ForcePasswordResetReason = result.ForcePasswordReset ? ForcePasswordResetReason.AdminForcePasswordReset : (ForcePasswordResetReason?)null, + UserDecryptionOptions = tokenResponse.UserDecryptionOptions, }, new Account.AccountTokens() { diff --git a/src/Core/Services/StateService.cs b/src/Core/Services/StateService.cs index 363c44efb..ec426015f 100644 --- a/src/Core/Services/StateService.cs +++ b/src/Core/Services/StateService.cs @@ -1280,6 +1280,13 @@ namespace Bit.Core.Services await SetValueAsync(Constants.PreLoginEmailKey, value, options); } + public async Task GetAccountDecryptionOptions(string userId = null) + { + return (await GetAccountAsync( + ReconcileOptions(new StorageOptions { UserId = userId }, await GetDefaultStorageOptionsAsync()) + ))?.Profile?.UserDecryptionOptions; + } + // Helpers [Obsolete("Use IStorageMediatorService instead")]