diff --git a/src/Android/Accessibility/AccessibilityService.cs b/src/Android/Accessibility/AccessibilityService.cs index 7f6aad01b..1345a5cef 100644 --- a/src/Android/Accessibility/AccessibilityService.cs +++ b/src/Android/Accessibility/AccessibilityService.cs @@ -10,7 +10,6 @@ using Android.Views; using Android.Views.Accessibility; using Android.Widget; using Bit.App.Resources; -using Bit.Core; using Bit.Core.Abstractions; using Bit.Core.Utilities; @@ -25,7 +24,7 @@ namespace Bit.Droid.Accessibility private const string BitwardenPackage = "com.x8bit.bitwarden"; private const string BitwardenWebsite = "vault.bitwarden.com"; - private IStorageService _storageService; + private IStateService _stateService; private IBroadcasterService _broadcasterService; private DateTime? _lastSettingsReload = null; private TimeSpan _settingsReloadSpan = TimeSpan.FromMinutes(1); @@ -444,9 +443,9 @@ namespace Bit.Droid.Accessibility private void LoadServices() { - if (_storageService == null) + if (_stateService == null) { - _storageService = ServiceContainer.Resolve("storageService"); + _stateService = ServiceContainer.Resolve("stateService"); } if (_broadcasterService == null) { @@ -460,12 +459,12 @@ namespace Bit.Droid.Accessibility if (_lastSettingsReload == null || (now - _lastSettingsReload.Value) > _settingsReloadSpan) { _lastSettingsReload = now; - var uris = await _storageService.GetAsync>(Constants.AutofillBlacklistedUrisKey); + var uris = await _stateService.GetAutofillBlacklistedUrisAsync(); if (uris != null) { _blacklistedUris = new HashSet(uris); } - var isAutoFillTileAdded = await _storageService.GetAsync(Constants.AutofillTileAdded); + var isAutoFillTileAdded = await _stateService.GetAutofillTileAddedAsync(); AccessibilityHelpers.IsAutofillTileAdded = isAutoFillTileAdded.GetValueOrDefault(); } } diff --git a/src/Android/Android.csproj b/src/Android/Android.csproj index 462afcaf9..52e4f9646 100644 --- a/src/Android/Android.csproj +++ b/src/Android/Android.csproj @@ -171,7 +171,8 @@ - + + diff --git a/src/Android/Autofill/AutofillService.cs b/src/Android/Autofill/AutofillService.cs index 0d0b1cc07..c339794bc 100644 --- a/src/Android/Autofill/AutofillService.cs +++ b/src/Android/Autofill/AutofillService.cs @@ -26,9 +26,8 @@ namespace Bit.Droid.Autofill { private ICipherService _cipherService; private IVaultTimeoutService _vaultTimeoutService; - private IStorageService _storageService; private IPolicyService _policyService; - private IUserService _userService; + private IStateService _stateService; public async override void OnFillRequest(FillRequest request, CancellationSignal cancellationSignal, FillCallback callback) @@ -44,18 +43,18 @@ namespace Bit.Droid.Autofill var parser = new Parser(structure, ApplicationContext); parser.Parse(); - if (_storageService == null) + if (_stateService == null) { - _storageService = ServiceContainer.Resolve("storageService"); + _stateService = ServiceContainer.Resolve("stateService"); } - var shouldAutofill = await parser.ShouldAutofillAsync(_storageService); + var shouldAutofill = await parser.ShouldAutofillAsync(_stateService); if (!shouldAutofill) { return; } - var inlineAutofillEnabled = await _storageService.GetAsync(Constants.InlineAutofillEnabledKey) ?? true; + var inlineAutofillEnabled = await _stateService.GetInlineAutofillEnabledAsync() ?? true; if (_vaultTimeoutService == null) { @@ -76,7 +75,7 @@ namespace Bit.Droid.Autofill // build response var response = AutofillHelpers.CreateFillResponse(parser, items, locked, inlineAutofillEnabled, request); - var disableSavePrompt = await _storageService.GetAsync(Constants.AutofillDisableSavePromptKey); + var disableSavePrompt = await _stateService.GetAutofillDisableSavePromptAsync(); if (!disableSavePrompt.GetValueOrDefault()) { AutofillHelpers.AddSaveInfo(parser, request, response, parser.FieldCollection); @@ -101,12 +100,12 @@ namespace Bit.Droid.Autofill return; } - if (_storageService == null) + if (_stateService == null) { - _storageService = ServiceContainer.Resolve("storageService"); + _stateService = ServiceContainer.Resolve("stateService"); } - var disableSavePrompt = await _storageService.GetAsync(Constants.AutofillDisableSavePromptKey); + var disableSavePrompt = await _stateService.GetAutofillDisableSavePromptAsync(); if (disableSavePrompt.GetValueOrDefault()) { return; diff --git a/src/Android/Autofill/Parser.cs b/src/Android/Autofill/Parser.cs index d097d4d1f..45a2fdb0b 100644 --- a/src/Android/Autofill/Parser.cs +++ b/src/Android/Autofill/Parser.cs @@ -80,13 +80,13 @@ namespace Bit.Droid.Autofill } } - public async Task ShouldAutofillAsync(IStorageService storageService) + public async Task ShouldAutofillAsync(IStateService stateService) { var fillable = !string.IsNullOrWhiteSpace(Uri) && !AutofillHelpers.BlacklistedUris.Contains(Uri) && FieldCollection != null && FieldCollection.Fillable; if (fillable) { - var blacklistedUris = await storageService.GetAsync>(Constants.AutofillBlacklistedUrisKey); + var blacklistedUris = await stateService.GetAutofillBlacklistedUrisAsync(); if (blacklistedUris != null && blacklistedUris.Count > 0) { fillable = !new HashSet(blacklistedUris).Contains(Uri); diff --git a/src/Android/MainActivity.cs b/src/Android/MainActivity.cs index c44c83a3a..c11f8b9f7 100644 --- a/src/Android/MainActivity.cs +++ b/src/Android/MainActivity.cs @@ -32,7 +32,7 @@ namespace Bit.Droid private IDeviceActionService _deviceActionService; private IMessagingService _messagingService; private IBroadcasterService _broadcasterService; - private IUserService _userService; + private IStateService _stateService; private IAppIdService _appIdService; private IEventService _eventService; private PendingIntent _eventUploadPendingIntent; @@ -53,7 +53,7 @@ namespace Bit.Droid _deviceActionService = ServiceContainer.Resolve("deviceActionService"); _messagingService = ServiceContainer.Resolve("messagingService"); _broadcasterService = ServiceContainer.Resolve("broadcasterService"); - _userService = ServiceContainer.Resolve("userService"); + _stateService = ServiceContainer.Resolve("stateService"); _appIdService = ServiceContainer.Resolve("appIdService"); _eventService = ServiceContainer.Resolve("eventService"); @@ -70,7 +70,7 @@ namespace Bit.Droid } #if !FDROID - var appCenterHelper = new AppCenterHelper(_appIdService, _userService); + var appCenterHelper = new AppCenterHelper(_appIdService, _stateService); var appCenterTask = appCenterHelper.InitAsync(); #endif @@ -375,7 +375,7 @@ namespace Bit.Droid { Window?.SetStatusBarColor(ThemeHelpers.NavBarBackgroundColor); Window?.DecorView.SetBackgroundColor(ThemeHelpers.BackgroundColor); - ThemeHelpers.SetAppearance(ThemeManager.GetTheme(true), ThemeManager.OsDarkModeEnabled()); + ThemeHelpers.SetAppearance(ThemeManager.GetTheme(), ThemeManager.OsDarkModeEnabled()); } private void ExitApp() diff --git a/src/Android/MainApplication.cs b/src/Android/MainApplication.cs index 1957f70a2..ff2fd307a 100644 --- a/src/Android/MainApplication.cs +++ b/src/Android/MainApplication.cs @@ -113,13 +113,16 @@ namespace Bit.Droid var secureStorageService = new SecureStorageService(); var cryptoPrimitiveService = new CryptoPrimitiveService(); var mobileStorageService = new MobileStorageService(preferencesStorage, liteDbStorage); - var deviceActionService = new DeviceActionService(mobileStorageService, messagingService, + var stateService = new StateService(mobileStorageService, secureStorageService); + var stateMigrationService = + new StateMigrationService(liteDbStorage, preferencesStorage, secureStorageService); + var deviceActionService = new DeviceActionService(stateService, messagingService, broadcasterService, () => ServiceContainer.Resolve("eventService")); var platformUtilsService = new MobilePlatformUtilsService(deviceActionService, messagingService, broadcasterService); var biometricService = new BiometricService(); var cryptoFunctionService = new PclCryptoFunctionService(cryptoPrimitiveService); - var cryptoService = new CryptoService(mobileStorageService, secureStorageService, cryptoFunctionService); + var cryptoService = new CryptoService(stateService, cryptoFunctionService); var passwordRepromptService = new MobilePasswordRepromptService(platformUtilsService, cryptoService); ServiceContainer.Register("broadcasterService", broadcasterService); @@ -129,7 +132,9 @@ namespace Bit.Droid ServiceContainer.Register("cryptoPrimitiveService", cryptoPrimitiveService); ServiceContainer.Register("storageService", mobileStorageService); ServiceContainer.Register("secureStorageService", secureStorageService); - ServiceContainer.Register("clipboardService", new ClipboardService(mobileStorageService)); + ServiceContainer.Register("stateService", stateService); + ServiceContainer.Register("stateMigrationService", stateMigrationService); + ServiceContainer.Register("clipboardService", new ClipboardService(stateService)); ServiceContainer.Register("deviceActionService", deviceActionService); ServiceContainer.Register("platformUtilsService", platformUtilsService); ServiceContainer.Register("biometricService", biometricService); @@ -148,7 +153,7 @@ namespace Bit.Droid ServiceContainer.Register( "pushNotificationListenerService", notificationListenerService); var androidPushNotificationService = new AndroidPushNotificationService( - mobileStorageService, notificationListenerService); + stateService, notificationListenerService); ServiceContainer.Register( "pushNotificationService", androidPushNotificationService); #endif @@ -164,10 +169,6 @@ namespace Bit.Droid private async Task BootstrapAsync() { - var disableFavicon = await ServiceContainer.Resolve("storageService") - .GetAsync(Constants.DisableFaviconKey); - await ServiceContainer.Resolve("stateService").SaveAsync( - Constants.DisableFaviconKey, disableFavicon); await ServiceContainer.Resolve("environmentService").SetUrlsFromStorageAsync(); } } diff --git a/src/Android/Push/FirebaseMessagingService.cs b/src/Android/Push/FirebaseMessagingService.cs index 5351436c2..676390aef 100644 --- a/src/Android/Push/FirebaseMessagingService.cs +++ b/src/Android/Push/FirebaseMessagingService.cs @@ -16,10 +16,10 @@ namespace Bit.Droid.Push { public async override void OnNewToken(string token) { - var storageService = ServiceContainer.Resolve("storageService"); + var stateService = ServiceContainer.Resolve("stateService"); var pushNotificationService = ServiceContainer.Resolve("pushNotificationService"); - await storageService.SaveAsync(Core.Constants.PushRegisteredTokenKey, token); + await stateService.SetPushRegisteredTokenAsync(token); await pushNotificationService.RegisterAsync(); } diff --git a/src/Android/Receivers/PackageReplacedReceiver.cs b/src/Android/Receivers/PackageReplacedReceiver.cs index 2af350320..a4a03f9c1 100644 --- a/src/Android/Receivers/PackageReplacedReceiver.cs +++ b/src/Android/Receivers/PackageReplacedReceiver.cs @@ -1,5 +1,4 @@ -using System; -using Android.App; +using Android.App; using Android.Content; using Bit.App.Abstractions; using Bit.App.Utilities; @@ -14,9 +13,10 @@ namespace Bit.Droid.Receivers { public override async void OnReceive(Context context, Intent intent) { - var storageService = ServiceContainer.Resolve("storageService"); - await AppHelpers.PerformUpdateTasksAsync(ServiceContainer.Resolve("syncService"), - ServiceContainer.Resolve("deviceActionService"), storageService); + await AppHelpers.PerformUpdateTasksAsync( + ServiceContainer.Resolve("syncService"), + ServiceContainer.Resolve("deviceActionService"), + ServiceContainer.Resolve("stateService")); } } } diff --git a/src/Android/Renderers/CustomTabbedRenderer.cs b/src/Android/Renderers/CustomTabbedRenderer.cs index ee11f56a2..dea31033a 100644 --- a/src/Android/Renderers/CustomTabbedRenderer.cs +++ b/src/Android/Renderers/CustomTabbedRenderer.cs @@ -1,7 +1,9 @@ using Android.Content; using Android.Views; +using Bit.App.Pages; using Bit.Droid.Renderers; using Google.Android.Material.BottomNavigation; +using Google.Android.Material.Navigation; using Xamarin.Forms; using Xamarin.Forms.Platform.Android; using Xamarin.Forms.Platform.Android.AppCompat; @@ -9,7 +11,7 @@ using Xamarin.Forms.Platform.Android.AppCompat; [assembly: ExportRenderer(typeof(TabbedPage), typeof(CustomTabbedRenderer))] namespace Bit.Droid.Renderers { - public class CustomTabbedRenderer : TabbedPageRenderer, BottomNavigationView.IOnNavigationItemReselectedListener + public class CustomTabbedRenderer : TabbedPageRenderer, NavigationBarView.IOnItemReselectedListener { private TabbedPage _page; @@ -21,7 +23,7 @@ namespace Bit.Droid.Renderers if (e.NewElement != null) { _page = e.NewElement; - GetBottomNavigationView()?.SetOnNavigationItemReselectedListener(this); + GetBottomNavigationView()?.SetOnItemReselectedListener(this); } else { @@ -53,6 +55,10 @@ namespace Bit.Droid.Renderers { if (_page?.CurrentPage?.Navigation != null && _page.CurrentPage.Navigation.NavigationStack.Count > 0) { + if (_page is TabsPage tabsPage) + { + tabsPage.OnPageReselected(); + } Device.BeginInvokeOnMainThread(async () => await _page.CurrentPage.Navigation.PopToRootAsync()); } } diff --git a/src/Android/Resources/drawable/cog_environment.xml b/src/Android/Resources/drawable/cog_environment.xml new file mode 100644 index 000000000..6e5b4ff76 --- /dev/null +++ b/src/Android/Resources/drawable/cog_environment.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/src/Android/Resources/drawable/cog.xml b/src/Android/Resources/drawable/cog_settings.xml similarity index 99% rename from src/Android/Resources/drawable/cog.xml rename to src/Android/Resources/drawable/cog_settings.xml index 311d0ad0c..b8c3ba603 100644 --- a/src/Android/Resources/drawable/cog.xml +++ b/src/Android/Resources/drawable/cog_settings.xml @@ -6,4 +6,4 @@ - + \ No newline at end of file diff --git a/src/Android/Services/AndroidPushNotificationService.cs b/src/Android/Services/AndroidPushNotificationService.cs index 097c5f708..e871393f6 100644 --- a/src/Android/Services/AndroidPushNotificationService.cs +++ b/src/Android/Services/AndroidPushNotificationService.cs @@ -3,7 +3,6 @@ using System; using System.Threading.Tasks; using AndroidX.Core.App; using Bit.App.Abstractions; -using Bit.Core; using Bit.Core.Abstractions; using Xamarin.Forms; @@ -11,14 +10,14 @@ namespace Bit.Droid.Services { public class AndroidPushNotificationService : IPushNotificationService { - private readonly IStorageService _storageService; + private readonly IStateService _stateService; private readonly IPushNotificationListenerService _pushNotificationListenerService; public AndroidPushNotificationService( - IStorageService storageService, + IStateService stateService, IPushNotificationListenerService pushNotificationListenerService) { - _storageService = storageService; + _stateService = stateService; _pushNotificationListenerService = pushNotificationListenerService; } @@ -26,12 +25,12 @@ namespace Bit.Droid.Services public async Task GetTokenAsync() { - return await _storageService.GetAsync(Constants.PushCurrentTokenKey); + return await _stateService.GetPushCurrentTokenAsync(); } public async Task RegisterAsync() { - var registeredToken = await _storageService.GetAsync(Constants.PushRegisteredTokenKey); + var registeredToken = await _stateService.GetPushRegisteredTokenAsync(); var currentToken = await GetTokenAsync(); if (!string.IsNullOrWhiteSpace(registeredToken) && registeredToken != currentToken) { @@ -39,7 +38,7 @@ namespace Bit.Droid.Services } else { - await _storageService.SaveAsync(Constants.PushLastRegistrationDateKey, DateTime.UtcNow); + await _stateService.SetPushLastRegistrationDateAsync(DateTime.UtcNow); } } diff --git a/src/Android/Services/ClipboardService.cs b/src/Android/Services/ClipboardService.cs index 82f1c21db..07e720c57 100644 --- a/src/Android/Services/ClipboardService.cs +++ b/src/Android/Services/ClipboardService.cs @@ -12,12 +12,12 @@ namespace Bit.Droid.Services { public class ClipboardService : IClipboardService { - private readonly IStorageService _storageService; + private readonly IStateService _stateService; private readonly Lazy _clearClipboardPendingIntent; - public ClipboardService(IStorageService storageService) + public ClipboardService(IStateService stateService) { - _storageService = storageService; + _stateService = stateService; _clearClipboardPendingIntent = new Lazy(() => PendingIntent.GetBroadcast(CrossCurrentActivity.Current.Activity, @@ -39,7 +39,7 @@ namespace Bit.Droid.Services if (clearMs < 0) { // if not set then we need to check if the user set this config - var clearSeconds = await _storageService.GetAsync(Constants.ClearClipboardKey); + var clearSeconds = await _stateService.GetClearClipboardAsync(); if (clearSeconds != null) { clearMs = clearSeconds.Value * 1000; diff --git a/src/Android/Services/DeviceActionService.cs b/src/Android/Services/DeviceActionService.cs index c0e5a79e1..4a3e52d22 100644 --- a/src/Android/Services/DeviceActionService.cs +++ b/src/Android/Services/DeviceActionService.cs @@ -35,7 +35,7 @@ namespace Bit.Droid.Services { public class DeviceActionService : IDeviceActionService { - private readonly IStorageService _storageService; + private readonly IStateService _stateService; private readonly IMessagingService _messagingService; private readonly IBroadcasterService _broadcasterService; private readonly Func _eventServiceFunc; @@ -47,12 +47,12 @@ namespace Bit.Droid.Services private string _userAgent; public DeviceActionService( - IStorageService storageService, + IStateService stateService, IMessagingService messagingService, IBroadcasterService broadcasterService, Func eventServiceFunc) { - _storageService = storageService; + _stateService = stateService; _messagingService = messagingService; _broadcasterService = broadcasterService; _eventServiceFunc = eventServiceFunc; @@ -333,7 +333,7 @@ namespace Bit.Droid.Services try { DeleteDir(CrossCurrentActivity.Current.Activity.CacheDir); - await _storageService.SaveAsync(Constants.LastFileCacheClearKey, DateTime.UtcNow); + await _stateService.SetLastFileCacheClearAsync(DateTime.UtcNow); } catch (Exception) { } } @@ -916,9 +916,8 @@ namespace Bit.Droid.Services { if (!string.IsNullOrWhiteSpace(cipher?.Login?.Totp)) { - var userService = ServiceContainer.Resolve("userService"); - var autoCopyDisabled = await _storageService.GetAsync(Constants.DisableAutoTotpCopyKey); - var canAccessPremium = await userService.CanAccessPremiumAsync(); + var autoCopyDisabled = await _stateService.GetDisableAutoTotpCopyAsync(); + var canAccessPremium = await _stateService.CanAccessPremiumAsync(); if ((canAccessPremium || cipher.OrganizationUseTotp) && !autoCopyDisabled.GetValueOrDefault()) { var totpService = ServiceContainer.Resolve("totpService"); diff --git a/src/Android/Tiles/AutofillTileService.cs b/src/Android/Tiles/AutofillTileService.cs index 305afce37..7f9af7d8b 100644 --- a/src/Android/Tiles/AutofillTileService.cs +++ b/src/Android/Tiles/AutofillTileService.cs @@ -4,7 +4,6 @@ using Android.Content; using Android.Runtime; using Android.Service.QuickSettings; using Bit.App.Resources; -using Bit.Core; using Bit.Core.Abstractions; using Bit.Core.Utilities; using Bit.Droid.Accessibility; @@ -18,7 +17,7 @@ namespace Bit.Droid.Tile [Register("com.x8bit.bitwarden.AutofillTileService")] public class AutofillTileService : TileService { - private IStorageService _storageService; + private IStateService _stateService; public override void OnTileAdded() { @@ -59,11 +58,11 @@ namespace Bit.Droid.Tile private void SetTileAdded(bool isAdded) { AccessibilityHelpers.IsAutofillTileAdded = isAdded; - if (_storageService == null) + if (_stateService == null) { - _storageService = ServiceContainer.Resolve("storageService"); + _stateService = ServiceContainer.Resolve("stateService"); } - _storageService.SaveAsync(Constants.AutofillTileAdded, isAdded); + _stateService.SetAutofillTileAddedAsync(isAdded); } private void ScanAndFill() diff --git a/src/Android/Utilities/AppCenterHelper.cs b/src/Android/Utilities/AppCenterHelper.cs index d580fbbc6..aa3313a3e 100644 --- a/src/Android/Utilities/AppCenterHelper.cs +++ b/src/Android/Utilities/AppCenterHelper.cs @@ -12,22 +12,22 @@ namespace Bit.Droid.Utilities private const string AppSecret = "d3834185-b4a6-4347-9047-b86c65293d42"; private readonly IAppIdService _appIdService; - private readonly IUserService _userService; + private readonly IStateService _stateService; private string _userId; private string _appId; public AppCenterHelper( IAppIdService appIdService, - IUserService userService) + IStateService stateService) { _appIdService = appIdService; - _userService = userService; + _stateService = stateService; } public async Task InitAsync() { - _userId = await _userService.GetUserIdAsync(); + _userId = await _stateService.GetActiveUserIdAsync(); _appId = await _appIdService.GetAppIdAsync(); AppCenter.Start(AppSecret, typeof(Crashes)); diff --git a/src/App/App.csproj b/src/App/App.csproj index b0916f2ed..b335c3a11 100644 --- a/src/App/App.csproj +++ b/src/App/App.csproj @@ -15,12 +15,13 @@ + + - + - @@ -121,17 +122,20 @@ SendGroupingsPage.xaml Code + + MSBuild:UpdateDesignTimeXaml + @@ -416,5 +420,6 @@ + diff --git a/src/App/App.xaml.cs b/src/App/App.xaml.cs index 44c6531f8..0dada2232 100644 --- a/src/App/App.xaml.cs +++ b/src/App/App.xaml.cs @@ -4,12 +4,12 @@ using Bit.App.Pages; using Bit.App.Resources; using Bit.App.Services; using Bit.App.Utilities; -using Bit.Core; using Bit.Core.Abstractions; +using Bit.Core.Models.Data; using Bit.Core.Utilities; using System; -using System.Threading; using System.Threading.Tasks; +using Bit.Core.Enums; using Xamarin.Forms; using Xamarin.Forms.Xaml; @@ -18,7 +18,6 @@ namespace Bit.App { public partial class App : Application { - private readonly IUserService _userService; private readonly IBroadcasterService _broadcasterService; private readonly IMessagingService _messagingService; private readonly IStateService _stateService; @@ -26,7 +25,6 @@ namespace Bit.App private readonly ISyncService _syncService; private readonly IPlatformUtilsService _platformUtilsService; private readonly IAuthService _authService; - private readonly IStorageService _storageService; private readonly IStorageService _secureStorageService; private readonly IDeviceActionService _deviceActionService; @@ -40,7 +38,6 @@ namespace Bit.App Current = this; return; } - _userService = ServiceContainer.Resolve("userService"); _broadcasterService = ServiceContainer.Resolve("broadcasterService"); _messagingService = ServiceContainer.Resolve("messagingService"); _stateService = ServiceContainer.Resolve("stateService"); @@ -48,7 +45,6 @@ namespace Bit.App _syncService = ServiceContainer.Resolve("syncService"); _authService = ServiceContainer.Resolve("authService"); _platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); - _storageService = ServiceContainer.Resolve("storageService"); _secureStorageService = ServiceContainer.Resolve("secureStorageService"); _deviceActionService = ServiceContainer.Resolve("deviceActionService"); @@ -85,8 +81,12 @@ namespace Bit.App } else if (message.Command == "logout") { + var extras = message.Data as Tuple; + var userId = extras?.Item1; + var userInitiated = extras?.Item2; + var expired = extras?.Item3; Device.BeginInvokeOnMainThread(async () => - await LogOutAsync((message.Data as bool?).GetValueOrDefault())); + await LogOutAsync(userId, userInitiated, expired)); } else if (message.Command == "loggedOut") { @@ -107,6 +107,18 @@ namespace Bit.App await SleptAsync(); } } + else if (message.Command == "addAccount") + { + await AddAccount(); + } + else if (message.Command == "accountAdded") + { + await UpdateThemeAsync(); + } + else if (message.Command == "switchedAccount") + { + await SwitchedAccountAsync(); + } else if (message.Command == "migrated") { await Task.Delay(1000); @@ -168,7 +180,7 @@ namespace Bit.App if (string.IsNullOrWhiteSpace(Options.Uri)) { var updated = await AppHelpers.PerformUpdateTasksAsync(_syncService, _deviceActionService, - _storageService); + _stateService); if (!updated) { SyncIfNeeded(); @@ -192,7 +204,7 @@ namespace Bit.App var isLocked = await _vaultTimeoutService.IsLockedAsync(); if (!isLocked) { - await _storageService.SaveAsync(Constants.LastActiveTimeKey, _deviceActionService.GetActiveTime()); + await _stateService.SetLastActiveTimeAsync(_deviceActionService.GetActiveTime()); } SetTabsPageFromAutofill(isLocked); await SleptAsync(); @@ -233,7 +245,7 @@ namespace Bit.App { await Device.InvokeOnMainThreadAsync(() => { - ThemeManager.SetTheme(Device.RuntimePlatform == Device.Android, Current.Resources); + ThemeManager.SetTheme(Current.Resources); _messagingService.Send("updatedTheme"); }); } @@ -246,27 +258,70 @@ namespace Bit.App new System.Globalization.UmAlQuraCalendar(); } - private async Task LogOutAsync(bool expired) + private async Task LogOutAsync(string userId, bool? userInitiated, bool? expired) { - await AppHelpers.LogOutAsync(); + await AppHelpers.LogOutAsync(userId, userInitiated.GetValueOrDefault(true)); + await SetMainPageAsync(); _authService.LogOut(() => { - Current.MainPage = new HomePage(); - if (expired) + if (expired.GetValueOrDefault()) { _platformUtilsService.ShowToast("warning", null, AppResources.LoginExpired); } }); } + private async Task AddAccount() + { + Device.BeginInvokeOnMainThread(async () => + { + Options.HideAccountSwitcher = false; + Current.MainPage = new NavigationPage(new HomePage(Options)); + }); + } + + private async Task SwitchedAccountAsync() + { + await AppHelpers.OnAccountSwitchAsync(); + var shouldTimeout = await _vaultTimeoutService.ShouldTimeoutAsync(); + Device.BeginInvokeOnMainThread(async () => + { + if (shouldTimeout) + { + await _vaultTimeoutService.ExecuteTimeoutActionAsync(); + } + else + { + await SetMainPageAsync(); + } + await Task.Delay(50); + await UpdateThemeAsync(); + }); + } + private async Task SetMainPageAsync() { - var authed = await _userService.IsAuthenticatedAsync(); + var authed = await _stateService.IsAuthenticatedAsync(); if (authed) { - if (await _vaultTimeoutService.IsLockedAsync()) + var isLocked = await _vaultTimeoutService.IsLockedAsync(); + var shouldTimeout = await _vaultTimeoutService.ShouldTimeoutAsync(); + if (isLocked || shouldTimeout) { - Current.MainPage = new NavigationPage(new LockPage(Options)); + var vaultTimeoutAction = await _stateService.GetVaultTimeoutActionAsync(); + if (vaultTimeoutAction == VaultTimeoutAction.Logout) + { + // TODO implement orgIdentifier flow to SSO Login page, same as email flow below + // var orgIdentifier = await _stateService.GetOrgIdentifierAsync(); + + var email = await _stateService.GetEmailAsync(); + Options.HideAccountSwitcher = await _stateService.GetActiveUserIdAsync() == null; + Current.MainPage = new NavigationPage(new LoginPage(email, Options)); + } + else + { + Current.MainPage = new NavigationPage(new LockPage(Options)); + } } else if (Options.FromAutofillFramework && Options.SaveType.HasValue) { @@ -287,13 +342,25 @@ namespace Bit.App } else { - Current.MainPage = new HomePage(Options); + Options.HideAccountSwitcher = await _stateService.GetActiveUserIdAsync() == null; + if (await _vaultTimeoutService.IsLoggedOutByTimeoutAsync()) + { + // TODO implement orgIdentifier flow to SSO Login page, same as email flow below + // var orgIdentifier = await _stateService.GetOrgIdentifierAsync(); + + var email = await _stateService.GetEmailAsync(); + Current.MainPage = new NavigationPage(new LoginPage(email, Options)); + } + else + { + Current.MainPage = new NavigationPage(new HomePage(Options)); + } } } private async Task ClearCacheIfNeededAsync() { - var lastClear = await _storageService.GetAsync(Constants.LastFileCacheClearKey); + var lastClear = await _stateService.GetLastFileCacheClearAsync(); if ((DateTime.UtcNow - lastClear.GetValueOrDefault(DateTime.MinValue)).TotalDays >= 1) { var task = Task.Run(() => _deviceActionService.ClearCacheAsync()); @@ -336,12 +403,12 @@ namespace Bit.App { InitializeComponent(); SetCulture(); - ThemeManager.SetTheme(Device.RuntimePlatform == Device.Android, Current.Resources); + ThemeManager.SetTheme(Current.Resources); Current.RequestedThemeChanged += (s, a) => { UpdateThemeAsync(); }; - Current.MainPage = new HomePage(); + Current.MainPage = new NavigationPage(new HomePage(Options)); var mainPageTask = SetMainPageAsync(); ServiceContainer.Resolve("platformUtilsService").Init(); } @@ -365,10 +432,9 @@ namespace Bit.App private async Task LockedAsync(bool autoPromptBiometric) { - await _stateService.PurgeAsync(); if (autoPromptBiometric && Device.RuntimePlatform == Device.iOS) { - var vaultTimeout = await _storageService.GetAsync(Constants.VaultTimeoutKey); + var vaultTimeout = await _stateService.GetVaultTimeoutAsync(); if (vaultTimeout == 0) { autoPromptBiometric = false; @@ -398,7 +464,7 @@ namespace Bit.App } } } - await _storageService.SaveAsync(Constants.PreviousPageKey, lastPageBeforeLock); + await _stateService.SetPreviousPageInfoAsync(lastPageBeforeLock); var lockPage = new LockPage(Options, autoPromptBiometric); Device.BeginInvokeOnMainThread(() => Current.MainPage = new NavigationPage(lockPage)); } diff --git a/src/App/Controls/AccountSwitchingOverlay/AccountSwitchingOverlayView.xaml b/src/App/Controls/AccountSwitchingOverlay/AccountSwitchingOverlayView.xaml new file mode 100644 index 000000000..0edbf4503 --- /dev/null +++ b/src/App/Controls/AccountSwitchingOverlay/AccountSwitchingOverlayView.xaml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/App/Controls/AccountSwitchingOverlay/AccountSwitchingOverlayView.xaml.cs b/src/App/Controls/AccountSwitchingOverlay/AccountSwitchingOverlayView.xaml.cs new file mode 100644 index 000000000..8f213dcba --- /dev/null +++ b/src/App/Controls/AccountSwitchingOverlay/AccountSwitchingOverlayView.xaml.cs @@ -0,0 +1,142 @@ +using System; +using System.Threading.Tasks; +using System.Windows.Input; +#if !FDROID +using Microsoft.AppCenter.Crashes; +#endif +using Xamarin.CommunityToolkit.ObjectModel; +using Xamarin.Forms; + +namespace Bit.App.Controls +{ + public partial class AccountSwitchingOverlayView : ContentView + { + public static readonly BindableProperty MainFabProperty = BindableProperty.Create( + nameof(MainFab), + typeof(View), + typeof(AccountSwitchingOverlayView), + defaultBindingMode: BindingMode.OneWay); + + public View MainFab + { + get => (View)GetValue(MainFabProperty); + set => SetValue(MainFabProperty, value); + } + + public AccountSwitchingOverlayView() + { + InitializeComponent(); + + ToggleVisibililtyCommand = new AsyncCommand(ToggleVisibilityAsync, +#if !FDROID + onException: ex => Crashes.TrackError(ex), +#endif + allowsMultipleExecutions: false); + } + + public AccountSwitchingOverlayViewModel ViewModel => BindingContext as AccountSwitchingOverlayViewModel; + + public ICommand ToggleVisibililtyCommand { get; } + + public async Task ToggleVisibilityAsync() + { + if (IsVisible) + { + await HideAsync(); + } + else + { + await ShowAsync(); + } + } + + public async Task ShowAsync() + { + await ViewModel?.RefreshAccountViewsAsync(); + + await Device.InvokeOnMainThreadAsync(async () => + { + // start listView in default (off-screen) position + await _accountListContainer.TranslateTo(0, _accountListContainer.Height * -1, 0); + + // set overlay opacity to zero before making visible and start fade-in + Opacity = 0; + IsVisible = true; + this.FadeTo(1, 100); + + if (Device.RuntimePlatform == Device.Android && MainFab != null) + { + // start fab fade-out + MainFab.FadeTo(0, 200); + } + + // slide account list into view + await _accountListContainer.TranslateTo(0, 0, 200, Easing.SinOut); + }); + } + + public async Task HideAsync() + { + if (!IsVisible) + { + // already hidden, don't animate again + return; + } + // Not all animations are awaited. This is intentional to allow multiple simultaneous animations. + await Device.InvokeOnMainThreadAsync(async () => + { + // start overlay fade-out + this.FadeTo(0, 200); + + if (Device.RuntimePlatform == Device.Android && MainFab != null) + { + // start fab fade-in + MainFab.FadeTo(1, 200); + } + + // slide account list out of view + await _accountListContainer.TranslateTo(0, _accountListContainer.Height * -1, 200, Easing.SinIn); + + // remove overlay + IsVisible = false; + }); + } + + private async void FreeSpaceOverlay_Tapped(object sender, EventArgs e) + { + try + { + await HideAsync(); + } + catch (Exception ex) + { +#if !FDROID + Crashes.TrackError(ex); +#endif + } + } + + async void AccountRow_Selected(object sender, SelectedItemChangedEventArgs e) + { + try + { + if (!(e.SelectedItem is AccountViewCellViewModel item)) + { + return; + } + + ((ListView)sender).SelectedItem = null; + await Task.Delay(100); + await HideAsync(); + + ViewModel?.SelectAccountCommand?.Execute(item); + } + catch (Exception ex) + { +#if !FDROID + Crashes.TrackError(ex); +#endif + } + } + } +} diff --git a/src/App/Controls/AccountSwitchingOverlay/AccountSwitchingOverlayViewModel.cs b/src/App/Controls/AccountSwitchingOverlay/AccountSwitchingOverlayViewModel.cs new file mode 100644 index 000000000..bc15fe96a --- /dev/null +++ b/src/App/Controls/AccountSwitchingOverlay/AccountSwitchingOverlayViewModel.cs @@ -0,0 +1,69 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Windows.Input; +using Bit.Core.Abstractions; +using Bit.Core.Models.View; +using Bit.Core.Utilities; +using Microsoft.AppCenter.Crashes; +using Xamarin.CommunityToolkit.ObjectModel; +using Xamarin.Forms; + +namespace Bit.App.Controls +{ + public class AccountSwitchingOverlayViewModel : ExtendedViewModel + { + private readonly IStateService _stateService; + private readonly IMessagingService _messagingService; + + public AccountSwitchingOverlayViewModel(IStateService stateService, + IMessagingService messagingService) + { + _stateService = stateService; + _messagingService = messagingService; + + SelectAccountCommand = new AsyncCommand(SelectAccountAsync, +#if !FDROID + onException: ex => Crashes.TrackError(ex), +#endif + allowsMultipleExecutions: false); + } + + // this needs to be a new list every time for the binding to get updated, + // XF doesn't currentlyl provide a direct way to update on same instance + // https://github.com/xamarin/Xamarin.Forms/issues/1950 + public List AccountViews => _stateService?.AccountViews is null ? null : new List(_stateService.AccountViews); + + public bool AllowActiveAccountSelection { get; set; } + + public bool AllowAddAccountRow { get; set; } + + public ICommand SelectAccountCommand { get; } + + private async Task SelectAccountAsync(AccountViewCellViewModel item) + { + if (item.AccountView.IsAccount) + { + if (!item.AccountView.IsActive) + { + await _stateService.SetActiveUserAsync(item.AccountView.UserId); + _messagingService.Send("switchedAccount"); + } + else if (AllowActiveAccountSelection) + { + _messagingService.Send("switchedAccount"); + } + } + else + { + _messagingService.Send("addAccount"); + } + } + + public async Task RefreshAccountViewsAsync() + { + await _stateService.RefreshAccountViewsAsync(AllowAddAccountRow); + + Device.BeginInvokeOnMainThread(() => TriggerPropertyChanged(nameof(AccountViews))); + } + } +} diff --git a/src/App/Controls/AccountViewCell/AccountViewCell.xaml b/src/App/Controls/AccountViewCell/AccountViewCell.xaml new file mode 100644 index 000000000..77773919a --- /dev/null +++ b/src/App/Controls/AccountViewCell/AccountViewCell.xaml @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/App/Controls/AccountViewCell/AccountViewCell.xaml.cs b/src/App/Controls/AccountViewCell/AccountViewCell.xaml.cs new file mode 100644 index 000000000..8195b429b --- /dev/null +++ b/src/App/Controls/AccountViewCell/AccountViewCell.xaml.cs @@ -0,0 +1,35 @@ +using Bit.Core.Models.View; +using Xamarin.Forms; + +namespace Bit.App.Controls +{ + public partial class AccountViewCell : ViewCell + { + public static readonly BindableProperty AccountProperty = BindableProperty.Create( + nameof(Account), typeof(AccountView), typeof(AccountViewCell), default(AccountView), BindingMode.OneWay); + + public AccountViewCell() + { + InitializeComponent(); + } + + public AccountView Account + { + get => GetValue(AccountProperty) as AccountView; + set => SetValue(AccountProperty, value); + } + + protected override void OnPropertyChanged(string propertyName = null) + { + base.OnPropertyChanged(propertyName); + if (propertyName == AccountProperty.PropertyName) + { + if (Account == null) + { + return; + } + BindingContext = new AccountViewCellViewModel(Account); + } + } + } +} diff --git a/src/App/Controls/AccountViewCell/AccountViewCellViewModel.cs b/src/App/Controls/AccountViewCell/AccountViewCellViewModel.cs new file mode 100644 index 000000000..3c7d96f13 --- /dev/null +++ b/src/App/Controls/AccountViewCell/AccountViewCellViewModel.cs @@ -0,0 +1,78 @@ +using Bit.Core; +using Bit.Core.Enums; +using Bit.Core.Models.View; +using Bit.Core.Utilities; + +namespace Bit.App.Controls +{ + public class AccountViewCellViewModel : ExtendedViewModel + { + private AccountView _accountView; + private AvatarImageSource _avatar; + + public AccountViewCellViewModel(AccountView accountView) + { + AccountView = accountView; + AvatarImageSource = new AvatarImageSource(AccountView.Name, AccountView.Email); + } + + public AccountView AccountView + { + get => _accountView; + set => SetProperty(ref _accountView, value); + } + + public AvatarImageSource AvatarImageSource + { + get => _avatar; + set => SetProperty(ref _avatar, value); + } + + public bool IsAccount + { + get => AccountView.IsAccount; + } + + public bool ShowHostname + { + get => !string.IsNullOrWhiteSpace(AccountView.Hostname) && AccountView.Hostname != "vault.bitwarden.com"; + } + + public bool IsActive + { + get => AccountView.IsActive; + } + + public bool IsUnlocked + { + get => AccountView.AuthStatus == AuthenticationStatus.Unlocked; + } + + public bool IsLocked + { + get => AccountView.AuthStatus == AuthenticationStatus.Locked; + } + + public bool IsLoggedOut + { + get => AccountView.AuthStatus == AuthenticationStatus.LoggedOut; + } + + public string AuthStatusIconActive + { + get => BitwardenIcons.CheckCircle; + } + + public string AuthStatusIconNotActive + { + get + { + if (IsUnlocked) + { + return BitwardenIcons.Unlock; + } + return BitwardenIcons.Lock; + } + } + } +} diff --git a/src/App/Controls/AvatarImageSource.cs b/src/App/Controls/AvatarImageSource.cs new file mode 100644 index 000000000..1ebe3509a --- /dev/null +++ b/src/App/Controls/AvatarImageSource.cs @@ -0,0 +1,149 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SkiaSharp; +using Xamarin.Forms; + +namespace Bit.App.Controls +{ + public class AvatarImageSource : StreamImageSource + { + private string _data; + + public override bool Equals(object obj) + { + if (obj is null) + { + return false; + } + + if (obj is AvatarImageSource avatar) + { + return avatar._data == _data; + } + + return base.Equals(obj); + } + + public override int GetHashCode() => _data?.GetHashCode() ?? -1; + + public AvatarImageSource(string name = null, string email = null) + { + _data = name; + if (string.IsNullOrWhiteSpace(_data)) + { + _data = email; + } + } + + public override Func> Stream => GetStreamAsync; + + private Task GetStreamAsync(CancellationToken userToken = new CancellationToken()) + { + OnLoadingStarted(); + userToken.Register(CancellationTokenSource.Cancel); + var result = Draw(); + OnLoadingCompleted(CancellationTokenSource.IsCancellationRequested); + return Task.FromResult(result); + } + + private Stream Draw() + { + string chars = null; + string upperData = null; + + if (string.IsNullOrEmpty(_data)) + { + chars = ".."; + } + else if (_data?.Length > 1) + { + upperData = _data.ToUpper(); + chars = GetFirstLetters(upperData, 2); + } + + var bgColor = StringToColor(upperData); + var textColor = Color.White; + var size = 50; + + var bitmap = new SKBitmap( + size * 2, + size * 2, + SKImageInfo.PlatformColorType, + SKAlphaType.Premul); + var canvas = new SKCanvas(bitmap); + canvas.Clear(SKColors.Transparent); + + var midX = canvas.LocalClipBounds.Size.ToSizeI().Width / 2; + var midY = canvas.LocalClipBounds.Size.ToSizeI().Height / 2; + var radius = midX - midX / 5; + + var circlePaint = new SKPaint + { + IsAntialias = true, + Style = SKPaintStyle.Fill, + StrokeJoin = SKStrokeJoin.Miter, + Color = SKColor.Parse(bgColor.ToHex()) + }; + canvas.DrawCircle(midX, midY, radius, circlePaint); + + var typeface = SKTypeface.FromFamilyName("Arial", SKFontStyle.Normal); + var textSize = midX / 1.3f; + var textPaint = new SKPaint + { + IsAntialias = true, + Style = SKPaintStyle.Fill, + Color = SKColor.Parse(textColor.ToHex()), + TextSize = textSize, + TextAlign = SKTextAlign.Center, + Typeface = typeface + }; + var rect = new SKRect(); + textPaint.MeasureText(chars, ref rect); + canvas.DrawText(chars, midX, midY + rect.Height / 2, textPaint); + + return SKImage.FromBitmap(bitmap).Encode(SKEncodedImageFormat.Png, 100).AsStream(); + } + + private string GetFirstLetters(string data, int charCount) + { + var parts = data.Split(); + if (parts.Length > 1 && charCount <= 2) + { + var text = ""; + for (int i = 0; i < charCount; i++) + { + text += parts[i].Substring(0,1); + } + return text; + } + if (data.Length > 2) + { + return data.Substring(0, 2); + } + return null; + } + + private Color StringToColor(string str) + { + if (str == null) + { + return Color.FromHex("#33ffffff"); + } + var hash = 0; + for (var i = 0; i < str.Length; i++) + { + hash = str[i] + ((hash << 5) - hash); + } + var color = "#FF"; + for (var i = 0; i < 3; i++) + { + var value = (hash >> (i * 8)) & 0xff; + var base16 = "00" + Convert.ToString(value, 16); + color += base16.Substring(base16.Length - 2); + } + return Color.FromHex(color); + } + } +} diff --git a/src/App/Controls/ExtendedToolbarItem.cs b/src/App/Controls/ExtendedToolbarItem.cs new file mode 100644 index 000000000..9c6efe59a --- /dev/null +++ b/src/App/Controls/ExtendedToolbarItem.cs @@ -0,0 +1,29 @@ +using System; +using Xamarin.Forms; + +namespace Bit.App.Controls +{ + public class ExtendedToolbarItem : ToolbarItem + { + public bool UseOriginalImage { get; set; } + + // HACK: For the issue of correctly updating the avatar toolbar item color on iOS + // we need to subscribe to the PropertyChanged event of the ToolbarItem on the CustomNavigationRenderer + // The problem is that there are a lot of private places where the navigation renderer disposes objects + // that we don't have access to, and that we should in order to properly prevent memory leaks + // So as a hack solution we have this OnAppearing/OnDisappearing actions and methods to be called on page lifecycle + // to subscribe/unsubscribe indirectly on the CustomNavigationRenderer + public Action OnAppearingAction { get; set; } + public Action OnDisappearingAction { get; set; } + + public void OnAppearing() + { + OnAppearingAction?.Invoke(); + } + + public void OnDisappearing() + { + OnDisappearingAction?.Invoke(); + } + } +} diff --git a/src/App/Effects/ScrollViewContentInsetAdjustmentBehaviorEffect.cs b/src/App/Effects/ScrollViewContentInsetAdjustmentBehaviorEffect.cs new file mode 100644 index 000000000..c80199c84 --- /dev/null +++ b/src/App/Effects/ScrollViewContentInsetAdjustmentBehaviorEffect.cs @@ -0,0 +1,33 @@ +using Xamarin.Forms; + +namespace Bit.App.Effects +{ + public enum ScrollContentInsetAdjustmentBehavior + { + Automatic, + ScrollableAxes, + Never, + Always + } + + public class ScrollViewContentInsetAdjustmentBehaviorEffect : RoutingEffect + { + public static readonly BindableProperty ContentInsetAdjustmentBehaviorProperty = + BindableProperty.CreateAttached("ContentInsetAdjustmentBehavior", typeof(ScrollContentInsetAdjustmentBehavior), typeof(ScrollViewContentInsetAdjustmentBehaviorEffect), ScrollContentInsetAdjustmentBehavior.Automatic); + + public static ScrollContentInsetAdjustmentBehavior GetContentInsetAdjustmentBehavior(BindableObject view) + { + return (ScrollContentInsetAdjustmentBehavior)view.GetValue(ContentInsetAdjustmentBehaviorProperty); + } + + public static void SetContentInsetAdjustmentBehavior(BindableObject view, ScrollContentInsetAdjustmentBehavior value) + { + view.SetValue(ContentInsetAdjustmentBehaviorProperty, value); + } + + public ScrollViewContentInsetAdjustmentBehaviorEffect() + : base($"Bitwarden.{nameof(ScrollViewContentInsetAdjustmentBehaviorEffect)}") + { + } + } +} diff --git a/src/App/Models/AppOptions.cs b/src/App/Models/AppOptions.cs index 7d7e2b22c..0a251d7cd 100644 --- a/src/App/Models/AppOptions.cs +++ b/src/App/Models/AppOptions.cs @@ -22,6 +22,7 @@ namespace Bit.App.Models public bool IosExtension { get; set; } public Tuple CreateSend { get; set; } public bool CopyInsteadOfShareAfterSaving { get; set; } + public bool HideAccountSwitcher { get; set; } public void SetAllFrom(AppOptions o) { @@ -46,6 +47,7 @@ namespace Bit.App.Models IosExtension = o.IosExtension; CreateSend = o.CreateSend; CopyInsteadOfShareAfterSaving = o.CopyInsteadOfShareAfterSaving; + HideAccountSwitcher = o.HideAccountSwitcher; } } } diff --git a/src/App/Pages/Accounts/BaseChangePasswordViewModel.cs b/src/App/Pages/Accounts/BaseChangePasswordViewModel.cs index b8ed2a230..9a6896428 100644 --- a/src/App/Pages/Accounts/BaseChangePasswordViewModel.cs +++ b/src/App/Pages/Accounts/BaseChangePasswordViewModel.cs @@ -14,7 +14,7 @@ namespace Bit.App.Pages public class BaseChangePasswordViewModel : BaseViewModel { protected readonly IPlatformUtilsService _platformUtilsService; - protected readonly IUserService _userService; + protected readonly IStateService _stateService; protected readonly IPolicyService _policyService; protected readonly IPasswordGenerationService _passwordGenerationService; protected readonly II18nService _i18nService; @@ -31,7 +31,7 @@ namespace Bit.App.Pages protected BaseChangePasswordViewModel() { _platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); - _userService = ServiceContainer.Resolve("userService"); + _stateService = ServiceContainer.Resolve("stateService"); _policyService = ServiceContainer.Resolve("policyService"); _passwordGenerationService = ServiceContainer.Resolve("passwordGenerationService"); @@ -172,7 +172,7 @@ namespace Bit.App.Pages private async Task> GetPasswordStrengthUserInput() { - var email = await _userService.GetEmailAsync(); + var email = await _stateService.GetEmailAsync(); List userInput = null; var atPosition = email.IndexOf('@'); if (atPosition > -1) diff --git a/src/App/Pages/Accounts/EnvironmentPage.xaml.cs b/src/App/Pages/Accounts/EnvironmentPage.xaml.cs index d6a916131..1f631af71 100644 --- a/src/App/Pages/Accounts/EnvironmentPage.xaml.cs +++ b/src/App/Pages/Accounts/EnvironmentPage.xaml.cs @@ -10,14 +10,11 @@ namespace Bit.App.Pages public partial class EnvironmentPage : BaseContentPage { private readonly IPlatformUtilsService _platformUtilsService; - private readonly IMessagingService _messagingService; private readonly EnvironmentPageViewModel _vm; public EnvironmentPage() { _platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); - _messagingService = ServiceContainer.Resolve("messagingService"); - _messagingService.Send("showStatusBar", true); InitializeComponent(); _vm = BindingContext as EnvironmentPageViewModel; _vm.Page = this; @@ -35,7 +32,6 @@ namespace Bit.App.Pages _vm.SubmitSuccessAction = () => Device.BeginInvokeOnMainThread(async () => await SubmitSuccessAsync()); _vm.CloseAction = async () => { - _messagingService.Send("showStatusBar", false); await Navigation.PopModalAsync(); }; } diff --git a/src/App/Pages/Accounts/HomePage.xaml b/src/App/Pages/Accounts/HomePage.xaml index 84c727ba6..d118c6078 100644 --- a/src/App/Pages/Accounts/HomePage.xaml +++ b/src/App/Pages/Accounts/HomePage.xaml @@ -6,49 +6,74 @@ xmlns:pages="clr-namespace:Bit.App.Pages" xmlns:controls="clr-namespace:Bit.App.Controls" xmlns:u="clr-namespace:Bit.App.Utilities" - xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore" x:DataType="pages:HomeViewModel" Title="{Binding PageTitle}"> - + - + + - - - - - - - - - - - - - - + - + + diff --git a/src/App/Pages/Vault/GroupingsPage/GroupingsPage.xaml.cs b/src/App/Pages/Vault/GroupingsPage/GroupingsPage.xaml.cs index 13bf9f6ec..7c806a82e 100644 --- a/src/App/Pages/Vault/GroupingsPage/GroupingsPage.xaml.cs +++ b/src/App/Pages/Vault/GroupingsPage/GroupingsPage.xaml.cs @@ -1,14 +1,13 @@ -using Bit.App.Abstractions; -using Bit.App.Models; -using Bit.App.Resources; -using Bit.Core; -using Bit.Core.Abstractions; -using Bit.Core.Enums; -using Bit.Core.Utilities; -using System; +using System; using System.Linq; using System.Threading.Tasks; +using Bit.App.Abstractions; using Bit.App.Controls; +using Bit.App.Resources; +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Bit.Core.Models.Data; +using Bit.Core.Utilities; using Xamarin.Forms; namespace Bit.App.Pages @@ -18,7 +17,7 @@ namespace Bit.App.Pages private readonly IBroadcasterService _broadcasterService; private readonly ISyncService _syncService; private readonly IPushNotificationService _pushNotificationService; - private readonly IStorageService _storageService; + private readonly IStateService _stateService; private readonly IVaultTimeoutService _vaultTimeoutService; private readonly ICipherService _cipherService; private readonly IDeviceActionService _deviceActionService; @@ -37,7 +36,7 @@ namespace Bit.App.Pages _broadcasterService = ServiceContainer.Resolve("broadcasterService"); _syncService = ServiceContainer.Resolve("syncService"); _pushNotificationService = ServiceContainer.Resolve("pushNotificationService"); - _storageService = ServiceContainer.Resolve("storageService"); + _stateService = ServiceContainer.Resolve("stateService"); _vaultTimeoutService = ServiceContainer.Resolve("vaultTimeoutService"); _cipherService = ServiceContainer.Resolve("cipherService"); _deviceActionService = ServiceContainer.Resolve("deviceActionService"); @@ -70,6 +69,10 @@ namespace Bit.App.Pages _absLayout.Children.Remove(_fab); ToolbarItems.Remove(_addItem); } + if (!mainPage) + { + ToolbarItems.Remove(_accountAvatar); + } } protected async override void OnAppearing() @@ -80,6 +83,12 @@ namespace Bit.App.Pages IsBusy = true; } + _accountAvatar?.OnAppearing(); + if (_vm.MainPage) + { + _vm.AvatarImageSource = await GetAvatarImageSourceAsync(); + } + _broadcasterService.Subscribe(_pageName, async (message) => { if (message.Command == "syncStarted") @@ -100,7 +109,6 @@ namespace Bit.App.Pages } }); - var migratedFromV1 = await _storageService.GetAsync(Constants.MigratedFromV1); await LoadOnAppearedAsync(_mainLayout, false, async () => { if (!_syncService.SyncInProgress || (await _cipherService.GetAllAsync()).Any()) @@ -123,18 +131,6 @@ namespace Bit.App.Pages await _vm.LoadAsync(); } } - // Forced sync if for some reason we have no data after a v1 migration - if (_vm.MainPage && !_syncService.SyncInProgress && migratedFromV1.GetValueOrDefault() && - !_vm.HasCiphers && - Xamarin.Essentials.Connectivity.NetworkAccess != Xamarin.Essentials.NetworkAccess.None) - { - var triedV1ReSync = await _storageService.GetAsync(Constants.TriedV1Resync); - if (!triedV1ReSync.GetValueOrDefault()) - { - await _storageService.SaveAsync(Constants.TriedV1Resync, true); - await _syncService.FullSyncAsync(true); - } - } await ShowPreviousPageAsync(); AdjustToolbar(); }, _mainContent); @@ -145,14 +141,14 @@ namespace Bit.App.Pages } // Push registration - var lastPushRegistration = await _storageService.GetAsync(Constants.PushLastRegistrationDateKey); + var lastPushRegistration = await _stateService.GetPushLastRegistrationDateAsync(); lastPushRegistration = lastPushRegistration.GetValueOrDefault(DateTime.MinValue); if (Device.RuntimePlatform == Device.iOS) { - var pushPromptShow = await _storageService.GetAsync(Constants.PushInitialPromptShownKey); + var pushPromptShow = await _stateService.GetPushInitialPromptShownAsync(); if (!pushPromptShow.GetValueOrDefault(false)) { - await _storageService.SaveAsync(Constants.PushInitialPromptShownKey, true); + await _stateService.SetPushInitialPromptShownAsync(true); await DisplayAlert(AppResources.EnableAutomaticSyncing, AppResources.PushNotificationAlert, AppResources.OkGotIt); } @@ -168,30 +164,26 @@ namespace Bit.App.Pages { await _pushNotificationService.RegisterAsync(); } - if (!_deviceActionService.AutofillAccessibilityServiceRunning() - && !_deviceActionService.AutofillServiceEnabled()) - { - if (migratedFromV1.GetValueOrDefault()) - { - var migratedFromV1AutofillPromptShown = await _storageService.GetAsync( - Constants.MigratedFromV1AutofillPromptShown); - if (!migratedFromV1AutofillPromptShown.GetValueOrDefault()) - { - await DisplayAlert(AppResources.Autofill, - AppResources.AutofillServiceNotEnabled, AppResources.Ok); - } - } - } - await _storageService.SaveAsync(Constants.MigratedFromV1AutofillPromptShown, true); } } + protected override bool OnBackButtonPressed() + { + if (_accountListOverlay.IsVisible) + { + _accountListOverlay.HideAsync().FireAndForget(); + return true; + } + return false; + } + protected override void OnDisappearing() { base.OnDisappearing(); IsBusy = false; _broadcasterService.Unsubscribe(_pageName); _vm.DisableRefreshing(); + _accountAvatar?.OnDisappearing(); } private async void RowSelected(object sender, SelectionChangedEventArgs e) @@ -230,6 +222,7 @@ namespace Bit.App.Pages private async void Search_Clicked(object sender, EventArgs e) { + await _accountListOverlay.HideAsync(); if (DoOnce()) { var page = new CiphersPage(_vm.Filter, _vm.FolderId != null, _vm.CollectionId != null, @@ -240,21 +233,31 @@ namespace Bit.App.Pages private async void Sync_Clicked(object sender, EventArgs e) { + await _accountListOverlay.HideAsync(); await _vm.SyncAsync(); } private async void Lock_Clicked(object sender, EventArgs e) { + await _accountListOverlay.HideAsync(); await _vaultTimeoutService.LockAsync(true, true); } private async void Exit_Clicked(object sender, EventArgs e) { + await _accountListOverlay.HideAsync(); await _vm.ExitAsync(); } private async void AddButton_Clicked(object sender, EventArgs e) { + var skipAction = _accountListOverlay.IsVisible && Device.RuntimePlatform == Device.Android; + await _accountListOverlay.HideAsync(); + if (skipAction) + { + // Account list in the process of closing via tapping on invisible FAB, skip this attempt + return; + } if (!_vm.Deleted && DoOnce()) { var page = new AddEditPage(null, _vm.Type, _vm.FolderId, _vm.CollectionId); @@ -268,6 +271,7 @@ namespace Bit.App.Pages { return; } + await _accountListOverlay.HideAsync(); if (_previousPage.Page == "view" && !string.IsNullOrWhiteSpace(_previousPage.CipherId)) { await Navigation.PushModalAsync(new NavigationPage(new ViewPage(_previousPage.CipherId))); @@ -284,5 +288,10 @@ namespace Bit.App.Pages _addItem.IsEnabled = !_vm.Deleted; _addItem.IconImageSource = _vm.Deleted ? null : "plus.png"; } + + public async Task HideAccountSwitchingOverlayAsync() + { + await _accountListOverlay.HideAsync(); + } } } diff --git a/src/App/Pages/Vault/GroupingsPage/GroupingsPageViewModel.cs b/src/App/Pages/Vault/GroupingsPage/GroupingsPageViewModel.cs index 228c94fb6..20e4ddafd 100644 --- a/src/App/Pages/Vault/GroupingsPage/GroupingsPageViewModel.cs +++ b/src/App/Pages/Vault/GroupingsPage/GroupingsPageViewModel.cs @@ -1,16 +1,16 @@ -using Bit.App.Abstractions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Bit.App.Abstractions; +using Bit.App.Controls; using Bit.App.Resources; using Bit.App.Utilities; -using Bit.Core; using Bit.Core.Abstractions; using Bit.Core.Enums; using Bit.Core.Models.Domain; using Bit.Core.Models.View; using Bit.Core.Utilities; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Xamarin.Forms; namespace Bit.App.Pages @@ -39,13 +39,11 @@ namespace Bit.App.Pages private readonly IFolderService _folderService; private readonly ICollectionService _collectionService; private readonly ISyncService _syncService; - private readonly IUserService _userService; private readonly IVaultTimeoutService _vaultTimeoutService; private readonly IDeviceActionService _deviceActionService; private readonly IPlatformUtilsService _platformUtilsService; private readonly IMessagingService _messagingService; private readonly IStateService _stateService; - private readonly IStorageService _storageService; private readonly IPasswordRepromptService _passwordRepromptService; public GroupingsPageViewModel() @@ -54,13 +52,11 @@ namespace Bit.App.Pages _folderService = ServiceContainer.Resolve("folderService"); _collectionService = ServiceContainer.Resolve("collectionService"); _syncService = ServiceContainer.Resolve("syncService"); - _userService = ServiceContainer.Resolve("userService"); _vaultTimeoutService = ServiceContainer.Resolve("vaultTimeoutService"); _deviceActionService = ServiceContainer.Resolve("deviceActionService"); _platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); _messagingService = ServiceContainer.Resolve("messagingService"); _stateService = ServiceContainer.Resolve("stateService"); - _storageService = ServiceContainer.Resolve("storageService"); _passwordRepromptService = ServiceContainer.Resolve("passwordRepromptService"); Loading = true; @@ -72,6 +68,11 @@ namespace Bit.App.Pages await LoadAsync(); }); CipherOptionsCommand = new Command(CipherOptionsAsync); + + AccountSwitchingOverlayViewModel = new AccountSwitchingOverlayViewModel(_stateService, _messagingService) + { + AllowAddAccountRow = true + }; } public bool MainPage { get; set; } @@ -80,7 +81,6 @@ namespace Bit.App.Pages public string CollectionId { get; set; } public Func Filter { get; set; } public bool Deleted { get; set; } - public bool HasCiphers { get; set; } public bool HasFolders { get; set; } public bool HasCollections { get; set; } @@ -139,6 +139,9 @@ namespace Bit.App.Pages get => _websiteIconsEnabled; set => SetProperty(ref _websiteIconsEnabled, value); } + + public AccountSwitchingOverlayViewModel AccountSwitchingOverlayViewModel { get; } + public ExtendedObservableCollection GroupedItems { get; set; } public Command RefreshCommand { get; set; } public Command CipherOptionsCommand { get; set; } @@ -150,7 +153,7 @@ namespace Bit.App.Pages { return; } - var authed = await _userService.IsAuthenticatedAsync(); + var authed = await _stateService.IsAuthenticatedAsync(); if (!authed) { return; @@ -159,7 +162,7 @@ namespace Bit.App.Pages { return; } - if (await _storageService.GetAsync(Constants.SyncOnRefreshKey) && Refreshing && !SyncRefreshing) + if (await _stateService.GetSyncOnRefreshAsync() && Refreshing && !SyncRefreshing) { SyncRefreshing = true; await _syncService.FullSyncAsync(false); @@ -175,8 +178,7 @@ namespace Bit.App.Pages var groupedItems = new List(); var page = Page as GroupingsPage; - WebsiteIconsEnabled = !(await _stateService.GetAsync(Constants.DisableFaviconKey)) - .GetValueOrDefault(); + WebsiteIconsEnabled = !(await _stateService.GetDisableFaviconAsync()).GetValueOrDefault(); try { await LoadDataAsync(); diff --git a/src/App/Pages/Vault/SharePageViewModel.cs b/src/App/Pages/Vault/SharePageViewModel.cs index 1f37f088a..0c8d0fd9d 100644 --- a/src/App/Pages/Vault/SharePageViewModel.cs +++ b/src/App/Pages/Vault/SharePageViewModel.cs @@ -16,7 +16,7 @@ namespace Bit.App.Pages private readonly IDeviceActionService _deviceActionService; private readonly ICipherService _cipherService; private readonly ICollectionService _collectionService; - private readonly IUserService _userService; + private readonly IOrganizationService _organizationService; private readonly IPlatformUtilsService _platformUtilsService; private CipherView _cipher; private int _organizationSelectedIndex; @@ -28,7 +28,7 @@ namespace Bit.App.Pages { _deviceActionService = ServiceContainer.Resolve("deviceActionService"); _cipherService = ServiceContainer.Resolve("cipherService"); - _userService = ServiceContainer.Resolve("userService"); + _organizationService = ServiceContainer.Resolve("organizationService"); _platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); _collectionService = ServiceContainer.Resolve("collectionService"); Collections = new ExtendedObservableCollection(); @@ -67,7 +67,7 @@ namespace Bit.App.Pages var allCollections = await _collectionService.GetAllDecryptedAsync(); _writeableCollections = allCollections.Where(c => !c.ReadOnly).ToList(); - var orgs = await _userService.GetAllOrganizationAsync(); + var orgs = await _organizationService.GetAllAsync(); OrganizationOptions = orgs.OrderBy(o => o.Name) .Where(o => o.Enabled && o.Status == OrganizationUserStatusType.Confirmed) .Select(o => new KeyValuePair(o.Name, o.Id)).ToList(); @@ -110,7 +110,7 @@ namespace Bit.App.Pages await _cipherService.ShareWithServerAsync(cipherView, OrganizationId, checkedCollectionIds); await _deviceActionService.HideLoadingAsync(); var movedItemToOrgText = string.Format(AppResources.MovedItemToOrg, cipherView.Name, - (await _userService.GetOrganizationAsync(OrganizationId)).Name); + (await _organizationService.GetAsync(OrganizationId)).Name); _platformUtilsService.ShowToast("success", null, movedItemToOrgText); await Page.Navigation.PopModalAsync(); return true; diff --git a/src/App/Pages/Vault/ViewPageViewModel.cs b/src/App/Pages/Vault/ViewPageViewModel.cs index 3caadcf39..1a73ab69a 100644 --- a/src/App/Pages/Vault/ViewPageViewModel.cs +++ b/src/App/Pages/Vault/ViewPageViewModel.cs @@ -10,10 +10,6 @@ using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.View; using Bit.Core.Utilities; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Bit.Core; using Xamarin.Forms; @@ -23,7 +19,7 @@ namespace Bit.App.Pages { private readonly IDeviceActionService _deviceActionService; private readonly ICipherService _cipherService; - private readonly IUserService _userService; + private readonly IStateService _stateService; private readonly ITotpService _totpService; private readonly IPlatformUtilsService _platformUtilsService; private readonly IAuditService _auditService; @@ -53,7 +49,7 @@ namespace Bit.App.Pages { _deviceActionService = ServiceContainer.Resolve("deviceActionService"); _cipherService = ServiceContainer.Resolve("cipherService"); - _userService = ServiceContainer.Resolve("userService"); + _stateService = ServiceContainer.Resolve("stateService"); _totpService = ServiceContainer.Resolve("totpService"); _platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); _auditService = ServiceContainer.Resolve("auditService"); @@ -253,7 +249,7 @@ namespace Bit.App.Pages return false; } Cipher = await cipher.DecryptAsync(); - CanAccessPremium = await _userService.CanAccessPremiumAsync(); + CanAccessPremium = await _stateService.CanAccessPremiumAsync(); Fields = Cipher.Fields?.Select(f => new ViewPageFieldViewModel(this, Cipher, f)).ToList(); if (Cipher.Type == Core.Enums.CipherType.Login && !string.IsNullOrWhiteSpace(Cipher.Login.Totp) && diff --git a/src/App/Resources/AppResources.Designer.cs b/src/App/Resources/AppResources.Designer.cs index c790f6f40..af92ac166 100644 --- a/src/App/Resources/AppResources.Designer.cs +++ b/src/App/Resources/AppResources.Designer.cs @@ -311,6 +311,30 @@ namespace Bit.App.Resources { } } + public static string RemoveAccount { + get { + return ResourceManager.GetString("RemoveAccount", resourceCulture); + } + } + + public static string RemoveAccountConfirmation { + get { + return ResourceManager.GetString("RemoveAccountConfirmation", resourceCulture); + } + } + + public static string AccountAlreadyAdded { + get { + return ResourceManager.GetString("AccountAlreadyAdded", resourceCulture); + } + } + + public static string SwitchToAlreadyAddedAccountConfirmation { + get { + return ResourceManager.GetString("SwitchToAlreadyAddedAccountConfirmation", resourceCulture); + } + } + public static string MasterPassword { get { return ResourceManager.GetString("MasterPassword", resourceCulture); @@ -3719,6 +3743,36 @@ namespace Bit.App.Resources { } } + public static string AddAccount { + get { + return ResourceManager.GetString("AddAccount", resourceCulture); + } + } + + public static string AccountUnlocked { + get { + return ResourceManager.GetString("AccountUnlocked", resourceCulture); + } + } + + public static string AccountLocked { + get { + return ResourceManager.GetString("AccountLocked", resourceCulture); + } + } + + public static string AccountLoggedOut { + get { + return ResourceManager.GetString("AccountLoggedOut", resourceCulture); + } + } + + public static string AccountSwitchedAutomatically { + get { + return ResourceManager.GetString("AccountSwitchedAutomatically", resourceCulture); + } + } + public static string DeleteAccount { get { return ResourceManager.GetString("DeleteAccount", resourceCulture); diff --git a/src/App/Resources/AppResources.resx b/src/App/Resources/AppResources.resx index 369fd5b70..5055b1b80 100644 --- a/src/App/Resources/AppResources.resx +++ b/src/App/Resources/AppResources.resx @@ -275,6 +275,18 @@ Are you sure you want to log out? + + Remove Account + + + Are you sure you want to remove this account? + + + Account Already Added + + + Would you like to switch to it now? + Master Password Label for a master password. @@ -2093,6 +2105,21 @@ One or more organization policies prevents your from exporting your personal vault. + + Add Account + + + Unlocked + + + Locked + + + Logged Out + + + Switched to next available account + Delete Account diff --git a/src/App/Services/MobileStorageService.cs b/src/App/Services/MobileStorageService.cs index d1fb15cc5..8f506a323 100644 --- a/src/App/Services/MobileStorageService.cs +++ b/src/App/Services/MobileStorageService.cs @@ -13,41 +13,26 @@ namespace Bit.App.Services private readonly HashSet _preferenceStorageKeys = new HashSet { - Constants.VaultTimeoutKey, - Constants.VaultTimeoutActionKey, - Constants.ThemeKey, - Constants.DefaultUriMatch, - Constants.DisableAutoTotpCopyKey, - Constants.DisableFaviconKey, - Constants.ClearClipboardKey, - Constants.AutofillDisableSavePromptKey, - Constants.LastActiveTimeKey, + Constants.StateVersionKey, + Constants.PreAuthEnvironmentUrlsKey, + Constants.AutofillTileAdded, + Constants.AddSitePromptShownKey, Constants.PushInitialPromptShownKey, Constants.LastFileCacheClearKey, Constants.PushLastRegistrationDateKey, Constants.PushRegisteredTokenKey, Constants.PushCurrentTokenKey, Constants.LastBuildKey, - Constants.MigratedFromV1, - Constants.MigratedFromV1AutofillPromptShown, - Constants.TriedV1Resync, Constants.ClearCiphersCacheKey, Constants.BiometricIntegrityKey, Constants.iOSAutoFillClearCiphersCacheKey, Constants.iOSAutoFillBiometricIntegrityKey, Constants.iOSExtensionClearCiphersCacheKey, Constants.iOSExtensionBiometricIntegrityKey, - Constants.EnvironmentUrlsKey, - Constants.InlineAutofillEnabledKey, - Constants.InvalidUnlockAttempts, + Constants.RememberedEmailKey, + Constants.RememberedOrgIdentifierKey, }; - private readonly HashSet _migrateToPreferences = new HashSet - { - Constants.EnvironmentUrlsKey, - }; - private readonly HashSet _haveMigratedToPreferences = new HashSet(); - public MobileStorageService( IStorageService preferenceStorageService, IStorageService liteDbStorageService) @@ -60,24 +45,9 @@ namespace Bit.App.Services { if (_preferenceStorageKeys.Contains(key)) { - var prefValue = await _preferencesStorageService.GetAsync(key); - if (prefValue != null || !_migrateToPreferences.Contains(key) || - _haveMigratedToPreferences.Contains(key)) - { - return prefValue; - } + return await _preferencesStorageService.GetAsync(key); } - var liteDbValue = await _liteDbStorageService.GetAsync(key); - if (_migrateToPreferences.Contains(key)) - { - if (liteDbValue != null) - { - await _preferencesStorageService.SaveAsync(key, liteDbValue); - await _liteDbStorageService.RemoveAsync(key); - } - _haveMigratedToPreferences.Add(key); - } - return liteDbValue; + return await _liteDbStorageService.GetAsync(key); } public Task SaveAsync(string key, T obj) diff --git a/src/App/Services/PushNotificationListenerService.cs b/src/App/Services/PushNotificationListenerService.cs index e0d34e197..9c3154641 100644 --- a/src/App/Services/PushNotificationListenerService.cs +++ b/src/App/Services/PushNotificationListenerService.cs @@ -21,9 +21,8 @@ namespace Bit.App.Services private bool _showNotification; private bool _resolved; - private IStorageService _storageService; private ISyncService _syncService; - private IUserService _userService; + private IStateService _stateService; private IAppIdService _appIdService; private IApiService _apiService; private IMessagingService _messagingService; @@ -64,8 +63,8 @@ namespace Bit.App.Services return; } - var myUserId = await _userService.GetUserIdAsync(); - var isAuthenticated = await _userService.IsAuthenticatedAsync(); + var myUserId = await _stateService.GetActiveUserIdAsync(); + var isAuthenticated = await _stateService.IsAuthenticatedAsync(); switch (notification.Type) { case NotificationType.SyncCipherUpdate: @@ -135,7 +134,7 @@ namespace Bit.App.Services { Resolve(); Debug.WriteLine($"{TAG} - Device Registered - Token : {token}"); - var isAuthenticated = await _userService.IsAuthenticatedAsync(); + var isAuthenticated = await _stateService.IsAuthenticatedAsync(); if (!isAuthenticated) { Debug.WriteLine($"{TAG} - not auth"); @@ -146,7 +145,7 @@ namespace Bit.App.Services try { #if DEBUG - await _storageService.RemoveAsync(Constants.PushInstallationRegistrationError); + await _stateService.SetPushInstallationRegistrationErrorAsync(null); #endif await _apiService.PutDeviceTokenAsync(appId, @@ -154,10 +153,10 @@ namespace Bit.App.Services Debug.WriteLine($"{TAG} Registered device with server."); - await _storageService.SaveAsync(Constants.PushLastRegistrationDateKey, DateTime.UtcNow); + await _stateService.SetPushLastRegistrationDateAsync(DateTime.UtcNow); if (deviceType == Device.Android) { - await _storageService.SaveAsync(Constants.PushCurrentTokenKey, token); + await _stateService.SetPushCurrentTokenAsync(token); } } #if DEBUG @@ -165,11 +164,11 @@ namespace Bit.App.Services { Debug.WriteLine($"{TAG} Failed to register device."); - await _storageService.SaveAsync(Constants.PushInstallationRegistrationError, apiEx.Error?.Message); + await _stateService.SetPushInstallationRegistrationErrorAsync(apiEx.Error?.Message); } catch (Exception e) { - await _storageService.SaveAsync(Constants.PushInstallationRegistrationError, e.Message); + await _stateService.SetPushInstallationRegistrationErrorAsync(e.Message); throw; } #else @@ -200,9 +199,8 @@ namespace Bit.App.Services { return; } - _storageService = ServiceContainer.Resolve("storageService"); _syncService = ServiceContainer.Resolve("syncService"); - _userService = ServiceContainer.Resolve("userService"); + _stateService = ServiceContainer.Resolve("stateService"); _appIdService = ServiceContainer.Resolve("appIdService"); _apiService = ServiceContainer.Resolve("apiService"); _messagingService = ServiceContainer.Resolve("messagingService"); diff --git a/src/App/Utilities/AppHelpers.cs b/src/App/Utilities/AppHelpers.cs index bf11e143a..82d8b6ec0 100644 --- a/src/App/Utilities/AppHelpers.cs +++ b/src/App/Utilities/AppHelpers.cs @@ -2,7 +2,6 @@ using Bit.App.Abstractions; using Bit.App.Pages; using Bit.App.Resources; -using Bit.Core; using Bit.Core.Abstractions; using Bit.Core.Models.View; using Bit.Core.Utilities; @@ -14,6 +13,7 @@ using System.Threading.Tasks; using Bit.App.Models; using Bit.Core.Enums; using Bit.Core.Exceptions; +using Bit.Core.Models.Data; using Newtonsoft.Json; using Xamarin.Essentials; using Xamarin.Forms; @@ -46,8 +46,8 @@ namespace Bit.App.Utilities } if (!string.IsNullOrWhiteSpace(cipher.Login.Totp)) { - var userService = ServiceContainer.Resolve("userService"); - var canAccessPremium = await userService.CanAccessPremiumAsync(); + var stateService = ServiceContainer.Resolve("stateService"); + var canAccessPremium = await stateService.CanAccessPremiumAsync(); if (canAccessPremium || cipher.OrganizationUseTotp) { options.Add(AppResources.CopyTotp); @@ -330,33 +330,15 @@ namespace Bit.App.Utilities } public static async Task PerformUpdateTasksAsync(ISyncService syncService, - IDeviceActionService deviceActionService, IStorageService storageService) + IDeviceActionService deviceActionService, IStateService stateService) { var currentBuild = deviceActionService.GetBuildNumber(); - var lastBuild = await storageService.GetAsync(Constants.LastBuildKey); - if (lastBuild == null) - { - // Installed - var currentTimeout = await storageService.GetAsync(Constants.VaultTimeoutKey); - if (currentTimeout == null) - { - await storageService.SaveAsync(Constants.VaultTimeoutKey, 15); - } - - var currentAction = await storageService.GetAsync(Constants.VaultTimeoutActionKey); - if (currentAction == null) - { - await storageService.SaveAsync(Constants.VaultTimeoutActionKey, "lock"); - } - } - else if (lastBuild != currentBuild) + var lastBuild = await stateService.GetLastBuildAsync(); + if (lastBuild == null || lastBuild != currentBuild) { // Updated var tasks = Task.Run(() => syncService.FullSyncAsync(true)); - } - if (lastBuild != currentBuild) - { - await storageService.SaveAsync(Constants.LastBuildKey, currentBuild); + await stateService.SetLastBuildAsync(currentBuild); return true; } return false; @@ -418,35 +400,34 @@ namespace Bit.App.Utilities public static async Task ClearPreviousPage() { - var storageService = ServiceContainer.Resolve("storageService"); - var previousPage = await storageService.GetAsync(Constants.PreviousPageKey); + var stateService = ServiceContainer.Resolve("stateService"); + var previousPage = await stateService.GetPreviousPageInfoAsync(); if (previousPage != null) { - await storageService.RemoveAsync(Constants.PreviousPageKey); + await stateService.SetPreviousPageInfoAsync(null); } return previousPage; } public static async Task IncrementInvalidUnlockAttemptsAsync() { - var storageService = ServiceContainer.Resolve("storageService"); - var invalidUnlockAttempts = await storageService.GetAsync(Constants.InvalidUnlockAttempts); + var stateService = ServiceContainer.Resolve("stateService"); + var invalidUnlockAttempts = await stateService.GetInvalidUnlockAttemptsAsync(); invalidUnlockAttempts++; - await storageService.SaveAsync(Constants.InvalidUnlockAttempts, invalidUnlockAttempts); + await stateService.SetInvalidUnlockAttemptsAsync(invalidUnlockAttempts); return invalidUnlockAttempts; } public static async Task ResetInvalidUnlockAttemptsAsync() { - var storageService = ServiceContainer.Resolve("storageService"); - await storageService.RemoveAsync(Constants.InvalidUnlockAttempts); + var stateService = ServiceContainer.Resolve("stateService"); + await stateService.SetInvalidUnlockAttemptsAsync(null); } public static async Task IsVaultTimeoutImmediateAsync() { - var storageService = ServiceContainer.Resolve("storageService"); - - var vaultTimeoutMinutes = await storageService.GetAsync(Constants.VaultTimeoutKey); + var stateService = ServiceContainer.Resolve("stateService"); + var vaultTimeoutMinutes = await stateService.GetVaultTimeoutAsync(); if (vaultTimeoutMinutes.GetValueOrDefault(-1) == 0) { return true; @@ -466,10 +447,8 @@ namespace Bit.App.Utilities return Convert.ToBase64String(Encoding.UTF8.GetBytes(multiByteEscaped)); } - public static async Task LogOutAsync() + public static async Task LogOutAsync(string userId, bool userInitiated = false) { - var userService = ServiceContainer.Resolve("userService"); - var syncService = ServiceContainer.Resolve("syncService"); var tokenService = ServiceContainer.Resolve("tokenService"); var cryptoService = ServiceContainer.Resolve("cryptoService"); var settingsService = ServiceContainer.Resolve("settingsService"); @@ -481,23 +460,70 @@ namespace Bit.App.Utilities var vaultTimeoutService = ServiceContainer.Resolve("vaultTimeoutService"); var stateService = ServiceContainer.Resolve("stateService"); var deviceActionService = ServiceContainer.Resolve("deviceActionService"); + var policyService = ServiceContainer.Resolve("policyService"); var searchService = ServiceContainer.Resolve("searchService"); - var userId = await userService.GetUserIdAsync(); + if (userId == null) + { + userId = await stateService.GetActiveUserIdAsync(); + } + await Task.WhenAll( - syncService.SetLastSyncAsync(DateTime.MinValue), - tokenService.ClearTokenAsync(), - cryptoService.ClearKeysAsync(), - userService.ClearAsync(), - settingsService.ClearAsync(userId), cipherService.ClearAsync(userId), folderService.ClearAsync(userId), collectionService.ClearAsync(userId), - passwordGenerationService.ClearAsync(), - vaultTimeoutService.ClearAsync(), - stateService.PurgeAsync(), + passwordGenerationService.ClearAsync(userId), + deviceActionService.ClearCacheAsync(), + tokenService.ClearTokenAsync(userId), + cryptoService.ClearKeysAsync(userId), + settingsService.ClearAsync(userId), + vaultTimeoutService.ClearAsync(userId), + policyService.ClearAsync(userId), + stateService.LogoutAccountAsync(userId, userInitiated)); + + stateService.BiometricLocked = true; + searchService.ClearIndex(); + + // check if we switched accounts automatically + if (userInitiated && await stateService.GetActiveUserIdAsync() != null) + { + var messagingService = ServiceContainer.Resolve("messagingService"); + messagingService.Send("switchedAccount"); + + var platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); + platformUtilsService.ShowToast("info", null, AppResources.AccountSwitchedAutomatically); + } + } + + public static async Task OnAccountSwitchAsync() + { + var environmentService = ServiceContainer.Resolve("environmentService"); + var tokenService = ServiceContainer.Resolve("tokenService"); + var cryptoService = ServiceContainer.Resolve("cryptoService"); + var settingsService = ServiceContainer.Resolve("settingsService"); + var cipherService = ServiceContainer.Resolve("cipherService"); + var folderService = ServiceContainer.Resolve("folderService"); + var collectionService = ServiceContainer.Resolve("collectionService"); + var sendService = ServiceContainer.Resolve("sendService"); + var passwordGenerationService = ServiceContainer.Resolve( + "passwordGenerationService"); + var deviceActionService = ServiceContainer.Resolve("deviceActionService"); + var policyService = ServiceContainer.Resolve("policyService"); + var searchService = ServiceContainer.Resolve("searchService"); + + await environmentService.SetUrlsFromStorageAsync(); + + await Task.WhenAll( + cipherService.ClearCacheAsync(), deviceActionService.ClearCacheAsync()); - vaultTimeoutService.BiometricLocked = true; + tokenService.ClearCache(); + cryptoService.ClearCache(); + settingsService.ClearCache(); + folderService.ClearCache(); + collectionService.ClearCache(); + sendService.ClearCache(); + passwordGenerationService.ClearCache(); + policyService.ClearCache(); searchService.ClearIndex(); } } diff --git a/src/App/Utilities/ThemeManager.cs b/src/App/Utilities/ThemeManager.cs index 9fc24305d..7bc50788b 100644 --- a/src/App/Utilities/ThemeManager.cs +++ b/src/App/Utilities/ThemeManager.cs @@ -1,8 +1,8 @@ using System; using Bit.App.Models; -using Bit.App.Services; using Bit.App.Styles; -using Bit.Core; +using Bit.Core.Abstractions; +using Bit.Core.Utilities; using Xamarin.Forms; using System.Linq; using System.Threading.Tasks; @@ -110,16 +110,15 @@ namespace Bit.App.Utilities } } - public static void SetTheme(bool android, ResourceDictionary resources) + public static void SetTheme(ResourceDictionary resources) { - SetThemeStyle(GetTheme(android), resources); + SetThemeStyle(GetTheme(), resources); } - public static string GetTheme(bool android) + public static string GetTheme() { - return Xamarin.Essentials.Preferences.Get( - string.Format(PreferencesStorageService.KeyFormat, Constants.ThemeKey), default(string), - !android ? "group.com.8bit.bitwarden" : default(string)); + var stateService = ServiceContainer.Resolve("stateService"); + return stateService.GetThemeAsync().GetAwaiter().GetResult(); } public static bool OsDarkModeEnabled() diff --git a/src/Core/Abstractions/ICollectionService.cs b/src/Core/Abstractions/ICollectionService.cs index 1f649dba4..21035b421 100644 --- a/src/Core/Abstractions/ICollectionService.cs +++ b/src/Core/Abstractions/ICollectionService.cs @@ -22,4 +22,4 @@ namespace Bit.Core.Abstractions Task UpsertAsync(CollectionData collection); Task UpsertAsync(List collection); } -} \ No newline at end of file +} diff --git a/src/Core/Abstractions/ICryptoService.cs b/src/Core/Abstractions/ICryptoService.cs index fc9376e63..c06b5b272 100644 --- a/src/Core/Abstractions/ICryptoService.cs +++ b/src/Core/Abstractions/ICryptoService.cs @@ -9,13 +9,14 @@ namespace Bit.Core.Abstractions { public interface ICryptoService { - Task ClearEncKeyAsync(bool memoryOnly = false); - Task ClearKeyAsync(); - Task ClearKeyHashAsync(); - Task ClearKeyPairAsync(bool memoryOnly = false); - Task ClearKeysAsync(); - Task ClearOrgKeysAsync(bool memoryOnly = false); - Task ClearPinProtectedKeyAsync(); + Task ClearEncKeyAsync(bool memoryOnly = false, string userId = null); + Task ClearKeyAsync(string userId = null); + Task ClearKeyHashAsync(string userId = null); + Task ClearKeyPairAsync(bool memoryOnly = false, string userId = null); + Task ClearKeysAsync(string userId = null); + Task ClearOrgKeysAsync(bool memoryOnly = false, string userId = null); + Task ClearPinProtectedKeyAsync(string userId = null); + void ClearCache(); Task DecryptFromBytesAsync(byte[] encBytes, SymmetricCryptoKey key); Task DecryptToBytesAsync(EncString encString, SymmetricCryptoKey key = null); Task DecryptToUtf8Async(EncString encString, SymmetricCryptoKey key = null); @@ -24,7 +25,7 @@ namespace Bit.Core.Abstractions Task EncryptToBytesAsync(byte[] plainValue, SymmetricCryptoKey key = null); Task GetEncKeyAsync(SymmetricCryptoKey key = null); Task> GetFingerprintAsync(string userId, byte[] publicKey = null); - Task GetKeyAsync(); + Task GetKeyAsync(string userId = null); Task GetKeyHashAsync(); Task GetOrgKeyAsync(string orgId); Task> GetOrgKeysAsync(); @@ -33,7 +34,7 @@ namespace Bit.Core.Abstractions Task CompareAndUpdateKeyHashAsync(string masterPassword, SymmetricCryptoKey key); Task HasEncKeyAsync(); Task HashPasswordAsync(string password, SymmetricCryptoKey key, HashPurpose hashPurpose = HashPurpose.ServerAuthorization); - Task HasKeyAsync(); + Task HasKeyAsync(string userId = null); Task> MakeEncKeyAsync(SymmetricCryptoKey key); Task MakeKeyAsync(string password, string salt, KdfType? kdf, int? kdfIterations); Task MakeKeyFromPinAsync(string pin, string salt, KdfType kdf, int kdfIterations, diff --git a/src/Core/Abstractions/IEventService.cs b/src/Core/Abstractions/IEventService.cs index 0f0a51bf0..6a8f8f42e 100644 --- a/src/Core/Abstractions/IEventService.cs +++ b/src/Core/Abstractions/IEventService.cs @@ -9,4 +9,4 @@ namespace Bit.Core.Abstractions Task CollectAsync(EventType eventType, string cipherId = null, bool uploadImmediately = false); Task UploadEventsAsync(); } -} \ No newline at end of file +} diff --git a/src/Core/Abstractions/IFolderService.cs b/src/Core/Abstractions/IFolderService.cs index 818921fa6..4fe215d1a 100644 --- a/src/Core/Abstractions/IFolderService.cs +++ b/src/Core/Abstractions/IFolderService.cs @@ -23,4 +23,4 @@ namespace Bit.Core.Abstractions Task UpsertAsync(FolderData folder); Task UpsertAsync(List folder); } -} \ No newline at end of file +} diff --git a/src/Core/Abstractions/IOrganizationService.cs b/src/Core/Abstractions/IOrganizationService.cs new file mode 100644 index 000000000..07cab5ac0 --- /dev/null +++ b/src/Core/Abstractions/IOrganizationService.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.Core.Models.Data; +using Bit.Core.Models.Domain; + +namespace Bit.Core.Abstractions +{ + public interface IOrganizationService + { + Task GetAsync(string id); + Task GetByIdentifierAsync(string identifier); + Task> GetAllAsync(string userId = null); + Task ReplaceAsync(Dictionary organizations); + Task ClearAllAsync(string userId); + } +} diff --git a/src/Core/Abstractions/IPasswordGenerationService.cs b/src/Core/Abstractions/IPasswordGenerationService.cs index 0bdc80b05..4e52f60a3 100644 --- a/src/Core/Abstractions/IPasswordGenerationService.cs +++ b/src/Core/Abstractions/IPasswordGenerationService.cs @@ -8,7 +8,8 @@ namespace Bit.Core.Abstractions public interface IPasswordGenerationService { Task AddHistoryAsync(string password, CancellationToken token = default(CancellationToken)); - Task ClearAsync(); + Task ClearAsync(string userId = null); + void ClearCache(); Task GeneratePassphraseAsync(PasswordGenerationOptions options); Task GeneratePasswordAsync(PasswordGenerationOptions options); Task> GetHistoryAsync(); diff --git a/src/Core/Abstractions/IPolicyService.cs b/src/Core/Abstractions/IPolicyService.cs index 78d9cb224..2a2d3504a 100644 --- a/src/Core/Abstractions/IPolicyService.cs +++ b/src/Core/Abstractions/IPolicyService.cs @@ -10,15 +10,15 @@ namespace Bit.Core.Abstractions public interface IPolicyService { void ClearCache(); - Task> GetAll(PolicyType? type); - Task Replace(Dictionary policies); - Task Clear(string userId); - Task GetMasterPasswordPolicyOptions(IEnumerable policies = null); + Task> GetAll(PolicyType? type, string userId = null); + Task Replace(Dictionary policies, string userId = null); + Task ClearAsync(string userId); + Task GetMasterPasswordPolicyOptions(IEnumerable policies = null, string userId = null); Task EvaluateMasterPassword(int passwordStrength, string newPassword, MasterPasswordPolicyOptions enforcedPolicyOptions); Tuple GetResetPasswordPolicyOptions(IEnumerable policies, string orgId); - Task PolicyAppliesToUser(PolicyType policyType, Func policyFilter = null); + Task PolicyAppliesToUser(PolicyType policyType, Func policyFilter = null, string userId = null); int? GetPolicyInt(Policy policy, string key); } } diff --git a/src/Core/Abstractions/ISettingsService.cs b/src/Core/Abstractions/ISettingsService.cs index a5b780e8d..8ad100f38 100644 --- a/src/Core/Abstractions/ISettingsService.cs +++ b/src/Core/Abstractions/ISettingsService.cs @@ -10,4 +10,4 @@ namespace Bit.Core.Abstractions Task>> GetEquivalentDomainsAsync(); Task SetEquivalentDomainsAsync(List> equivalentDomains); } -} \ No newline at end of file +} diff --git a/src/Core/Abstractions/IStateMigrationService.cs b/src/Core/Abstractions/IStateMigrationService.cs new file mode 100644 index 000000000..bdd9164e3 --- /dev/null +++ b/src/Core/Abstractions/IStateMigrationService.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; + +namespace Bit.Core.Abstractions +{ + public interface IStateMigrationService + { + Task NeedsMigration(); + Task Migrate(); + } +} diff --git a/src/Core/Abstractions/IStateService.cs b/src/Core/Abstractions/IStateService.cs index 484ed6d52..db65c442a 100644 --- a/src/Core/Abstractions/IStateService.cs +++ b/src/Core/Abstractions/IStateService.cs @@ -1,12 +1,147 @@ -using System.Threading.Tasks; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.Core.Enums; +using Bit.Core.Models.Data; +using Bit.Core.Models.Domain; +using Bit.Core.Models.View; namespace Bit.Core.Abstractions { public interface IStateService { - Task GetAsync(string key); - Task RemoveAsync(string key); - Task SaveAsync(string key, T obj); - Task PurgeAsync(); + bool BiometricLocked { get; set; } + List AccountViews { get; } + Task GetActiveUserIdAsync(); + Task SetActiveUserAsync(string userId); + Task IsAuthenticatedAsync(string userId = null); + Task GetUserIdAsync(string email); + Task RefreshAccountViewsAsync(bool allowAddAccountRow); + Task AddAccountAsync(Account account); + Task LogoutAccountAsync(string userId, bool userInitiated); + Task GetPreAuthEnvironmentUrlsAsync(); + Task SetPreAuthEnvironmentUrlsAsync(EnvironmentUrlData value); + Task GetEnvironmentUrlsAsync(string userId = null); + Task GetBiometricUnlockAsync(string userId = null); + Task SetBiometricUnlockAsync(bool? value, string userId = null); + Task CanAccessPremiumAsync(string userId = null); + Task GetProtectedPinAsync(string userId = null); + Task SetProtectedPinAsync(string value, string userId = null); + Task GetPinProtectedAsync(string userId = null); + Task SetPinProtectedAsync(string value, string userId = null); + Task GetPinProtectedKeyAsync(string userId = null); + Task SetPinProtectedKeyAsync(EncString value, string userId = null); + Task GetKdfTypeAsync(string userId = null); + Task SetKdfTypeAsync(KdfType? value, string userId = null); + Task GetKdfIterationsAsync(string userId = null); + Task SetKdfIterationsAsync(int? value, string userId = null); + Task GetKeyEncryptedAsync(string userId = null); + Task SetKeyEncryptedAsync(string value, string userId = null); + Task GetKeyDecryptedAsync(string userId = null); + Task SetKeyDecryptedAsync(SymmetricCryptoKey value, string userId = null); + Task GetKeyHashAsync(string userId = null); + Task SetKeyHashAsync(string value, string userId = null); + Task GetEncKeyEncryptedAsync(string userId = null); + Task SetEncKeyEncryptedAsync(string value, string userId = null); + Task> GetOrgKeysEncryptedAsync(string userId = null); + Task SetOrgKeysEncryptedAsync(Dictionary value, string userId = null); + Task GetPrivateKeyEncryptedAsync(string userId = null); + Task SetPrivateKeyEncryptedAsync(string value, string userId = null); + Task> GetAutofillBlacklistedUrisAsync(string userId = null); + Task SetAutofillBlacklistedUrisAsync(List value, string userId = null); + Task GetAutofillTileAddedAsync(); + Task SetAutofillTileAddedAsync(bool? value); + Task GetEmailAsync(string userId = null); + Task GetNameAsync(string userId = null); + Task GetOrgIdentifierAsync(string userId = null); + Task GetLastActiveTimeAsync(string userId = null); + Task SetLastActiveTimeAsync(long? value, string userId = null); + Task GetVaultTimeoutAsync(string userId = null); + Task SetVaultTimeoutAsync(int? value, string userId = null); + Task GetVaultTimeoutActionAsync(string userId = null); + Task SetVaultTimeoutActionAsync(VaultTimeoutAction? value, string userId = null); + Task GetLastFileCacheClearAsync(string userId = null); + Task SetLastFileCacheClearAsync(DateTime? value, string userId = null); + Task GetPreviousPageInfoAsync(string userId = null); + Task SetPreviousPageInfoAsync(PreviousPageInfo value, string userId = null); + Task GetInvalidUnlockAttemptsAsync(string userId = null); + Task SetInvalidUnlockAttemptsAsync(int? value, string userId = null); + Task GetLastBuildAsync(); + Task SetLastBuildAsync(string value); + Task GetDisableFaviconAsync(string userId = null); + Task SetDisableFaviconAsync(bool? value, string userId = null); + Task GetDisableAutoTotpCopyAsync(string userId = null); + Task SetDisableAutoTotpCopyAsync(bool? value, string userId = null); + Task GetInlineAutofillEnabledAsync(string userId = null); + Task SetInlineAutofillEnabledAsync(bool? value, string userId = null); + Task GetAutofillDisableSavePromptAsync(string userId = null); + Task SetAutofillDisableSavePromptAsync(bool? value, string userId = null); + Task>> GetLocalDataAsync(string userId = null); + Task SetLocalDataAsync(Dictionary> value, string userId = null); + Task> GetEncryptedCiphersAsync(string userId = null); + Task SetEncryptedCiphersAsync(Dictionary value, string userId = null); + Task GetDefaultUriMatchAsync(string userId = null); + Task SetDefaultUriMatchAsync(int? value, string userId = null); + Task> GetNeverDomainsAsync(string userId = null); + Task SetNeverDomainsAsync(HashSet value, string userId = null); + Task GetClearClipboardAsync(string userId = null); + Task SetClearClipboardAsync(int? value, string userId = null); + Task> GetEncryptedCollectionsAsync(string userId = null); + Task SetEncryptedCollectionsAsync(Dictionary value, string userId = null); + Task GetPasswordRepromptAutofillAsync(string userId = null); + Task SetPasswordRepromptAutofillAsync(bool? value, string userId = null); + Task GetPasswordVerifiedAutofillAsync(string userId = null); + Task SetPasswordVerifiedAutofillAsync(bool? value, string userId = null); + Task GetLastSyncAsync(string userId = null); + Task SetLastSyncAsync(DateTime? value, string userId = null); + Task GetSecurityStampAsync(string userId = null); + Task SetSecurityStampAsync(string value, string userId = null); + Task GetEmailVerifiedAsync(string userId = null); + Task SetEmailVerifiedAsync(bool? value, string userId = null); + Task GetSyncOnRefreshAsync(string userId = null); + Task SetSyncOnRefreshAsync(bool? value, string userId = null); + Task GetRememberedEmailAsync(); + Task SetRememberedEmailAsync(string value); + Task GetRememberedOrgIdentifierAsync(); + Task SetRememberedOrgIdentifierAsync(string value); + Task GetThemeAsync(string userId = null); + Task SetThemeAsync(string value, string userId = null); + Task ApplyThemeGloballyAsync(string value); + Task GetAddSitePromptShownAsync(string userId = null); + Task SetAddSitePromptShownAsync(bool? value, string userId = null); + Task GetPushInitialPromptShownAsync(); + Task SetPushInitialPromptShownAsync(bool? value); + Task GetPushLastRegistrationDateAsync(); + Task SetPushLastRegistrationDateAsync(DateTime? value); + Task GetPushInstallationRegistrationErrorAsync(); + Task SetPushInstallationRegistrationErrorAsync(string value); + Task GetPushCurrentTokenAsync(); + Task SetPushCurrentTokenAsync(string value); + Task> GetEventCollectionAsync(); + Task SetEventCollectionAsync(List value); + Task> GetEncryptedFoldersAsync(string userId = null); + Task SetEncryptedFoldersAsync(Dictionary value, string userId = null); + Task> GetEncryptedPoliciesAsync(string userId = null); + Task SetEncryptedPoliciesAsync(Dictionary value, string userId = null); + Task GetPushRegisteredTokenAsync(); + Task SetPushRegisteredTokenAsync(string value); + Task GetUsesKeyConnectorAsync(string userId = null); + Task SetUsesKeyConnectorAsync(bool? value, string userId = null); + Task> GetOrganizationsAsync(string userId = null); + Task SetOrganizationsAsync(Dictionary organizations, string userId = null); + Task GetPasswordGenerationOptionsAsync(string userId = null); + Task SetPasswordGenerationOptionsAsync(PasswordGenerationOptions value, string userId = null); + Task> GetEncryptedPasswordGenerationHistory(string userId = null); + Task SetEncryptedPasswordGenerationHistoryAsync(List value, string userId = null); + Task> GetEncryptedSendsAsync(string userId = null); + Task SetEncryptedSendsAsync(Dictionary value, string userId = null); + Task> GetSettingsAsync(string userId = null); + Task SetSettingsAsync(Dictionary value, string userId = null); + Task GetAccessTokenAsync(string userId = null); + Task SetAccessTokenAsync(string value, bool skipTokenStorage, string userId = null); + Task GetRefreshTokenAsync(string userId = null); + Task SetRefreshTokenAsync(string value, bool skipTokenStorage, string userId = null); + Task GetTwoFactorTokenAsync(string email = null); + Task SetTwoFactorTokenAsync(string value, string email = null); } -} \ No newline at end of file +} diff --git a/src/Core/Abstractions/ITokenService.cs b/src/Core/Abstractions/ITokenService.cs index b08391865..578c97d8f 100644 --- a/src/Core/Abstractions/ITokenService.cs +++ b/src/Core/Abstractions/ITokenService.cs @@ -6,15 +6,16 @@ namespace Bit.Core.Abstractions { public interface ITokenService { - Task ClearTokenAsync(); + Task ClearTokenAsync(string userId = null); Task ClearTwoFactorTokenAsync(string email); + void ClearCache(); JObject DecodeToken(); string GetEmail(); bool GetEmailVerified(); string GetIssuer(); string GetName(); bool GetPremium(); - bool GetIsExternal(); + Task GetIsExternal(); Task GetRefreshTokenAsync(); Task GetTokenAsync(); Task ToggleTokensAsync(); @@ -22,7 +23,7 @@ namespace Bit.Core.Abstractions Task GetTwoFactorTokenAsync(string email); string GetUserId(); Task SetRefreshTokenAsync(string refreshToken); - Task SetTokenAsync(string token); + Task SetAccessTokenAsync(string token, bool forDecodeOnly = false); Task SetTokensAsync(string accessToken, string refreshToken); Task SetTwoFactorTokenAsync(string token, string email); bool TokenNeedsRefresh(int minutes = 5); diff --git a/src/Core/Abstractions/IUserService.cs b/src/Core/Abstractions/IUserService.cs deleted file mode 100644 index d7aaef82b..000000000 --- a/src/Core/Abstractions/IUserService.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using Bit.Core.Enums; -using Bit.Core.Models.Data; -using Bit.Core.Models.Domain; - -namespace Bit.Core.Abstractions -{ - public interface IUserService - { - Task CanAccessPremiumAsync(); - Task ClearAsync(); - Task ClearOrganizationsAsync(string userId); - Task> GetAllOrganizationAsync(); - Task GetEmailAsync(); - Task GetKdfAsync(); - Task GetKdfIterationsAsync(); - Task GetOrganizationAsync(string id); - Task GetOrganizationByIdentifierAsync(string identifier); - Task GetSecurityStampAsync(); - Task GetEmailVerifiedAsync(); - Task GetForcePasswordReset(); - Task GetUserIdAsync(); - Task IsAuthenticatedAsync(); - Task ReplaceOrganizationsAsync(Dictionary organizations); - Task SetInformationAsync(string userId, string email, KdfType kdf, int? kdfIterations); - Task SetSecurityStampAsync(string stamp); - Task SetEmailVerifiedAsync(bool emailVerified); - Task SetForcePasswordReset(bool forcePasswordReset); - } -} diff --git a/src/Core/Abstractions/IVaultTimeoutService.cs b/src/Core/Abstractions/IVaultTimeoutService.cs index bcdb776a8..8fe21b675 100644 --- a/src/Core/Abstractions/IVaultTimeoutService.cs +++ b/src/Core/Abstractions/IVaultTimeoutService.cs @@ -1,23 +1,24 @@ using System; using System.Threading.Tasks; -using Bit.Core.Models.Domain; +using Bit.Core.Enums; namespace Bit.Core.Abstractions { public interface IVaultTimeoutService { - EncString PinProtectedKey { get; set; } - bool BiometricLocked { get; set; } long? DelayLockAndLogoutMs { get; set; } Task CheckVaultTimeoutAsync(); - Task ClearAsync(); - Task IsLockedAsync(); - Task> IsPinLockSetAsync(); - Task IsBiometricLockSetAsync(); - Task LockAsync(bool allowSoftLock = false, bool userInitiated = false); - Task LogOutAsync(); - Task SetVaultTimeoutOptionsAsync(int? timeout, string action); - Task GetVaultTimeout(); + Task ShouldTimeoutAsync(string userId = null); + Task ExecuteTimeoutActionAsync(string userId = null); + Task ClearAsync(string userId = null); + Task IsLockedAsync(string userId = null); + Task IsLoggedOutByTimeoutAsync(string userId = null); + Task> IsPinLockSetAsync(string userId = null); + Task IsBiometricLockSetAsync(string userId = null); + Task LockAsync(bool allowSoftLock = false, bool userInitiated = false, string userId = null); + Task LogOutAsync(bool userInitiated = true, string userId = null); + Task SetVaultTimeoutOptionsAsync(int? timeout, VaultTimeoutAction? action); + Task GetVaultTimeout(string userId = null); } } diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 1751856aa..119588bdf 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -2,32 +2,20 @@ { public static class Constants { + public const int MaxAccounts = 5; public const string AndroidAppProtocol = "androidapp://"; public const string iOSAppProtocol = "iosapp://"; - public static string SyncOnRefreshKey = "syncOnRefresh"; - public static string VaultTimeoutKey = "lockOption"; - public static string VaultTimeoutActionKey = "vaultTimeoutAction"; - public static string LastActiveTimeKey = "lastActiveTime"; - public static string BiometricUnlockKey = "fingerprintUnlock"; - public static string ProtectedPin = "protectedPin"; - public static string PinProtectedKey = "pinProtectedKey"; - public static string DefaultUriMatch = "defaultUriMatch"; - public static string DisableAutoTotpCopyKey = "disableAutoTotpCopy"; - public static string EnvironmentUrlsKey = "environmentUrls"; + public static string StateVersionKey = "stateVersion"; + public static string StateKey = "state"; + public static string PreAuthEnvironmentUrlsKey = "preAuthEnvironmentUrls"; public static string LastFileCacheClearKey = "lastFileCacheClear"; - public static string AutofillDisableSavePromptKey = "autofillDisableSavePrompt"; - public static string AutofillBlacklistedUrisKey = "autofillBlacklistedUris"; public static string AutofillTileAdded = "autofillTileAdded"; - public static string DisableFaviconKey = "disableFavicon"; public static string PushRegisteredTokenKey = "pushRegisteredToken"; public static string PushCurrentTokenKey = "pushCurrentToken"; public static string PushLastRegistrationDateKey = "pushLastRegistrationDate"; public static string PushInitialPromptShownKey = "pushInitialPromptShown"; - public static string PushInstallationRegistrationError = "pushInstallationRegistrationError"; - public static string ThemeKey = "theme"; - public static string ClearClipboardKey = "clearClipboard"; + public static string PushInstallationRegistrationErrorKey = "pushInstallationRegistrationError"; public static string LastBuildKey = "lastBuild"; - public static string OldUserIdKey = "userId"; public static string AddSitePromptShownKey = "addSitePromptShown"; public static string ClearCiphersCacheKey = "clearCiphersCache"; public static string BiometricIntegrityKey = "biometricIntegrityState"; @@ -35,15 +23,9 @@ public static string iOSAutoFillBiometricIntegrityKey = "iOSAutoFillBiometricIntegrityState"; public static string iOSExtensionClearCiphersCacheKey = "iOSExtensionClearCiphersCache"; public static string iOSExtensionBiometricIntegrityKey = "iOSExtensionBiometricIntegrityState"; - public static string MigratedFromV1 = "migratedFromV1"; - public static string MigratedFromV1AutofillPromptShown = "migratedV1AutofillPromptShown"; - public static string TriedV1Resync = "triedV1Resync"; public static string EventCollectionKey = "eventCollection"; - public static string PreviousPageKey = "previousPage"; - public static string InlineAutofillEnabledKey = "inlineAutofillEnabled"; - public static string InvalidUnlockAttempts = "invalidUnlockAttempts"; - public static string PasswordRepromptAutofillKey = "passwordRepromptAutofillKey"; - public static string PasswordVerifiedAutofillKey = "passwordVerifiedAutofillKey"; + public static string RememberedEmailKey = "rememberedEmail"; + public static string RememberedOrgIdentifierKey = "rememberedOrgIdentifier"; public const int SelectFileRequestCode = 42; public const int SelectFilePermissionRequestCode = 43; public const int SaveFileRequestCode = 44; @@ -59,5 +41,42 @@ iOSAutoFillClearCiphersCacheKey, iOSExtensionClearCiphersCacheKey }; + + public static string CiphersKey(string userId) => $"ciphers_{userId}"; + public static string FoldersKey(string userId) => $"folders_{userId}"; + public static string CollectionsKey(string userId) => $"collections_{userId}"; + public static string OrganizationsKey(string userId) => $"organizations_{userId}"; + public static string LocalDataKey(string userId) => $"ciphersLocalData_{userId}"; + public static string NeverDomainsKey(string userId) => $"neverDomains_{userId}"; + public static string SendsKey(string userId) => $"sends_{userId}"; + public static string PoliciesKey(string userId) => $"policies_{userId}"; + public static string KeyKey(string userId) => $"key_{userId}"; + public static string EncOrgKeysKey(string userId) => $"encOrgKeys_{userId}"; + public static string EncPrivateKeyKey(string userId) => $"encPrivateKey_{userId}"; + public static string EncKeyKey(string userId) => $"encKey_{userId}"; + public static string KeyHashKey(string userId) => $"keyHash_{userId}"; + public static string PinProtectedKey(string userId) => $"pinProtectedKey_{userId}"; + public static string PassGenOptionsKey(string userId) => $"passwordGenerationOptions_{userId}"; + public static string PassGenHistoryKey(string userId) => $"generatedPasswordHistory_{userId}"; + public static string TwoFactorTokenKey(string email) => $"twoFactorToken_{email}"; + public static string LastActiveTimeKey(string userId) => $"lastActiveTime_{userId}"; + public static string InvalidUnlockAttemptsKey(string userId) => $"invalidUnlockAttempts_{userId}"; + public static string InlineAutofillEnabledKey(string userId) => $"inlineAutofillEnabled_{userId}"; + public static string AutofillDisableSavePromptKey(string userId) => $"autofillDisableSavePrompt_{userId}"; + public static string AutofillBlacklistedUrisKey(string userId) => $"autofillBlacklistedUris_{userId}"; + public static string ClearClipboardKey(string userId) => $"clearClipboard_{userId}"; + public static string SyncOnRefreshKey(string userId) => $"syncOnRefresh_{userId}"; + public static string DisableFaviconKey(string userId) => $"disableFavicon_{userId}"; + public static string DefaultUriMatchKey(string userId) => $"defaultUriMatch_{userId}"; + public static string ThemeKey(string userId) => $"theme_{userId}"; + public static string DisableAutoTotpCopyKey(string userId) => $"disableAutoTotpCopy_{userId}"; + public static string PreviousPageKey(string userId) => $"previousPage_{userId}"; + public static string PasswordRepromptAutofillKey(string userId) => $"passwordRepromptAutofillKey_{userId}"; + public static string PasswordVerifiedAutofillKey(string userId) => $"passwordVerifiedAutofillKey_{userId}"; + public static string SettingsKey(string userId) => $"settings_{userId}"; + public static string UsesKeyConnectorKey(string userId) => $"usesKeyConnector_{userId}"; + public static string ProtectedPinKey(string userId) => $"protectedPin_{userId}"; + public static string LastSyncKey(string userId) => $"lastSync_{userId}"; + public static string BiometricUnlockKey(string userId) => $"biometricUnlock_{userId}"; } } diff --git a/src/Core/Enums/AuthenticationStatus.cs b/src/Core/Enums/AuthenticationStatus.cs new file mode 100644 index 000000000..2d81e071d --- /dev/null +++ b/src/Core/Enums/AuthenticationStatus.cs @@ -0,0 +1,9 @@ +namespace Bit.Core.Enums +{ + public enum AuthenticationStatus : byte + { + LoggedOut = 0, + Locked = 1, + Unlocked = 2, + } +} diff --git a/src/Core/Enums/StorageLocation.cs b/src/Core/Enums/StorageLocation.cs new file mode 100644 index 000000000..faa9476c4 --- /dev/null +++ b/src/Core/Enums/StorageLocation.cs @@ -0,0 +1,9 @@ +namespace Bit.Core.Enums +{ + public enum StorageLocation + { + Both = 0, + Disk = 1, + Memory = 2 + } +} diff --git a/src/Core/Enums/VaultTimeoutAction.cs b/src/Core/Enums/VaultTimeoutAction.cs new file mode 100644 index 000000000..5a9c33909 --- /dev/null +++ b/src/Core/Enums/VaultTimeoutAction.cs @@ -0,0 +1,8 @@ +namespace Bit.Core.Enums +{ + public enum VaultTimeoutAction + { + Lock = 0, + Logout = 1, + } +} diff --git a/src/App/Models/PreviousPageInfo.cs b/src/Core/Models/Data/PreviousPageInfo.cs similarity index 86% rename from src/App/Models/PreviousPageInfo.cs rename to src/Core/Models/Data/PreviousPageInfo.cs index 3f6bba7de..bcf8e29c3 100644 --- a/src/App/Models/PreviousPageInfo.cs +++ b/src/Core/Models/Data/PreviousPageInfo.cs @@ -1,4 +1,4 @@ -namespace Bit.App.Models +namespace Bit.Core.Models.Data { public class PreviousPageInfo { diff --git a/src/Core/Models/Domain/Account.cs b/src/Core/Models/Domain/Account.cs new file mode 100644 index 000000000..43360c9cd --- /dev/null +++ b/src/Core/Models/Domain/Account.cs @@ -0,0 +1,110 @@ +using Bit.Core.Enums; +using Bit.Core.Models.Data; + +namespace Bit.Core.Models.Domain +{ + public class Account : Domain + { + public AccountProfile Profile; + public AccountTokens Tokens; + public AccountSettings Settings; + public AccountKeys Keys; + + public Account() { } + + public Account(AccountProfile profile, AccountTokens tokens) + { + Profile = profile; + Tokens = tokens; + Settings = new AccountSettings(); + Keys = new AccountKeys(); + } + + public Account(Account account) + { + // Copy constructor excludes Keys (for storage) + Profile = new AccountProfile(account.Profile); + Tokens = new AccountTokens(account.Tokens); + Settings = new AccountSettings(account.Settings); + } + + public class AccountProfile + { + public AccountProfile() { } + + public AccountProfile(AccountProfile copy) + { + if (copy == null) + { + return; + } + + UserId = copy.UserId; + Email = copy.Email; + Name = copy.Name; + Stamp = copy.Stamp; + OrgIdentifier = copy.OrgIdentifier; + KdfType = copy.KdfType; + KdfIterations = copy.KdfIterations; + EmailVerified = copy.EmailVerified; + HasPremiumPersonally = copy.HasPremiumPersonally; + } + + public string UserId; + public string Email; + public string Name; + public string Stamp; + public string OrgIdentifier; + public KdfType? KdfType; + public int? KdfIterations; + public bool? EmailVerified; + public bool? HasPremiumPersonally; + } + + public class AccountTokens + { + public AccountTokens() { } + + public AccountTokens(AccountTokens copy) + { + if (copy == null) + { + return; + } + + AccessToken = copy.AccessToken; + RefreshToken = copy.RefreshToken; + } + + public string AccessToken; + public string RefreshToken; + } + + public class AccountSettings + { + public AccountSettings() { } + + public AccountSettings(AccountSettings copy) + { + if (copy == null) + { + return; + } + + EnvironmentUrls = copy.EnvironmentUrls; + VaultTimeout = copy.VaultTimeout; + VaultTimeoutAction = copy.VaultTimeoutAction; + } + + public EnvironmentUrlData EnvironmentUrls; + public int? VaultTimeout; + public VaultTimeoutAction? VaultTimeoutAction; + } + + public class AccountKeys + { + public SymmetricCryptoKey Key; + public EncString PinProtectedKey; + } + } +} diff --git a/src/Core/Models/Domain/State.cs b/src/Core/Models/Domain/State.cs new file mode 100644 index 000000000..f8180fab8 --- /dev/null +++ b/src/Core/Models/Domain/State.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace Bit.Core.Models.Domain +{ + public class State : Domain + { + public Dictionary Accounts { get; set; } + public string ActiveUserId { get; set; } + } +} diff --git a/src/Core/Models/Domain/StorageOptions.cs b/src/Core/Models/Domain/StorageOptions.cs new file mode 100644 index 000000000..0d9061876 --- /dev/null +++ b/src/Core/Models/Domain/StorageOptions.cs @@ -0,0 +1,13 @@ +using Bit.Core.Enums; + +namespace Bit.Core.Models.Domain +{ + public class StorageOptions : Domain + { + public StorageLocation? StorageLocation { get; set; } + public bool? UseSecureStorage { get; set; } + public string UserId { get; set; } + public string Email { get; set; } + public bool? SkipTokenStorage { get; set; } + } +} diff --git a/src/Core/Models/View/AccountView.cs b/src/Core/Models/View/AccountView.cs new file mode 100644 index 000000000..e89afc511 --- /dev/null +++ b/src/Core/Models/View/AccountView.cs @@ -0,0 +1,41 @@ +using Bit.Core.Enums; +using Bit.Core.Models.Domain; +using Bit.Core.Utilities; + +namespace Bit.Core.Models.View +{ + public class AccountView : View + { + public AccountView() { } + + public AccountView(Account a = null, bool isActive = false) + { + if (a == null) + { + // null will render as "Add Account" row + return; + } + IsAccount = true; + IsActive = isActive; + UserId = a.Profile?.UserId; + Email = a.Profile?.Email; + Name = a.Profile?.Name; + if (!string.IsNullOrWhiteSpace(a.Settings?.EnvironmentUrls?.WebVault)) + { + Hostname = CoreHelpers.GetHostname(a.Settings?.EnvironmentUrls?.WebVault); + } + else if (!string.IsNullOrWhiteSpace(a.Settings?.EnvironmentUrls?.Base)) + { + Hostname = CoreHelpers.GetHostname(a.Settings?.EnvironmentUrls?.Base); + } + } + + public bool IsAccount { get; set; } + public AuthenticationStatus? AuthStatus { get; set; } + public bool IsActive { get; set; } + public string UserId { get; set; } + public string Email { get; set; } + public string Name { get; set; } + public string Hostname { get; set; } + } +} diff --git a/src/Core/Services/ApiService.cs b/src/Core/Services/ApiService.cs index c0b11a6a1..78853c233 100644 --- a/src/Core/Services/ApiService.cs +++ b/src/Core/Services/ApiService.cs @@ -25,12 +25,12 @@ namespace Bit.Core.Services private readonly HttpClient _httpClient = new HttpClient(); private readonly ITokenService _tokenService; private readonly IPlatformUtilsService _platformUtilsService; - private readonly Func _logoutCallbackAsync; + private readonly Func, Task> _logoutCallbackAsync; public ApiService( ITokenService tokenService, IPlatformUtilsService platformUtilsService, - Func logoutCallbackAsync, + Func, Task> logoutCallbackAsync, string customUserAgent = null) { _tokenService = tokenService; @@ -709,7 +709,7 @@ namespace Bit.Core.Services response.StatusCode == HttpStatusCode.Forbidden )) { - await _logoutCallbackAsync(true); + await _logoutCallbackAsync(new Tuple(null, false, true)); return null; } try diff --git a/src/Core/Services/AuthService.cs b/src/Core/Services/AuthService.cs index ca76c437f..f919f2766 100644 --- a/src/Core/Services/AuthService.cs +++ b/src/Core/Services/AuthService.cs @@ -14,13 +14,12 @@ namespace Bit.Core.Services private readonly ICryptoService _cryptoService; private readonly ICryptoFunctionService _cryptoFunctionService; private readonly IApiService _apiService; - private readonly IUserService _userService; + private readonly IStateService _stateService; private readonly ITokenService _tokenService; private readonly IAppIdService _appIdService; private readonly II18nService _i18nService; private readonly IPlatformUtilsService _platformUtilsService; private readonly IMessagingService _messagingService; - private readonly IVaultTimeoutService _vaultTimeoutService; private readonly IKeyConnectorService _keyConnectorService; private readonly bool _setCryptoKeys; @@ -30,7 +29,7 @@ namespace Bit.Core.Services ICryptoService cryptoService, ICryptoFunctionService cryptoFunctionService, IApiService apiService, - IUserService userService, + IStateService stateService, ITokenService tokenService, IAppIdService appIdService, II18nService i18nService, @@ -43,13 +42,12 @@ namespace Bit.Core.Services _cryptoService = cryptoService; _cryptoFunctionService = cryptoFunctionService; _apiService = apiService; - _userService = userService; + _stateService = stateService; _tokenService = tokenService; _appIdService = appIdService; _i18nService = i18nService; _platformUtilsService = platformUtilsService; _messagingService = messagingService; - _vaultTimeoutService = vaultTimeoutService; _keyConnectorService = keyConnectorService; _setCryptoKeys = setCryptoKeys; @@ -354,9 +352,26 @@ namespace Bit.Core.Services { await _tokenService.SetTwoFactorTokenAsync(tokenResponse.TwoFactorToken, email); } - await _tokenService.SetTokensAsync(tokenResponse.AccessToken, tokenResponse.RefreshToken); - await _userService.SetInformationAsync(_tokenService.GetUserId(), _tokenService.GetEmail(), - tokenResponse.Kdf, tokenResponse.KdfIterations); + await _tokenService.SetAccessTokenAsync(tokenResponse.AccessToken, true); + await _stateService.AddAccountAsync( + new Account( + new Account.AccountProfile() + { + UserId = _tokenService.GetUserId(), + Email = _tokenService.GetEmail(), + Name = _tokenService.GetName(), + KdfType = tokenResponse.Kdf, + KdfIterations = tokenResponse.KdfIterations, + HasPremiumPersonally = _tokenService.GetPremium(), + }, + new Account.AccountTokens() + { + AccessToken = tokenResponse.AccessToken, + RefreshToken = tokenResponse.RefreshToken, + } + ) + ); + _messagingService.Send("accountAdded"); if (_setCryptoKeys) { if (key != null) @@ -430,7 +445,7 @@ namespace Bit.Core.Services } - _vaultTimeoutService.BiometricLocked = false; + _stateService.BiometricLocked = false; _messagingService.Send("loggedIn"); return result; } diff --git a/src/Core/Services/CipherService.cs b/src/Core/Services/CipherService.cs index 5f644472d..e39f5cb6d 100644 --- a/src/Core/Services/CipherService.cs +++ b/src/Core/Services/CipherService.cs @@ -19,15 +19,11 @@ namespace Bit.Core.Services { public class CipherService : ICipherService { - private const string Keys_CiphersFormat = "ciphers_{0}"; - private const string Keys_LocalData = "ciphersLocalData"; - private const string Keys_NeverDomains = "neverDomains"; - private readonly string[] _ignoredSearchTerms = new string[] { "com", "net", "org", "android", "io", "co", "uk", "au", "nz", "fr", "de", "tv", "info", "app", "apps", "eu", "me", "dev", "jp", "mobile" }; private List _decryptedCipherCache; private readonly ICryptoService _cryptoService; - private readonly IUserService _userService; + private readonly IStateService _stateService; private readonly ISettingsService _settingsService; private readonly IApiService _apiService; private readonly IFileUploadService _fileUploadService; @@ -45,7 +41,7 @@ namespace Bit.Core.Services public CipherService( ICryptoService cryptoService, - IUserService userService, + IStateService stateService, ISettingsService settingsService, IApiService apiService, IFileUploadService fileUploadService, @@ -56,7 +52,7 @@ namespace Bit.Core.Services string[] allClearCipherCacheKeys) { _cryptoService = cryptoService; - _userService = userService; + _stateService = stateService; _settingsService = settingsService; _apiService = apiService; _fileUploadService = fileUploadService; @@ -211,11 +207,8 @@ namespace Bit.Core.Services public async Task GetAsync(string id) { - var userId = await _userService.GetUserIdAsync(); - var localData = await _storageService.GetAsync>>( - Keys_LocalData); - var ciphers = await _storageService.GetAsync>( - string.Format(Keys_CiphersFormat, userId)); + var localData = await _stateService.GetLocalDataAsync(); + var ciphers = await _stateService.GetEncryptedCiphersAsync(); if (!ciphers?.ContainsKey(id) ?? true) { return null; @@ -226,11 +219,8 @@ namespace Bit.Core.Services public async Task> GetAllAsync() { - var userId = await _userService.GetUserIdAsync(); - var localData = await _storageService.GetAsync>>( - Keys_LocalData); - var ciphers = await _storageService.GetAsync>( - string.Format(Keys_CiphersFormat, userId)); + var localData = await _stateService.GetLocalDataAsync(); + var ciphers = await _stateService.GetEncryptedCiphersAsync(); var response = ciphers?.Select(c => new Cipher(c.Value, false, localData?.ContainsKey(c.Key) ?? false ? localData[c.Key] : null)); return response?.ToList() ?? new List(); @@ -347,7 +337,7 @@ namespace Bit.Core.Services var others = new List(); var ciphers = await ciphersTask; - var defaultMatch = (UriMatchType?)(await _storageService.GetAsync(Constants.DefaultUriMatch)); + var defaultMatch = (UriMatchType?)(await _stateService.GetDefaultUriMatchAsync()); if (defaultMatch == null) { defaultMatch = UriMatchType.Domain; @@ -457,8 +447,7 @@ namespace Bit.Core.Services public async Task UpdateLastUsedDateAsync(string id) { - var ciphersLocalData = await _storageService.GetAsync>>( - Keys_LocalData); + var ciphersLocalData = await _stateService.GetLocalDataAsync(); if (ciphersLocalData == null) { ciphersLocalData = new Dictionary>(); @@ -476,7 +465,7 @@ namespace Bit.Core.Services ciphersLocalData[id].Add("lastUsedDate", DateTime.UtcNow); } - await _storageService.SaveAsync(Keys_LocalData, ciphersLocalData); + await _stateService.SetLocalDataAsync(ciphersLocalData); // Update cache if (DecryptedCipherCache == null) { @@ -495,13 +484,13 @@ namespace Bit.Core.Services { return; } - var domains = await _storageService.GetAsync>(Keys_NeverDomains); + var domains = await _stateService.GetNeverDomainsAsync(); if (domains == null) { domains = new HashSet(); } domains.Add(domain); - await _storageService.SaveAsync(Keys_NeverDomains, domains); + await _stateService.SetNeverDomainsAsync(domains); } public async Task SaveWithServerAsync(Cipher cipher) @@ -526,7 +515,7 @@ namespace Bit.Core.Services var request = new CipherRequest(cipher); response = await _apiService.PutCipherAsync(cipher.Id, request); } - var userId = await _userService.GetUserIdAsync(); + var userId = await _stateService.GetActiveUserIdAsync(); var data = new CipherData(response, userId, cipher.CollectionIds); await UpsertAsync(data); } @@ -550,7 +539,7 @@ namespace Bit.Core.Services var encCipher = await EncryptAsync(cipher); var request = new CipherShareRequest(encCipher); var response = await _apiService.PutShareCipherAsync(cipher.Id, request); - var userId = await _userService.GetUserIdAsync(); + var userId = await _stateService.GetActiveUserIdAsync(); var data = new CipherData(response, userId, collectionIds); await UpsertAsync(data); } @@ -581,7 +570,7 @@ namespace Bit.Core.Services response = await LegacyServerAttachmentFileUploadAsync(cipher.Id, encFileName, encFileData, orgEncAttachmentKey); } - var userId = await _userService.GetUserIdAsync(); + var userId = await _stateService.GetActiveUserIdAsync(); var cData = new CipherData(response, userId, cipher.CollectionIds); await UpsertAsync(cData); return new Cipher(cData); @@ -602,16 +591,14 @@ namespace Bit.Core.Services { var request = new CipherCollectionsRequest(cipher.CollectionIds?.ToList()); await _apiService.PutCipherCollectionsAsync(cipher.Id, request); - var userId = await _userService.GetUserIdAsync(); + var userId = await _stateService.GetActiveUserIdAsync(); var data = cipher.ToCipherData(userId); await UpsertAsync(data); } public async Task UpsertAsync(CipherData cipher) { - var userId = await _userService.GetUserIdAsync(); - var storageKey = string.Format(Keys_CiphersFormat, userId); - var ciphers = await _storageService.GetAsync>(storageKey); + var ciphers = await _stateService.GetEncryptedCiphersAsync(); if (ciphers == null) { ciphers = new Dictionary(); @@ -621,15 +608,13 @@ namespace Bit.Core.Services ciphers.Add(cipher.Id, null); } ciphers[cipher.Id] = cipher; - await _storageService.SaveAsync(storageKey, ciphers); + await _stateService.SetEncryptedCiphersAsync(ciphers); await ClearCacheAsync(); } public async Task UpsertAsync(List cipher) { - var userId = await _userService.GetUserIdAsync(); - var storageKey = string.Format(Keys_CiphersFormat, userId); - var ciphers = await _storageService.GetAsync>(storageKey); + var ciphers = await _stateService.GetEncryptedCiphersAsync(); if (ciphers == null) { ciphers = new Dictionary(); @@ -642,28 +627,25 @@ namespace Bit.Core.Services } ciphers[c.Id] = c; } - await _storageService.SaveAsync(storageKey, ciphers); + await _stateService.SetEncryptedCiphersAsync(ciphers); await ClearCacheAsync(); } public async Task ReplaceAsync(Dictionary ciphers) { - var userId = await _userService.GetUserIdAsync(); - await _storageService.SaveAsync(string.Format(Keys_CiphersFormat, userId), ciphers); + await _stateService.SetEncryptedCiphersAsync(ciphers); await ClearCacheAsync(); } public async Task ClearAsync(string userId) { - await _storageService.RemoveAsync(string.Format(Keys_CiphersFormat, userId)); + await _stateService.SetEncryptedCiphersAsync(null, userId); await ClearCacheAsync(); } public async Task DeleteAsync(string id) { - var userId = await _userService.GetUserIdAsync(); - var cipherKey = string.Format(Keys_CiphersFormat, userId); - var ciphers = await _storageService.GetAsync>(cipherKey); + var ciphers = await _stateService.GetEncryptedCiphersAsync(); if (ciphers == null) { return; @@ -673,15 +655,13 @@ namespace Bit.Core.Services return; } ciphers.Remove(id); - await _storageService.SaveAsync(cipherKey, ciphers); + await _stateService.SetEncryptedCiphersAsync(ciphers); await ClearCacheAsync(); } public async Task DeleteAsync(List ids) { - var userId = await _userService.GetUserIdAsync(); - var cipherKey = string.Format(Keys_CiphersFormat, userId); - var ciphers = await _storageService.GetAsync>(cipherKey); + var ciphers = await _stateService.GetEncryptedCiphersAsync(); if (ciphers == null) { return; @@ -694,7 +674,7 @@ namespace Bit.Core.Services } ciphers.Remove(id); } - await _storageService.SaveAsync(cipherKey, ciphers); + await _stateService.SetEncryptedCiphersAsync(ciphers); await ClearCacheAsync(); } @@ -706,9 +686,7 @@ namespace Bit.Core.Services public async Task DeleteAttachmentAsync(string id, string attachmentId) { - var userId = await _userService.GetUserIdAsync(); - var cipherKey = string.Format(Keys_CiphersFormat, userId); - var ciphers = await _storageService.GetAsync>(cipherKey); + var ciphers = await _stateService.GetEncryptedCiphersAsync(); if (ciphers == null || !ciphers.ContainsKey(id) || ciphers[id].Attachments == null) { return; @@ -718,7 +696,7 @@ namespace Bit.Core.Services { ciphers[id].Attachments.Remove(attachment); } - await _storageService.SaveAsync(cipherKey, ciphers); + await _stateService.SetEncryptedCiphersAsync(ciphers); await ClearCacheAsync(); } @@ -771,9 +749,7 @@ namespace Bit.Core.Services public async Task SoftDeleteWithServerAsync(string id) { - var userId = await _userService.GetUserIdAsync(); - var cipherKey = string.Format(Keys_CiphersFormat, userId); - var ciphers = await _storageService.GetAsync>(cipherKey); + var ciphers = await _stateService.GetEncryptedCiphersAsync(); if (ciphers == null) { return; @@ -785,15 +761,13 @@ namespace Bit.Core.Services await _apiService.PutDeleteCipherAsync(id); ciphers[id].DeletedDate = DateTime.UtcNow; - await _storageService.SaveAsync(cipherKey, ciphers); + await _stateService.SetEncryptedCiphersAsync(ciphers); await ClearCacheAsync(); } public async Task RestoreWithServerAsync(string id) { - var userId = await _userService.GetUserIdAsync(); - var cipherKey = string.Format(Keys_CiphersFormat, userId); - var ciphers = await _storageService.GetAsync>(cipherKey); + var ciphers = await _stateService.GetEncryptedCiphersAsync(); if (ciphers == null) { return; @@ -805,7 +779,7 @@ namespace Bit.Core.Services var response = await _apiService.PutRestoreCipherAsync(id); ciphers[id].DeletedDate = null; ciphers[id].RevisionDate = response.RevisionDate; - await _storageService.SaveAsync(cipherKey, ciphers); + await _stateService.SetEncryptedCiphersAsync(ciphers); await ClearCacheAsync(); } diff --git a/src/Core/Services/CollectionService.cs b/src/Core/Services/CollectionService.cs index aadce803e..ddd7cfb12 100644 --- a/src/Core/Services/CollectionService.cs +++ b/src/Core/Services/CollectionService.cs @@ -13,24 +13,20 @@ namespace Bit.Core.Services { public class CollectionService : ICollectionService { - private const string Keys_CollectionsFormat = "collections_{0}"; private const char NestingDelimiter = '/'; private List _decryptedCollectionCache; private readonly ICryptoService _cryptoService; - private readonly IUserService _userService; - private readonly IStorageService _storageService; + private readonly IStateService _stateService; private readonly II18nService _i18nService; public CollectionService( ICryptoService cryptoService, - IUserService userService, - IStorageService storageService, + IStateService stateService, II18nService i18nService) { _cryptoService = cryptoService; - _userService = userService; - _storageService = storageService; + _stateService = stateService; _i18nService = i18nService; } @@ -83,9 +79,7 @@ namespace Bit.Core.Services public async Task GetAsync(string id) { - var userId = await _userService.GetUserIdAsync(); - var collections = await _storageService.GetAsync>( - string.Format(Keys_CollectionsFormat, userId)); + var collections = await _stateService.GetEncryptedCollectionsAsync(); if (!collections?.ContainsKey(id) ?? true) { return null; @@ -95,9 +89,7 @@ namespace Bit.Core.Services public async Task> GetAllAsync() { - var userId = await _userService.GetUserIdAsync(); - var collections = await _storageService.GetAsync>( - string.Format(Keys_CollectionsFormat, userId)); + var collections = await _stateService.GetEncryptedCollectionsAsync(); var response = collections?.Select(c => new Collection(c.Value)); return response?.ToList() ?? new List(); } @@ -148,9 +140,7 @@ namespace Bit.Core.Services public async Task UpsertAsync(CollectionData collection) { - var userId = await _userService.GetUserIdAsync(); - var storageKey = string.Format(Keys_CollectionsFormat, userId); - var collections = await _storageService.GetAsync>(storageKey); + var collections = await _stateService.GetEncryptedCollectionsAsync(); if (collections == null) { collections = new Dictionary(); @@ -160,15 +150,13 @@ namespace Bit.Core.Services collections.Add(collection.Id, null); } collections[collection.Id] = collection; - await _storageService.SaveAsync(storageKey, collections); + await _stateService.SetEncryptedCollectionsAsync(collections); _decryptedCollectionCache = null; } public async Task UpsertAsync(List collection) { - var userId = await _userService.GetUserIdAsync(); - var storageKey = string.Format(Keys_CollectionsFormat, userId); - var collections = await _storageService.GetAsync>(storageKey); + var collections = await _stateService.GetEncryptedCollectionsAsync(); if (collections == null) { collections = new Dictionary(); @@ -181,34 +169,31 @@ namespace Bit.Core.Services } collections[c.Id] = c; } - await _storageService.SaveAsync(storageKey, collections); + await _stateService.SetEncryptedCollectionsAsync(collections); _decryptedCollectionCache = null; } public async Task ReplaceAsync(Dictionary collections) { - var userId = await _userService.GetUserIdAsync(); - await _storageService.SaveAsync(string.Format(Keys_CollectionsFormat, userId), collections); + await _stateService.SetEncryptedCollectionsAsync(collections); _decryptedCollectionCache = null; } public async Task ClearAsync(string userId) { - await _storageService.RemoveAsync(string.Format(Keys_CollectionsFormat, userId)); + await _stateService.SetEncryptedCollectionsAsync(null, userId); _decryptedCollectionCache = null; } public async Task DeleteAsync(string id) { - var userId = await _userService.GetUserIdAsync(); - var collectionKey = string.Format(Keys_CollectionsFormat, userId); - var collections = await _storageService.GetAsync>(collectionKey); + var collections = await _stateService.GetEncryptedCollectionsAsync(); if (collections == null || !collections.ContainsKey(id)) { return; } collections.Remove(id); - await _storageService.SaveAsync(collectionKey, collections); + await _stateService.SetEncryptedCollectionsAsync(collections); _decryptedCollectionCache = null; } diff --git a/src/Core/Services/CryptoService.cs b/src/Core/Services/CryptoService.cs index 375f6d467..3b3318be7 100644 --- a/src/Core/Services/CryptoService.cs +++ b/src/Core/Services/CryptoService.cs @@ -14,11 +14,9 @@ namespace Bit.Core.Services { public class CryptoService : ICryptoService { - private readonly IStorageService _storageService; - private readonly IStorageService _secureStorageService; + private readonly IStateService _stateService; private readonly ICryptoFunctionService _cryptoFunctionService; - private SymmetricCryptoKey _key; private SymmetricCryptoKey _encKey; private SymmetricCryptoKey _legacyEtmKey; private string _keyHash; @@ -28,39 +26,31 @@ namespace Bit.Core.Services private Task _getEncKeysTask; private Task> _getOrgKeysTask; - private const string Keys_Key = "key"; - private const string Keys_EncOrgKeys = "encOrgKeys"; - private const string Keys_EncPrivateKey = "encPrivateKey"; - private const string Keys_EncKey = "encKey"; - private const string Keys_KeyHash = "keyHash"; - public CryptoService( - IStorageService storageService, - IStorageService secureStorageService, + IStateService stateService, ICryptoFunctionService cryptoFunctionService) { - _storageService = storageService; - _secureStorageService = secureStorageService; + _stateService = stateService; _cryptoFunctionService = cryptoFunctionService; } public async Task SetKeyAsync(SymmetricCryptoKey key) { - _key = key; - var option = await _storageService.GetAsync(Constants.VaultTimeoutKey); - var biometric = await _storageService.GetAsync(Constants.BiometricUnlockKey); + await _stateService.SetKeyDecryptedAsync(key); + var option = await _stateService.GetVaultTimeoutAsync(); + var biometric = await _stateService.GetBiometricUnlockAsync(); if (option.HasValue && !biometric.GetValueOrDefault()) { // If we have a lock option set, we do not store the key return; } - await _secureStorageService.SaveAsync(Keys_Key, key?.KeyB64); + await _stateService.SetKeyEncryptedAsync(key?.KeyB64); } public async Task SetKeyHashAsync(string keyHash) { _keyHash = keyHash; - await _storageService.SaveAsync(Keys_KeyHash, keyHash); + await _stateService.SetKeyHashAsync(keyHash); } public async Task SetEncKeyAsync(string encKey) @@ -69,7 +59,7 @@ namespace Bit.Core.Services { return; } - await _storageService.SaveAsync(Keys_EncKey, encKey); + await _stateService.SetEncKeyEncryptedAsync(encKey); _encKey = null; } @@ -79,7 +69,7 @@ namespace Bit.Core.Services { return; } - await _storageService.SaveAsync(Keys_EncPrivateKey, encPrivateKey); + await _stateService.SetPrivateKeyEncryptedAsync(encPrivateKey); _privateKey = null; } @@ -87,21 +77,23 @@ namespace Bit.Core.Services { var orgKeys = orgs.ToDictionary(org => org.Id, org => org.Key); _orgKeys = null; - await _storageService.SaveAsync(Keys_EncOrgKeys, orgKeys); + await _stateService.SetOrgKeysEncryptedAsync(orgKeys); } - public async Task GetKeyAsync() + public async Task GetKeyAsync(string userId = null) { - if (_key != null) + var inMemoryKey = await _stateService.GetKeyDecryptedAsync(userId); + if (inMemoryKey != null) { - return _key; + return inMemoryKey; } - var key = await _secureStorageService.GetAsync(Keys_Key); + var key = await _stateService.GetKeyEncryptedAsync(userId); if (key != null) { - _key = new SymmetricCryptoKey(Convert.FromBase64String(key)); + inMemoryKey = new SymmetricCryptoKey(Convert.FromBase64String(key)); + await _stateService.SetKeyDecryptedAsync(inMemoryKey, userId); } - return _key; + return inMemoryKey; } public async Task GetKeyHashAsync() @@ -110,7 +102,7 @@ namespace Bit.Core.Services { return _keyHash; } - var keyHash = await _storageService.GetAsync(Keys_KeyHash); + var keyHash = await _stateService.GetKeyHashAsync(); if (keyHash != null) { _keyHash = keyHash; @@ -132,7 +124,7 @@ namespace Bit.Core.Services { try { - var encKey = await _storageService.GetAsync(Keys_EncKey); + var encKey = await _stateService.GetEncKeyEncryptedAsync(); if (encKey == null) { return null; @@ -200,7 +192,7 @@ namespace Bit.Core.Services { return _privateKey; } - var encPrivateKey = await _storageService.GetAsync(Keys_EncPrivateKey); + var encPrivateKey = await _stateService.GetPrivateKeyEncryptedAsync(); if (encPrivateKey == null) { return null; @@ -238,7 +230,7 @@ namespace Bit.Core.Services { try { - var encOrgKeys = await _storageService.GetAsync>(Keys_EncOrgKeys); + var encOrgKeys = await _stateService.GetOrgKeysEncryptedAsync(); if (encOrgKeys == null) { return null; @@ -303,84 +295,95 @@ namespace Bit.Core.Services return false; } - public async Task HasKeyAsync() + public async Task HasKeyAsync(string userId = null) { - var key = await GetKeyAsync(); + var key = await GetKeyAsync(userId); return key != null; } public async Task HasEncKeyAsync() { - var encKey = await _storageService.GetAsync(Keys_EncKey); + var encKey = await _stateService.GetEncKeyEncryptedAsync(); return encKey != null; } - public async Task ClearKeyAsync() + public async Task ClearKeyAsync(string userId = null) { - _key = _legacyEtmKey = null; - await _secureStorageService.RemoveAsync(Keys_Key); + await _stateService.SetKeyDecryptedAsync(null, userId); + _legacyEtmKey = null; + await _stateService.SetKeyEncryptedAsync(null, userId); } - public async Task ClearKeyHashAsync() + public async Task ClearKeyHashAsync(string userId = null) { _keyHash = null; - await _storageService.RemoveAsync(Keys_KeyHash); + await _stateService.SetKeyHashAsync(null, userId); } - public async Task ClearEncKeyAsync(bool memoryOnly = false) + public async Task ClearEncKeyAsync(bool memoryOnly = false, string userId = null) { _encKey = null; if (!memoryOnly) { - await _storageService.RemoveAsync(Keys_EncKey); + await _stateService.SetEncKeyEncryptedAsync(null, userId); } } - public async Task ClearKeyPairAsync(bool memoryOnly = false) + public async Task ClearKeyPairAsync(bool memoryOnly = false, string userId = null) { _publicKey = _privateKey = null; if (!memoryOnly) { - await _storageService.RemoveAsync(Keys_EncPrivateKey); + await _stateService.SetPrivateKeyEncryptedAsync(null, userId); } } - public async Task ClearOrgKeysAsync(bool memoryOnly = false) + public async Task ClearOrgKeysAsync(bool memoryOnly = false, string userId = null) { _orgKeys = null; if (!memoryOnly) { - await _storageService.RemoveAsync(Keys_EncOrgKeys); + await _stateService.SetOrgKeysEncryptedAsync(null, userId); } } - public async Task ClearPinProtectedKeyAsync() + public async Task ClearPinProtectedKeyAsync(string userId = null) { - await _storageService.RemoveAsync(Constants.PinProtectedKey); + await _stateService.SetPinProtectedAsync(null, userId); } - public async Task ClearKeysAsync() + public void ClearCache() + { + _encKey = null; + _legacyEtmKey = null; + _keyHash = null; + _publicKey = null; + _privateKey = null; + _orgKeys = null; + } + + public async Task ClearKeysAsync(string userId = null) { await Task.WhenAll(new Task[] { - ClearKeyAsync(), - ClearKeyHashAsync(), - ClearOrgKeysAsync(), - ClearEncKeyAsync(), - ClearKeyPairAsync(), - ClearPinProtectedKeyAsync() + ClearKeyAsync(userId), + ClearKeyHashAsync(userId), + ClearOrgKeysAsync(false, userId), + ClearEncKeyAsync(false, userId), + ClearKeyPairAsync(false, userId), + ClearPinProtectedKeyAsync(userId) }); } public async Task ToggleKeyAsync() { var key = await GetKeyAsync(); - var option = await _storageService.GetAsync(Constants.VaultTimeoutKey); - var biometric = await _storageService.GetAsync(Constants.BiometricUnlockKey); + var option = await _stateService.GetVaultTimeoutAsync(); + var biometric = await _stateService.GetBiometricUnlockAsync(); if (!biometric.GetValueOrDefault() && (option != null || option == 0)) { await ClearKeyAsync(); - _key = key; + await _stateService.SetKeyDecryptedAsync(key); return; } await SetKeyAsync(key); @@ -415,7 +418,7 @@ namespace Bit.Core.Services { if (protectedKeyCs == null) { - var pinProtectedKey = await _storageService.GetAsync(Constants.PinProtectedKey); + var pinProtectedKey = await _stateService.GetPinProtectedAsync(); if (pinProtectedKey == null) { throw new Exception("No PIN protected key found."); diff --git a/src/Core/Services/EnvironmentService.cs b/src/Core/Services/EnvironmentService.cs index 315e27126..b5a6e4511 100644 --- a/src/Core/Services/EnvironmentService.cs +++ b/src/Core/Services/EnvironmentService.cs @@ -9,14 +9,14 @@ namespace Bit.Core.Services public class EnvironmentService : IEnvironmentService { private readonly IApiService _apiService; - private readonly IStorageService _storageService; + private readonly IStateService _stateService; public EnvironmentService( IApiService apiService, - IStorageService storageService) + IStateService stateService) { _apiService = apiService; - _storageService = storageService; + _stateService = stateService; } public string BaseUrl { get; set; } @@ -42,7 +42,11 @@ namespace Bit.Core.Services public async Task SetUrlsFromStorageAsync() { - var urls = await _storageService.GetAsync(Constants.EnvironmentUrlsKey); + var urls = await _stateService.GetEnvironmentUrlsAsync(); + if (urls == null) + { + urls = await _stateService.GetPreAuthEnvironmentUrlsAsync(); + } if (urls == null) { urls = new EnvironmentUrlData(); @@ -72,7 +76,7 @@ namespace Bit.Core.Services urls.Icons = FormatUrl(urls.Icons); urls.Notifications = FormatUrl(urls.Notifications); urls.Events = FormatUrl(urls.Events); - await _storageService.SaveAsync(Constants.EnvironmentUrlsKey, urls); + await _stateService.SetPreAuthEnvironmentUrlsAsync(urls); BaseUrl = urls.Base; WebVaultUrl = urls.WebVault; ApiUrl = urls.Api; diff --git a/src/Core/Services/EventService.cs b/src/Core/Services/EventService.cs index 3a599d17a..179c689c2 100644 --- a/src/Core/Services/EventService.cs +++ b/src/Core/Services/EventService.cs @@ -12,31 +12,31 @@ namespace Bit.Core.Services { public class EventService : IEventService { - private readonly IStorageService _storageService; private readonly IApiService _apiService; - private readonly IUserService _userService; + private readonly IStateService _stateService; + private readonly IOrganizationService _organizationService; private readonly ICipherService _cipherService; public EventService( - IStorageService storageService, IApiService apiService, - IUserService userService, + IStateService stateService, + IOrganizationService organizationService, ICipherService cipherService) { - _storageService = storageService; _apiService = apiService; - _userService = userService; + _stateService = stateService; + _organizationService = organizationService; _cipherService = cipherService; } public async Task CollectAsync(EventType eventType, string cipherId = null, bool uploadImmediately = false) { - var authed = await _userService.IsAuthenticatedAsync(); + var authed = await _stateService.IsAuthenticatedAsync(); if (!authed) { return; } - var organizations = await _userService.GetAllOrganizationAsync(); + var organizations = await _organizationService.GetAllAsync(); if (organizations == null) { return; @@ -54,7 +54,7 @@ namespace Bit.Core.Services return; } } - var eventCollection = await _storageService.GetAsync>(Constants.EventCollectionKey); + var eventCollection = await _stateService.GetEventCollectionAsync(); if (eventCollection == null) { eventCollection = new List(); @@ -65,7 +65,7 @@ namespace Bit.Core.Services CipherId = cipherId, Date = DateTime.UtcNow }); - await _storageService.SaveAsync(Constants.EventCollectionKey, eventCollection); + await _stateService.SetEventCollectionAsync(eventCollection); if (uploadImmediately) { await UploadEventsAsync(); @@ -74,12 +74,12 @@ namespace Bit.Core.Services public async Task UploadEventsAsync() { - var authed = await _userService.IsAuthenticatedAsync(); + var authed = await _stateService.IsAuthenticatedAsync(); if (!authed) { return; } - var eventCollection = await _storageService.GetAsync>(Constants.EventCollectionKey); + var eventCollection = await _stateService.GetEventCollectionAsync(); if (eventCollection == null || !eventCollection.Any()) { return; @@ -100,7 +100,7 @@ namespace Bit.Core.Services public async Task ClearEventsAsync() { - await _storageService.RemoveAsync(Constants.EventCollectionKey); + await _stateService.SetEventCollectionAsync(null); } } } diff --git a/src/Core/Services/FolderService.cs b/src/Core/Services/FolderService.cs index 6eb38ef23..fdc882476 100644 --- a/src/Core/Services/FolderService.cs +++ b/src/Core/Services/FolderService.cs @@ -15,30 +15,25 @@ namespace Bit.Core.Services { public class FolderService : IFolderService { - private const string Keys_CiphersFormat = "ciphers_{0}"; - private const string Keys_FoldersFormat = "folders_{0}"; private const char NestingDelimiter = '/'; private List _decryptedFolderCache; private readonly ICryptoService _cryptoService; - private readonly IUserService _userService; + private readonly IStateService _stateService; private readonly IApiService _apiService; - private readonly IStorageService _storageService; private readonly II18nService _i18nService; private readonly ICipherService _cipherService; public FolderService( ICryptoService cryptoService, - IUserService userService, + IStateService stateService, IApiService apiService, - IStorageService storageService, II18nService i18nService, ICipherService cipherService) { _cryptoService = cryptoService; - _userService = userService; + _stateService = stateService; _apiService = apiService; - _storageService = storageService; _i18nService = i18nService; _cipherService = cipherService; } @@ -60,9 +55,7 @@ namespace Bit.Core.Services public async Task GetAsync(string id) { - var userId = await _userService.GetUserIdAsync(); - var folders = await _storageService.GetAsync>( - string.Format(Keys_FoldersFormat, userId)); + var folders = await _stateService.GetEncryptedFoldersAsync(); if (!folders?.ContainsKey(id) ?? true) { return null; @@ -72,9 +65,7 @@ namespace Bit.Core.Services public async Task> GetAllAsync() { - var userId = await _userService.GetUserIdAsync(); - var folders = await _storageService.GetAsync>( - string.Format(Keys_FoldersFormat, userId)); + var folders = await _stateService.GetEncryptedFoldersAsync(); var response = folders?.Select(f => new Folder(f.Value)); return response?.ToList() ?? new List(); } @@ -153,16 +144,14 @@ namespace Bit.Core.Services { response = await _apiService.PutFolderAsync(folder.Id, request); } - var userId = await _userService.GetUserIdAsync(); + var userId = await _stateService.GetActiveUserIdAsync(); var data = new FolderData(response, userId); await UpsertAsync(data); } public async Task UpsertAsync(FolderData folder) { - var userId = await _userService.GetUserIdAsync(); - var storageKey = string.Format(Keys_FoldersFormat, userId); - var folders = await _storageService.GetAsync>(storageKey); + var folders = await _stateService.GetEncryptedFoldersAsync(); if (folders == null) { folders = new Dictionary(); @@ -172,15 +161,13 @@ namespace Bit.Core.Services folders.Add(folder.Id, null); } folders[folder.Id] = folder; - await _storageService.SaveAsync(storageKey, folders); + await _stateService.SetEncryptedFoldersAsync(folders); _decryptedFolderCache = null; } public async Task UpsertAsync(List folder) { - var userId = await _userService.GetUserIdAsync(); - var storageKey = string.Format(Keys_FoldersFormat, userId); - var folders = await _storageService.GetAsync>(storageKey); + var folders = await _stateService.GetEncryptedFoldersAsync(); if (folders == null) { folders = new Dictionary(); @@ -193,39 +180,35 @@ namespace Bit.Core.Services } folders[f.Id] = f; } - await _storageService.SaveAsync(storageKey, folders); + await _stateService.SetEncryptedFoldersAsync(folders); _decryptedFolderCache = null; } public async Task ReplaceAsync(Dictionary folders) { - var userId = await _userService.GetUserIdAsync(); - await _storageService.SaveAsync(string.Format(Keys_FoldersFormat, userId), folders); + await _stateService.SetEncryptedFoldersAsync(folders); _decryptedFolderCache = null; } public async Task ClearAsync(string userId) { - await _storageService.RemoveAsync(string.Format(Keys_FoldersFormat, userId)); + await _stateService.SetEncryptedFoldersAsync(null, userId); _decryptedFolderCache = null; } public async Task DeleteAsync(string id) { - var userId = await _userService.GetUserIdAsync(); - var folderKey = string.Format(Keys_FoldersFormat, userId); - var folders = await _storageService.GetAsync>(folderKey); + var folders = await _stateService.GetEncryptedFoldersAsync(); if (folders == null || !folders.ContainsKey(id)) { return; } folders.Remove(id); - await _storageService.SaveAsync(folderKey, folders); + await _stateService.SetEncryptedFoldersAsync(folders); _decryptedFolderCache = null; // Items in a deleted folder are re-assigned to "No Folder" - var ciphers = await _storageService.GetAsync>( - string.Format(Keys_CiphersFormat, userId)); + var ciphers = await _stateService.GetEncryptedCiphersAsync(); if (ciphers != null) { var updates = new List(); diff --git a/src/Core/Services/KeyConnectorService.cs b/src/Core/Services/KeyConnectorService.cs index 733d79e78..ed8ae5acc 100644 --- a/src/Core/Services/KeyConnectorService.cs +++ b/src/Core/Services/KeyConnectorService.cs @@ -1,7 +1,6 @@ using System; using System.Threading.Tasks; using Bit.Core.Abstractions; -using Bit.Core.Exceptions; using Bit.Core.Models.Domain; using Bit.Core.Models.Request; @@ -9,24 +8,20 @@ namespace Bit.Core.Services { public class KeyConnectorService : IKeyConnectorService { - private const string Keys_UsesKeyConnector = "usesKeyConnector"; - - private readonly IUserService _userService; + private readonly IStateService _stateService; private readonly ICryptoService _cryptoService; - private readonly IStorageService _storageService; private readonly ITokenService _tokenService; private readonly IApiService _apiService; + private readonly IOrganizationService _organizationService; - private bool? _usesKeyConnector; - - public KeyConnectorService(IUserService userService, ICryptoService cryptoService, - IStorageService storageService, ITokenService tokenService, IApiService apiService) + public KeyConnectorService(IStateService stateService, ICryptoService cryptoService, + ITokenService tokenService, IApiService apiService, OrganizationService organizationService) { - _userService = userService; + _stateService = stateService; _cryptoService = cryptoService; - _storageService = storageService; _tokenService = tokenService; _apiService = apiService; + _organizationService = organizationService; } public async Task GetAndSetKey(string url) @@ -46,23 +41,17 @@ namespace Bit.Core.Services public async Task SetUsesKeyConnector(bool usesKeyConnector) { - _usesKeyConnector = usesKeyConnector; - await _storageService.SaveAsync(Keys_UsesKeyConnector, usesKeyConnector); + await _stateService.SetUsesKeyConnectorAsync(usesKeyConnector); } public async Task GetUsesKeyConnector() { - if (!_usesKeyConnector.HasValue) - { - _usesKeyConnector = await _storageService.GetAsync(Keys_UsesKeyConnector); - } - - return _usesKeyConnector.Value; + return await _stateService.GetUsesKeyConnectorAsync(); } public async Task GetManagingOrganization() { - var orgs = await _userService.GetAllOrganizationAsync(); + var orgs = await _organizationService.GetAllAsync(); return orgs.Find(o => o.UsesKeyConnector && !o.IsAdmin); @@ -88,7 +77,7 @@ namespace Bit.Core.Services public async Task UserNeedsMigration() { - var loggedInUsingSso = _tokenService.GetIsExternal(); + var loggedInUsingSso = await _tokenService.GetIsExternal(); var requiredByOrganization = await GetManagingOrganization() != null; var userIsNotUsingKeyConnector = !await GetUsesKeyConnector(); diff --git a/src/Core/Services/OrganizationService.cs b/src/Core/Services/OrganizationService.cs new file mode 100644 index 000000000..131a9d03d --- /dev/null +++ b/src/Core/Services/OrganizationService.cs @@ -0,0 +1,55 @@ +using Bit.Core.Abstractions; +using Bit.Core.Models.Data; +using Bit.Core.Models.Domain; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Bit.Core.Services +{ + public class OrganizationService : IOrganizationService + { + private readonly IStateService _stateService; + + public OrganizationService(IStateService stateService) + { + _stateService = stateService; + } + + public async Task GetAsync(string id) + { + var organizations = await _stateService.GetOrganizationsAsync(); + if (organizations == null || !organizations.ContainsKey(id)) + { + return null; + } + return new Organization(organizations[id]); + } + + public async Task GetByIdentifierAsync(string identifier) + { + var organizations = await GetAllAsync(); + if (organizations == null || organizations.Count == 0) + { + return null; + } + return organizations.FirstOrDefault(o => o.Identifier == identifier); + } + + public async Task> GetAllAsync(string userId = null) + { + var organizations = await _stateService.GetOrganizationsAsync(userId); + return organizations?.Select(o => new Organization(o.Value)).ToList() ?? new List(); + } + + public async Task ReplaceAsync(Dictionary organizations) + { + await _stateService.SetOrganizationsAsync(organizations); + } + + public async Task ClearAllAsync(string userId) + { + await _stateService.SetOrganizationsAsync(null, userId); + } + } +} diff --git a/src/Core/Services/PasswordGenerationService.cs b/src/Core/Services/PasswordGenerationService.cs index 746d9b345..cc776052d 100644 --- a/src/Core/Services/PasswordGenerationService.cs +++ b/src/Core/Services/PasswordGenerationService.cs @@ -14,8 +14,6 @@ namespace Bit.Core.Services { public class PasswordGenerationService : IPasswordGenerationService { - private const string Keys_Options = "passwordGenerationOptions"; - private const string Keys_History = "generatedPasswordHistory"; private const int MaxPasswordsInHistory = 100; private const string LowercaseCharSet = "abcdefghijkmnopqrstuvwxyz"; private const string UppercaseCharSet = "ABCDEFGHJKLMNPQRSTUVWXYZ"; @@ -23,7 +21,7 @@ namespace Bit.Core.Services private const string SpecialCharSet = "!@#$%^&*"; private readonly ICryptoService _cryptoService; - private readonly IStorageService _storageService; + private readonly IStateService _stateService; private readonly ICryptoFunctionService _cryptoFunctionService; private readonly IPolicyService _policyService; private PasswordGenerationOptions _defaultOptions = new PasswordGenerationOptions(true); @@ -32,12 +30,12 @@ namespace Bit.Core.Services public PasswordGenerationService( ICryptoService cryptoService, - IStorageService storageService, + IStateService stateService, ICryptoFunctionService cryptoFunctionService, IPolicyService policyService) { _cryptoService = cryptoService; - _storageService = storageService; + _stateService = stateService; _cryptoFunctionService = cryptoFunctionService; _policyService = policyService; } @@ -160,6 +158,12 @@ namespace Bit.Core.Services return password.ToString(); } + public void ClearCache() + { + _optionsCache = null; + _history = null; + } + public async Task GeneratePassphraseAsync(PasswordGenerationOptions options) { options.Merge(_defaultOptions); @@ -204,7 +208,7 @@ namespace Bit.Core.Services { if (_optionsCache == null) { - var options = await _storageService.GetAsync(Keys_Options); + var options = await _stateService.GetPasswordGenerationOptionsAsync(); if (options == null) { _optionsCache = _defaultOptions; @@ -432,7 +436,7 @@ namespace Bit.Core.Services public async Task SaveOptionsAsync(PasswordGenerationOptions options) { - await _storageService.SaveAsync(Keys_Options, options); + await _stateService.SetPasswordGenerationOptionsAsync(options); _optionsCache = options; } @@ -445,7 +449,7 @@ namespace Bit.Core.Services } if (_history == null) { - var encrypted = await _storageService.GetAsync>(Keys_History); + var encrypted = await _stateService.GetEncryptedPasswordGenerationHistory(); _history = await DecryptHistoryAsync(encrypted); } return _history ?? new List(); @@ -473,13 +477,13 @@ namespace Bit.Core.Services } var newHistory = await EncryptHistoryAsync(currentHistory); token.ThrowIfCancellationRequested(); - await _storageService.SaveAsync(Keys_History, newHistory); + await _stateService.SetEncryptedPasswordGenerationHistoryAsync(newHistory); } - public async Task ClearAsync() + public async Task ClearAsync(string userId = null) { _history = new List(); - await _storageService.RemoveAsync(Keys_History); + await _stateService.SetEncryptedPasswordGenerationHistoryAsync(null, userId); } public Result PasswordStrength(string password, List userInputs = null) diff --git a/src/Core/Services/PolicyService.cs b/src/Core/Services/PolicyService.cs index 95529ab26..e1c2b8c75 100644 --- a/src/Core/Services/PolicyService.cs +++ b/src/Core/Services/PolicyService.cs @@ -12,19 +12,17 @@ namespace Bit.Core.Services { public class PolicyService : IPolicyService { - private const string Keys_PoliciesPrefix = "policies_{0}"; - - private readonly IStorageService _storageService; - private readonly IUserService _userService; + private readonly IStateService _stateService; + private readonly IOrganizationService _organizationService; private IEnumerable _policyCache; public PolicyService( - IStorageService storageService, - IUserService userService) + IStateService stateService, + IOrganizationService organizationService) { - _storageService = storageService; - _userService = userService; + _stateService = stateService; + _organizationService = organizationService; } public void ClearCache() @@ -32,13 +30,11 @@ namespace Bit.Core.Services _policyCache = null; } - public async Task> GetAll(PolicyType? type) + public async Task> GetAll(PolicyType? type, string userId = null) { if (_policyCache == null) { - var userId = await _userService.GetUserIdAsync(); - var policies = await _storageService.GetAsync>( - string.Format(Keys_PoliciesPrefix, userId)); + var policies = await _stateService.GetEncryptedPoliciesAsync(userId); if (policies == null) { return null; @@ -56,27 +52,26 @@ namespace Bit.Core.Services } } - public async Task Replace(Dictionary policies) + public async Task Replace(Dictionary policies, string userId = null) { - var userId = await _userService.GetUserIdAsync(); - await _storageService.SaveAsync(string.Format(Keys_PoliciesPrefix, userId), policies); + await _stateService.SetEncryptedPoliciesAsync(policies, userId); _policyCache = null; } - public async Task Clear(string userId) + public async Task ClearAsync(string userId) { - await _storageService.RemoveAsync(string.Format(Keys_PoliciesPrefix, userId)); + await _stateService.SetEncryptedPoliciesAsync(null, userId); _policyCache = null; } public async Task GetMasterPasswordPolicyOptions( - IEnumerable policies = null) + IEnumerable policies = null, string userId = null) { MasterPasswordPolicyOptions enforcedOptions = null; if (policies == null) { - policies = await GetAll(PolicyType.MasterPassword); + policies = await GetAll(PolicyType.MasterPassword, userId); } else { @@ -198,14 +193,14 @@ namespace Bit.Core.Services return new Tuple(resetPasswordPolicyOptions, policy != null); } - public async Task PolicyAppliesToUser(PolicyType policyType, Func policyFilter) + public async Task PolicyAppliesToUser(PolicyType policyType, Func policyFilter, string userId = null) { - var policies = await GetAll(policyType); + var policies = await GetAll(policyType, userId); if (policies == null) { return false; } - var organizations = await _userService.GetAllOrganizationAsync(); + var organizations = await _organizationService.GetAllAsync(userId); IEnumerable filteredPolicies; diff --git a/src/Core/Services/SendService.cs b/src/Core/Services/SendService.cs index d0b535df8..1ca9d4930 100644 --- a/src/Core/Services/SendService.cs +++ b/src/Core/Services/SendService.cs @@ -20,9 +20,8 @@ namespace Bit.Core.Services { private List _decryptedSendsCache; private readonly ICryptoService _cryptoService; - private readonly IUserService _userService; + private readonly IStateService _stateService; private readonly IApiService _apiService; - private readonly IStorageService _storageService; private readonly II18nService _i18nService; private readonly ICryptoFunctionService _cryptoFunctionService; private Task> _getAllDecryptedTask; @@ -30,27 +29,23 @@ namespace Bit.Core.Services public SendService( ICryptoService cryptoService, - IUserService userService, + IStateService stateService, IApiService apiService, IFileUploadService fileUploadService, - IStorageService storageService, II18nService i18nService, ICryptoFunctionService cryptoFunctionService) { _cryptoService = cryptoService; - _userService = userService; + _stateService = stateService; _apiService = apiService; _fileUploadService = fileUploadService; - _storageService = storageService; _i18nService = i18nService; _cryptoFunctionService = cryptoFunctionService; } - public static string GetSendKey(string userId) => string.Format("sends_{0}", userId); - public async Task ClearAsync(string userId) { - await _storageService.RemoveAsync(GetSendKey(userId)); + await _stateService.SetEncryptedSendsAsync(null, userId); ClearCache(); } @@ -58,8 +53,7 @@ namespace Bit.Core.Services public async Task DeleteAsync(params string[] ids) { - var userId = await _userService.GetUserIdAsync(); - var sends = await _storageService.GetAsync>(GetSendKey(userId)); + var sends = await _stateService.GetEncryptedSendsAsync(); if (sends == null) { @@ -71,7 +65,7 @@ namespace Bit.Core.Services sends.Remove(id); } - await _storageService.SaveAsync(GetSendKey(userId), sends); + await _stateService.SetEncryptedSendsAsync(sends); ClearCache(); } @@ -138,8 +132,7 @@ namespace Bit.Core.Services public async Task> GetAllAsync() { - var userId = await _userService.GetUserIdAsync(); - var sends = await _storageService.GetAsync>(GetSendKey(userId)); + var sends = await _stateService.GetEncryptedSendsAsync(); return sends?.Select(kvp => new Send(kvp.Value)).ToList() ?? new List(); } @@ -179,8 +172,7 @@ namespace Bit.Core.Services public async Task GetAsync(string id) { - var userId = await _userService.GetUserIdAsync(); - var sends = await _storageService.GetAsync>(GetSendKey(userId)); + var sends = await _stateService.GetEncryptedSendsAsync(); if (sends == null || !sends.ContainsKey(id)) { @@ -192,8 +184,7 @@ namespace Bit.Core.Services public async Task ReplaceAsync(Dictionary sends) { - var userId = await _userService.GetUserIdAsync(); - await _storageService.SaveAsync(GetSendKey(userId), sends); + await _stateService.SetEncryptedSendsAsync(sends); _decryptedSendsCache = null; } @@ -237,7 +228,7 @@ namespace Bit.Core.Services response = await _apiService.PutSendAsync(send.Id, request); } - var userId = await _userService.GetUserIdAsync(); + var userId = await _stateService.GetActiveUserIdAsync(); await UpsertAsync(new SendData(response, userId)); return response.Id; } @@ -255,8 +246,7 @@ namespace Bit.Core.Services public async Task UpsertAsync(params SendData[] sends) { - var userId = await _userService.GetUserIdAsync(); - var knownSends = await _storageService.GetAsync>(GetSendKey(userId)) ?? + var knownSends = await _stateService.GetEncryptedSendsAsync() ?? new Dictionary(); foreach (var send in sends) @@ -264,14 +254,14 @@ namespace Bit.Core.Services knownSends[send.Id] = send; } - await _storageService.SaveAsync(GetSendKey(userId), knownSends); + await _stateService.SetEncryptedSendsAsync(knownSends); _decryptedSendsCache = null; } public async Task RemovePasswordWithServerAsync(string id) { var response = await _apiService.PutSendRemovePasswordAsync(id); - var userId = await _userService.GetUserIdAsync(); + var userId = await _stateService.GetActiveUserIdAsync(); await UpsertAsync(new SendData(response, userId)); } diff --git a/src/Core/Services/SettingsService.cs b/src/Core/Services/SettingsService.cs index c0e776009..14984bc72 100644 --- a/src/Core/Services/SettingsService.cs +++ b/src/Core/Services/SettingsService.cs @@ -1,7 +1,5 @@ using Bit.Core.Abstractions; -using Bit.Core.Utilities; using Newtonsoft.Json.Linq; -using System; using System.Collections.Generic; using System.Threading.Tasks; @@ -9,20 +7,16 @@ namespace Bit.Core.Services { public class SettingsService : ISettingsService { - private const string Keys_SettingsFormat = "settings_{0}"; private const string Keys_EquivalentDomains = "equivalentDomains"; - private readonly IUserService _userService; - private readonly IStorageService _storageService; + private readonly IStateService _stateService; private Dictionary _settingsCache; public SettingsService( - IUserService userService, - IStorageService storageService) + IStateService stateService) { - _userService = userService; - _storageService = storageService; + _stateService = stateService; } public void ClearCache() @@ -49,7 +43,7 @@ namespace Bit.Core.Services public async Task ClearAsync(string userId) { - await _storageService.RemoveAsync(string.Format(Keys_SettingsFormat, userId)); + await _stateService.SetSettingsAsync(null, userId); ClearCache(); } @@ -59,16 +53,13 @@ namespace Bit.Core.Services { if (_settingsCache == null) { - var userId = await _userService.GetUserIdAsync(); - _settingsCache = await _storageService.GetAsync>( - string.Format(Keys_SettingsFormat, userId)); + _settingsCache = await _stateService.GetSettingsAsync(); } return _settingsCache; } private async Task SetSettingsKeyAsync(string key, T value) { - var userId = await _userService.GetUserIdAsync(); var settings = await GetSettingsAsync(); if (settings == null) { @@ -82,7 +73,7 @@ namespace Bit.Core.Services { settings.Add(key, value); } - await _storageService.SaveAsync(string.Format(Keys_SettingsFormat, userId), settings); + await _stateService.SetSettingsAsync(settings); _settingsCache = settings; } } diff --git a/src/Core/Services/StateMigrationService.cs b/src/Core/Services/StateMigrationService.cs new file mode 100644 index 000000000..ce3c7cf92 --- /dev/null +++ b/src/Core/Services/StateMigrationService.cs @@ -0,0 +1,415 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading.Tasks; +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Bit.Core.Models.Data; +using Bit.Core.Models.Domain; +using Bit.Core.Utilities; +using Newtonsoft.Json; + +namespace Bit.Core.Services +{ + public class StateMigrationService : IStateMigrationService + { + private const int StateVersion = 3; + + private readonly IStorageService _preferencesStorageService; + private readonly IStorageService _liteDbStorageService; + private readonly IStorageService _secureStorageService; + + private enum Storage + { + LiteDb, + Prefs, + Secure, + } + + public StateMigrationService(IStorageService liteDbStorageService, IStorageService preferenceStorageService, + IStorageService secureStorageService) + { + _liteDbStorageService = liteDbStorageService; + _preferencesStorageService = preferenceStorageService; + _secureStorageService = secureStorageService; + } + + public async Task NeedsMigration() + { + var lastVersion = await GetLastStateVersionAsync(); + if (lastVersion == 0) + { + // fresh install, set current/latest version for availability going forward + lastVersion = StateVersion; + await SetLastStateVersionAsync(lastVersion); + } + return lastVersion < StateVersion; + } + + public async Task Migrate() + { + var lastVersion = await GetLastStateVersionAsync(); + switch (lastVersion) + { + case 1: + await MigrateFrom1To2Async(); + goto case 2; + case 2: + await MigrateFrom2To3Async(); + break; + } + } + + // v1 to v2 Migration + + private class V1Keys + { + internal const string EnvironmentUrlsKey = "environmentUrls"; + + } + + private async Task MigrateFrom1To2Async() + { + // move environmentUrls from LiteDB to prefs + var environmentUrls = await GetValueAsync(Storage.LiteDb, V1Keys.EnvironmentUrlsKey); + if (environmentUrls == null) + { + throw new Exception("'environmentUrls' must be in LiteDB during migration from 1 to 2"); + } + await SetValueAsync(Storage.Prefs, V2Keys.EnvironmentUrlsKey, environmentUrls); + + // Update stored version + await SetLastStateVersionAsync(2); + + // Remove old data + await RemoveValueAsync(Storage.LiteDb, V1Keys.EnvironmentUrlsKey); + } + + // v2 to v3 Migration + + private class V2Keys + { + internal const string SyncOnRefreshKey = "syncOnRefresh"; + internal const string VaultTimeoutKey = "lockOption"; + internal const string VaultTimeoutActionKey = "vaultTimeoutAction"; + internal const string LastActiveTimeKey = "lastActiveTime"; + internal const string BiometricUnlockKey = "fingerprintUnlock"; + internal const string ProtectedPin = "protectedPin"; + internal const string PinProtectedKey = "pinProtectedKey"; + internal const string DefaultUriMatch = "defaultUriMatch"; + internal const string DisableAutoTotpCopyKey = "disableAutoTotpCopy"; + internal const string EnvironmentUrlsKey = "environmentUrls"; + internal const string AutofillDisableSavePromptKey = "autofillDisableSavePrompt"; + internal const string AutofillBlacklistedUrisKey = "autofillBlacklistedUris"; + internal const string DisableFaviconKey = "disableFavicon"; + internal const string ThemeKey = "theme"; + internal const string ClearClipboardKey = "clearClipboard"; + internal const string PreviousPageKey = "previousPage"; + internal const string InlineAutofillEnabledKey = "inlineAutofillEnabled"; + internal const string InvalidUnlockAttempts = "invalidUnlockAttempts"; + internal const string PasswordRepromptAutofillKey = "passwordRepromptAutofillKey"; + internal const string PasswordVerifiedAutofillKey = "passwordVerifiedAutofillKey"; + internal const string MigratedFromV1 = "migratedFromV1"; + internal const string MigratedFromV1AutofillPromptShown = "migratedV1AutofillPromptShown"; + internal const string TriedV1Resync = "triedV1Resync"; + internal const string Keys_UserId = "userId"; + internal const string Keys_UserEmail = "userEmail"; + internal const string Keys_Stamp = "securityStamp"; + internal const string Keys_Kdf = "kdf"; + internal const string Keys_KdfIterations = "kdfIterations"; + internal const string Keys_EmailVerified = "emailVerified"; + internal const string Keys_ForcePasswordReset = "forcePasswordReset"; + internal const string Keys_AccessToken = "accessToken"; + internal const string Keys_RefreshToken = "refreshToken"; + internal const string Keys_LocalData = "ciphersLocalData"; + internal const string Keys_NeverDomains = "neverDomains"; + internal const string Keys_Key = "key"; + internal const string Keys_EncOrgKeys = "encOrgKeys"; + internal const string Keys_EncPrivateKey = "encPrivateKey"; + internal const string Keys_EncKey = "encKey"; + internal const string Keys_KeyHash = "keyHash"; + internal const string Keys_UsesKeyConnector = "usesKeyConnector"; + internal const string Keys_PassGenOptions = "passwordGenerationOptions"; + internal const string Keys_PassGenHistory = "generatedPasswordHistory"; + } + + private async Task MigrateFrom2To3Async() + { + // build account and state + var userId = await GetValueAsync(Storage.LiteDb, V2Keys.Keys_UserId); + var email = await GetValueAsync(Storage.LiteDb, V2Keys.Keys_UserEmail); + string name = null; + var hasPremiumPersonally = false; + var accessToken = await GetValueAsync(Storage.LiteDb, V2Keys.Keys_AccessToken); + if (!string.IsNullOrWhiteSpace(accessToken)) + { + var tokenService = ServiceContainer.Resolve("tokenService"); + await tokenService.SetAccessTokenAsync(accessToken, true); + + if (string.IsNullOrWhiteSpace(userId)) + { + userId = tokenService.GetUserId(); + } + if (string.IsNullOrWhiteSpace(email)) + { + email = tokenService.GetEmail(); + } + name = tokenService.GetName(); + hasPremiumPersonally = tokenService.GetPremium(); + } + if (string.IsNullOrWhiteSpace(userId)) + { + throw new Exception("'userId' must be in LiteDB during migration from 2 to 3"); + } + + var kdfType = await GetValueAsync(Storage.LiteDb, V2Keys.Keys_Kdf); + var kdfIterations = await GetValueAsync(Storage.LiteDb, V2Keys.Keys_KdfIterations); + var stamp = await GetValueAsync(Storage.LiteDb, V2Keys.Keys_Stamp); + var emailVerified = await GetValueAsync(Storage.LiteDb, V2Keys.Keys_EmailVerified); + var refreshToken = await GetValueAsync(Storage.LiteDb, V2Keys.Keys_RefreshToken); + var account = new Account( + new Account.AccountProfile() + { + UserId = userId, + Email = email, + Name = name, + Stamp = stamp, + KdfType = (KdfType?)kdfType, + KdfIterations = kdfIterations, + EmailVerified = emailVerified, + HasPremiumPersonally = hasPremiumPersonally, + }, + new Account.AccountTokens() + { + AccessToken = accessToken, + RefreshToken = refreshToken, + } + ); + var environmentUrls = await GetValueAsync(Storage.Prefs, V2Keys.EnvironmentUrlsKey); + var vaultTimeout = await GetValueAsync(Storage.Prefs, V2Keys.VaultTimeoutKey); + var vaultTimeoutAction = await GetValueAsync(Storage.Prefs, V2Keys.VaultTimeoutActionKey); + account.Settings = new Account.AccountSettings() + { + EnvironmentUrls = environmentUrls, + VaultTimeout = vaultTimeout, + VaultTimeoutAction = + vaultTimeoutAction == "logout" ? VaultTimeoutAction.Logout : VaultTimeoutAction.Lock, + }; + var state = new State { Accounts = new Dictionary { [userId] = account } }; + state.ActiveUserId = userId; + await SetValueAsync(Storage.LiteDb, Constants.StateKey, state); + + // migrate user-specific non-state data + var syncOnRefresh = await GetValueAsync(Storage.LiteDb, V2Keys.SyncOnRefreshKey); + await SetValueAsync(Storage.LiteDb, Constants.SyncOnRefreshKey(userId), syncOnRefresh); + var lastActiveTime = await GetValueAsync(Storage.Prefs, V2Keys.LastActiveTimeKey); + await SetValueAsync(Storage.LiteDb, Constants.LastActiveTimeKey(userId), lastActiveTime); + var biometricUnlock = await GetValueAsync(Storage.LiteDb, V2Keys.BiometricUnlockKey); + await SetValueAsync(Storage.LiteDb, Constants.BiometricUnlockKey(userId), biometricUnlock); + var protectedPin = await GetValueAsync(Storage.LiteDb, V2Keys.ProtectedPin); + await SetValueAsync(Storage.LiteDb, Constants.ProtectedPinKey(userId), protectedPin); + var pinProtectedKey = await GetValueAsync(Storage.LiteDb, V2Keys.PinProtectedKey); + await SetValueAsync(Storage.LiteDb, Constants.PinProtectedKey(userId), pinProtectedKey); + var defaultUriMatch = await GetValueAsync(Storage.Prefs, V2Keys.DefaultUriMatch); + await SetValueAsync(Storage.LiteDb, Constants.DefaultUriMatchKey(userId), defaultUriMatch); + var disableAutoTotpCopy = await GetValueAsync(Storage.Prefs, V2Keys.DisableAutoTotpCopyKey); + await SetValueAsync(Storage.LiteDb, Constants.DisableAutoTotpCopyKey(userId), disableAutoTotpCopy); + var autofillDisableSavePrompt = + await GetValueAsync(Storage.Prefs, V2Keys.AutofillDisableSavePromptKey); + await SetValueAsync(Storage.LiteDb, Constants.AutofillDisableSavePromptKey(userId), + autofillDisableSavePrompt); + var autofillBlacklistedUris = + await GetValueAsync>(Storage.LiteDb, V2Keys.AutofillBlacklistedUrisKey); + await SetValueAsync(Storage.LiteDb, Constants.AutofillBlacklistedUrisKey(userId), autofillBlacklistedUris); + var disableFavicon = await GetValueAsync(Storage.Prefs, V2Keys.DisableFaviconKey); + await SetValueAsync(Storage.LiteDb, Constants.DisableFaviconKey(userId), disableFavicon); + var theme = await GetValueAsync(Storage.Prefs, V2Keys.ThemeKey); + await SetValueAsync(Storage.LiteDb, Constants.ThemeKey(userId), theme); + var clearClipboard = await GetValueAsync(Storage.Prefs, V2Keys.ClearClipboardKey); + await SetValueAsync(Storage.LiteDb, Constants.ClearClipboardKey(userId), clearClipboard); + var previousPage = await GetValueAsync(Storage.LiteDb, V2Keys.PreviousPageKey); + await SetValueAsync(Storage.LiteDb, Constants.PreviousPageKey(userId), previousPage); + var inlineAutofillEnabled = await GetValueAsync(Storage.Prefs, V2Keys.InlineAutofillEnabledKey); + await SetValueAsync(Storage.LiteDb, Constants.InlineAutofillEnabledKey(userId), inlineAutofillEnabled); + var invalidUnlockAttempts = await GetValueAsync(Storage.Prefs, V2Keys.InvalidUnlockAttempts); + await SetValueAsync(Storage.LiteDb, Constants.InvalidUnlockAttemptsKey(userId), invalidUnlockAttempts); + var passwordRepromptAutofill = + await GetValueAsync(Storage.LiteDb, V2Keys.PasswordRepromptAutofillKey); + await SetValueAsync(Storage.LiteDb, Constants.PasswordRepromptAutofillKey(userId), + passwordRepromptAutofill); + var passwordVerifiedAutofill = + await GetValueAsync(Storage.LiteDb, V2Keys.PasswordVerifiedAutofillKey); + await SetValueAsync(Storage.LiteDb, Constants.PasswordVerifiedAutofillKey(userId), + passwordVerifiedAutofill); + var cipherLocalData = await GetValueAsync>>(Storage.LiteDb, + V2Keys.Keys_LocalData); + await SetValueAsync(Storage.LiteDb, Constants.LocalDataKey(userId), cipherLocalData); + var neverDomains = await GetValueAsync>(Storage.LiteDb, V2Keys.Keys_NeverDomains); + await SetValueAsync(Storage.LiteDb, Constants.NeverDomainsKey(userId), neverDomains); + var key = await GetValueAsync(Storage.Secure, V2Keys.Keys_Key); + await SetValueAsync(Storage.Secure, Constants.KeyKey(userId), key); + var encOrgKeys = await GetValueAsync>(Storage.LiteDb, V2Keys.Keys_EncOrgKeys); + await SetValueAsync(Storage.LiteDb, Constants.EncOrgKeysKey(userId), encOrgKeys); + var encPrivateKey = await GetValueAsync(Storage.LiteDb, V2Keys.Keys_EncPrivateKey); + await SetValueAsync(Storage.LiteDb, Constants.EncPrivateKeyKey(userId), encPrivateKey); + var encKey = await GetValueAsync(Storage.LiteDb, V2Keys.Keys_EncKey); + await SetValueAsync(Storage.LiteDb, Constants.EncKeyKey(userId), encKey); + var keyHash = await GetValueAsync(Storage.LiteDb, V2Keys.Keys_KeyHash); + await SetValueAsync(Storage.LiteDb, Constants.KeyHashKey(userId), keyHash); + var usesKeyConnector = await GetValueAsync(Storage.LiteDb, V2Keys.Keys_UsesKeyConnector); + await SetValueAsync(Storage.LiteDb, Constants.UsesKeyConnectorKey(userId), usesKeyConnector); + var passGenOptions = + await GetValueAsync(Storage.LiteDb, V2Keys.Keys_PassGenOptions); + await SetValueAsync(Storage.LiteDb, Constants.PassGenOptionsKey(userId), passGenOptions); + var passGenHistory = + await GetValueAsync>(Storage.LiteDb, V2Keys.Keys_PassGenHistory); + await SetValueAsync(Storage.LiteDb, Constants.PassGenHistoryKey(userId), passGenHistory); + + // migrate global non-state data + await SetValueAsync(Storage.Prefs, Constants.PreAuthEnvironmentUrlsKey, environmentUrls); + + // Update stored version + await SetLastStateVersionAsync(3); + + // Remove old data + await RemoveValueAsync(Storage.LiteDb, V2Keys.Keys_UserId); + await RemoveValueAsync(Storage.LiteDb, V2Keys.Keys_UserEmail); + await RemoveValueAsync(Storage.LiteDb, V2Keys.Keys_AccessToken); + await RemoveValueAsync(Storage.LiteDb, V2Keys.Keys_RefreshToken); + await RemoveValueAsync(Storage.LiteDb, V2Keys.Keys_Kdf); + await RemoveValueAsync(Storage.LiteDb, V2Keys.Keys_KdfIterations); + await RemoveValueAsync(Storage.LiteDb, V2Keys.Keys_Stamp); + await RemoveValueAsync(Storage.LiteDb, V2Keys.Keys_EmailVerified); + await RemoveValueAsync(Storage.LiteDb, V2Keys.Keys_ForcePasswordReset); + await RemoveValueAsync(Storage.Prefs, V2Keys.EnvironmentUrlsKey); + await RemoveValueAsync(Storage.LiteDb, V2Keys.PinProtectedKey); + await RemoveValueAsync(Storage.Prefs, V2Keys.VaultTimeoutKey); + await RemoveValueAsync(Storage.Prefs, V2Keys.VaultTimeoutActionKey); + await RemoveValueAsync(Storage.LiteDb, V2Keys.SyncOnRefreshKey); + await RemoveValueAsync(Storage.Prefs, V2Keys.LastActiveTimeKey); + await RemoveValueAsync(Storage.LiteDb, V2Keys.BiometricUnlockKey); + await RemoveValueAsync(Storage.LiteDb, V2Keys.ProtectedPin); + await RemoveValueAsync(Storage.Prefs, V2Keys.DefaultUriMatch); + await RemoveValueAsync(Storage.Prefs, V2Keys.DisableAutoTotpCopyKey); + await RemoveValueAsync(Storage.Prefs, V2Keys.AutofillDisableSavePromptKey); + await RemoveValueAsync(Storage.LiteDb, V2Keys.AutofillBlacklistedUrisKey); + await RemoveValueAsync(Storage.Prefs, V2Keys.DisableFaviconKey); + await RemoveValueAsync(Storage.Prefs, V2Keys.ThemeKey); + await RemoveValueAsync(Storage.Prefs, V2Keys.ClearClipboardKey); + await RemoveValueAsync(Storage.LiteDb, V2Keys.PreviousPageKey); + await RemoveValueAsync(Storage.Prefs, V2Keys.InlineAutofillEnabledKey); + await RemoveValueAsync(Storage.Prefs, V2Keys.InvalidUnlockAttempts); + await RemoveValueAsync(Storage.LiteDb, V2Keys.PasswordRepromptAutofillKey); + await RemoveValueAsync(Storage.LiteDb, V2Keys.PasswordVerifiedAutofillKey); + await RemoveValueAsync(Storage.Prefs, V2Keys.MigratedFromV1); + await RemoveValueAsync(Storage.Prefs, V2Keys.MigratedFromV1AutofillPromptShown); + await RemoveValueAsync(Storage.Prefs, V2Keys.TriedV1Resync); + await RemoveValueAsync(Storage.LiteDb, V2Keys.Keys_LocalData); + await RemoveValueAsync(Storage.LiteDb, V2Keys.Keys_NeverDomains); + await RemoveValueAsync(Storage.Secure, V2Keys.Keys_Key); + await RemoveValueAsync(Storage.LiteDb, V2Keys.Keys_EncOrgKeys); + await RemoveValueAsync(Storage.LiteDb, V2Keys.Keys_EncPrivateKey); + await RemoveValueAsync(Storage.LiteDb, V2Keys.Keys_EncKey); + await RemoveValueAsync(Storage.LiteDb, V2Keys.Keys_KeyHash); + await RemoveValueAsync(Storage.LiteDb, V2Keys.Keys_UsesKeyConnector); + await RemoveValueAsync(Storage.LiteDb, V2Keys.Keys_PassGenOptions); + await RemoveValueAsync(Storage.LiteDb, V2Keys.Keys_PassGenHistory); + } + + // Helpers + + private async Task GetLastStateVersionAsync() + { + var lastVersion = await GetValueAsync(Storage.Prefs, Constants.StateVersionKey); + if (lastVersion != null) + { + return lastVersion.Value; + } + + // check for original v1 migration + var envUrlsLiteDb = await GetValueAsync(Storage.LiteDb, V1Keys.EnvironmentUrlsKey); + if (envUrlsLiteDb != null) + { + // environmentUrls still in LiteDB (never migrated to prefs), this is v1 + return 1; + } + + // check for original v2 migration + var envUrlsPrefs = await GetValueAsync(Storage.Prefs, V2Keys.EnvironmentUrlsKey); + if (envUrlsPrefs != null) + { + // environmentUrls in Prefs (migrated from LiteDB), this is v2 + return 2; + } + + // this is a fresh install + return 0; + } + + private async Task SetLastStateVersionAsync(int value) + { + await SetValueAsync(Storage.Prefs, Constants.StateVersionKey, value); + } + + private async Task GetValueAsync(Storage storage, string key) + { + var value = await GetStorageService(storage).GetAsync(key); + Log("GET", storage, key, JsonConvert.SerializeObject(value)); + return value; + } + + private async Task SetValueAsync(Storage storage, string key, T value) + { + if (value == null) + { + await RemoveValueAsync(storage, key); + return; + } + Log("SET", storage, key, JsonConvert.SerializeObject(value)); + await GetStorageService(storage).SaveAsync(key, value); + } + + private async Task RemoveValueAsync(Storage storage, string key) + { + Log("REMOVE", storage, key, null); + await GetStorageService(storage).RemoveAsync(key); + } + + private IStorageService GetStorageService(Storage storage) + { + switch (storage) + { + case Storage.Secure: + return _secureStorageService; + case Storage.Prefs: + return _preferencesStorageService; + default: + return _liteDbStorageService; + } + } + + private void Log(string tag, Storage storage, string key, string value) + { + // TODO Remove this once all bugs are squished + string text; + switch (storage) + { + case Storage.Secure: + text = "SECURE / "; + break; + case Storage.Prefs: + text = "PREFS / "; + break; + default: + text = "LITEDB / "; + break; + } + text += "Key: " + key + " / "; + if (value != null) + { + text += "Value: " + value; + } + Debug.WriteLine(text, ">>> " + tag); + } + } +} diff --git a/src/Core/Services/StateService.cs b/src/Core/Services/StateService.cs index a40bd6fb7..ba06c70ac 100644 --- a/src/Core/Services/StateService.cs +++ b/src/Core/Services/StateService.cs @@ -1,44 +1,1546 @@ -using Bit.Core.Abstractions; +using System; +using Bit.Core.Abstractions; using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; using System.Threading.Tasks; +using Bit.Core.Enums; +using Bit.Core.Models.Data; +using Bit.Core.Models.Domain; +using Bit.Core.Models.View; +using Bit.Core.Utilities; +using Newtonsoft.Json; namespace Bit.Core.Services { public class StateService : IStateService { - private readonly Dictionary _state = new Dictionary(); + private readonly IStorageService _storageService; + private readonly IStorageService _secureStorageService; - public Task GetAsync(string key) + private State _state; + private bool _migrationChecked; + + public bool BiometricLocked { get; set; } + + public List AccountViews { get; set; } + + public StateService(IStorageService storageService, IStorageService secureStorageService) { - return Task.FromResult(_state.ContainsKey(key) ? (T)_state[key] : (T)(object)null); + _storageService = storageService; + _secureStorageService = secureStorageService; } - public Task SaveAsync(string key, T obj) + public async Task GetActiveUserIdAsync() { - if (_state.ContainsKey(key)) + await CheckStateAsync(); + + var activeUserId = _state?.ActiveUserId; + if (activeUserId == null) { - _state[key] = obj; + var state = await GetStateFromStorageAsync(); + activeUserId = state?.ActiveUserId; + } + return activeUserId; + } + + public async Task SetActiveUserAsync(string userId) + { + if (userId != null) + { + await ValidateUserAsync(userId); + } + await CheckStateAsync(); + var state = await GetStateFromStorageAsync(); + state.ActiveUserId = userId; + await SaveStateToStorageAsync(state); + _state.ActiveUserId = userId; + + // Update pre-auth settings based on now-active user + await SetRememberedEmailAsync(await GetEmailAsync()); + await SetRememberedOrgIdentifierAsync(await GetRememberedOrgIdentifierAsync()); + await SetPreAuthEnvironmentUrlsAsync(await GetEnvironmentUrlsAsync()); + } + + public async Task IsAuthenticatedAsync(string userId = null) + { + return await GetAccessTokenAsync(userId) != null; + } + + public async Task GetUserIdAsync(string email) + { + if (string.IsNullOrWhiteSpace(email)) + { + throw new ArgumentNullException(nameof(email)); + } + + await CheckStateAsync(); + if (_state?.Accounts != null) + { + foreach (var account in _state.Accounts) + { + var accountEmail = account.Value?.Profile?.Email; + if (accountEmail == email) + { + return account.Value.Profile.UserId; + } + } + } + return null; + } + + public async Task RefreshAccountViewsAsync(bool allowAddAccountRow) + { + await CheckStateAsync(); + + if (AccountViews == null) + { + AccountViews = new List(); } else { - _state.Add(key, obj); + AccountViews.Clear(); } - return Task.FromResult(0); - } - public Task RemoveAsync(string key) - { - if (_state.ContainsKey(key)) + var accountList = _state?.Accounts?.Values.ToList(); + if (accountList == null) { - _state.Remove(key); + return; + } + var vaultTimeoutService = ServiceContainer.Resolve("vaultTimeoutService"); + foreach (var account in accountList) + { + var isActiveAccount = account.Profile.UserId == _state.ActiveUserId; + var accountView = new AccountView(account, isActiveAccount); + if (isActiveAccount) + { + AccountViews.Add(accountView); + continue; + } + var isLocked = await vaultTimeoutService.IsLockedAsync(accountView.UserId); + var shouldTimeout = await vaultTimeoutService.ShouldTimeoutAsync(accountView.UserId); + if (isLocked || shouldTimeout) + { + var action = account.Settings.VaultTimeoutAction; + accountView.AuthStatus = action == VaultTimeoutAction.Logout ? AuthenticationStatus.LoggedOut + : AuthenticationStatus.Locked; + } + else + { + accountView.AuthStatus = AuthenticationStatus.Unlocked; + } + AccountViews.Add(accountView); + } + if (allowAddAccountRow && AccountViews.Count < Constants.MaxAccounts) + { + AccountViews.Add(new AccountView()); } - return Task.FromResult(0); } - public Task PurgeAsync() + public async Task AddAccountAsync(Account account) { - _state.Clear(); - return Task.FromResult(0); + await ScaffoldNewAccountAsync(account); + await SetActiveUserAsync(account.Profile.UserId); + await RefreshAccountViewsAsync(true); + } + + public async Task LogoutAccountAsync(string userId, bool userInitiated) + { + if (string.IsNullOrWhiteSpace(userId)) + { + throw new ArgumentNullException(nameof(userId)); + } + + await CheckStateAsync(); + await RemoveAccountAsync(userId, userInitiated); + + // If user initiated logout (not vault timeout) find the next user to make active, if any + if (userInitiated && _state?.Accounts != null) + { + foreach (var account in _state.Accounts) + { + var uid = account.Value?.Profile?.UserId; + if (uid == null) + { + continue; + } + await SetActiveUserAsync(uid); + break; + } + } + } + + public async Task GetPreAuthEnvironmentUrlsAsync() + { + return await GetValueAsync( + Constants.PreAuthEnvironmentUrlsKey, await GetDefaultStorageOptionsAsync()); + } + + public async Task SetPreAuthEnvironmentUrlsAsync(EnvironmentUrlData value) + { + await SetValueAsync( + Constants.PreAuthEnvironmentUrlsKey, value, await GetDefaultStorageOptionsAsync()); + } + + public async Task GetEnvironmentUrlsAsync(string userId = null) + { + return (await GetAccountAsync( + ReconcileOptions(new StorageOptions { UserId = userId }, await GetDefaultStorageOptionsAsync()) + ))?.Settings?.EnvironmentUrls; + } + + public async Task GetBiometricUnlockAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.BiometricUnlockKey(reconciledOptions.UserId); + return await GetValueAsync(key, reconciledOptions); + } + + public async Task SetBiometricUnlockAsync(bool? value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.BiometricUnlockKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task CanAccessPremiumAsync(string userId = null) + { + if (userId == null) + { + userId = await GetActiveUserIdAsync(); + } + if (!await IsAuthenticatedAsync(userId)) + { + return false; + } + + var account = await GetAccountAsync( + ReconcileOptions(new StorageOptions { UserId = userId }, await GetDefaultStorageOptionsAsync())); + if (account?.Profile?.HasPremiumPersonally.GetValueOrDefault() ?? false) + { + return true; + } + + var organizationService = ServiceContainer.Resolve("organizationService"); + var organizations = await organizationService.GetAllAsync(userId); + return organizations?.Any(o => o.UsersGetPremium && o.Enabled) ?? false; + } + + public async Task GetProtectedPinAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.ProtectedPinKey(reconciledOptions.UserId); + return await GetValueAsync(key, reconciledOptions); + } + + public async Task SetProtectedPinAsync(string value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.ProtectedPinKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task GetPinProtectedAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.PinProtectedKey(reconciledOptions.UserId); + return await GetValueAsync(key, reconciledOptions); + } + + public async Task SetPinProtectedAsync(string value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.PinProtectedKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task GetPinProtectedKeyAsync(string userId = null) + { + return (await GetAccountAsync( + ReconcileOptions(new StorageOptions { UserId = userId }, await GetDefaultInMemoryOptionsAsync()) + ))?.Keys?.PinProtectedKey; + } + + public async Task SetPinProtectedKeyAsync(EncString value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultInMemoryOptionsAsync()); + var account = await GetAccountAsync(reconciledOptions); + account.Keys.PinProtectedKey = value; + await SaveAccountAsync(account, reconciledOptions); + } + + public async Task GetKdfTypeAsync(string userId = null) + { + return (await GetAccountAsync( + ReconcileOptions(new StorageOptions { UserId = userId }, await GetDefaultStorageOptionsAsync()) + ))?.Profile?.KdfType; + } + + public async Task SetKdfTypeAsync(KdfType? value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var account = await GetAccountAsync(reconciledOptions); + account.Profile.KdfType = value; + await SaveAccountAsync(account, reconciledOptions); + } + + public async Task GetKdfIterationsAsync(string userId = null) + { + return (await GetAccountAsync( + ReconcileOptions(new StorageOptions { UserId = userId }, await GetDefaultStorageOptionsAsync()) + ))?.Profile?.KdfIterations; + } + + public async Task SetKdfIterationsAsync(int? value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var account = await GetAccountAsync(reconciledOptions); + account.Profile.KdfIterations = value; + await SaveAccountAsync(account, reconciledOptions); + } + + public async Task GetKeyEncryptedAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultSecureStorageOptionsAsync()); + var key = Constants.KeyKey(reconciledOptions.UserId); + return await GetValueAsync(key, reconciledOptions); + } + + public async Task SetKeyEncryptedAsync(string value, string userId) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultSecureStorageOptionsAsync()); + var key = Constants.KeyKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task GetKeyDecryptedAsync(string userId = null) + { + return (await GetAccountAsync( + ReconcileOptions(new StorageOptions { UserId = userId }, await GetDefaultInMemoryOptionsAsync()) + ))?.Keys?.Key; + } + + public async Task SetKeyDecryptedAsync(SymmetricCryptoKey value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultInMemoryOptionsAsync()); + var account = await GetAccountAsync(reconciledOptions); + account.Keys.Key = value; + await SaveAccountAsync(account, reconciledOptions); + } + + public async Task GetKeyHashAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.KeyHashKey(reconciledOptions.UserId); + return await GetValueAsync(key, reconciledOptions); + } + + public async Task SetKeyHashAsync(string value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.KeyHashKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task GetEncKeyEncryptedAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.EncKeyKey(reconciledOptions.UserId); + return await GetValueAsync(key, reconciledOptions); + } + + public async Task SetEncKeyEncryptedAsync(string value, string userId) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.EncKeyKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task> GetOrgKeysEncryptedAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.EncOrgKeysKey(reconciledOptions.UserId); + return await GetValueAsync>(key, reconciledOptions); + } + + public async Task SetOrgKeysEncryptedAsync(Dictionary value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.EncOrgKeysKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task GetPrivateKeyEncryptedAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.EncPrivateKeyKey(reconciledOptions.UserId); + return await GetValueAsync(key, reconciledOptions); + } + + public async Task SetPrivateKeyEncryptedAsync(string value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.EncPrivateKeyKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task> GetAutofillBlacklistedUrisAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.AutofillBlacklistedUrisKey(reconciledOptions.UserId); + return await GetValueAsync>(key, reconciledOptions); + } + + public async Task SetAutofillBlacklistedUrisAsync(List value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.AutofillBlacklistedUrisKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task GetAutofillTileAddedAsync() + { + var options = await GetDefaultStorageOptionsAsync(); + var key = Constants.AutofillTileAdded; + return await GetValueAsync(key, options); + } + + public async Task SetAutofillTileAddedAsync(bool? value) + { + var options = await GetDefaultStorageOptionsAsync(); + var key = Constants.AutofillTileAdded; + await SetValueAsync(key, value, options); + } + + public async Task GetEmailAsync(string userId = null) + { + return (await GetAccountAsync( + ReconcileOptions(new StorageOptions { UserId = userId }, await GetDefaultStorageOptionsAsync()) + ))?.Profile?.Email; + } + + public async Task GetNameAsync(string userId = null) + { + return (await GetAccountAsync( + ReconcileOptions(new StorageOptions { UserId = userId }, await GetDefaultStorageOptionsAsync()) + ))?.Profile?.Name; + } + + public async Task GetOrgIdentifierAsync(string userId = null) + { + return (await GetAccountAsync( + ReconcileOptions(new StorageOptions { UserId = userId }, await GetDefaultStorageOptionsAsync()) + ))?.Profile?.OrgIdentifier; + } + + public async Task GetLastActiveTimeAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.LastActiveTimeKey(reconciledOptions.UserId); + return await GetValueAsync(key, reconciledOptions); + } + + public async Task SetLastActiveTimeAsync(long? value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.LastActiveTimeKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task GetVaultTimeoutAsync(string userId = null) + { + return (await GetAccountAsync( + ReconcileOptions(new StorageOptions { UserId = userId }, await GetDefaultStorageOptionsAsync()) + ))?.Settings?.VaultTimeout; + } + + public async Task SetVaultTimeoutAsync(int? value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var account = await GetAccountAsync(reconciledOptions); + account.Settings.VaultTimeout = value; + await SaveAccountAsync(account, reconciledOptions); + } + + public async Task GetVaultTimeoutActionAsync(string userId = null) + { + return (await GetAccountAsync( + ReconcileOptions(new StorageOptions { UserId = userId }, await GetDefaultStorageOptionsAsync()) + ))?.Settings?.VaultTimeoutAction; + } + + public async Task SetVaultTimeoutActionAsync(VaultTimeoutAction? value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var account = await GetAccountAsync(reconciledOptions); + account.Settings.VaultTimeoutAction = value; + await SaveAccountAsync(account, reconciledOptions); + } + + public async Task GetLastFileCacheClearAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.LastFileCacheClearKey; + return await GetValueAsync(key, reconciledOptions); + } + + public async Task SetLastFileCacheClearAsync(DateTime? value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.LastFileCacheClearKey; + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task GetPreviousPageInfoAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.PreviousPageKey(reconciledOptions.UserId); + return await GetValueAsync(key, reconciledOptions); + } + + public async Task SetPreviousPageInfoAsync(PreviousPageInfo value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.PreviousPageKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task GetInvalidUnlockAttemptsAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.InvalidUnlockAttemptsKey(reconciledOptions.UserId); + return await GetValueAsync(key, reconciledOptions); + } + + public async Task SetInvalidUnlockAttemptsAsync(int? value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.InvalidUnlockAttemptsKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task GetLastBuildAsync() + { + var options = await GetDefaultStorageOptionsAsync(); + var key = Constants.LastBuildKey; + return await GetValueAsync(key, options); + } + + public async Task SetLastBuildAsync(string value) + { + var options = await GetDefaultStorageOptionsAsync(); + var key = Constants.LastBuildKey; + await SetValueAsync(key, value, options); + } + + public async Task GetDisableFaviconAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.DisableFaviconKey(reconciledOptions.UserId); + return await GetValueAsync(key, reconciledOptions); + } + + public async Task SetDisableFaviconAsync(bool? value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.DisableFaviconKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task GetDisableAutoTotpCopyAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.DisableAutoTotpCopyKey(reconciledOptions.UserId); + return await GetValueAsync(key, reconciledOptions); + } + + public async Task SetDisableAutoTotpCopyAsync(bool? value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.DisableAutoTotpCopyKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task GetInlineAutofillEnabledAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.InlineAutofillEnabledKey(reconciledOptions.UserId); + return await GetValueAsync(key, reconciledOptions); + } + + public async Task SetInlineAutofillEnabledAsync(bool? value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.InlineAutofillEnabledKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task GetAutofillDisableSavePromptAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.AutofillDisableSavePromptKey(reconciledOptions.UserId); + return await GetValueAsync(key, reconciledOptions); + } + + public async Task SetAutofillDisableSavePromptAsync(bool? value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.AutofillDisableSavePromptKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task>> GetLocalDataAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.LocalDataKey(reconciledOptions.UserId); + return await GetValueAsync>>(key, reconciledOptions); + } + + public async Task SetLocalDataAsync(Dictionary> value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.LocalDataKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task> GetEncryptedCiphersAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.CiphersKey(reconciledOptions.UserId); + return await GetValueAsync>(key, reconciledOptions); + } + + public async Task SetEncryptedCiphersAsync(Dictionary value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.CiphersKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task GetDefaultUriMatchAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.DefaultUriMatchKey(reconciledOptions.UserId); + return await GetValueAsync(key, reconciledOptions); + } + + public async Task SetDefaultUriMatchAsync(int? value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.DefaultUriMatchKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task> GetNeverDomainsAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.NeverDomainsKey(reconciledOptions.UserId); + return await GetValueAsync>(key, reconciledOptions); + } + + public async Task SetNeverDomainsAsync(HashSet value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.NeverDomainsKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task GetClearClipboardAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.ClearClipboardKey(reconciledOptions.UserId); + return await GetValueAsync(key, reconciledOptions); + } + + public async Task SetClearClipboardAsync(int? value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.ClearClipboardKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task> GetEncryptedCollectionsAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.CollectionsKey(reconciledOptions.UserId); + return await GetValueAsync>(key, reconciledOptions); + } + + public async Task SetEncryptedCollectionsAsync(Dictionary value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.CollectionsKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task GetPasswordRepromptAutofillAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.PasswordRepromptAutofillKey(reconciledOptions.UserId); + return await GetValueAsync(key, reconciledOptions) ?? false; + } + + public async Task SetPasswordRepromptAutofillAsync(bool? value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.PasswordRepromptAutofillKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task GetPasswordVerifiedAutofillAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.PasswordVerifiedAutofillKey(reconciledOptions.UserId); + return await GetValueAsync(key, reconciledOptions) ?? false; + } + + public async Task SetPasswordVerifiedAutofillAsync(bool? value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.PasswordVerifiedAutofillKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task GetLastSyncAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.LastSyncKey(reconciledOptions.UserId); + return await GetValueAsync(key, reconciledOptions); + } + + public async Task SetLastSyncAsync(DateTime? value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.LastSyncKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task GetSecurityStampAsync(string userId = null) + { + return (await GetAccountAsync( + ReconcileOptions(new StorageOptions { UserId = userId }, await GetDefaultStorageOptionsAsync()) + ))?.Profile?.Stamp; + } + + public async Task SetSecurityStampAsync(string value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var account = await GetAccountAsync(reconciledOptions); + account.Profile.Stamp = value; + await SaveAccountAsync(account, reconciledOptions); + } + + public async Task GetEmailVerifiedAsync(string userId = null) + { + return (await GetAccountAsync( + ReconcileOptions(new StorageOptions { UserId = userId }, await GetDefaultStorageOptionsAsync()) + ))?.Profile?.EmailVerified ?? false; + } + + public async Task SetEmailVerifiedAsync(bool? value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var account = await GetAccountAsync(reconciledOptions); + account.Profile.EmailVerified = value; + await SaveAccountAsync(account, reconciledOptions); + } + + public async Task GetSyncOnRefreshAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.SyncOnRefreshKey(reconciledOptions.UserId); + return await GetValueAsync(key, reconciledOptions) ?? false; + } + + public async Task SetSyncOnRefreshAsync(bool? value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.SyncOnRefreshKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task GetRememberedEmailAsync() + { + var options = await GetDefaultStorageOptionsAsync(); + var key = Constants.RememberedEmailKey; + return await GetValueAsync(key, options); + } + + public async Task SetRememberedEmailAsync(string value) + { + var options = await GetDefaultStorageOptionsAsync(); + var key = Constants.RememberedEmailKey; + await SetValueAsync(key, value, options); + } + + public async Task GetRememberedOrgIdentifierAsync() + { + var options = await GetDefaultStorageOptionsAsync(); + var key = Constants.RememberedOrgIdentifierKey; + return await GetValueAsync(key, options); + } + + public async Task SetRememberedOrgIdentifierAsync(string value) + { + var options = await GetDefaultStorageOptionsAsync(); + var key = Constants.RememberedOrgIdentifierKey; + await SetValueAsync(key, value, options); + } + + public async Task GetThemeAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.ThemeKey(reconciledOptions.UserId); + return await GetValueAsync(key, reconciledOptions); + } + + public async Task SetThemeAsync(string value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.ThemeKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task ApplyThemeGloballyAsync(string value) + { + // TODO remove this method (ApplyThemeGlobally) to restore per-account theme support + await CheckStateAsync(); + if (_state?.Accounts == null) + { + return; + } + var activeUserId = await GetActiveUserIdAsync(); + foreach (var account in _state.Accounts) + { + var uid = account.Value?.Profile?.UserId; + // skip active user (theme already set) + if (uid != null && uid != activeUserId) + { + await SetThemeAsync(value, uid); + } + } + } + + public async Task GetAddSitePromptShownAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.AddSitePromptShownKey; + return await GetValueAsync(key, reconciledOptions); + } + + public async Task SetAddSitePromptShownAsync(bool? value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.AddSitePromptShownKey; + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task GetPushInitialPromptShownAsync() + { + var options = await GetDefaultStorageOptionsAsync(); + var key = Constants.PushInitialPromptShownKey; + return await GetValueAsync(key, options); + } + + public async Task SetPushInitialPromptShownAsync(bool? value) + { + var options = await GetDefaultStorageOptionsAsync(); + var key = Constants.PushInitialPromptShownKey; + await SetValueAsync(key, value, options); + } + + public async Task GetPushLastRegistrationDateAsync() + { + var options = await GetDefaultStorageOptionsAsync(); + var key = Constants.PushLastRegistrationDateKey; + return await GetValueAsync(key, options); + } + + public async Task SetPushLastRegistrationDateAsync(DateTime? value) + { + var options = await GetDefaultStorageOptionsAsync(); + var key = Constants.PushLastRegistrationDateKey; + await SetValueAsync(key, value, options); + } + + public async Task GetPushInstallationRegistrationErrorAsync() + { + var options = await GetDefaultStorageOptionsAsync(); + var key = Constants.PushInstallationRegistrationErrorKey; + return await GetValueAsync(key, options); + } + + public async Task SetPushInstallationRegistrationErrorAsync(string value) + { + var options = await GetDefaultStorageOptionsAsync(); + var key = Constants.PushInstallationRegistrationErrorKey; + await SetValueAsync(key, value, options); + } + + public async Task GetPushCurrentTokenAsync() + { + var options = await GetDefaultStorageOptionsAsync(); + var key = Constants.PushCurrentTokenKey; + return await GetValueAsync(key, options); + } + + public async Task SetPushCurrentTokenAsync(string value) + { + var options = await GetDefaultStorageOptionsAsync(); + var key = Constants.PushCurrentTokenKey; + await SetValueAsync(key, value, options); + } + + public async Task> GetEventCollectionAsync() + { + var options = await GetDefaultStorageOptionsAsync(); + var key = Constants.EventCollectionKey; + return await GetValueAsync>(key, options); + } + + public async Task SetEventCollectionAsync(List value) + { + var options = await GetDefaultStorageOptionsAsync(); + var key = Constants.EventCollectionKey; + await SetValueAsync(key, value, options); + } + + public async Task> GetEncryptedFoldersAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.FoldersKey(reconciledOptions.UserId); + return await GetValueAsync>(key, reconciledOptions); + } + + public async Task SetEncryptedFoldersAsync(Dictionary value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.FoldersKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task> GetEncryptedPoliciesAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.PoliciesKey(reconciledOptions.UserId); + return await GetValueAsync>(key, reconciledOptions); + } + + public async Task SetEncryptedPoliciesAsync(Dictionary value, string userId) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.PoliciesKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task GetPushRegisteredTokenAsync() + { + var options = await GetDefaultStorageOptionsAsync(); + var key = Constants.PushRegisteredTokenKey; + return await GetValueAsync(key, options); + } + + public async Task SetPushRegisteredTokenAsync(string value) + { + var options = await GetDefaultStorageOptionsAsync(); + var key = Constants.PushRegisteredTokenKey; + await SetValueAsync(key, value, options); + } + + public async Task GetUsesKeyConnectorAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.UsesKeyConnectorKey(reconciledOptions.UserId); + return await GetValueAsync(key, reconciledOptions) ?? false; + } + + public async Task SetUsesKeyConnectorAsync(bool? value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.UsesKeyConnectorKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task> GetOrganizationsAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.OrganizationsKey(reconciledOptions.UserId); + return await GetValueAsync>(key, reconciledOptions); + } + + public async Task SetOrganizationsAsync(Dictionary value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.OrganizationsKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task GetPasswordGenerationOptionsAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.PassGenOptionsKey(reconciledOptions.UserId); + return await GetValueAsync(key, reconciledOptions); + } + + public async Task SetPasswordGenerationOptionsAsync(PasswordGenerationOptions value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.PassGenOptionsKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task> GetEncryptedPasswordGenerationHistory(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.PassGenHistoryKey(reconciledOptions.UserId); + return await GetValueAsync>(key, reconciledOptions); + } + + public async Task SetEncryptedPasswordGenerationHistoryAsync(List value, + string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.PassGenHistoryKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task> GetEncryptedSendsAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.SendsKey(reconciledOptions.UserId); + return await GetValueAsync>(key, reconciledOptions); + } + + public async Task SetEncryptedSendsAsync(Dictionary value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.SendsKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task> GetSettingsAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.SettingsKey(reconciledOptions.UserId); + return await GetValueAsync>(key, reconciledOptions); + } + + public async Task SetSettingsAsync(Dictionary value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var key = Constants.SettingsKey(reconciledOptions.UserId); + await SetValueAsync(key, value, reconciledOptions); + } + + public async Task GetAccessTokenAsync(string userId = null) + { + return (await GetAccountAsync( + ReconcileOptions(new StorageOptions { UserId = userId }, await GetDefaultStorageOptionsAsync()) + ))?.Tokens?.AccessToken; + } + + public async Task SetAccessTokenAsync(string value, bool skipTokenStorage, string userId = null) + { + var reconciledOptions = ReconcileOptions( + new StorageOptions { UserId = userId, SkipTokenStorage = skipTokenStorage }, + await GetDefaultStorageOptionsAsync()); + var account = await GetAccountAsync(reconciledOptions); + account.Tokens.AccessToken = value; + await SaveAccountAsync(account, reconciledOptions); + } + + public async Task GetRefreshTokenAsync(string userId = null) + { + return (await GetAccountAsync( + ReconcileOptions(new StorageOptions { UserId = userId }, await GetDefaultStorageOptionsAsync()) + ))?.Tokens?.RefreshToken; + } + + public async Task SetRefreshTokenAsync(string value, bool skipTokenStorage, string userId = null) + { + var reconciledOptions = ReconcileOptions( + new StorageOptions { UserId = userId, SkipTokenStorage = skipTokenStorage }, + await GetDefaultStorageOptionsAsync()); + var account = await GetAccountAsync(reconciledOptions); + account.Tokens.RefreshToken = value; + await SaveAccountAsync(account, reconciledOptions); + } + + public async Task GetTwoFactorTokenAsync(string email = null) + { + var reconciledOptions = + ReconcileOptions(new StorageOptions { Email = email }, await GetDefaultStorageOptionsAsync()); + var key = Constants.TwoFactorTokenKey(reconciledOptions.Email); + return await GetValueAsync(key, reconciledOptions); + } + + public async Task SetTwoFactorTokenAsync(string value, string email = null) + { + var reconciledOptions = + ReconcileOptions(new StorageOptions { Email = email }, await GetDefaultStorageOptionsAsync()); + var key = Constants.TwoFactorTokenKey(reconciledOptions.Email); + await SetValueAsync(key, value, reconciledOptions); + } + + // Helpers + + private async Task GetValueAsync(string key, StorageOptions options) + { + var value = await GetStorageService(options).GetAsync(key); + Log("GET", options, key, JsonConvert.SerializeObject(value)); + return value; + } + + private async Task SetValueAsync(string key, T value, StorageOptions options) + { + if (value == null) + { + Log("REMOVE", options, key, null); + await GetStorageService(options).RemoveAsync(key); + return; + } + Log("SET", options, key, JsonConvert.SerializeObject(value)); + await GetStorageService(options).SaveAsync(key, value); + } + + private IStorageService GetStorageService(StorageOptions options) + { + return options.UseSecureStorage.GetValueOrDefault(false) ? _secureStorageService : _storageService; + } + + private async Task GetAccountAsync(StorageOptions options) + { + await CheckStateAsync(); + + if (options?.UserId == null) + { + return null; + } + + // Memory + if (_state?.Accounts?.ContainsKey(options.UserId) ?? false) + { + if (_state.Accounts[options.UserId].Keys == null) + { + _state.Accounts[options.UserId].Keys = new Account.AccountKeys(); + } + return _state.Accounts[options.UserId]; + } + + // Storage + _state = await GetStateFromStorageAsync(); + if (_state?.Accounts?.ContainsKey(options.UserId) ?? false) + { + if (_state.Accounts[options.UserId].Keys == null) + { + _state.Accounts[options.UserId].Keys = new Account.AccountKeys(); + } + return _state.Accounts[options.UserId]; + } + + return null; + } + + private async Task SaveAccountAsync(Account account, StorageOptions options = null) + { + if (account?.Profile?.UserId == null) + { + throw new Exception("account?.Profile?.UserId cannot be null"); + } + + await CheckStateAsync(); + + // Memory + if (UseMemory(options)) + { + if (_state.Accounts == null) + { + _state.Accounts = new Dictionary(); + } + _state.Accounts[account.Profile.UserId] = account; + } + + // Storage + if (UseDisk(options)) + { + var state = await GetStateFromStorageAsync() ?? new State(); + if (state.Accounts == null) + { + state.Accounts = new Dictionary(); + } + + // Use Account copy constructor to clone with keys excluded (for storage) + state.Accounts[account.Profile.UserId] = new Account(account); + + // If we have a vault timeout and the action is log out, don't store token + if (options?.SkipTokenStorage.GetValueOrDefault() ?? false) + { + state.Accounts[account.Profile.UserId].Tokens.AccessToken = null; + state.Accounts[account.Profile.UserId].Tokens.RefreshToken = null; + } + + await SaveStateToStorageAsync(state); + } + } + + private async Task RemoveAccountAsync(string userId, bool userInitiated) + { + if (string.IsNullOrWhiteSpace(userId)) + { + throw new ArgumentNullException(nameof(userId)); + } + + var email = await GetEmailAsync(userId); + + // Memory + if (_state?.Accounts?.ContainsKey(userId) ?? false) + { + if (userInitiated) + { + _state.Accounts.Remove(userId); + } + else + { + _state.Accounts[userId].Tokens.AccessToken = null; + _state.Accounts[userId].Tokens.RefreshToken = null; + _state.Accounts[userId].Keys.Key = null; + } + } + if (userInitiated && _state?.ActiveUserId == userId) + { + _state.ActiveUserId = null; + } + + // Storage + var stateModified = false; + var state = await GetStateFromStorageAsync(); + if (state?.Accounts?.ContainsKey(userId) ?? false) + { + if (userInitiated) + { + state.Accounts.Remove(userId); + } + else + { + state.Accounts[userId].Tokens.AccessToken = null; + state.Accounts[userId].Tokens.RefreshToken = null; + } + stateModified = true; + } + if (userInitiated && state?.ActiveUserId == userId) + { + state.ActiveUserId = null; + stateModified = true; + } + if (stateModified) + { + await SaveStateToStorageAsync(state); + } + + // Non-state storage + await SetBiometricUnlockAsync(null, userId); + await SetProtectedPinAsync(null, userId); + await SetPinProtectedAsync(null, userId); + await SetKeyEncryptedAsync(null, userId); + await SetKeyHashAsync(null, userId); + await SetEncKeyEncryptedAsync(null, userId); + await SetOrgKeysEncryptedAsync(null, userId); + await SetPrivateKeyEncryptedAsync(null, userId); + await SetLastActiveTimeAsync(null, userId); + await SetLastFileCacheClearAsync(null, userId); + await SetPreviousPageInfoAsync(null, userId); + await SetInvalidUnlockAttemptsAsync(null, userId); + await SetLocalDataAsync(null, userId); + await SetEncryptedCiphersAsync(null, userId); + await SetEncryptedCollectionsAsync(null, userId); + await SetLastSyncAsync(null, userId); + await SetEncryptedFoldersAsync(null, userId); + await SetEncryptedPoliciesAsync(null, userId); + await SetUsesKeyConnectorAsync(null, userId); + await SetOrganizationsAsync(null, userId); + await SetEncryptedPasswordGenerationHistoryAsync(null, userId); + await SetEncryptedSendsAsync(null, userId); + await SetSettingsAsync(null, userId); + if (!string.IsNullOrWhiteSpace(email)) + { + await SetTwoFactorTokenAsync(null, email); + } + + if (userInitiated) + { + // user initiated logout (not vault timeout or scaffolding new account) so remove remaining settings + await SetAutofillBlacklistedUrisAsync(null, userId); + await SetDisableFaviconAsync(null, userId); + await SetDisableAutoTotpCopyAsync(null, userId); + await SetInlineAutofillEnabledAsync(null, userId); + await SetAutofillDisableSavePromptAsync(null, userId); + await SetDefaultUriMatchAsync(null, userId); + await SetNeverDomainsAsync(null, userId); + await SetClearClipboardAsync(null, userId); + await SetPasswordRepromptAutofillAsync(null, userId); + await SetPasswordVerifiedAutofillAsync(null, userId); + await SetSyncOnRefreshAsync(null, userId); + await SetThemeAsync(null, userId); + await SetAddSitePromptShownAsync(null, userId); + await SetPasswordGenerationOptionsAsync(null, userId); + } + } + + private async Task ScaffoldNewAccountAsync(Account account) + { + await CheckStateAsync(); + var currentTheme = await GetThemeAsync(); + + account.Settings.EnvironmentUrls = await GetPreAuthEnvironmentUrlsAsync(); + + // Storage + var state = await GetStateFromStorageAsync() ?? new State(); + if (state.Accounts == null) + { + state.Accounts = new Dictionary(); + } + if (state.Accounts.ContainsKey(account.Profile.UserId)) + { + // Run cleanup pass on existing account before proceeding + await RemoveAccountAsync(account.Profile.UserId, false); + var existingAccount = state.Accounts[account.Profile.UserId]; + account.Settings.VaultTimeout = existingAccount.Settings.VaultTimeout; + account.Settings.VaultTimeoutAction = existingAccount.Settings.VaultTimeoutAction; + } + + // New account defaults + if (account.Settings.VaultTimeout == null) + { + account.Settings.VaultTimeout = 15; + } + if (account.Settings.VaultTimeoutAction == null) + { + account.Settings.VaultTimeoutAction = VaultTimeoutAction.Lock; + } + await SetThemeAsync(currentTheme, account.Profile.UserId); + + state.Accounts[account.Profile.UserId] = account; + await SaveStateToStorageAsync(state); + + // Memory + if (_state == null) + { + _state = state; + } + else + { + if (_state.Accounts == null) + { + _state.Accounts = new Dictionary(); + } + _state.Accounts[account.Profile.UserId] = account; + } + } + + private StorageOptions ReconcileOptions(StorageOptions requestedOptions, StorageOptions defaultOptions) + { + if (requestedOptions == null) + { + return defaultOptions; + } + requestedOptions.StorageLocation = requestedOptions.StorageLocation ?? defaultOptions.StorageLocation; + requestedOptions.UseSecureStorage = requestedOptions.UseSecureStorage ?? defaultOptions.UseSecureStorage; + requestedOptions.UserId = requestedOptions.UserId ?? defaultOptions.UserId; + requestedOptions.Email = requestedOptions.Email ?? defaultOptions.Email; + requestedOptions.SkipTokenStorage = requestedOptions.SkipTokenStorage ?? defaultOptions.SkipTokenStorage; + return requestedOptions; + } + + private async Task GetDefaultStorageOptionsAsync() + { + return new StorageOptions() + { + StorageLocation = StorageLocation.Both, + UserId = await GetActiveUserIdAsync(), + }; + } + + private async Task GetDefaultSecureStorageOptionsAsync() + { + return new StorageOptions() + { + StorageLocation = StorageLocation.Disk, + UseSecureStorage = true, + UserId = await GetActiveUserIdAsync(), + }; + } + + private async Task GetDefaultInMemoryOptionsAsync() + { + return new StorageOptions() + { + StorageLocation = StorageLocation.Memory, + UserId = await GetActiveUserIdAsync(), + }; + } + + private bool UseMemory(StorageOptions options) + { + return options?.StorageLocation == StorageLocation.Memory || + options?.StorageLocation == StorageLocation.Both; + } + + private bool UseDisk(StorageOptions options) + { + return options?.StorageLocation == StorageLocation.Disk || + options?.StorageLocation == StorageLocation.Both; + } + + private async Task GetStateFromStorageAsync() + { + var state = await _storageService.GetAsync(Constants.StateKey); + // TODO Remove logging once all bugs are squished + Debug.WriteLine(JsonConvert.SerializeObject(state, Formatting.Indented), + ">>> GetStateFromStorageAsync()"); + return state; + } + + private async Task SaveStateToStorageAsync(State state) + { + await _storageService.SaveAsync(Constants.StateKey, state); + // TODO Remove logging once all bugs are squished + Debug.WriteLine(JsonConvert.SerializeObject(state, Formatting.Indented), + ">>> SaveStateToStorageAsync()"); + } + + private async Task CheckStateAsync() + { + if (!_migrationChecked) + { + var migrationService = ServiceContainer.Resolve("stateMigrationService"); + if (await migrationService.NeedsMigration()) + { + await migrationService.Migrate(); + } + _migrationChecked = true; + } + + if (_state == null) + { + _state = await GetStateFromStorageAsync() ?? new State(); + } + } + + private async Task ValidateUserAsync(string userId) + { + if (string.IsNullOrWhiteSpace(userId)) + { + throw new ArgumentNullException(nameof(userId)); + } + await CheckStateAsync(); + var accounts = _state?.Accounts; + if (accounts == null || !accounts.Any()) + { + throw new Exception("At least one account required to validate user"); + } + foreach (var account in accounts) + { + if (account.Key == userId) + { + // found match, user is valid + return; + } + } + throw new Exception("User does not exist in account list"); + } + + private void Log(string tag, StorageOptions options, string key, string value) + { + // TODO Remove this once all bugs are squished + var text = options?.UseSecureStorage ?? false ? "SECURE / " : ""; + text += "Key: " + key + " / "; + if (value != null) + { + text += "Value: " + value; + } + Debug.WriteLine(text, ">>> " + tag); } } } diff --git a/src/Core/Services/SyncService.cs b/src/Core/Services/SyncService.cs index 7d7795a4e..9f339bbb4 100644 --- a/src/Core/Services/SyncService.cs +++ b/src/Core/Services/SyncService.cs @@ -12,45 +12,43 @@ namespace Bit.Core.Services { public class SyncService : ISyncService { - private const string Keys_LastSyncFormat = "lastSync_{0}"; - - private readonly IUserService _userService; + private readonly IStateService _stateService; private readonly IApiService _apiService; private readonly ISettingsService _settingsService; private readonly IFolderService _folderService; private readonly ICipherService _cipherService; private readonly ICryptoService _cryptoService; private readonly ICollectionService _collectionService; - private readonly IStorageService _storageService; + private readonly IOrganizationService _organizationService; private readonly IMessagingService _messagingService; private readonly IPolicyService _policyService; private readonly ISendService _sendService; private readonly IKeyConnectorService _keyConnectorService; - private readonly Func _logoutCallbackAsync; + private readonly Func, Task> _logoutCallbackAsync; public SyncService( - IUserService userService, + IStateService stateService, IApiService apiService, ISettingsService settingsService, IFolderService folderService, ICipherService cipherService, ICryptoService cryptoService, ICollectionService collectionService, - IStorageService storageService, + IOrganizationService organizationService, IMessagingService messagingService, IPolicyService policyService, ISendService sendService, IKeyConnectorService keyConnectorService, - Func logoutCallbackAsync) + Func, Task> logoutCallbackAsync) { - _userService = userService; + _stateService = stateService; _apiService = apiService; _settingsService = settingsService; _folderService = folderService; _cipherService = cipherService; _cryptoService = cryptoService; _collectionService = collectionService; - _storageService = storageService; + _organizationService = organizationService; _messagingService = messagingService; _policyService = policyService; _sendService = sendService; @@ -62,28 +60,26 @@ namespace Bit.Core.Services public async Task GetLastSyncAsync() { - var userId = await _userService.GetUserIdAsync(); - if (userId == null) + if (await _stateService.GetActiveUserIdAsync() == null) { return null; } - return await _storageService.GetAsync(string.Format(Keys_LastSyncFormat, userId)); + return await _stateService.GetLastSyncAsync(); } public async Task SetLastSyncAsync(DateTime date) { - var userId = await _userService.GetUserIdAsync(); - if (userId == null) + if (await _stateService.GetActiveUserIdAsync() == null) { return; } - await _storageService.SaveAsync(string.Format(Keys_LastSyncFormat, userId), date); + await _stateService.SetLastSyncAsync(date); } public async Task FullSyncAsync(bool forceSync, bool allowThrowOnError = false) { SyncStarted(); - var isAuthenticated = await _userService.IsAuthenticatedAsync(); + var isAuthenticated = await _stateService.IsAuthenticatedAsync(); if (!isAuthenticated) { return SyncCompleted(false); @@ -101,7 +97,7 @@ namespace Bit.Core.Services await SetLastSyncAsync(now); return SyncCompleted(false); } - var userId = await _userService.GetUserIdAsync(); + var userId = await _stateService.GetActiveUserIdAsync(); try { var response = await _apiService.GetSyncAsync(); @@ -131,7 +127,7 @@ namespace Bit.Core.Services public async Task SyncUpsertFolderAsync(SyncFolderNotification notification, bool isEdit) { SyncStarted(); - if (await _userService.IsAuthenticatedAsync()) + if (await _stateService.IsAuthenticatedAsync()) { try { @@ -142,7 +138,7 @@ namespace Bit.Core.Services var remoteFolder = await _apiService.GetFolderAsync(notification.Id); if (remoteFolder != null) { - var userId = await _userService.GetUserIdAsync(); + var userId = await _stateService.GetActiveUserIdAsync(); await _folderService.UpsertAsync(new FolderData(remoteFolder, userId)); _messagingService.Send("syncedUpsertedFolder", new Dictionary { @@ -160,7 +156,7 @@ namespace Bit.Core.Services public async Task SyncDeleteFolderAsync(SyncFolderNotification notification) { SyncStarted(); - if (await _userService.IsAuthenticatedAsync()) + if (await _stateService.IsAuthenticatedAsync()) { await _folderService.DeleteAsync(notification.Id); _messagingService.Send("syncedDeletedFolder", new Dictionary @@ -175,7 +171,7 @@ namespace Bit.Core.Services public async Task SyncUpsertCipherAsync(SyncCipherNotification notification, bool isEdit) { SyncStarted(); - if (await _userService.IsAuthenticatedAsync()) + if (await _stateService.IsAuthenticatedAsync()) { try { @@ -230,7 +226,7 @@ namespace Bit.Core.Services var remoteCipher = await _apiService.GetCipherAsync(notification.Id); if (remoteCipher != null) { - var userId = await _userService.GetUserIdAsync(); + var userId = await _stateService.GetActiveUserIdAsync(); await _cipherService.UpsertAsync(new CipherData(remoteCipher, userId)); _messagingService.Send("syncedUpsertedCipher", new Dictionary { @@ -259,7 +255,7 @@ namespace Bit.Core.Services public async Task SyncDeleteCipherAsync(SyncCipherNotification notification) { SyncStarted(); - if (await _userService.IsAuthenticatedAsync()) + if (await _stateService.IsAuthenticatedAsync()) { await _cipherService.DeleteAsync(notification.Id); _messagingService.Send("syncedDeletedCipher", new Dictionary @@ -315,23 +311,22 @@ namespace Bit.Core.Services private async Task SyncProfileAsync(ProfileResponse response) { - var stamp = await _userService.GetSecurityStampAsync(); + var stamp = await _stateService.GetSecurityStampAsync(); if (stamp != null && stamp != response.SecurityStamp) { if (_logoutCallbackAsync != null) { - await _logoutCallbackAsync(true); + await _logoutCallbackAsync(new Tuple(response.Id, false, true)); } return; } await _cryptoService.SetEncKeyAsync(response.Key); await _cryptoService.SetEncPrivateKeyAsync(response.PrivateKey); await _cryptoService.SetOrgKeysAsync(response.Organizations); - await _userService.SetSecurityStampAsync(response.SecurityStamp); + await _stateService.SetSecurityStampAsync(response.SecurityStamp); var organizations = response.Organizations.ToDictionary(o => o.Id, o => new OrganizationData(o)); - await _userService.ReplaceOrganizationsAsync(organizations); - await _userService.SetEmailVerifiedAsync(response.EmailVerified); - await _userService.SetForcePasswordReset(response.ForcePasswordReset); + await _organizationService.ReplaceAsync(organizations); + await _stateService.SetEmailVerifiedAsync(response.EmailVerified); await _keyConnectorService.SetUsesKeyConnector(response.UsesKeyConnector); } diff --git a/src/Core/Services/TokenService.cs b/src/Core/Services/TokenService.cs index 09f1c44cc..817cf6ad4 100644 --- a/src/Core/Services/TokenService.cs +++ b/src/Core/Services/TokenService.cs @@ -5,132 +5,104 @@ using System; using System.Text; using System.Threading.Tasks; using System.Linq; +using Bit.Core.Enums; namespace Bit.Core.Services { public class TokenService : ITokenService { - private readonly IStorageService _storageService; + private readonly IStateService _stateService; - private string _token; - private JObject _decodedToken; - private string _refreshToken; + private string _accessTokenForDecoding; + private JObject _decodedAccessToken; - private const string Keys_AccessToken = "accessToken"; - private const string Keys_RefreshToken = "refreshToken"; - private const string Keys_TwoFactorTokenFormat = "twoFactorToken_{0}"; - - public TokenService(IStorageService storageService) + public TokenService(IStateService stateService) { - _storageService = storageService; + _stateService = stateService; } public async Task SetTokensAsync(string accessToken, string refreshToken) { await Task.WhenAll( - SetTokenAsync(accessToken), + SetAccessTokenAsync(accessToken), SetRefreshTokenAsync(refreshToken)); } - public async Task SetTokenAsync(string token) + public async Task SetAccessTokenAsync(string accessToken, bool forDecodeOnly = false) { - _token = token; - _decodedToken = null; - - if (await SkipTokenStorage()) + _accessTokenForDecoding = accessToken; + _decodedAccessToken = null; + if (!forDecodeOnly) { - // If we have a vault timeout and the action is log out, don't store token - return; + await _stateService.SetAccessTokenAsync(accessToken, await SkipTokenStorage()); } - - await _storageService.SaveAsync(Keys_AccessToken, token); } public async Task GetTokenAsync() { - if (_token != null) - { - return _token; - } - _token = await _storageService.GetAsync(Keys_AccessToken); - return _token; + _accessTokenForDecoding = await _stateService.GetAccessTokenAsync(); + return _accessTokenForDecoding; } public async Task SetRefreshTokenAsync(string refreshToken) { - _refreshToken = refreshToken; - - if (await SkipTokenStorage()) - { - // If we have a vault timeout and the action is log out, don't store token - return; - } - - await _storageService.SaveAsync(Keys_RefreshToken, refreshToken); + await _stateService.SetRefreshTokenAsync(refreshToken, await SkipTokenStorage()); } public async Task GetRefreshTokenAsync() { - if (_refreshToken != null) - { - return _refreshToken; - } - _refreshToken = await _storageService.GetAsync(Keys_RefreshToken); - return _refreshToken; + return await _stateService.GetRefreshTokenAsync(); } public async Task ToggleTokensAsync() { + // load and re-save tokens to reflect latest value of SkipTokenStorage() var token = await GetTokenAsync(); var refreshToken = await GetRefreshTokenAsync(); - if (await SkipTokenStorage()) - { - await ClearTokenAsync(); - _token = token; - _refreshToken = refreshToken; - return; - } - - await SetTokenAsync(token); + await SetAccessTokenAsync(token); await SetRefreshTokenAsync(refreshToken); } public async Task SetTwoFactorTokenAsync(string token, string email) { - await _storageService.SaveAsync(string.Format(Keys_TwoFactorTokenFormat, email), token); + await _stateService.SetTwoFactorTokenAsync(token, email); } public async Task GetTwoFactorTokenAsync(string email) { - return await _storageService.GetAsync(string.Format(Keys_TwoFactorTokenFormat, email)); + return await _stateService.GetTwoFactorTokenAsync(email); } public async Task ClearTwoFactorTokenAsync(string email) { - await _storageService.RemoveAsync(string.Format(Keys_TwoFactorTokenFormat, email)); + await _stateService.SetTwoFactorTokenAsync(null, email); } - public async Task ClearTokenAsync() + public async Task ClearTokenAsync(string userId = null) { - _token = null; - _decodedToken = null; - _refreshToken = null; + ClearCache(); await Task.WhenAll( - _storageService.RemoveAsync(Keys_AccessToken), - _storageService.RemoveAsync(Keys_RefreshToken)); + _stateService.SetAccessTokenAsync(null, false, userId), + _stateService.SetRefreshTokenAsync(null, false, userId)); + } + + public void ClearCache() + { + _accessTokenForDecoding = null; + _decodedAccessToken = null; } public JObject DecodeToken() { - if (_decodedToken != null) + if (_decodedAccessToken != null) { - return _decodedToken; + return _decodedAccessToken; } - if (_token == null) + if (_accessTokenForDecoding == null) { - throw new InvalidOperationException("Token not found."); + throw new InvalidOperationException("Access token not found."); } - var parts = _token.Split('.'); + var parts = _accessTokenForDecoding.Split('.'); if (parts.Length != 3) { throw new InvalidOperationException("JWT must have 3 parts."); @@ -140,8 +112,8 @@ namespace Bit.Core.Services { throw new InvalidOperationException("Cannot decode the token."); } - _decodedToken = JObject.Parse(Encoding.UTF8.GetString(decodedBytes)); - return _decodedToken; + _decodedAccessToken = JObject.Parse(Encoding.UTF8.GetString(decodedBytes)); + return _decodedAccessToken; } public DateTime? GetTokenExpirationDate() @@ -231,8 +203,12 @@ namespace Bit.Core.Services return decoded["iss"].Value(); } - public bool GetIsExternal() + public async Task GetIsExternal() { + if (_accessTokenForDecoding == null) + { + await GetTokenAsync(); + } var decoded = DecodeToken(); if (decoded?["amr"] == null) { @@ -243,9 +219,9 @@ namespace Bit.Core.Services private async Task SkipTokenStorage() { - var timeout = await _storageService.GetAsync(Constants.VaultTimeoutKey); - var action = await _storageService.GetAsync(Constants.VaultTimeoutActionKey); - return timeout.HasValue && action == "logOut"; + var timeout = await _stateService.GetVaultTimeoutAsync(); + var action = await _stateService.GetVaultTimeoutActionAsync(); + return timeout.HasValue && action == VaultTimeoutAction.Logout; } } } diff --git a/src/Core/Services/TotpService.cs b/src/Core/Services/TotpService.cs index 0cf4dce15..07c64285a 100644 --- a/src/Core/Services/TotpService.cs +++ b/src/Core/Services/TotpService.cs @@ -10,14 +10,14 @@ namespace Bit.Core.Services { private const string SteamChars = "23456789BCDFGHJKMNPQRTVWXY"; - private readonly IStorageService _storageService; + private readonly IStateService _stateService; private readonly ICryptoFunctionService _cryptoFunctionService; public TotpService( - IStorageService storageService, + IStateService stateService, ICryptoFunctionService cryptoFunctionService) { - _storageService = storageService; + _stateService = stateService; _cryptoFunctionService = cryptoFunctionService; } @@ -135,7 +135,7 @@ namespace Bit.Core.Services public async Task IsAutoCopyEnabledAsync() { - var disabled = await _storageService.GetAsync(Constants.DisableAutoTotpCopyKey); + var disabled = await _stateService.GetDisableAutoTotpCopyAsync(); return !disabled.GetValueOrDefault(); } } diff --git a/src/Core/Services/UserService.cs b/src/Core/Services/UserService.cs deleted file mode 100644 index c71e52e07..000000000 --- a/src/Core/Services/UserService.cs +++ /dev/null @@ -1,221 +0,0 @@ -using Bit.Core.Abstractions; -using Bit.Core.Enums; -using Bit.Core.Models.Data; -using Bit.Core.Models.Domain; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Bit.Core.Services -{ - public class UserService : IUserService - { - private string _userId; - private string _email; - private string _stamp; - private KdfType? _kdf; - private int? _kdfIterations; - private bool? _emailVerified; - private bool? _forcePasswordReset; - - private const string Keys_UserId = "userId"; - private const string Keys_UserEmail = "userEmail"; - private const string Keys_Stamp = "securityStamp"; - private const string Keys_Kdf = "kdf"; - private const string Keys_KdfIterations = "kdfIterations"; - private const string Keys_OrganizationsFormat = "organizations_{0}"; - private const string Keys_EmailVerified = "emailVerified"; - private const string Keys_ForcePasswordReset = "forcePasswordReset"; - - private readonly IStorageService _storageService; - private readonly ITokenService _tokenService; - - public UserService(IStorageService storageService, ITokenService tokenService) - { - _storageService = storageService; - _tokenService = tokenService; - } - - public async Task SetInformationAsync(string userId, string email, KdfType kdf, int? kdfIterations) - { - _email = email; - _userId = userId; - _kdf = kdf; - _kdfIterations = kdfIterations; - await Task.WhenAll( - _storageService.SaveAsync(Keys_UserEmail, email), - _storageService.SaveAsync(Keys_UserId, userId), - _storageService.SaveAsync(Keys_Kdf, (int)kdf), - _storageService.SaveAsync(Keys_KdfIterations, kdfIterations)); - } - - public async Task SetSecurityStampAsync(string stamp) - { - _stamp = stamp; - await _storageService.SaveAsync(Keys_Stamp, stamp); - } - - public async Task SetEmailVerifiedAsync(bool emailVerified) - { - _emailVerified = emailVerified; - await _storageService.SaveAsync(Keys_EmailVerified, emailVerified); - } - - public async Task SetForcePasswordReset(bool forcePasswordReset) - { - _forcePasswordReset = forcePasswordReset; - await _storageService.SaveAsync(Keys_ForcePasswordReset, forcePasswordReset); - } - - public async Task GetUserIdAsync() - { - if (_userId == null) - { - _userId = await _storageService.GetAsync(Keys_UserId); - } - return _userId; - } - - public async Task GetEmailAsync() - { - if (_email == null) - { - _email = await _storageService.GetAsync(Keys_UserEmail); - } - return _email; - } - - public async Task GetSecurityStampAsync() - { - if (_stamp == null) - { - _stamp = await _storageService.GetAsync(Keys_Stamp); - } - return _stamp; - } - - public async Task GetEmailVerifiedAsync() - { - if (_emailVerified == null) - { - _emailVerified = await _storageService.GetAsync(Keys_EmailVerified); - } - return _emailVerified.GetValueOrDefault(); - } - - public async Task GetKdfAsync() - { - if (_kdf == null) - { - _kdf = (KdfType?)(await _storageService.GetAsync(Keys_Kdf)); - } - return _kdf; - } - - public async Task GetKdfIterationsAsync() - { - if (_kdfIterations == null) - { - _kdfIterations = await _storageService.GetAsync(Keys_KdfIterations); - } - return _kdfIterations; - } - - public async Task GetForcePasswordReset() - { - if (_forcePasswordReset == null) - { - _forcePasswordReset = await _storageService.GetAsync(Keys_ForcePasswordReset); - } - return _forcePasswordReset.GetValueOrDefault(); - } - - public async Task ClearAsync() - { - var userId = await GetUserIdAsync(); - await Task.WhenAll( - _storageService.RemoveAsync(Keys_UserId), - _storageService.RemoveAsync(Keys_UserEmail), - _storageService.RemoveAsync(Keys_Stamp), - _storageService.RemoveAsync(Keys_Kdf), - _storageService.RemoveAsync(Keys_KdfIterations), - _storageService.RemoveAsync(Keys_ForcePasswordReset), - ClearOrganizationsAsync(userId)); - _userId = _email = _stamp = null; - _kdf = null; - _kdfIterations = null; - } - - public async Task IsAuthenticatedAsync() - { - var token = await _tokenService.GetTokenAsync(); - if (token == null) - { - return false; - } - var userId = await GetUserIdAsync(); - return userId != null; - } - - public async Task CanAccessPremiumAsync() - { - var authed = await IsAuthenticatedAsync(); - if (!authed) - { - return false; - } - - var tokenPremium = _tokenService.GetPremium(); - if (tokenPremium) - { - return true; - } - var orgs = await GetAllOrganizationAsync(); - return orgs?.Any(o => o.UsersGetPremium && o.Enabled) ?? false; - } - - public async Task GetOrganizationAsync(string id) - { - var userId = await GetUserIdAsync(); - var organizations = await _storageService.GetAsync>( - string.Format(Keys_OrganizationsFormat, userId)); - if (organizations == null || !organizations.ContainsKey(id)) - { - return null; - } - return new Organization(organizations[id]); - } - - public async Task GetOrganizationByIdentifierAsync(string identifier) - { - var userId = await GetUserIdAsync(); - var organizations = await GetAllOrganizationAsync(); - - if (organizations == null || organizations.Count == 0) - { - return null; - } - - return organizations.FirstOrDefault(o => o.Identifier == identifier); - } - - public async Task> GetAllOrganizationAsync() - { - var userId = await GetUserIdAsync(); - var organizations = await _storageService.GetAsync>( - string.Format(Keys_OrganizationsFormat, userId)); - return organizations?.Select(o => new Organization(o.Value)).ToList() ?? new List(); - } - - public async Task ReplaceOrganizationsAsync(Dictionary organizations) - { - var userId = await GetUserIdAsync(); - await _storageService.SaveAsync(string.Format(Keys_OrganizationsFormat, userId), organizations); - } - - public async Task ClearOrganizationsAsync(string userId) - { - await _storageService.RemoveAsync(string.Format(Keys_OrganizationsFormat, userId)); - } - } -} diff --git a/src/Core/Services/VaultTimeoutService.cs b/src/Core/Services/VaultTimeoutService.cs index b6768e602..692cd3ccd 100644 --- a/src/Core/Services/VaultTimeoutService.cs +++ b/src/Core/Services/VaultTimeoutService.cs @@ -1,5 +1,4 @@ using Bit.Core.Abstractions; -using Bit.Core.Models.Domain; using System; using System.Linq; using System.Threading.Tasks; @@ -10,9 +9,8 @@ namespace Bit.Core.Services public class VaultTimeoutService : IVaultTimeoutService { private readonly ICryptoService _cryptoService; - private readonly IUserService _userService; + private readonly IStateService _stateService; private readonly IPlatformUtilsService _platformUtilsService; - private readonly IStorageService _storageService; private readonly IFolderService _folderService; private readonly ICipherService _cipherService; private readonly ICollectionService _collectionService; @@ -22,13 +20,12 @@ namespace Bit.Core.Services private readonly IPolicyService _policyService; private readonly IKeyConnectorService _keyConnectorService; private readonly Action _lockedCallback; - private readonly Func _loggedOutCallback; + private readonly Func, Task> _loggedOutCallback; public VaultTimeoutService( ICryptoService cryptoService, - IUserService userService, + IStateService stateService, IPlatformUtilsService platformUtilsService, - IStorageService storageService, IFolderService folderService, ICipherService cipherService, ICollectionService collectionService, @@ -38,12 +35,11 @@ namespace Bit.Core.Services IPolicyService policyService, IKeyConnectorService keyConnectorService, Action lockedCallback, - Func loggedOutCallback) + Func, Task> loggedOutCallback) { _cryptoService = cryptoService; - _userService = userService; + _stateService = stateService; _platformUtilsService = platformUtilsService; - _storageService = storageService; _folderService = folderService; _cipherService = cipherService; _collectionService = collectionService; @@ -56,23 +52,28 @@ namespace Bit.Core.Services _loggedOutCallback = loggedOutCallback; } - public EncString PinProtectedKey { get; set; } = null; - public bool BiometricLocked { get; set; } = true; public long? DelayLockAndLogoutMs { get; set; } - public async Task IsLockedAsync() + public async Task IsLockedAsync(string userId = null) { - var hasKey = await _cryptoService.HasKeyAsync(); + var hasKey = await _cryptoService.HasKeyAsync(userId); if (hasKey) { - var biometricSet = await IsBiometricLockSetAsync(); - if (biometricSet && BiometricLocked) + var biometricSet = await IsBiometricLockSetAsync(userId); + if (biometricSet && _stateService.BiometricLocked) { return true; } } return !hasKey; } + + public async Task IsLoggedOutByTimeoutAsync(string userId = null) + { + var authed = await _stateService.IsAuthenticatedAsync(userId); + var email = await _stateService.GetEmailAsync(userId); + return !authed && !string.IsNullOrWhiteSpace(email); + } public async Task CheckVaultTimeoutAsync() { @@ -80,79 +81,85 @@ namespace Bit.Core.Services { return; } - var authed = await _userService.IsAuthenticatedAsync(); + + if (await ShouldTimeoutAsync()) + { + await ExecuteTimeoutActionAsync(); + } + } + + public async Task ShouldTimeoutAsync(string userId = null) + { + var authed = await _stateService.IsAuthenticatedAsync(userId); if (!authed) { - return; + return false; } - if (await IsLockedAsync()) + var vaultTimeoutAction = await _stateService.GetVaultTimeoutActionAsync(userId); + if (vaultTimeoutAction == VaultTimeoutAction.Lock && await IsLockedAsync(userId)) { - return; + return false; } - var vaultTimeoutMinutes = await GetVaultTimeout(); + var vaultTimeoutMinutes = await GetVaultTimeout(userId); if (vaultTimeoutMinutes < 0 || vaultTimeoutMinutes == null) { - return; + return false; } if (vaultTimeoutMinutes == 0 && !DelayLockAndLogoutMs.HasValue) { - await LockOrLogout(); + return true; } - var lastActiveTime = await _storageService.GetAsync(Constants.LastActiveTimeKey); + var lastActiveTime = await _stateService.GetLastActiveTimeAsync(userId); if (lastActiveTime == null) { - return; + return false; } var diffMs = _platformUtilsService.GetActiveTime() - lastActiveTime; if (DelayLockAndLogoutMs.HasValue && diffMs < DelayLockAndLogoutMs) { - return; + return false; } var vaultTimeoutMs = vaultTimeoutMinutes * 60000; - if (diffMs >= vaultTimeoutMs) - { - await LockOrLogout(); - } - + return diffMs >= vaultTimeoutMs; } - private async Task LockOrLogout() + public async Task ExecuteTimeoutActionAsync(string userId = null) { - // Pivot based on saved action - var action = await _storageService.GetAsync(Constants.VaultTimeoutActionKey); - if (action == "logOut") + var action = await _stateService.GetVaultTimeoutActionAsync(userId); + if (action == VaultTimeoutAction.Logout) { - await LogOutAsync(); + await LogOutAsync(false, userId); } else { - await LockAsync(true); + await LockAsync(true, false, userId); } } - public async Task LockAsync(bool allowSoftLock = false, bool userInitiated = false) + public async Task LockAsync(bool allowSoftLock = false, bool userInitiated = false, string userId = null) { - var authed = await _userService.IsAuthenticatedAsync(); + var authed = await _stateService.IsAuthenticatedAsync(userId); if (!authed) { return; } if (await _keyConnectorService.GetUsesKeyConnector()) { - var pinSet = await IsPinLockSetAsync(); - var pinLock = (pinSet.Item1 && PinProtectedKey != null) || pinSet.Item2; + var (isPinProtected, isPinProtectedWithKey) = await IsPinLockSetAsync(userId); + var pinLock = (isPinProtected && await _stateService.GetPinProtectedKeyAsync(userId) != null) || + isPinProtectedWithKey; if (!pinLock && !await IsBiometricLockSetAsync()) { - await LogOutAsync(); + await LogOutAsync(userInitiated, userId); return; } } if (allowSoftLock) { - BiometricLocked = await IsBiometricLockSetAsync(); - if (BiometricLocked) + _stateService.BiometricLocked = await IsBiometricLockSetAsync(); + if (_stateService.BiometricLocked) { _messagingService.Send("locked", userInitiated); _lockedCallback?.Invoke(userInitiated); @@ -160,10 +167,10 @@ namespace Bit.Core.Services } } await Task.WhenAll( - _cryptoService.ClearKeyAsync(), - _cryptoService.ClearOrgKeysAsync(true), - _cryptoService.ClearKeyPairAsync(true), - _cryptoService.ClearEncKeyAsync(true)); + _cryptoService.ClearKeyAsync(userId), + _cryptoService.ClearOrgKeysAsync(true, userId), + _cryptoService.ClearKeyPairAsync(true, userId), + _cryptoService.ClearEncKeyAsync(true, userId)); _folderService.ClearCache(); await _cipherService.ClearCacheAsync(); @@ -173,46 +180,46 @@ namespace Bit.Core.Services _lockedCallback?.Invoke(userInitiated); } - public async Task LogOutAsync() + public async Task LogOutAsync(bool userInitiated = true, string userId = null) { if(_loggedOutCallback != null) { - await _loggedOutCallback.Invoke(false); + await _loggedOutCallback.Invoke(new Tuple(userId, userInitiated, false)); } } - public async Task SetVaultTimeoutOptionsAsync(int? timeout, string action) + public async Task SetVaultTimeoutOptionsAsync(int? timeout, VaultTimeoutAction? action) { - await _storageService.SaveAsync(Constants.VaultTimeoutKey, timeout); - await _storageService.SaveAsync(Constants.VaultTimeoutActionKey, action); + await _stateService.SetVaultTimeoutAsync(timeout); + await _stateService.SetVaultTimeoutActionAsync(action); await _cryptoService.ToggleKeyAsync(); await _tokenService.ToggleTokensAsync(); } - public async Task> IsPinLockSetAsync() + public async Task> IsPinLockSetAsync(string userId = null) { - var protectedPin = await _storageService.GetAsync(Constants.ProtectedPin); - var pinProtectedKey = await _storageService.GetAsync(Constants.PinProtectedKey); + var protectedPin = await _stateService.GetProtectedPinAsync(userId); + var pinProtectedKey = await _stateService.GetPinProtectedAsync(userId); return new Tuple(protectedPin != null, pinProtectedKey != null); } - public async Task IsBiometricLockSetAsync() + public async Task IsBiometricLockSetAsync(string userId = null) { - var biometricLock = await _storageService.GetAsync(Constants.BiometricUnlockKey); + var biometricLock = await _stateService.GetBiometricUnlockAsync(userId); return biometricLock.GetValueOrDefault(); } - public async Task ClearAsync() + public async Task ClearAsync(string userId = null) { - PinProtectedKey = null; - await _storageService.RemoveAsync(Constants.ProtectedPin); + await _stateService.SetPinProtectedKeyAsync(null, userId); + await _stateService.SetProtectedPinAsync(null, userId); } - public async Task GetVaultTimeout() { - var vaultTimeout = await _storageService.GetAsync(Constants.VaultTimeoutKey); + public async Task GetVaultTimeout(string userId = null) { + var vaultTimeout = await _stateService.GetVaultTimeoutAsync(userId); - if (await _policyService.PolicyAppliesToUser(PolicyType.MaximumVaultTimeout)) { - var policy = (await _policyService.GetAll(PolicyType.MaximumVaultTimeout)).First(); + if (await _policyService.PolicyAppliesToUser(PolicyType.MaximumVaultTimeout, null, userId)) { + var policy = (await _policyService.GetAll(PolicyType.MaximumVaultTimeout, userId)).First(); // Remove negative values, and ensure it's smaller than maximum allowed value according to policy var policyTimeout = _policyService.GetPolicyInt(policy, "minutes"); if (!policyTimeout.HasValue) @@ -228,7 +235,7 @@ namespace Bit.Core.Services // We really shouldn't need to set the value here, but multiple services relies on this value being correct. if (vaultTimeout != timeout) { - await _storageService.SaveAsync(Constants.VaultTimeoutKey, timeout); + await _stateService.SetVaultTimeoutAsync(timeout, userId); } return timeout; diff --git a/src/Core/Utilities/ServiceContainer.cs b/src/Core/Utilities/ServiceContainer.cs index dcafe40c5..2f30b1795 100644 --- a/src/Core/Utilities/ServiceContainer.cs +++ b/src/Core/Utilities/ServiceContainer.cs @@ -22,64 +22,65 @@ namespace Bit.Core.Utilities var platformUtilsService = Resolve("platformUtilsService"); var storageService = Resolve("storageService"); - var secureStorageService = Resolve("secureStorageService"); + var stateService = Resolve("stateService"); var i18nService = Resolve("i18nService"); var messagingService = Resolve("messagingService"); var cryptoFunctionService = Resolve("cryptoFunctionService"); var cryptoService = Resolve("cryptoService"); SearchService searchService = null; - var stateService = new StateService(); - var tokenService = new TokenService(storageService); - var apiService = new ApiService(tokenService, platformUtilsService, (bool expired) => + var tokenService = new TokenService(stateService); + var apiService = new ApiService(tokenService, platformUtilsService, (extras) => { - messagingService.Send("logout", expired); + messagingService.Send("logout", extras); return Task.FromResult(0); }, customUserAgent); var appIdService = new AppIdService(storageService); - var userService = new UserService(storageService, tokenService); - var settingsService = new SettingsService(userService, storageService); + var organizationService = new OrganizationService(stateService); + var settingsService = new SettingsService(stateService); var fileUploadService = new FileUploadService(apiService); - var cipherService = new CipherService(cryptoService, userService, settingsService, apiService, fileUploadService, - storageService, i18nService, () => searchService, clearCipherCacheKey, allClearCipherCacheKeys); - var folderService = new FolderService(cryptoService, userService, apiService, storageService, - i18nService, cipherService); - var collectionService = new CollectionService(cryptoService, userService, storageService, i18nService); - var sendService = new SendService(cryptoService, userService, apiService, fileUploadService, storageService, - i18nService, cryptoFunctionService); + var cipherService = new CipherService(cryptoService, stateService, settingsService, apiService, + fileUploadService, storageService, i18nService, () => searchService, clearCipherCacheKey, + allClearCipherCacheKeys); + var folderService = new FolderService(cryptoService, stateService, apiService, i18nService, cipherService); + var collectionService = new CollectionService(cryptoService, stateService, i18nService); + var sendService = new SendService(cryptoService, stateService, apiService, fileUploadService, i18nService, + cryptoFunctionService); searchService = new SearchService(cipherService, sendService); - var policyService = new PolicyService(storageService, userService); - var keyConnectorService = new KeyConnectorService(userService, cryptoService, storageService, tokenService, apiService); - var vaultTimeoutService = new VaultTimeoutService(cryptoService, userService, platformUtilsService, - storageService, folderService, cipherService, collectionService, searchService, messagingService, tokenService, - policyService, keyConnectorService, null, (expired) => + var policyService = new PolicyService(stateService, organizationService); + var keyConnectorService = new KeyConnectorService(stateService, cryptoService, tokenService, apiService, + organizationService); + var vaultTimeoutService = new VaultTimeoutService(cryptoService, stateService, platformUtilsService, + folderService, cipherService, collectionService, searchService, messagingService, tokenService, + policyService, keyConnectorService, null, (extras) => { - messagingService.Send("logout", expired); + messagingService.Send("logout", extras); return Task.FromResult(0); }); - var syncService = new SyncService(userService, apiService, settingsService, folderService, - cipherService, cryptoService, collectionService, storageService, messagingService, policyService, sendService, - keyConnectorService, (bool expired) => + var syncService = new SyncService(stateService, apiService, settingsService, folderService, cipherService, + cryptoService, collectionService, organizationService, messagingService, policyService, sendService, + keyConnectorService, (extras) => { - messagingService.Send("logout", expired); + messagingService.Send("logout", extras); return Task.FromResult(0); }); - var passwordGenerationService = new PasswordGenerationService(cryptoService, storageService, + var passwordGenerationService = new PasswordGenerationService(cryptoService, stateService, cryptoFunctionService, policyService); - var totpService = new TotpService(storageService, cryptoFunctionService); - var authService = new AuthService(cryptoService, cryptoFunctionService, apiService, userService, tokenService, appIdService, - i18nService, platformUtilsService, messagingService, vaultTimeoutService, keyConnectorService); + var totpService = new TotpService(stateService, cryptoFunctionService); + var authService = new AuthService(cryptoService, cryptoFunctionService, apiService, stateService, + tokenService, appIdService, i18nService, platformUtilsService, messagingService, vaultTimeoutService, + keyConnectorService); var exportService = new ExportService(folderService, cipherService, cryptoService); var auditService = new AuditService(cryptoFunctionService, apiService); - var environmentService = new EnvironmentService(apiService, storageService); - var eventService = new EventService(storageService, apiService, userService, cipherService); - var userVerificationService = new UserVerificationService(apiService, platformUtilsService, i18nService, cryptoService); + var environmentService = new EnvironmentService(apiService, stateService); + var eventService = new EventService(apiService, stateService, organizationService, cipherService); + var userVerificationService = new UserVerificationService(apiService, platformUtilsService, i18nService, + cryptoService); - Register("stateService", stateService); Register("tokenService", tokenService); Register("apiService", apiService); Register("appIdService", appIdService); - Register("userService", userService); + Register("organizationService", organizationService); Register("settingsService", settingsService); Register("cipherService", cipherService); Register("folderService", folderService); diff --git a/src/iOS.Autofill/CredentialProviderViewController.cs b/src/iOS.Autofill/CredentialProviderViewController.cs index 2a9fed346..e0e529bdb 100644 --- a/src/iOS.Autofill/CredentialProviderViewController.cs +++ b/src/iOS.Autofill/CredentialProviderViewController.cs @@ -79,9 +79,9 @@ namespace Bit.iOS.Autofill public override async void ProvideCredentialWithoutUserInteraction(ASPasswordCredentialIdentity credentialIdentity) { InitAppIfNeeded(); - var storageService = ServiceContainer.Resolve("storageService"); - await storageService.SaveAsync(Bit.Core.Constants.PasswordRepromptAutofillKey, false); - await storageService.SaveAsync(Bit.Core.Constants.PasswordVerifiedAutofillKey, false); + var stateService = ServiceContainer.Resolve("stateService"); + await stateService.SetPasswordRepromptAutofillAsync(false); + await stateService.SetPasswordVerifiedAutofillAsync(false); if (!await IsAuthed() || await IsLocked()) { var err = new NSError(new NSString("ASExtensionErrorDomain"), @@ -230,7 +230,7 @@ namespace Bit.iOS.Autofill return; } - var storageService = ServiceContainer.Resolve("storageService"); + var stateService = ServiceContainer.Resolve("stateService"); var decCipher = await cipher.DecryptAsync(); if (decCipher.Reprompt != Bit.Core.Enums.CipherRepromptType.None) { @@ -238,13 +238,13 @@ namespace Bit.iOS.Autofill // already verified the password. if (!userInteraction) { - await storageService.SaveAsync(Bit.Core.Constants.PasswordRepromptAutofillKey, true); + await stateService.SetPasswordRepromptAutofillAsync(true); var err = new NSError(new NSString("ASExtensionErrorDomain"), Convert.ToInt32(ASExtensionErrorCode.UserInteractionRequired), null); ExtensionContext?.CancelRequest(err); return; } - else if (!await storageService.GetAsync(Bit.Core.Constants.PasswordVerifiedAutofillKey)) + else if (!await stateService.GetPasswordVerifiedAutofillAsync()) { // Add a timeout to resolve keyboard not always showing up. await Task.Delay(250); @@ -259,11 +259,10 @@ namespace Bit.iOS.Autofill } } string totpCode = null; - var disableTotpCopy = await storageService.GetAsync(Bit.Core.Constants.DisableAutoTotpCopyKey); + var disableTotpCopy = await stateService.GetDisableAutoTotpCopyAsync(); if (!disableTotpCopy.GetValueOrDefault(false)) { - var userService = ServiceContainer.Resolve("userService"); - var canAccessPremiumAsync = await userService.CanAccessPremiumAsync(); + var canAccessPremiumAsync = await stateService.CanAccessPremiumAsync(); if (!string.IsNullOrWhiteSpace(decCipher.Login.Totp) && (canAccessPremiumAsync || cipher.OrganizationUseTotp)) { @@ -277,8 +276,8 @@ namespace Bit.iOS.Autofill private async void CheckLock(Action notLockedAction) { - var storageService = ServiceContainer.Resolve("storageService"); - if (await IsLocked() || await storageService.GetAsync(Bit.Core.Constants.PasswordRepromptAutofillKey)) + var stateService = ServiceContainer.Resolve("stateService"); + if (await IsLocked() || await stateService.GetPasswordRepromptAutofillAsync()) { PerformSegue("lockPasswordSegue", this); } @@ -296,8 +295,8 @@ namespace Bit.iOS.Autofill private Task IsAuthed() { - var userService = ServiceContainer.Resolve("userService"); - return userService.IsAuthenticatedAsync(); + var stateService = ServiceContainer.Resolve("stateService"); + return stateService.IsAuthenticatedAsync(); } private void LogoutIfAuthed() @@ -306,7 +305,8 @@ namespace Bit.iOS.Autofill { if (await IsAuthed()) { - await AppHelpers.LogOutAsync(); + var stateService = ServiceContainer.Resolve("stateService"); + await AppHelpers.LogOutAsync(await stateService.GetActiveUserIdAsync()); var deviceActionService = ServiceContainer.Resolve("deviceActionService"); if (deviceActionService.SystemMajorVersion() >= 12) { @@ -337,7 +337,7 @@ namespace Bit.iOS.Autofill } iOSCoreHelpers.Bootstrap(); var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(false, app.Resources); + ThemeManager.SetTheme(app.Resources); iOSCoreHelpers.AppearanceAdjustments(); _nfcDelegate = new Core.NFCReaderDelegate((success, message) => messagingService.Send("gotYubiKeyOTP", message)); @@ -356,7 +356,7 @@ namespace Bit.iOS.Autofill { var homePage = new HomePage(); var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(false, app.Resources); + ThemeManager.SetTheme(app.Resources); ThemeManager.ApplyResourcesToPage(homePage); if (homePage.BindingContext is HomeViewModel vm) { @@ -379,7 +379,7 @@ namespace Bit.iOS.Autofill { var environmentPage = new EnvironmentPage(); var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(false, app.Resources); + ThemeManager.SetTheme(app.Resources); ThemeManager.ApplyResourcesToPage(environmentPage); if (environmentPage.BindingContext is EnvironmentPageViewModel vm) { @@ -397,7 +397,7 @@ namespace Bit.iOS.Autofill { var registerPage = new RegisterPage(null); var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(false, app.Resources); + ThemeManager.SetTheme(app.Resources); ThemeManager.ApplyResourcesToPage(registerPage); if (registerPage.BindingContext is RegisterPageViewModel vm) { @@ -415,7 +415,7 @@ namespace Bit.iOS.Autofill { var loginPage = new LoginPage(email); var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(false, app.Resources); + ThemeManager.SetTheme(app.Resources); ThemeManager.ApplyResourcesToPage(loginPage); if (loginPage.BindingContext is LoginPageViewModel vm) { @@ -437,7 +437,7 @@ namespace Bit.iOS.Autofill { var loginPage = new LoginSsoPage(); var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(false, app.Resources); + ThemeManager.SetTheme(app.Resources); ThemeManager.ApplyResourcesToPage(loginPage); if (loginPage.BindingContext is LoginSsoPageViewModel vm) { @@ -460,7 +460,7 @@ namespace Bit.iOS.Autofill { var twoFactorPage = new TwoFactorPage(authingWithSso); var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(false, app.Resources); + ThemeManager.SetTheme(app.Resources); ThemeManager.ApplyResourcesToPage(twoFactorPage); if (twoFactorPage.BindingContext is TwoFactorPageViewModel vm) { @@ -487,7 +487,7 @@ namespace Bit.iOS.Autofill { var setPasswordPage = new SetPasswordPage(); var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(false, app.Resources); + ThemeManager.SetTheme(app.Resources); ThemeManager.ApplyResourcesToPage(setPasswordPage); if (setPasswordPage.BindingContext is SetPasswordPageViewModel vm) { @@ -506,7 +506,7 @@ namespace Bit.iOS.Autofill { var updateTempPasswordPage = new UpdateTempPasswordPage(); var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(false, app.Resources); + ThemeManager.SetTheme(app.Resources); ThemeManager.ApplyResourcesToPage(updateTempPasswordPage); if (updateTempPasswordPage.BindingContext is UpdateTempPasswordPageViewModel vm) { diff --git a/src/iOS.Autofill/Utilities/AutofillHelpers.cs b/src/iOS.Autofill/Utilities/AutofillHelpers.cs index fa1895968..2f274b913 100644 --- a/src/iOS.Autofill/Utilities/AutofillHelpers.cs +++ b/src/iOS.Autofill/Utilities/AutofillHelpers.cs @@ -41,12 +41,11 @@ namespace Bit.iOS.Autofill.Utilities if (!string.IsNullOrWhiteSpace(item.Username) && !string.IsNullOrWhiteSpace(item.Password)) { string totp = null; - var storageService = ServiceContainer.Resolve("storageService"); - var disableTotpCopy = await storageService.GetAsync(Bit.Core.Constants.DisableAutoTotpCopyKey); + var stateService = ServiceContainer.Resolve("stateService"); + var disableTotpCopy = await stateService.GetDisableAutoTotpCopyAsync(); if (!disableTotpCopy.GetValueOrDefault(false)) { - var userService = ServiceContainer.Resolve("userService"); - var canAccessPremiumAsync = await userService.CanAccessPremiumAsync(); + var canAccessPremiumAsync = await stateService.CanAccessPremiumAsync(); if (!string.IsNullOrWhiteSpace(item.Totp) && (canAccessPremiumAsync || item.CipherView.OrganizationUseTotp)) { @@ -118,4 +117,4 @@ namespace Bit.iOS.Autofill.Utilities } } } -} \ No newline at end of file +} diff --git a/src/iOS.Core/Controllers/LockPasswordViewController.cs b/src/iOS.Core/Controllers/LockPasswordViewController.cs index 2a238111e..c2f57fd41 100644 --- a/src/iOS.Core/Controllers/LockPasswordViewController.cs +++ b/src/iOS.Core/Controllers/LockPasswordViewController.cs @@ -23,13 +23,13 @@ namespace Bit.iOS.Core.Controllers private IVaultTimeoutService _vaultTimeoutService; private ICryptoService _cryptoService; private IDeviceActionService _deviceActionService; - private IUserService _userService; - private IStorageService _storageService; + private IStateService _stateService; private IStorageService _secureStorageService; private IPlatformUtilsService _platformUtilsService; private IBiometricService _biometricService; private IKeyConnectorService _keyConnectorService; - private Tuple _pinSet; + private bool _isPinProtected; + private bool _isPinProtectedWithKey; private bool _pinLock; private bool _biometricLock; private bool _biometricIntegrityValid = true; @@ -84,8 +84,7 @@ namespace Bit.iOS.Core.Controllers _vaultTimeoutService = ServiceContainer.Resolve("vaultTimeoutService"); _cryptoService = ServiceContainer.Resolve("cryptoService"); _deviceActionService = ServiceContainer.Resolve("deviceActionService"); - _userService = ServiceContainer.Resolve("userService"); - _storageService = ServiceContainer.Resolve("storageService"); + _stateService = ServiceContainer.Resolve("stateService"); _secureStorageService = ServiceContainer.Resolve("secureStorageService"); _platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); _biometricService = ServiceContainer.Resolve("biometricService"); @@ -93,21 +92,22 @@ namespace Bit.iOS.Core.Controllers // We re-use the lock screen for autofill extension to verify master password // when trying to access protected items. - if (autofillExtension && await _storageService.GetAsync(Bit.Core.Constants.PasswordRepromptAutofillKey)) + if (autofillExtension && await _stateService.GetPasswordRepromptAutofillAsync()) { _passwordReprompt = true; - _pinSet = Tuple.Create(false, false); + _isPinProtected = false; + _isPinProtectedWithKey = false; _pinLock = false; _biometricLock = false; } else { - _pinSet = _vaultTimeoutService.IsPinLockSetAsync().GetAwaiter().GetResult(); - _pinLock = (_pinSet.Item1 && _vaultTimeoutService.PinProtectedKey != null) || _pinSet.Item2; - _biometricLock = _vaultTimeoutService.IsBiometricLockSetAsync().GetAwaiter().GetResult() && - _cryptoService.HasKeyAsync().GetAwaiter().GetResult(); - _biometricIntegrityValid = _biometricService.ValidateIntegrityAsync(BiometricIntegrityKey).GetAwaiter() - .GetResult(); + (_isPinProtected, _isPinProtectedWithKey) = await _vaultTimeoutService.IsPinLockSetAsync(); + _pinLock = (_isPinProtected && await _stateService.GetPinProtectedKeyAsync() != null) || + _isPinProtectedWithKey; + _biometricLock = await _vaultTimeoutService.IsBiometricLockSetAsync() && + await _cryptoService.HasKeyAsync(); + _biometricIntegrityValid = await _biometricService.ValidateIntegrityAsync(BiometricIntegrityKey); _usesKeyConnector = await _keyConnectorService.GetUsesKeyConnector(); _biometricUnlockOnly = _usesKeyConnector && _biometricLock && !_pinLock; } @@ -213,9 +213,9 @@ namespace Bit.iOS.Core.Controllers return; } - var email = await _userService.GetEmailAsync(); - var kdf = await _userService.GetKdfAsync(); - var kdfIterations = await _userService.GetKdfIterationsAsync(); + var email = await _stateService.GetEmailAsync(); + var kdf = await _stateService.GetKdfTypeAsync(); + var kdfIterations = await _stateService.GetKdfIterationsAsync(); var inputtedValue = MasterPasswordCell.TextField.Text; if (_pinLock) @@ -223,13 +223,13 @@ namespace Bit.iOS.Core.Controllers var failed = true; try { - if (_pinSet.Item1) + if (_isPinProtected) { var key = await _cryptoService.MakeKeyFromPinAsync(inputtedValue, email, kdf.GetValueOrDefault(KdfType.PBKDF2_SHA256), kdfIterations.GetValueOrDefault(5000), - _vaultTimeoutService.PinProtectedKey); + await _stateService.GetPinProtectedKeyAsync()); var encKey = await _cryptoService.GetEncKeyAsync(key); - var protectedPin = await _storageService.GetAsync(Bit.Core.Constants.ProtectedPin); + var protectedPin = await _stateService.GetProtectedPinAsync(); var decPin = await _cryptoService.DecryptToUtf8Async(new EncString(protectedPin), encKey); failed = decPin != inputtedValue; if (!failed) @@ -280,14 +280,14 @@ namespace Bit.iOS.Core.Controllers var passwordValid = await _cryptoService.CompareAndUpdateKeyHashAsync(inputtedValue, key2); if (passwordValid) { - if (_pinSet.Item1) + if (_isPinProtected) { - var protectedPin = await _storageService.GetAsync(Bit.Core.Constants.ProtectedPin); + var protectedPin = await _stateService.GetProtectedPinAsync(); var encKey = await _cryptoService.GetEncKeyAsync(key2); var decPin = await _cryptoService.DecryptToUtf8Async(new EncString(protectedPin), encKey); var pinKey = await _cryptoService.MakePinKeyAysnc(decPin, email, kdf.GetValueOrDefault(KdfType.PBKDF2_SHA256), kdfIterations.GetValueOrDefault(5000)); - _vaultTimeoutService.PinProtectedKey = await _cryptoService.EncryptAsync(key2.Key, pinKey); + await _stateService.SetPinProtectedKeyAsync(await _cryptoService.EncryptAsync(key2.Key, pinKey)); } await AppHelpers.ResetInvalidUnlockAttemptsAsync(); await SetKeyAndContinueAsync(key2, true); @@ -314,7 +314,7 @@ namespace Bit.iOS.Core.Controllers var success = await _platformUtilsService.AuthenticateBiometricAsync(null, _pinLock ? AppResources.PIN : AppResources.MasterPassword, () => MasterPasswordCell.TextField.BecomeFirstResponder()); - _vaultTimeoutService.BiometricLocked = !success; + _stateService.BiometricLocked = !success; if (success) { DoContinue(); @@ -325,7 +325,7 @@ namespace Bit.iOS.Core.Controllers { var loginPage = new LoginSsoPage(); var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(false, app.Resources); + ThemeManager.SetTheme(app.Resources); ThemeManager.ApplyResourcesToPage(loginPage); if (loginPage.BindingContext is LoginSsoPageViewModel vm) { @@ -353,10 +353,10 @@ namespace Bit.iOS.Core.Controllers { if (masterPassword) { - await _storageService.SaveAsync(Bit.Core.Constants.PasswordVerifiedAutofillKey, true); + await _stateService.SetPasswordVerifiedAutofillAsync(true); } await EnableBiometricsIfNeeded(); - _vaultTimeoutService.BiometricLocked = false; + _stateService.BiometricLocked = false; MasterPasswordCell.TextField.ResignFirstResponder(); Success(); } @@ -385,7 +385,7 @@ namespace Bit.iOS.Core.Controllers private async Task LogOutAsync() { - await AppHelpers.LogOutAsync(); + await AppHelpers.LogOutAsync(await _stateService.GetActiveUserIdAsync()); var authService = ServiceContainer.Resolve("authService"); authService.LogOut(() => { diff --git a/src/iOS.Core/Effects/ScrollViewContentInsetAdjustmentBehaviorEffect.cs b/src/iOS.Core/Effects/ScrollViewContentInsetAdjustmentBehaviorEffect.cs new file mode 100644 index 000000000..17450523c --- /dev/null +++ b/src/iOS.Core/Effects/ScrollViewContentInsetAdjustmentBehaviorEffect.cs @@ -0,0 +1,37 @@ +using Bit.iOS.Core.Effects; +using UIKit; +using Xamarin.Forms; +using Xamarin.Forms.Platform.iOS; + +[assembly: ExportEffect(typeof(ScrollViewContentInsetAdjustmentBehaviorEffect), nameof(ScrollViewContentInsetAdjustmentBehaviorEffect))] +namespace Bit.iOS.Core.Effects +{ + public class ScrollViewContentInsetAdjustmentBehaviorEffect : PlatformEffect + { + protected override void OnAttached() + { + if (Element != null && Control is UIScrollView scrollView) + { + switch (App.Effects.ScrollViewContentInsetAdjustmentBehaviorEffect.GetContentInsetAdjustmentBehavior(Element)) + { + case App.Effects.ScrollContentInsetAdjustmentBehavior.Automatic: + scrollView.ContentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentBehavior.Automatic; + break; + case App.Effects.ScrollContentInsetAdjustmentBehavior.ScrollableAxes: + scrollView.ContentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentBehavior.ScrollableAxes; + break; + case App.Effects.ScrollContentInsetAdjustmentBehavior.Never: + scrollView.ContentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentBehavior.Never; + break; + case App.Effects.ScrollContentInsetAdjustmentBehavior.Always: + scrollView.ContentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentBehavior.Always; + break; + } + } + } + + protected override void OnDetached() + { + } + } +} diff --git a/src/iOS.Core/Renderers/CustomNavigationRenderer.cs b/src/iOS.Core/Renderers/CustomNavigationRenderer.cs new file mode 100644 index 000000000..4242ca882 --- /dev/null +++ b/src/iOS.Core/Renderers/CustomNavigationRenderer.cs @@ -0,0 +1,87 @@ +using System; +using System.ComponentModel; +using System.Linq; +using Bit.App.Controls; +using Bit.iOS.Core.Renderers; +using CoreFoundation; +using UIKit; +using Xamarin.Forms; +using Xamarin.Forms.Platform.iOS; + +[assembly: ExportRenderer(typeof(NavigationPage), typeof(CustomNavigationRenderer))] +namespace Bit.iOS.Core.Renderers +{ + public class CustomNavigationRenderer : NavigationRenderer + { + public override void PushViewController(UIViewController viewController, bool animated) + { + base.PushViewController(viewController, animated); + + var currentPage = (Element as NavigationPage)?.CurrentPage; + if (currentPage == null) + { + return; + } + var toolbarItems = currentPage.ToolbarItems; + if (!toolbarItems.Any()) + { + return; + } + + // In order to get the correct index we need to do the same as XF and reverse the toolbar items list + // https://github.com/xamarin/Xamarin.Forms/blob/8f765bd87a2968bef9c86122d88c9c47be9196d2/Xamarin.Forms.Platform.iOS/Renderers/NavigationRenderer.cs#L1432 + toolbarItems = toolbarItems.Where(t => t.Order != ToolbarItemOrder.Secondary) + .Reverse() + .ToList(); + + var uiBarButtonItems = TopViewController.NavigationItem.RightBarButtonItems; + if (uiBarButtonItems == null) + { + return; + } + + foreach (ExtendedToolbarItem toolbarItem in toolbarItems.Where(t => t is ExtendedToolbarItem eti + && + eti.UseOriginalImage)) + { + var index = toolbarItems.IndexOf(toolbarItem); + if (index < 0 || index >= uiBarButtonItems.Length) + { + continue; + } + + // HACK: this is awful but I can't find another way to properly prevent memory leaks from + // subscribing on the PropertyChanged event; there are several private places where Xamarin Forms + // disposes objects that are not accessible from here so I think this should cover the (un)subscription + // but we need to remember to call the internal methods of ExtendedToolbarItem on the lifecycle of the Page + toolbarItem.OnAppearingAction = () => toolbarItem.PropertyChanged += ToolbarItem_PropertyChanged; + toolbarItem.OnDisappearingAction = () => toolbarItem.PropertyChanged -= ToolbarItem_PropertyChanged; + + // HACK: XF PimaryToolbarItem is sealed so we can't override it, and also it doesn't provide any + // direct way to replace it with our custom one (we can but we need to rewrite several parts of the NavigationRenderer) + // So I think this is the easiest soolution for now to set UIImageRenderingMode.AlwaysOriginal + // on the toolbar item image + void ToolbarItem_PropertyChanged(object s, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(ExtendedToolbarItem.IconImageSource)) + { + var uiBarButtonItem = uiBarButtonItems[index]; + + DispatchQueue.MainQueue.DispatchAsync(() => + { + try + { + uiBarButtonItem.Image = uiBarButtonItem.Image.ImageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal); + } + catch (ObjectDisposedException) + { + // Do nothing, we can't access the proper place to properly dispose this, so here + // we can just catch and ignore the exception. This should only happen when logging out a user. + } + }); + } + }; + } + } + } +} diff --git a/src/iOS.Core/Renderers/CustomTabbedRenderer.cs b/src/iOS.Core/Renderers/CustomTabbedRenderer.cs index c17d9d656..a8904ef6b 100644 --- a/src/iOS.Core/Renderers/CustomTabbedRenderer.cs +++ b/src/iOS.Core/Renderers/CustomTabbedRenderer.cs @@ -1,4 +1,6 @@ -using Bit.App.Abstractions; +using System; +using Bit.App.Abstractions; +using Bit.App.Pages; using Bit.Core.Abstractions; using Bit.Core.Utilities; using Bit.iOS.Core.Renderers; @@ -14,6 +16,7 @@ namespace Bit.iOS.Core.Renderers public class CustomTabbedRenderer : TabbedRenderer { private IBroadcasterService _broadcasterService; + private UITabBarItem _previousSelectedItem; public CustomTabbedRenderer() { @@ -39,6 +42,25 @@ namespace Bit.iOS.Core.Renderers UpdateTabBarAppearance(); } + public override void ViewDidAppear(bool animated) + { + base.ViewDidAppear(animated); + + if (SelectedIndex < TabBar.Items.Length) + { + _previousSelectedItem = TabBar.Items[SelectedIndex]; + } + } + + public override void ItemSelected(UITabBar tabbar, UITabBarItem item) + { + if (_previousSelectedItem == item && Element is TabsPage tabsPage) + { + tabsPage.OnPageReselected(); + } + _previousSelectedItem = item; + } + protected override void Dispose(bool disposing) { if (disposing) diff --git a/src/iOS.Core/Services/ClipboardService.cs b/src/iOS.Core/Services/ClipboardService.cs index 6014da332..67f7743ac 100644 --- a/src/iOS.Core/Services/ClipboardService.cs +++ b/src/iOS.Core/Services/ClipboardService.cs @@ -9,11 +9,11 @@ namespace Bit.iOS.Core.Services { public class ClipboardService : IClipboardService { - private readonly IStorageService _storageService; + private readonly IStateService _stateService; - public ClipboardService(IStorageService storageService) + public ClipboardService(IStateService stateService) { - _storageService = storageService; + _stateService = stateService; } public async Task CopyTextAsync(string text, int expiresInMs = -1) @@ -21,7 +21,7 @@ namespace Bit.iOS.Core.Services int clearSeconds = -1; if (expiresInMs < 0) { - clearSeconds = await _storageService.GetAsync(Bit.Core.Constants.ClearClipboardKey) ?? -1; + clearSeconds = await _stateService.GetClearClipboardAsync() ?? -1; } else { diff --git a/src/iOS.Core/Services/DeviceActionService.cs b/src/iOS.Core/Services/DeviceActionService.cs index 76c46b066..39e59c9af 100644 --- a/src/iOS.Core/Services/DeviceActionService.cs +++ b/src/iOS.Core/Services/DeviceActionService.cs @@ -22,17 +22,17 @@ namespace Bit.iOS.Core.Services { public class DeviceActionService : IDeviceActionService { - private readonly IStorageService _storageService; + private readonly IStateService _stateService; private readonly IMessagingService _messagingService; private Toast _toast; private UIAlertController _progressAlert; private string _userAgent; public DeviceActionService( - IStorageService storageService, + IStateService stateService, IMessagingService messagingService) { - _storageService = storageService; + _stateService = stateService; _messagingService = messagingService; } @@ -152,7 +152,7 @@ namespace Bit.iOS.Core.Services NSFileManager.DefaultManager.Remove(item, out NSError itemError); } } - await _storageService.SaveAsync(Bit.Core.Constants.LastFileCacheClearKey, DateTime.UtcNow); + await _stateService.SetLastFileCacheClearAsync(DateTime.UtcNow); } public Task SelectFileAsync() diff --git a/src/iOS.Core/Utilities/ASHelpers.cs b/src/iOS.Core/Utilities/ASHelpers.cs index 5e81438fb..74c7665a9 100644 --- a/src/iOS.Core/Utilities/ASHelpers.cs +++ b/src/iOS.Core/Utilities/ASHelpers.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Threading.Tasks; using AuthenticationServices; using Bit.Core.Abstractions; +using Bit.Core.Enums; using Bit.Core.Models.View; using Bit.Core.Utilities; @@ -15,8 +16,9 @@ namespace Bit.iOS.Core.Utilities if (await AutofillEnabled()) { var storageService = ServiceContainer.Resolve("storageService"); - var timeoutAction = await storageService.GetAsync(Bit.Core.Constants.VaultTimeoutActionKey); - if (timeoutAction == "logOut") + var stateService = ServiceContainer.Resolve("stateService"); + var timeoutAction = await stateService.GetVaultTimeoutActionAsync(); + if (timeoutAction == VaultTimeoutAction.Logout) { return; } @@ -47,9 +49,9 @@ namespace Bit.iOS.Core.Utilities public static async Task IdentitiesCanIncremental() { - var storageService = ServiceContainer.Resolve("storageService"); - var timeoutAction = await storageService.GetAsync(Bit.Core.Constants.VaultTimeoutActionKey); - if (timeoutAction == "logOut") + var stateService = ServiceContainer.Resolve("stateService"); + var timeoutAction = await stateService.GetVaultTimeoutActionAsync(); + if (timeoutAction == VaultTimeoutAction.Logout) { return false; } diff --git a/src/iOS.Core/Utilities/AppCenterHelper.cs b/src/iOS.Core/Utilities/AppCenterHelper.cs index 50b64d154..ff19b62a5 100644 --- a/src/iOS.Core/Utilities/AppCenterHelper.cs +++ b/src/iOS.Core/Utilities/AppCenterHelper.cs @@ -11,22 +11,22 @@ namespace Bit.iOS.Core.Utilities private const string AppSecret = "51f96ae5-68ba-45f6-99a1-8ad9f63046c3"; private readonly IAppIdService _appIdService; - private readonly IUserService _userService; + private readonly IStateService _stateService; private string _userId; private string _appId; public AppCenterHelper( IAppIdService appIdService, - IUserService userService) + IStateService stateService) { _appIdService = appIdService; - _userService = userService; + _stateService = stateService; } public async Task InitAsync() { - _userId = await _userService.GetUserIdAsync(); + _userId = await _stateService.GetActiveUserIdAsync(); _appId = await _appIdService.GetAppIdAsync(); AppCenter.Start(AppSecret, typeof(Crashes)); diff --git a/src/iOS.Core/Utilities/iOSCoreHelpers.cs b/src/iOS.Core/Utilities/iOSCoreHelpers.cs index 1bac23f65..be8034106 100644 --- a/src/iOS.Core/Utilities/iOSCoreHelpers.cs +++ b/src/iOS.Core/Utilities/iOSCoreHelpers.cs @@ -29,7 +29,7 @@ namespace Bit.iOS.Core.Utilities { var appCenterHelper = new AppCenterHelper( ServiceContainer.Resolve("appIdService"), - ServiceContainer.Resolve("userService")); + ServiceContainer.Resolve("stateService")); var appCenterTask = appCenterHelper.InitAsync(); } @@ -52,13 +52,16 @@ namespace Bit.iOS.Core.Utilities () => ServiceContainer.Resolve("appIdService").GetAppIdAsync()); var cryptoPrimitiveService = new CryptoPrimitiveService(); var mobileStorageService = new MobileStorageService(preferencesStorage, liteDbStorage); - var deviceActionService = new DeviceActionService(mobileStorageService, messagingService); - var clipboardService = new ClipboardService(mobileStorageService); + var stateService = new StateService(mobileStorageService, secureStorageService); + var stateMigrationService = + new StateMigrationService(liteDbStorage, preferencesStorage, secureStorageService); + var deviceActionService = new DeviceActionService(stateService, messagingService); + var clipboardService = new ClipboardService(stateService); var platformUtilsService = new MobilePlatformUtilsService(deviceActionService, messagingService, broadcasterService); var biometricService = new BiometricService(mobileStorageService); var cryptoFunctionService = new PclCryptoFunctionService(cryptoPrimitiveService); - var cryptoService = new CryptoService(mobileStorageService, secureStorageService, cryptoFunctionService); + var cryptoService = new CryptoService(stateService, cryptoFunctionService); var passwordRepromptService = new MobilePasswordRepromptService(platformUtilsService, cryptoService); ServiceContainer.Register("broadcasterService", broadcasterService); @@ -68,6 +71,8 @@ namespace Bit.iOS.Core.Utilities ServiceContainer.Register("cryptoPrimitiveService", cryptoPrimitiveService); ServiceContainer.Register("storageService", mobileStorageService); ServiceContainer.Register("secureStorageService", secureStorageService); + ServiceContainer.Register("stateService", stateService); + ServiceContainer.Register("stateMigrationService", stateMigrationService); ServiceContainer.Register("deviceActionService", deviceActionService); ServiceContainer.Register("clipboardService", clipboardService); ServiceContainer.Register("platformUtilsService", platformUtilsService); @@ -89,7 +94,7 @@ namespace Bit.iOS.Core.Utilities public static void AppearanceAdjustments() { - ThemeHelpers.SetAppearance(ThemeManager.GetTheme(false), ThemeManager.OsDarkModeEnabled()); + ThemeHelpers.SetAppearance(ThemeManager.GetTheme(), ThemeManager.OsDarkModeEnabled()); UIApplication.SharedApplication.StatusBarHidden = false; UIApplication.SharedApplication.StatusBarStyle = UIStatusBarStyle.LightContent; } @@ -144,10 +149,6 @@ namespace Bit.iOS.Core.Utilities private static async Task BootstrapAsync(Func postBootstrapFunc = null) { - var disableFavicon = await ServiceContainer.Resolve("storageService").GetAsync( - Bit.Core.Constants.DisableFaviconKey); - await ServiceContainer.Resolve("stateService").SaveAsync( - Bit.Core.Constants.DisableFaviconKey, disableFavicon); await ServiceContainer.Resolve("environmentService").SetUrlsFromStorageAsync(); // TODO: Update when https://github.com/bitwarden/mobile/pull/1662 gets merged diff --git a/src/iOS.Core/Views/ExtensionTableSource.cs b/src/iOS.Core/Views/ExtensionTableSource.cs index 9aa5e1b4b..4bd856b1e 100644 --- a/src/iOS.Core/Views/ExtensionTableSource.cs +++ b/src/iOS.Core/Views/ExtensionTableSource.cs @@ -23,7 +23,7 @@ namespace Bit.iOS.Core.Views private IEnumerable _allItems = new List(); protected ICipherService _cipherService; protected ITotpService _totpService; - protected IUserService _userService; + protected IStateService _stateService; protected ISearchService _searchService; private AppExtensionContext _context; private UIViewController _controller; @@ -32,7 +32,7 @@ namespace Bit.iOS.Core.Views { _cipherService = ServiceContainer.Resolve("cipherService"); _totpService = ServiceContainer.Resolve("totpService"); - _userService = ServiceContainer.Resolve("userService"); + _stateService = ServiceContainer.Resolve("stateService"); _searchService = ServiceContainer.Resolve("searchService"); _context = context; _controller = controller; @@ -135,7 +135,7 @@ namespace Bit.iOS.Core.Views public async Task GetTotpAsync(CipherViewModel item) { string totp = null; - var accessPremium = await _userService.CanAccessPremiumAsync(); + var accessPremium = await _stateService.CanAccessPremiumAsync(); if (accessPremium || (item?.CipherView.OrganizationUseTotp ?? false)) { if (item != null && !string.IsNullOrWhiteSpace(item.Totp)) diff --git a/src/iOS.Core/iOS.Core.csproj b/src/iOS.Core/iOS.Core.csproj index 34cd2477e..823b2bacd 100644 --- a/src/iOS.Core/iOS.Core.csproj +++ b/src/iOS.Core/iOS.Core.csproj @@ -156,6 +156,7 @@ + @@ -193,6 +194,7 @@ + diff --git a/src/iOS.Extension/LoadingViewController.cs b/src/iOS.Extension/LoadingViewController.cs index 44acc0084..a7b6cf484 100644 --- a/src/iOS.Extension/LoadingViewController.cs +++ b/src/iOS.Extension/LoadingViewController.cs @@ -415,7 +415,7 @@ namespace Bit.iOS.Extension } iOSCoreHelpers.Bootstrap(); var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(false, app.Resources); + ThemeManager.SetTheme(app.Resources); iOSCoreHelpers.AppearanceAdjustments(); _nfcDelegate = new NFCReaderDelegate((success, message) => messagingService.Send("gotYubiKeyOTP", message)); @@ -430,8 +430,8 @@ namespace Bit.iOS.Extension private Task IsAuthed() { - var userService = ServiceContainer.Resolve("userService"); - return userService.IsAuthenticatedAsync(); + var stateService = ServiceContainer.Resolve("stateService"); + return stateService.IsAuthenticatedAsync(); } private void LogoutIfAuthed() @@ -440,7 +440,8 @@ namespace Bit.iOS.Extension { if (await IsAuthed()) { - await AppHelpers.LogOutAsync(); + var stateService = ServiceContainer.Resolve("stateService"); + await AppHelpers.LogOutAsync(await stateService.GetActiveUserIdAsync()); var deviceActionService = ServiceContainer.Resolve("deviceActionService"); if (deviceActionService.SystemMajorVersion() >= 12) { @@ -454,7 +455,7 @@ namespace Bit.iOS.Extension { var homePage = new HomePage(); var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(false, app.Resources); + ThemeManager.SetTheme(app.Resources); ThemeManager.ApplyResourcesToPage(homePage); if (homePage.BindingContext is HomeViewModel vm) { @@ -477,7 +478,7 @@ namespace Bit.iOS.Extension { var environmentPage = new EnvironmentPage(); var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(false, app.Resources); + ThemeManager.SetTheme(app.Resources); ThemeManager.ApplyResourcesToPage(environmentPage); if (environmentPage.BindingContext is EnvironmentPageViewModel vm) { @@ -495,7 +496,7 @@ namespace Bit.iOS.Extension { var registerPage = new RegisterPage(null); var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(false, app.Resources); + ThemeManager.SetTheme(app.Resources); ThemeManager.ApplyResourcesToPage(registerPage); if (registerPage.BindingContext is RegisterPageViewModel vm) { @@ -513,7 +514,7 @@ namespace Bit.iOS.Extension { var loginPage = new LoginPage(email); var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(false, app.Resources); + ThemeManager.SetTheme(app.Resources); ThemeManager.ApplyResourcesToPage(loginPage); if (loginPage.BindingContext is LoginPageViewModel vm) { @@ -535,7 +536,7 @@ namespace Bit.iOS.Extension { var loginPage = new LoginSsoPage(); var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(false, app.Resources); + ThemeManager.SetTheme(app.Resources); ThemeManager.ApplyResourcesToPage(loginPage); if (loginPage.BindingContext is LoginSsoPageViewModel vm) { @@ -558,7 +559,7 @@ namespace Bit.iOS.Extension { var twoFactorPage = new TwoFactorPage(); var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(false, app.Resources); + ThemeManager.SetTheme(app.Resources); ThemeManager.ApplyResourcesToPage(twoFactorPage); if (twoFactorPage.BindingContext is TwoFactorPageViewModel vm) { @@ -585,7 +586,7 @@ namespace Bit.iOS.Extension { var setPasswordPage = new SetPasswordPage(); var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(false, app.Resources); + ThemeManager.SetTheme(app.Resources); ThemeManager.ApplyResourcesToPage(setPasswordPage); if (setPasswordPage.BindingContext is SetPasswordPageViewModel vm) { @@ -604,7 +605,7 @@ namespace Bit.iOS.Extension { var updateTempPasswordPage = new UpdateTempPasswordPage(); var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(false, app.Resources); + ThemeManager.SetTheme(app.Resources); ThemeManager.ApplyResourcesToPage(updateTempPasswordPage); if (updateTempPasswordPage.BindingContext is UpdateTempPasswordPageViewModel vm) { diff --git a/src/iOS.Extension/LoginListViewController.cs b/src/iOS.Extension/LoginListViewController.cs index 2dd2f7455..ea7917c76 100644 --- a/src/iOS.Extension/LoginListViewController.cs +++ b/src/iOS.Extension/LoginListViewController.cs @@ -9,8 +9,6 @@ using MobileCoreServices; using Bit.iOS.Core.Controllers; using Bit.App.Resources; using Bit.iOS.Core.Views; -using Bit.Core.Utilities; -using Bit.Core.Abstractions; namespace Bit.iOS.Extension { @@ -127,9 +125,7 @@ namespace Bit.iOS.Extension if (_controller.CanAutoFill() && !string.IsNullOrWhiteSpace(item.Password)) { string totp = null; - var storageService = ServiceContainer.Resolve("storageService"); - var disableTotpCopy = storageService.GetAsync( - Bit.Core.Constants.DisableAutoTotpCopyKey).GetAwaiter().GetResult(); + var disableTotpCopy = _stateService.GetDisableAutoTotpCopyAsync().GetAwaiter().GetResult(); if (!disableTotpCopy.GetValueOrDefault(false)) { totp = GetTotpAsync(item).GetAwaiter().GetResult(); diff --git a/src/iOS.ShareExtension/LoadingViewController.cs b/src/iOS.ShareExtension/LoadingViewController.cs index 651fe21ec..6a3304f26 100644 --- a/src/iOS.ShareExtension/LoadingViewController.cs +++ b/src/iOS.ShareExtension/LoadingViewController.cs @@ -31,7 +31,7 @@ namespace Bit.iOS.ShareExtension private NFCNdefReaderSession _nfcSession = null; private Core.NFCReaderDelegate _nfcDelegate = null; - readonly LazyResolve _userService = new LazyResolve("userService"); + readonly LazyResolve _stateService = new LazyResolve("stateervice"); readonly LazyResolve _vaultTimeoutService = new LazyResolve("vaultTimeoutService"); readonly LazyResolve _deviceActionService = new LazyResolve("deviceActionService"); readonly LazyResolve _eventService = new LazyResolve("eventService"); @@ -148,7 +148,7 @@ namespace Bit.iOS.ShareExtension }; var app = new App.App(appOptions); - ThemeManager.SetTheme(false, app.Resources); + ThemeManager.SetTheme(app.Resources); ThemeManager.ApplyResourcesToPage(sendAddEditPage); var navigationPage = new NavigationPage(sendAddEditPage); @@ -224,7 +224,7 @@ namespace Bit.iOS.ShareExtension iOSCoreHelpers.Bootstrap(); var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(false, app.Resources); + ThemeManager.SetTheme(app.Resources); iOSCoreHelpers.AppearanceAdjustments(); _nfcDelegate = new NFCReaderDelegate((success, message) => @@ -239,7 +239,7 @@ namespace Bit.iOS.ShareExtension private Task IsAuthed() { - return _userService.Value.IsAuthenticatedAsync(); + return _stateService.Value.IsAuthenticatedAsync(); } private void LogoutIfAuthed() @@ -248,7 +248,7 @@ namespace Bit.iOS.ShareExtension { if (await IsAuthed()) { - await AppHelpers.LogOutAsync(); + await AppHelpers.LogOutAsync(await _stateService.Value.GetActiveUserIdAsync()); if (_deviceActionService.Value.SystemMajorVersion() >= 12) { await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync(); @@ -261,7 +261,7 @@ namespace Bit.iOS.ShareExtension { var homePage = new HomePage(); var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(false, app.Resources); + ThemeManager.SetTheme(app.Resources); ThemeManager.ApplyResourcesToPage(homePage); if (homePage.BindingContext is HomeViewModel vm) { @@ -284,7 +284,7 @@ namespace Bit.iOS.ShareExtension { var environmentPage = new EnvironmentPage(); var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(false, app.Resources); + ThemeManager.SetTheme(app.Resources); ThemeManager.ApplyResourcesToPage(environmentPage); if (environmentPage.BindingContext is EnvironmentPageViewModel vm) { @@ -302,7 +302,7 @@ namespace Bit.iOS.ShareExtension { var registerPage = new RegisterPage(null); var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(false, app.Resources); + ThemeManager.SetTheme(app.Resources); ThemeManager.ApplyResourcesToPage(registerPage); if (registerPage.BindingContext is RegisterPageViewModel vm) { @@ -320,7 +320,7 @@ namespace Bit.iOS.ShareExtension { var loginPage = new LoginPage(email); var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(false, app.Resources); + ThemeManager.SetTheme(app.Resources); ThemeManager.ApplyResourcesToPage(loginPage); if (loginPage.BindingContext is LoginPageViewModel vm) { @@ -342,7 +342,7 @@ namespace Bit.iOS.ShareExtension { var loginPage = new LoginSsoPage(); var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(false, app.Resources); + ThemeManager.SetTheme(app.Resources); ThemeManager.ApplyResourcesToPage(loginPage); if (loginPage.BindingContext is LoginSsoPageViewModel vm) { @@ -365,7 +365,7 @@ namespace Bit.iOS.ShareExtension { var twoFactorPage = new TwoFactorPage(); var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(false, app.Resources); + ThemeManager.SetTheme(app.Resources); ThemeManager.ApplyResourcesToPage(twoFactorPage); if (twoFactorPage.BindingContext is TwoFactorPageViewModel vm) { @@ -392,7 +392,7 @@ namespace Bit.iOS.ShareExtension { var setPasswordPage = new SetPasswordPage(); var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(false, app.Resources); + ThemeManager.SetTheme(app.Resources); ThemeManager.ApplyResourcesToPage(setPasswordPage); if (setPasswordPage.BindingContext is SetPasswordPageViewModel vm) { @@ -411,7 +411,7 @@ namespace Bit.iOS.ShareExtension { var updateTempPasswordPage = new UpdateTempPasswordPage(); var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(false, app.Resources); + ThemeManager.SetTheme(app.Resources); ThemeManager.ApplyResourcesToPage(updateTempPasswordPage); if (updateTempPasswordPage.BindingContext is UpdateTempPasswordPageViewModel vm) { diff --git a/src/iOS/AppDelegate.cs b/src/iOS/AppDelegate.cs index 48c1bdd64..a426a6a3b 100644 --- a/src/iOS/AppDelegate.cs +++ b/src/iOS/AppDelegate.cs @@ -8,6 +8,7 @@ using Bit.App.Services; using Bit.App.Utilities; using Bit.Core; using Bit.Core.Abstractions; +using Bit.Core.Enums; using Bit.Core.Services; using Bit.Core.Utilities; using Bit.iOS.Core.Utilities; @@ -35,7 +36,7 @@ namespace Bit.iOS private IMessagingService _messagingService; private IBroadcasterService _broadcasterService; private IStorageService _storageService; - private IVaultTimeoutService _vaultTimeoutService; + private IStateService _stateService; private IEventService _eventService; public override bool FinishedLaunching(UIApplication app, NSDictionary options) @@ -47,7 +48,7 @@ namespace Bit.iOS _messagingService = ServiceContainer.Resolve("messagingService"); _broadcasterService = ServiceContainer.Resolve("broadcasterService"); _storageService = ServiceContainer.Resolve("storageService"); - _vaultTimeoutService = ServiceContainer.Resolve("vaultTimeoutService"); + _stateService = ServiceContainer.Resolve("stateService"); _eventService = ServiceContainer.Resolve("eventService"); LoadApplication(new App.App(null)); @@ -88,11 +89,6 @@ namespace Bit.iOS { Device.BeginInvokeOnMainThread(() => ShowAppExtension((ExtensionPageViewModel)message.Data)); } - else if (message.Command == "showStatusBar") - { - Device.BeginInvokeOnMainThread(() => - UIApplication.SharedApplication.SetStatusBarHidden(!(bool)message.Data, false)); - } else if (message.Command == "syncCompleted") { if (message.Data is Dictionary data && data.ContainsKey("successfully")) @@ -146,11 +142,16 @@ namespace Bit.iOS await ASHelpers.ReplaceAllIdentities(); } } - else if (message.Command == "loggedOut") + else if (message.Command == "logout") { if (_deviceActionService.SystemMajorVersion() >= 12) { - await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync(); + var extras = message.Data as Tuple; + var userId = extras?.Item1; + var userInitiated = extras?.Item2; + var expired = extras?.Item3; + // TODO make specific to userId + // await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync(); } } else if ((message.Command == "softDeletedCipher" || message.Command == "restoredCipher") @@ -160,10 +161,12 @@ namespace Bit.iOS } else if (message.Command == "vaultTimeoutActionChanged") { - var timeoutAction = await _storageService.GetAsync(Constants.VaultTimeoutActionKey); - if (timeoutAction == "logOut") + var timeoutAction = await _stateService.GetVaultTimeoutActionAsync(); + if (timeoutAction == VaultTimeoutAction.Logout) { - await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync(); + var userId = await _stateService.GetActiveUserIdAsync(); + // TODO make specific to userId + // await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync(); } else { @@ -195,13 +198,12 @@ namespace Bit.iOS UIApplication.SharedApplication.KeyWindow.AddSubview(view); UIApplication.SharedApplication.KeyWindow.BringSubviewToFront(view); UIApplication.SharedApplication.KeyWindow.EndEditing(true); - UIApplication.SharedApplication.SetStatusBarHidden(true, false); base.OnResignActivation(uiApplication); } public override void DidEnterBackground(UIApplication uiApplication) { - _storageService?.SaveAsync(Constants.LastActiveTimeKey, _deviceActionService.GetActiveTime()); + _stateService?.SetLastActiveTimeAsync(_deviceActionService.GetActiveTime()); _messagingService?.Send("slept"); base.DidEnterBackground(uiApplication); } @@ -214,7 +216,6 @@ namespace Bit.iOS if (view != null) { view.RemoveFromSuperview(); - UIApplication.SharedApplication.SetStatusBarHidden(false, false); } ThemeManager.UpdateThemeOnPagesAsync(); diff --git a/src/iOS/LaunchScreen.storyboard b/src/iOS/LaunchScreen.storyboard index 393e62a39..6cec090bc 100644 --- a/src/iOS/LaunchScreen.storyboard +++ b/src/iOS/LaunchScreen.storyboard @@ -88,4 +88,4 @@ - + \ No newline at end of file diff --git a/src/iOS/Resources/cog.png b/src/iOS/Resources/cog_environment.png similarity index 100% rename from src/iOS/Resources/cog.png rename to src/iOS/Resources/cog_environment.png diff --git a/src/iOS/Resources/cog@2x.png b/src/iOS/Resources/cog_environment@2x.png similarity index 100% rename from src/iOS/Resources/cog@2x.png rename to src/iOS/Resources/cog_environment@2x.png diff --git a/src/iOS/Resources/cog@3x.png b/src/iOS/Resources/cog_environment@3x.png similarity index 100% rename from src/iOS/Resources/cog@3x.png rename to src/iOS/Resources/cog_environment@3x.png diff --git a/src/iOS/Resources/cog_settings.png b/src/iOS/Resources/cog_settings.png new file mode 100644 index 000000000..95d6271b8 Binary files /dev/null and b/src/iOS/Resources/cog_settings.png differ diff --git a/src/iOS/Resources/cog_settings@2x.png b/src/iOS/Resources/cog_settings@2x.png new file mode 100644 index 000000000..8d884844f Binary files /dev/null and b/src/iOS/Resources/cog_settings@2x.png differ diff --git a/src/iOS/Resources/cog_settings@3x.png b/src/iOS/Resources/cog_settings@3x.png new file mode 100644 index 000000000..a46070614 Binary files /dev/null and b/src/iOS/Resources/cog_settings@3x.png differ diff --git a/src/iOS/iOS.csproj b/src/iOS/iOS.csproj index d2caf681a..1e901529b 100644 --- a/src/iOS/iOS.csproj +++ b/src/iOS/iOS.csproj @@ -143,6 +143,7 @@ + false @@ -155,19 +156,21 @@ - - - - - - + + + + + + + + - - + + diff --git a/test/Core.Test/Services/SendServiceTests.cs b/test/Core.Test/Services/SendServiceTests.cs index fa2e3f94b..badf2c742 100644 --- a/test/Core.Test/Services/SendServiceTests.cs +++ b/test/Core.Test/Services/SendServiceTests.cs @@ -28,20 +28,17 @@ namespace Bit.Core.Test.Services { public class SendServiceTests { - private string GetSendKey(string userId) => SendService.GetSendKey(userId); - [Theory] [InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(TextSendCustomization) })] [InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(FileSendCustomization) })] public async Task ReplaceAsync_Success(SutProvider sutProvider, string userId, IEnumerable sendDatas) { var actualSendDataDict = sendDatas.ToDictionary(d => d.Id, d => d); - sutProvider.GetDependency().GetUserIdAsync().Returns(userId); + sutProvider.GetDependency().GetActiveUserIdAsync().Returns(userId); await sutProvider.Sut.ReplaceAsync(actualSendDataDict); - await sutProvider.GetDependency() - .Received(1).SaveAsync(GetSendKey(userId), actualSendDataDict); + await sutProvider.GetDependency().SetEncryptedSendsAsync(actualSendDataDict); } [Theory] @@ -53,9 +50,8 @@ namespace Bit.Core.Test.Services public async Task DeleteAsync_Success(int numberToDelete, SutProvider sutProvider, string userId, IEnumerable sendDatas) { var actualSendDataDict = sendDatas.ToDictionary(d => d.Id, d => d); - sutProvider.GetDependency().GetUserIdAsync().Returns(userId); - sutProvider.GetDependency() - .GetAsync>(GetSendKey(userId)).Returns(actualSendDataDict); + sutProvider.GetDependency().GetActiveUserIdAsync().Returns(userId); + sutProvider.GetDependency().GetEncryptedSendsAsync().Returns(actualSendDataDict); var idsToDelete = actualSendDataDict.Take(numberToDelete).Select(kvp => kvp.Key).ToArray(); var expectedSends = actualSendDataDict.Skip(numberToDelete).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); @@ -63,9 +59,8 @@ namespace Bit.Core.Test.Services await sutProvider.Sut.DeleteAsync(idsToDelete); - await sutProvider.GetDependency().Received(1) - .SaveAsync(GetSendKey(userId), - Arg.Is>(s => TestHelper.AssertEqualExpectedPredicate(expectedSends)(s))); + await sutProvider.GetDependency().SetEncryptedSendsAsync( + Arg.Is>(s => TestHelper.AssertEqualExpectedPredicate(expectedSends)(s))); } [Theory, SutAutoData] @@ -73,7 +68,7 @@ namespace Bit.Core.Test.Services { await sutProvider.Sut.ClearAsync(userId); - await sutProvider.GetDependency().Received(1).RemoveAsync(GetSendKey(userId)); + await sutProvider.GetDependency().SetEncryptedSendsAsync(null, userId); } [Theory] @@ -84,16 +79,15 @@ namespace Bit.Core.Test.Services var initialSendDatas = sendDatas.ToDictionary(d => d.Id, d => d); var idToDelete = initialSendDatas.First().Key; var expectedSends = initialSendDatas.Skip(1).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); - sutProvider.GetDependency().GetUserIdAsync().Returns(userId); - sutProvider.GetDependency() - .GetAsync>(Arg.Any()).Returns(initialSendDatas); + sutProvider.GetDependency().GetActiveUserIdAsync().Returns(userId); + sutProvider.GetDependency() + .GetEncryptedSendsAsync(Arg.Any()).Returns(initialSendDatas); await sutProvider.Sut.DeleteWithServerAsync(idToDelete); await sutProvider.GetDependency().Received(1).DeleteSendAsync(idToDelete); - await sutProvider.GetDependency().Received(1) - .SaveAsync(GetSendKey(userId), - Arg.Is>(s => TestHelper.AssertEqualExpectedPredicate(expectedSends)(s))); + await sutProvider.GetDependency().SetEncryptedSendsAsync( + Arg.Is>(s => TestHelper.AssertEqualExpectedPredicate(expectedSends)(s))); } [Theory] @@ -102,8 +96,8 @@ namespace Bit.Core.Test.Services public async Task GetAsync_Success(SutProvider sutProvider, string userId, IEnumerable sendDatas) { var sendDataDict = sendDatas.ToDictionary(d => d.Id, d => d); - sutProvider.GetDependency().GetUserIdAsync().Returns(userId); - sutProvider.GetDependency().GetAsync>(GetSendKey(userId)).Returns(sendDataDict); + sutProvider.GetDependency().GetActiveUserIdAsync().Returns(userId); + sutProvider.GetDependency().GetEncryptedSendsAsync().Returns(sendDataDict); foreach (var dataKvp in sendDataDict) { @@ -114,11 +108,11 @@ namespace Bit.Core.Test.Services } [Theory, SutAutoData] - public async Task GetAsync_NonExistringId_ReturnsNull(SutProvider sutProvider, string userId, IEnumerable sendDatas) + public async Task GetAsync_NonExistingId_ReturnsNull(SutProvider sutProvider, string userId, IEnumerable sendDatas) { var sendDataDict = sendDatas.ToDictionary(d => d.Id, d => d); - sutProvider.GetDependency().GetUserIdAsync().Returns(userId); - sutProvider.GetDependency().GetAsync>(GetSendKey(userId)).Returns(sendDataDict); + sutProvider.GetDependency().GetActiveUserIdAsync().Returns(userId); + sutProvider.GetDependency().GetEncryptedSendsAsync().Returns(sendDataDict); var actual = await sutProvider.Sut.GetAsync(Guid.NewGuid().ToString()); @@ -131,8 +125,8 @@ namespace Bit.Core.Test.Services public async Task GetAllAsync_Success(SutProvider sutProvider, string userId, IEnumerable sendDatas) { var sendDataDict = sendDatas.ToDictionary(d => d.Id, d => d); - sutProvider.GetDependency().GetUserIdAsync().Returns(userId); - sutProvider.GetDependency().GetAsync>(GetSendKey(userId)).Returns(sendDataDict); + sutProvider.GetDependency().GetActiveUserIdAsync().Returns(userId); + sutProvider.GetDependency().GetEncryptedSendsAsync().Returns(sendDataDict); var allExpected = sendDataDict.Select(kvp => new Send(kvp.Value)); var allActual = await sutProvider.Sut.GetAllAsync(); @@ -154,8 +148,8 @@ namespace Bit.Core.Test.Services sutProvider.GetDependency().HasKeyAsync().Returns(true); ServiceContainer.Register("cryptoService", sutProvider.GetDependency()); sutProvider.GetDependency().StringComparer.Returns(StringComparer.CurrentCulture); - sutProvider.GetDependency().GetUserIdAsync().Returns(userId); - sutProvider.GetDependency().GetAsync>(GetSendKey(userId)).Returns(sendDataDict); + sutProvider.GetDependency().GetActiveUserIdAsync().Returns(userId); + sutProvider.GetDependency().GetEncryptedSendsAsync().Returns(sendDataDict); var actual = await sutProvider.Sut.GetAllDecryptedAsync(); @@ -175,7 +169,7 @@ namespace Bit.Core.Test.Services public async Task SaveWithServerAsync_NewTextSend_Success(SutProvider sutProvider, string userId, SendResponse response, Send send) { send.Id = null; - sutProvider.GetDependency().GetUserIdAsync().Returns(userId); + sutProvider.GetDependency().GetActiveUserIdAsync().Returns(userId); sutProvider.GetDependency().PostSendAsync(Arg.Any()).Returns(response); var fileContentBytes = new EncByteArray(Encoding.UTF8.GetBytes("This is the file content")); @@ -208,7 +202,7 @@ namespace Bit.Core.Test.Services { send.Id = null; response.FileUploadType = FileUploadType.Azure; - sutProvider.GetDependency().GetUserIdAsync().Returns(userId); + sutProvider.GetDependency().GetActiveUserIdAsync().Returns(userId); sutProvider.GetDependency().PostFileTypeSendAsync(Arg.Any()).Returns(response); var fileContentBytes = new EncByteArray(Encoding.UTF8.GetBytes("This is the file content")); @@ -231,7 +225,7 @@ namespace Bit.Core.Test.Services public async Task SaveWithServerAsync_NewFileSend_LegacyFallback_Success(SutProvider sutProvider, string userId, Send send, SendResponse response) { send.Id = null; - sutProvider.GetDependency().GetUserIdAsync().Returns(userId); + sutProvider.GetDependency().GetActiveUserIdAsync().Returns(userId); var error = new ErrorResponse(null, System.Net.HttpStatusCode.NotFound); sutProvider.GetDependency().PostFileTypeSendAsync(Arg.Any()).Throws(new ApiException(error)); sutProvider.GetDependency().PostSendFileAsync(Arg.Any()).Returns(response); @@ -248,7 +242,7 @@ namespace Bit.Core.Test.Services [InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(FileSendCustomization) })] public async Task SaveWithServerAsync_PutSend_Success(SutProvider sutProvider, string userId, SendResponse response, Send send) { - sutProvider.GetDependency().GetUserIdAsync().Returns(userId); + sutProvider.GetDependency().GetActiveUserIdAsync().Returns(userId); sutProvider.GetDependency().PutSendAsync(send.Id, Arg.Any()).Returns(response); await sutProvider.Sut.SaveWithServerAsync(send, null); @@ -272,7 +266,7 @@ namespace Bit.Core.Test.Services await sutProvider.Sut.RemovePasswordWithServerAsync(sendId); await sutProvider.GetDependency().Received(1).PutSendRemovePasswordAsync(sendId); - await sutProvider.GetDependency().ReceivedWithAnyArgs(1).SaveAsync>(default, default); + await sutProvider.GetDependency().SetEncryptedSendsAsync(default, default); } [Theory] @@ -281,8 +275,8 @@ namespace Bit.Core.Test.Services public async Task UpsertAsync_Update_Success(SutProvider sutProvider, string userId, IEnumerable initialSends) { var initialSendDict = initialSends.ToDictionary(s => s.Id, s => s); - sutProvider.GetDependency().GetUserIdAsync().Returns(userId); - sutProvider.GetDependency().GetAsync>(GetSendKey(userId)).Returns(initialSendDict); + sutProvider.GetDependency().GetActiveUserIdAsync().Returns(userId); + sutProvider.GetDependency().GetEncryptedSendsAsync().Returns(initialSendDict); var updatedSends = CoreHelpers.Clone(initialSendDict); foreach (var kvp in updatedSends) @@ -302,7 +296,8 @@ namespace Bit.Core.Test.Services } return true; }; - await sutProvider.GetDependency().Received(1).SaveAsync(GetSendKey(userId), Arg.Is>(d => matchSendsPredicate(d))); + await sutProvider.GetDependency().SetEncryptedSendsAsync( + Arg.Is>(d => matchSendsPredicate(d))); } [Theory] @@ -311,8 +306,8 @@ namespace Bit.Core.Test.Services public async Task UpsertAsync_NewSends_Success(SutProvider sutProvider, string userId, IEnumerable initialSends, IEnumerable newSends) { var initialSendDict = initialSends.ToDictionary(s => s.Id, s => s); - sutProvider.GetDependency().GetUserIdAsync().Returns(userId); - sutProvider.GetDependency().GetAsync>(GetSendKey(userId)).Returns(initialSendDict); + sutProvider.GetDependency().GetActiveUserIdAsync().Returns(userId); + sutProvider.GetDependency().GetEncryptedSendsAsync().Returns(initialSendDict); var expectedDict = CoreHelpers.Clone(initialSendDict).Concat(newSends.Select(s => new KeyValuePair(s.Id, s))); @@ -328,7 +323,8 @@ namespace Bit.Core.Test.Services } return true; }; - await sutProvider.GetDependency().Received(1).SaveAsync(GetSendKey(userId), Arg.Is>(d => matchSendsPredicate(d))); + await sutProvider.GetDependency().SetEncryptedSendsAsync( + Arg.Is>(d => matchSendsPredicate(d))); } [Theory]