diff --git a/src/Android/MainApplication.cs b/src/Android/MainApplication.cs index aadd11564..4b5e706e3 100644 --- a/src/Android/MainApplication.cs +++ b/src/Android/MainApplication.cs @@ -20,6 +20,7 @@ using System.Net.Http; using System.Net; using Bit.App.Utilities; using Bit.App.Pages; +using Bit.App.Utilities.AccountManagement; #if !FDROID using Android.Gms.Security; #endif @@ -62,6 +63,15 @@ namespace Bit.Droid ServiceContainer.Resolve("passwordRepromptService"), ServiceContainer.Resolve("cryptoService")); ServiceContainer.Register("verificationActionsFlowHelper", verificationActionsFlowHelper); + + var accountsManager = new AccountsManager( + ServiceContainer.Resolve("broadcasterService"), + ServiceContainer.Resolve("vaultTimeoutService"), + ServiceContainer.Resolve("secureStorageService"), + ServiceContainer.Resolve("stateService"), + ServiceContainer.Resolve("platformUtilsService"), + ServiceContainer.Resolve("authService")); + ServiceContainer.Register("accountsManager", accountsManager); } #if !FDROID if (Build.VERSION.SdkInt <= BuildVersionCodes.Kitkat) diff --git a/src/App/Abstractions/IAccountsManager.cs b/src/App/Abstractions/IAccountsManager.cs new file mode 100644 index 000000000..684230d01 --- /dev/null +++ b/src/App/Abstractions/IAccountsManager.cs @@ -0,0 +1,12 @@ +using System; +using System.Threading.Tasks; +using Bit.App.Models; + +namespace Bit.App.Abstractions +{ + public interface IAccountsManager + { + void Init(Func getOptionsFunc, IAccountsManagerHost accountsManagerHost); + Task NavigateOnAccountChangeAsync(bool? isAuthed = null); + } +} diff --git a/src/App/Abstractions/IAccountsManagerHost.cs b/src/App/Abstractions/IAccountsManagerHost.cs new file mode 100644 index 000000000..17e5ae0e2 --- /dev/null +++ b/src/App/Abstractions/IAccountsManagerHost.cs @@ -0,0 +1,14 @@ +using System.Threading.Tasks; +using Bit.Core.Enums; + +namespace Bit.App.Abstractions +{ + public interface INavigationParams { } + + public interface IAccountsManagerHost + { + Task SetPreviousPageInfoAsync(); + void Navigate(NavigationTarget navTarget, INavigationParams navParams = null); + Task UpdateThemeAsync(); + } +} diff --git a/src/App/App.csproj b/src/App/App.csproj index 2293da7e1..1a94bff94 100644 --- a/src/App/App.csproj +++ b/src/App/App.csproj @@ -128,6 +128,7 @@ + @@ -420,5 +421,6 @@ + diff --git a/src/App/App.xaml.cs b/src/App/App.xaml.cs index bf6736836..3b2b1229e 100644 --- a/src/App/App.xaml.cs +++ b/src/App/App.xaml.cs @@ -6,6 +6,7 @@ using Bit.App.Pages; using Bit.App.Resources; using Bit.App.Services; using Bit.App.Utilities; +using Bit.App.Utilities.AccountManagement; using Bit.Core.Abstractions; using Bit.Core.Enums; using Bit.Core.Models.Data; @@ -16,7 +17,7 @@ using Xamarin.Forms.Xaml; [assembly: XamlCompilation(XamlCompilationOptions.Compile)] namespace Bit.App { - public partial class App : Application + public partial class App : Application, IAccountsManagerHost { private readonly IBroadcasterService _broadcasterService; private readonly IMessagingService _messagingService; @@ -27,6 +28,7 @@ namespace Bit.App private readonly IAuthService _authService; private readonly IStorageService _secureStorageService; private readonly IDeviceActionService _deviceActionService; + private readonly IAccountsManager _accountsManager; private static bool _isResumed; @@ -47,6 +49,9 @@ namespace Bit.App _platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); _secureStorageService = ServiceContainer.Resolve("secureStorageService"); _deviceActionService = ServiceContainer.Resolve("deviceActionService"); + _accountsManager = ServiceContainer.Resolve("accountsManager"); + + _accountsManager.Init(() => Options, this); Bootstrap(); _broadcasterService.Subscribe(nameof(App), async (message) => @@ -71,30 +76,6 @@ namespace Bit.App _messagingService.Send("showDialogResolve", new Tuple(details.DialogId, confirmed)); }); } - else if (message.Command == "locked") - { - var extras = message.Data as Tuple; - var userId = extras?.Item1; - var userInitiated = extras?.Item2 ?? false; - Device.BeginInvokeOnMainThread(async () => await LockedAsync(userId, userInitiated)); - } - else if (message.Command == "lockVault") - { - await _vaultTimeoutService.LockAsync(true); - } - else if (message.Command == "logout") - { - var extras = message.Data as Tuple; - var userId = extras?.Item1; - var userInitiated = extras?.Item2 ?? true; - var expired = extras?.Item3 ?? false; - Device.BeginInvokeOnMainThread(async () => await LogOutAsync(userId, userInitiated, expired)); - } - else if (message.Command == "loggedOut") - { - // Clean up old migrated key if they ever log out. - await _secureStorageService.RemoveAsync("oldKey"); - } else if (message.Command == "resumed") { if (Device.RuntimePlatform == Device.iOS) @@ -109,22 +90,10 @@ 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); - await SetMainPageAsync(); + await _accountsManager.NavigateOnAccountChangeAsync(); } else if (message.Command == "popAllAndGoToTabGenerator" || message.Command == "popAllAndGoToTabMyVault" || @@ -168,7 +137,6 @@ namespace Bit.App new NavigationPage(new RemoveMasterPasswordPage())); }); } - }); } @@ -263,102 +231,6 @@ namespace Bit.App new System.Globalization.UmAlQuraCalendar(); } - private async Task LogOutAsync(string userId, bool userInitiated, bool expired) - { - await AppHelpers.LogOutAsync(userId, userInitiated); - await SetMainPageAsync(); - _authService.LogOut(() => - { - if (expired) - { - _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(); - Device.BeginInvokeOnMainThread(async () => - { - if (await _vaultTimeoutService.ShouldTimeoutAsync()) - { - await _vaultTimeoutService.ExecuteTimeoutActionAsync(); - } - else - { - await SetMainPageAsync(); - } - await Task.Delay(50); - await UpdateThemeAsync(); - }); - } - - private async Task SetMainPageAsync() - { - var authed = await _stateService.IsAuthenticatedAsync(); - if (authed) - { - if (await _vaultTimeoutService.IsLoggedOutByTimeoutAsync() || - await _vaultTimeoutService.ShouldLogOutByTimeoutAsync()) - { - // 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 if (await _vaultTimeoutService.IsLockedAsync() || - await _vaultTimeoutService.ShouldLockAsync()) - { - Current.MainPage = new NavigationPage(new LockPage(Options)); - } - else if (Options.FromAutofillFramework && Options.SaveType.HasValue) - { - Current.MainPage = new NavigationPage(new AddEditPage(appOptions: Options)); - } - else if (Options.Uri != null) - { - Current.MainPage = new NavigationPage(new AutofillCiphersPage(Options)); - } - else if (Options.CreateSend != null) - { - Current.MainPage = new NavigationPage(new SendAddEditPage(Options)); - } - else - { - Current.MainPage = new TabsPage(Options); - } - } - else - { - Options.HideAccountSwitcher = await _stateService.GetActiveUserIdAsync() == null; - if (await _vaultTimeoutService.IsLoggedOutByTimeoutAsync() || - await _vaultTimeoutService.ShouldLogOutByTimeoutAsync()) - { - // 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 _stateService.GetLastFileCacheClearAsync(); @@ -420,7 +292,7 @@ namespace Bit.App UpdateThemeAsync(); }; Current.MainPage = new NavigationPage(new HomePage(Options)); - var mainPageTask = SetMainPageAsync(); + var mainPageTask = _accountsManager.NavigateOnAccountChangeAsync(); ServiceContainer.Resolve("platformUtilsService").Init(); } @@ -441,23 +313,8 @@ namespace Bit.App }); } - private async Task LockedAsync(string userId, bool userInitiated) + public async Task SetPreviousPageInfoAsync() { - if (!await _stateService.IsActiveAccountAsync(userId)) - { - _platformUtilsService.ShowToast("info", null, AppResources.AccountLockedSuccessfully); - return; - } - - var autoPromptBiometric = !userInitiated; - if (autoPromptBiometric && Device.RuntimePlatform == Device.iOS) - { - var vaultTimeout = await _stateService.GetVaultTimeoutAsync(); - if (vaultTimeout == 0) - { - autoPromptBiometric = false; - } - } PreviousPageInfo lastPageBeforeLock = null; if (Current.MainPage is TabbedPage tabbedPage && tabbedPage.Navigation.ModalStack.Count > 0) { @@ -483,8 +340,44 @@ namespace Bit.App } } await _stateService.SetPreviousPageInfoAsync(lastPageBeforeLock); - var lockPage = new LockPage(Options, autoPromptBiometric); - Device.BeginInvokeOnMainThread(() => Current.MainPage = new NavigationPage(lockPage)); + } + + public void Navigate(NavigationTarget navTarget, INavigationParams navParams) + { + switch (navTarget) + { + case NavigationTarget.HomeLogin: + Current.MainPage = new NavigationPage(new HomePage(Options)); + break; + case NavigationTarget.Login: + if (navParams is LoginNavigationParams loginParams) + { + Current.MainPage = new NavigationPage(new LoginPage(loginParams.Email, Options)); + } + break; + case NavigationTarget.Lock: + if (navParams is LockNavigationParams lockParams) + { + Current.MainPage = new NavigationPage(new LockPage(Options, lockParams.AutoPromptBiometric)); + } + else + { + Current.MainPage = new NavigationPage(new LockPage(Options)); + } + break; + case NavigationTarget.Home: + Current.MainPage = new TabsPage(Options); + break; + case NavigationTarget.AddEditCipher: + Current.MainPage = new NavigationPage(new AddEditPage(appOptions: Options)); + break; + case NavigationTarget.AutofillCiphers: + Current.MainPage = new NavigationPage(new AutofillCiphersPage(Options)); + break; + case NavigationTarget.SendAddEdit: + Current.MainPage = new NavigationPage(new SendAddEditPage(Options)); + break; + } } } } diff --git a/src/App/Controls/AccountSwitchingOverlay/AccountSwitchingOverlayView.xaml.cs b/src/App/Controls/AccountSwitchingOverlay/AccountSwitchingOverlayView.xaml.cs index 8096f9e19..89a0777d3 100644 --- a/src/App/Controls/AccountSwitchingOverlay/AccountSwitchingOverlayView.xaml.cs +++ b/src/App/Controls/AccountSwitchingOverlay/AccountSwitchingOverlayView.xaml.cs @@ -63,6 +63,8 @@ namespace Bit.App.Controls public int AccountListRowHeight => Device.RuntimePlatform == Device.Android ? 74 : 70; + public bool LongPressAccountEnabled { get; set; } = true; + public async Task ToggleVisibilityAsync() { if (IsVisible) @@ -167,7 +169,7 @@ namespace Bit.App.Controls private async Task LongPressAccountAsync(AccountViewCellViewModel item) { - if (!item.IsAccount) + if (!LongPressAccountEnabled || !item.IsAccount) { return; } diff --git a/src/App/Utilities/AccountManagement/AccountsManager.cs b/src/App/Utilities/AccountManagement/AccountsManager.cs new file mode 100644 index 000000000..875078af9 --- /dev/null +++ b/src/App/Utilities/AccountManagement/AccountsManager.cs @@ -0,0 +1,227 @@ +using System; +using System.Threading.Tasks; +using Bit.App.Abstractions; +using Bit.App.Models; +using Bit.App.Resources; +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Bit.Core.Models.Domain; +using Xamarin.Forms; + +namespace Bit.App.Utilities.AccountManagement +{ + public static class AccountsManagerMessageCommands + { + public const string LOCKED = "locked"; + public const string LOCK_VAULT = "lockVault"; + public const string LOGOUT = "logout"; + public const string LOGGED_OUT = "loggedOut"; + public const string ADD_ACCOUNT = "addAccount"; + public const string ACCOUNT_ADDED = "accountAdded"; + public const string SWITCHED_ACCOUNT = "switchedAccount"; + } + + public class AccountsManager : IAccountsManager + { + private readonly IBroadcasterService _broadcasterService; + private readonly IVaultTimeoutService _vaultTimeoutService; + private readonly IStorageService _secureStorageService; + private readonly IStateService _stateService; + private readonly IPlatformUtilsService _platformUtilsService; + private readonly IAuthService _authService; + + Func _getOptionsFunc; + private IAccountsManagerHost _accountsManagerHost; + + public AccountsManager(IBroadcasterService broadcasterService, + IVaultTimeoutService vaultTimeoutService, + IStorageService secureStorageService, + IStateService stateService, + IPlatformUtilsService platformUtilsService, + IAuthService authService) + { + _broadcasterService = broadcasterService; + _vaultTimeoutService = vaultTimeoutService; + _secureStorageService = secureStorageService; + _stateService = stateService; + _platformUtilsService = platformUtilsService; + _authService = authService; + } + + private AppOptions Options => _getOptionsFunc?.Invoke() ?? new AppOptions { IosExtension = true }; + + public void Init(Func getOptionsFunc, IAccountsManagerHost accountsManagerHost) + { + _getOptionsFunc = getOptionsFunc; + _accountsManagerHost = accountsManagerHost; + + _broadcasterService.Subscribe(nameof(AccountsManager), OnMessage); + } + + public async Task NavigateOnAccountChangeAsync(bool? isAuthed = null) + { + // TODO: this could be improved by doing chain of responsability pattern + // but for now it may be an overkill, if logic gets more complex consider refactoring it + + var authed = isAuthed ?? await _stateService.IsAuthenticatedAsync(); + if (authed) + { + if (await _vaultTimeoutService.IsLoggedOutByTimeoutAsync() || + await _vaultTimeoutService.ShouldLogOutByTimeoutAsync()) + { + // 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; + _accountsManagerHost.Navigate(NavigationTarget.Login, new LoginNavigationParams(email)); + } + else if (await _vaultTimeoutService.IsLockedAsync() || + await _vaultTimeoutService.ShouldLockAsync()) + { + _accountsManagerHost.Navigate(NavigationTarget.Lock); + } + else if (Options.FromAutofillFramework && Options.SaveType.HasValue) + { + _accountsManagerHost.Navigate(NavigationTarget.AddEditCipher); + } + else if (Options.Uri != null) + { + _accountsManagerHost.Navigate(NavigationTarget.AutofillCiphers); + } + else if (Options.CreateSend != null) + { + _accountsManagerHost.Navigate(NavigationTarget.SendAddEdit); + } + else + { + _accountsManagerHost.Navigate(NavigationTarget.Home); + } + } + else + { + Options.HideAccountSwitcher = await _stateService.GetActiveUserIdAsync() == null; + if (await _vaultTimeoutService.IsLoggedOutByTimeoutAsync() || + await _vaultTimeoutService.ShouldLogOutByTimeoutAsync()) + { + // TODO implement orgIdentifier flow to SSO Login page, same as email flow below + // var orgIdentifier = await _stateService.GetOrgIdentifierAsync(); + + var email = await _stateService.GetEmailAsync(); + _accountsManagerHost.Navigate(NavigationTarget.Login, new LoginNavigationParams(email)); + } + else + { + _accountsManagerHost.Navigate(NavigationTarget.HomeLogin); + } + } + } + + private async void OnMessage(Message message) + { + switch (message.Command) + { + case AccountsManagerMessageCommands.LOCKED: + Locked(message.Data as Tuple); + break; + case AccountsManagerMessageCommands.LOCK_VAULT: + await _vaultTimeoutService.LockAsync(true); + break; + case AccountsManagerMessageCommands.LOGOUT: + LogOut(message.Data as Tuple); + break; + case AccountsManagerMessageCommands.LOGGED_OUT: + // Clean up old migrated key if they ever log out. + await _secureStorageService.RemoveAsync("oldKey"); + break; + case AccountsManagerMessageCommands.ADD_ACCOUNT: + AddAccount(); + break; + case AccountsManagerMessageCommands.ACCOUNT_ADDED: + await _accountsManagerHost.UpdateThemeAsync(); + break; + case AccountsManagerMessageCommands.SWITCHED_ACCOUNT: + await SwitchedAccountAsync(); + break; + } + } + + private void Locked(Tuple extras) + { + var userId = extras?.Item1; + var userInitiated = extras?.Item2 ?? false; + Device.BeginInvokeOnMainThread(async () => await LockedAsync(userId, userInitiated)); + } + + private async Task LockedAsync(string userId, bool userInitiated) + { + if (!await _stateService.IsActiveAccountAsync(userId)) + { + _platformUtilsService.ShowToast("info", null, AppResources.AccountLockedSuccessfully); + return; + } + + var autoPromptBiometric = !userInitiated; + if (autoPromptBiometric && Device.RuntimePlatform == Device.iOS) + { + var vaultTimeout = await _stateService.GetVaultTimeoutAsync(); + if (vaultTimeout == 0) + { + autoPromptBiometric = false; + } + } + + await _accountsManagerHost.SetPreviousPageInfoAsync(); + + Device.BeginInvokeOnMainThread(() => _accountsManagerHost.Navigate(NavigationTarget.Lock, new LockNavigationParams(autoPromptBiometric))); + } + + private void AddAccount() + { + Device.BeginInvokeOnMainThread(() => + { + Options.HideAccountSwitcher = false; + _accountsManagerHost.Navigate(NavigationTarget.HomeLogin); + }); + } + + private void LogOut(Tuple extras) + { + var userId = extras?.Item1; + var userInitiated = extras?.Item2 ?? true; + var expired = extras?.Item3 ?? false; + Device.BeginInvokeOnMainThread(async () => await LogOutAsync(userId, userInitiated, expired)); + } + + private async Task LogOutAsync(string userId, bool userInitiated, bool expired) + { + await AppHelpers.LogOutAsync(userId, userInitiated); + await NavigateOnAccountChangeAsync(); + _authService.LogOut(() => + { + if (expired) + { + _platformUtilsService.ShowToast("warning", null, AppResources.LoginExpired); + } + }); + } + + private async Task SwitchedAccountAsync() + { + await AppHelpers.OnAccountSwitchAsync(); + Device.BeginInvokeOnMainThread(async () => + { + if (await _vaultTimeoutService.ShouldTimeoutAsync()) + { + await _vaultTimeoutService.ExecuteTimeoutActionAsync(); + } + else + { + await NavigateOnAccountChangeAsync(); + } + await Task.Delay(50); + await _accountsManagerHost.UpdateThemeAsync(); + }); + } + } +} diff --git a/src/App/Utilities/AccountManagement/LockNavigationParams.cs b/src/App/Utilities/AccountManagement/LockNavigationParams.cs new file mode 100644 index 000000000..00ee04cbe --- /dev/null +++ b/src/App/Utilities/AccountManagement/LockNavigationParams.cs @@ -0,0 +1,14 @@ +using Bit.App.Abstractions; + +namespace Bit.App.Utilities.AccountManagement +{ + public class LockNavigationParams : INavigationParams + { + public LockNavigationParams(bool autoPromptBiometric = true) + { + AutoPromptBiometric = autoPromptBiometric; + } + + public bool AutoPromptBiometric { get; } + } +} diff --git a/src/App/Utilities/AccountManagement/LoginNavigationParams.cs b/src/App/Utilities/AccountManagement/LoginNavigationParams.cs new file mode 100644 index 000000000..2e0f9749c --- /dev/null +++ b/src/App/Utilities/AccountManagement/LoginNavigationParams.cs @@ -0,0 +1,14 @@ +using Bit.App.Abstractions; + +namespace Bit.App.Utilities.AccountManagement +{ + public class LoginNavigationParams : INavigationParams + { + public LoginNavigationParams(string email) + { + Email = email; + } + + public string Email { get; } + } +} diff --git a/src/Core/Enums/NavigationTarget.cs b/src/Core/Enums/NavigationTarget.cs new file mode 100644 index 000000000..d226ff283 --- /dev/null +++ b/src/Core/Enums/NavigationTarget.cs @@ -0,0 +1,13 @@ +namespace Bit.Core.Enums +{ + public enum NavigationTarget + { + HomeLogin, + Login, + Lock, + Home, + AddEditCipher, + AutofillCiphers, + SendAddEdit + } +} diff --git a/src/iOS.Autofill/CredentialProviderViewController.cs b/src/iOS.Autofill/CredentialProviderViewController.cs index e8ae87b4b..298a6e1cb 100644 --- a/src/iOS.Autofill/CredentialProviderViewController.cs +++ b/src/iOS.Autofill/CredentialProviderViewController.cs @@ -1,27 +1,32 @@ -using AuthenticationServices; +using System; +using System.Threading.Tasks; +using AuthenticationServices; using Bit.App.Abstractions; +using Bit.App.Models; +using Bit.App.Pages; +using Bit.App.Utilities; +using Bit.App.Utilities.AccountManagement; using Bit.Core.Abstractions; +using Bit.Core.Enums; using Bit.Core.Utilities; using Bit.iOS.Autofill.Models; using Bit.iOS.Core.Utilities; -using Foundation; -using System; -using System.Threading.Tasks; -using Bit.App.Pages; -using UIKit; -using Xamarin.Forms; -using Bit.App.Utilities; -using Bit.App.Models; using Bit.iOS.Core.Views; using CoreNFC; +using Foundation; +using UIKit; +using Xamarin.Forms; namespace Bit.iOS.Autofill { - public partial class CredentialProviderViewController : ASCredentialProviderViewController + public partial class CredentialProviderViewController : ASCredentialProviderViewController, IAccountsManagerHost { private Context _context; private NFCNdefReaderSession _nfcSession = null; private Core.NFCReaderDelegate _nfcDelegate = null; + private IAccountsManager _accountsManager; + + private readonly LazyResolve _stateService = new LazyResolve("stateService"); public CredentialProviderViewController(IntPtr handle) : base(handle) @@ -56,7 +61,7 @@ namespace Bit.iOS.Autofill } if (!await IsAuthed()) { - LaunchHomePage(); + await _accountsManager.NavigateOnAccountChangeAsync(false); } else if (await IsLocked()) { @@ -78,9 +83,8 @@ namespace Bit.iOS.Autofill public override async void ProvideCredentialWithoutUserInteraction(ASPasswordCredentialIdentity credentialIdentity) { InitAppIfNeeded(); - var stateService = ServiceContainer.Resolve("stateService"); - await stateService.SetPasswordRepromptAutofillAsync(false); - await stateService.SetPasswordVerifiedAutofillAsync(false); + await _stateService.Value.SetPasswordRepromptAutofillAsync(false); + await _stateService.Value.SetPasswordVerifiedAutofillAsync(false); if (!await IsAuthed() || await IsLocked()) { var err = new NSError(new NSString("ASExtensionErrorDomain"), @@ -97,7 +101,7 @@ namespace Bit.iOS.Autofill InitAppIfNeeded(); if (!await IsAuthed()) { - LaunchHomePage(); + await _accountsManager.NavigateOnAccountChangeAsync(false); return; } _context.CredentialIdentity = credentialIdentity; @@ -110,7 +114,7 @@ namespace Bit.iOS.Autofill _context.Configuring = true; if (!await IsAuthed()) { - LaunchHomePage(); + await _accountsManager.NavigateOnAccountChangeAsync(false); return; } CheckLock(() => PerformSegue("setupSegue", this)); @@ -229,7 +233,6 @@ namespace Bit.iOS.Autofill return; } - var stateService = ServiceContainer.Resolve("stateService"); var decCipher = await cipher.DecryptAsync(); if (decCipher.Reprompt != Bit.Core.Enums.CipherRepromptType.None) { @@ -237,13 +240,13 @@ namespace Bit.iOS.Autofill // already verified the password. if (!userInteraction) { - await stateService.SetPasswordRepromptAutofillAsync(true); + await _stateService.Value.SetPasswordRepromptAutofillAsync(true); var err = new NSError(new NSString("ASExtensionErrorDomain"), Convert.ToInt32(ASExtensionErrorCode.UserInteractionRequired), null); ExtensionContext?.CancelRequest(err); return; } - else if (!await stateService.GetPasswordVerifiedAutofillAsync()) + else if (!await _stateService.Value.GetPasswordVerifiedAutofillAsync()) { // Add a timeout to resolve keyboard not always showing up. await Task.Delay(250); @@ -258,10 +261,10 @@ namespace Bit.iOS.Autofill } } string totpCode = null; - var disableTotpCopy = await stateService.GetDisableAutoTotpCopyAsync(); + var disableTotpCopy = await _stateService.Value.GetDisableAutoTotpCopyAsync(); if (!disableTotpCopy.GetValueOrDefault(false)) { - var canAccessPremiumAsync = await stateService.CanAccessPremiumAsync(); + var canAccessPremiumAsync = await _stateService.Value.CanAccessPremiumAsync(); if (!string.IsNullOrWhiteSpace(decCipher.Login.Totp) && (canAccessPremiumAsync || cipher.OrganizationUseTotp)) { @@ -275,8 +278,7 @@ namespace Bit.iOS.Autofill private async void CheckLock(Action notLockedAction) { - var stateService = ServiceContainer.Resolve("stateService"); - if (await IsLocked() || await stateService.GetPasswordRepromptAutofillAsync()) + if (await IsLocked() || await _stateService.Value.GetPasswordRepromptAutofillAsync()) { PerformSegue("lockPasswordSegue", this); } @@ -294,8 +296,7 @@ namespace Bit.iOS.Autofill private Task IsAuthed() { - var stateService = ServiceContainer.Resolve("stateService"); - return stateService.IsAuthenticatedAsync(); + return _stateService.Value.IsAuthenticatedAsync(); } private void LogoutIfAuthed() @@ -304,8 +305,7 @@ namespace Bit.iOS.Autofill { if (await IsAuthed()) { - var stateService = ServiceContainer.Resolve("stateService"); - await AppHelpers.LogOutAsync(await stateService.GetActiveUserIdAsync()); + await AppHelpers.LogOutAsync(await _stateService.Value.GetActiveUserIdAsync()); var deviceActionService = ServiceContainer.Resolve("deviceActionService"); if (deviceActionService.SystemMajorVersion() >= 12) { @@ -331,12 +331,16 @@ namespace Bit.iOS.Autofill Bit.Core.Constants.iOSAutoFillClearCiphersCacheKey, Bit.Core.Constants.iOSAllClearCipherCacheKeys); iOSCoreHelpers.InitLogger(); iOSCoreHelpers.Bootstrap(); - var app = new App.App(new AppOptions { IosExtension = true }); + var appOptions = new AppOptions { IosExtension = true }; + var app = new App.App(appOptions); ThemeManager.SetTheme(app.Resources); iOSCoreHelpers.AppearanceAdjustments(); _nfcDelegate = new Core.NFCReaderDelegate((success, message) => messagingService.Send("gotYubiKeyOTP", message)); iOSCoreHelpers.SubscribeBroadcastReceiver(this, _nfcSession, _nfcDelegate); + + _accountsManager = ServiceContainer.Resolve("accountsManager"); + _accountsManager.Init(() => appOptions, this); } private void InitAppIfNeeded() @@ -514,5 +518,35 @@ namespace Bit.iOS.Autofill updateTempPasswordController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen; PresentViewController(updateTempPasswordController, true, null); } + + public Task SetPreviousPageInfoAsync() => Task.CompletedTask; + public Task UpdateThemeAsync() => Task.CompletedTask; + + public void Navigate(NavigationTarget navTarget, INavigationParams navParams = null) + { + switch (navTarget) + { + case NavigationTarget.HomeLogin: + DismissViewController(false, () => LaunchHomePage()); + break; + case NavigationTarget.Login: + if (navParams is LoginNavigationParams loginParams) + { + DismissViewController(false, () => LaunchLoginFlow(loginParams.Email)); + } + else + { + DismissViewController(false, () => LaunchLoginFlow()); + } + break; + case NavigationTarget.Lock: + DismissViewController(false, () => PerformSegue("lockPasswordSegue", this)); + break; + case NavigationTarget.AutofillCiphers: + case NavigationTarget.Home: + DismissViewController(false, () => PerformSegue("loginListSegue", this)); + break; + } + } } } diff --git a/src/iOS.Autofill/LockPasswordViewController.cs b/src/iOS.Autofill/LockPasswordViewController.cs index d1cf7e009..1913d60a0 100644 --- a/src/iOS.Autofill/LockPasswordViewController.cs +++ b/src/iOS.Autofill/LockPasswordViewController.cs @@ -1,10 +1,17 @@ using System; +using Bit.App.Controls; +using Bit.iOS.Core.Utilities; using UIKit; namespace Bit.iOS.Autofill { - public partial class LockPasswordViewController : Core.Controllers.LockPasswordViewController + public partial class LockPasswordViewController : Core.Controllers.BaseLockPasswordViewController { + AccountSwitchingOverlayView _accountSwitchingOverlayView; + AccountSwitchingOverlayHelper _accountSwitchingOverlayHelper; + + public override UITableView TableView => MainTableView; + public LockPasswordViewController(IntPtr handle) : base(handle) { @@ -20,6 +27,21 @@ namespace Bit.iOS.Autofill public override Action Success => () => CPViewController.DismissLockAndContinue(); public override Action Cancel => () => CPViewController.CompleteRequest(); + public override async void ViewDidLoad() + { + base.ViewDidLoad(); + + _accountSwitchingOverlayHelper = new AccountSwitchingOverlayHelper(); + AccountSwitchingBarButton.Image = await _accountSwitchingOverlayHelper.CreateAvatarImageAsync(); + + _accountSwitchingOverlayView = _accountSwitchingOverlayHelper.CreateAccountSwitchingOverlayView(OverlayView); + } + + partial void AccountSwitchingBarButton_Activated(UIBarButtonItem sender) + { + _accountSwitchingOverlayHelper.OnToolbarItemActivated(_accountSwitchingOverlayView, OverlayView); + } + partial void SubmitButton_Activated(UIBarButtonItem sender) { var task = CheckPasswordAsync(); diff --git a/src/iOS.Autofill/LockPasswordViewController.designer.cs b/src/iOS.Autofill/LockPasswordViewController.designer.cs index 3b37484c6..4fa4f737e 100644 --- a/src/iOS.Autofill/LockPasswordViewController.designer.cs +++ b/src/iOS.Autofill/LockPasswordViewController.designer.cs @@ -1,64 +1,79 @@ // WARNING // -// This file has been generated automatically by Visual Studio from the outlets and -// actions declared in your storyboard file. -// Manual changes to this file will not be maintained. +// This file has been generated automatically by Visual Studio to store outlets and +// actions made in the UI designer. If it is removed, they will be lost. +// Manual changes to this file may not be handled correctly. // using Foundation; -using System; using System.CodeDom.Compiler; -using UIKit; namespace Bit.iOS.Autofill { - [Register ("LockPasswordViewController")] - partial class LockPasswordViewController - { - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UIBarButtonItem CancelButton { get; set; } + [Register ("LockPasswordViewController")] + partial class LockPasswordViewController + { + [Outlet] + UIKit.UIBarButtonItem AccountSwitchingBarButton { get; set; } - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UITableView MainTableView { get; set; } + [Outlet] + [GeneratedCode ("iOS Designer", "1.0")] + UIKit.UIBarButtonItem CancelButton { get; set; } - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UINavigationItem NavItem { get; set; } + [Outlet] + [GeneratedCode ("iOS Designer", "1.0")] + UIKit.UITableView MainTableView { get; set; } - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UIBarButtonItem SubmitButton { get; set; } + [Outlet] + [GeneratedCode ("iOS Designer", "1.0")] + UIKit.UINavigationItem NavItem { get; set; } - [Action ("CancelButton_Activated:")] - [GeneratedCode ("iOS Designer", "1.0")] - partial void CancelButton_Activated (UIKit.UIBarButtonItem sender); + [Outlet] + UIKit.UIView OverlayView { get; set; } - [Action ("SubmitButton_Activated:")] - [GeneratedCode ("iOS Designer", "1.0")] - partial void SubmitButton_Activated (UIKit.UIBarButtonItem sender); + [Outlet] + [GeneratedCode ("iOS Designer", "1.0")] + UIKit.UIBarButtonItem SubmitButton { get; set; } - void ReleaseDesignerOutlets () - { - if (CancelButton != null) { - CancelButton.Dispose (); - CancelButton = null; - } + [Action ("AccountSwitchingBarButton_Activated:")] + partial void AccountSwitchingBarButton_Activated (UIKit.UIBarButtonItem sender); - if (MainTableView != null) { - MainTableView.Dispose (); - MainTableView = null; - } + [Action ("CancelButton_Activated:")] + partial void CancelButton_Activated (UIKit.UIBarButtonItem sender); - if (NavItem != null) { - NavItem.Dispose (); - NavItem = null; - } + [Action ("SubmitButton_Activated:")] + partial void SubmitButton_Activated (UIKit.UIBarButtonItem sender); + + void ReleaseDesignerOutlets () + { + if (AccountSwitchingBarButton != null) { + AccountSwitchingBarButton.Dispose (); + AccountSwitchingBarButton = null; + } - if (SubmitButton != null) { - SubmitButton.Dispose (); - SubmitButton = null; - } - } - } -} \ No newline at end of file + if (CancelButton != null) { + CancelButton.Dispose (); + CancelButton = null; + } + + if (MainTableView != null) { + MainTableView.Dispose (); + MainTableView = null; + } + + if (NavItem != null) { + NavItem.Dispose (); + NavItem = null; + } + + if (SubmitButton != null) { + SubmitButton.Dispose (); + SubmitButton = null; + } + + if (OverlayView != null) { + OverlayView.Dispose (); + OverlayView = null; + } + } + } +} diff --git a/src/iOS.Autofill/LoginListViewController.cs b/src/iOS.Autofill/LoginListViewController.cs index 3233be168..59691c505 100644 --- a/src/iOS.Autofill/LoginListViewController.cs +++ b/src/iOS.Autofill/LoginListViewController.cs @@ -1,19 +1,21 @@ using System; +using Bit.App.Abstractions; +using Bit.App.Controls; +using Bit.App.Resources; +using Bit.Core.Abstractions; +using Bit.Core.Utilities; using Bit.iOS.Autofill.Models; +using Bit.iOS.Autofill.Utilities; +using Bit.iOS.Core.Controllers; +using Bit.iOS.Core.Utilities; +using Bit.iOS.Core.Views; +using CoreFoundation; using Foundation; using UIKit; -using Bit.iOS.Core.Controllers; -using Bit.App.Resources; -using Bit.iOS.Core.Views; -using Bit.iOS.Autofill.Utilities; -using Bit.iOS.Core.Utilities; -using Bit.Core.Utilities; -using Bit.Core.Abstractions; -using Bit.App.Abstractions; namespace Bit.iOS.Autofill { - public partial class LoginListViewController : ExtendedUITableViewController + public partial class LoginListViewController : ExtendedUIViewController { public LoginListViewController(IntPtr handle) : base(handle) @@ -26,17 +28,30 @@ namespace Bit.iOS.Autofill public CredentialProviderViewController CPViewController { get; set; } public IPasswordRepromptService PasswordRepromptService { get; private set; } + AccountSwitchingOverlayView _accountSwitchingOverlayView; + AccountSwitchingOverlayHelper _accountSwitchingOverlayHelper; + + LazyResolve _broadcasterService = new LazyResolve("broadcasterService"); + LazyResolve _logger = new LazyResolve("logger"); + bool _alreadyLoadItemsOnce = false; + public async override void ViewDidLoad() { base.ViewDidLoad(); + + SubscribeSyncCompleted(); + NavItem.Title = AppResources.Items; CancelBarButton.Title = AppResources.Cancel; TableView.RowHeight = UITableView.AutomaticDimension; TableView.EstimatedRowHeight = 44; + TableView.BackgroundColor = ThemeHelpers.BackgroundColor; TableView.Source = new TableSource(this); await ((TableSource)TableView.Source).LoadItemsAsync(); + _alreadyLoadItemsOnce = true; + var storageService = ServiceContainer.Resolve("storageService"); var needsAutofillReplacement = await storageService.GetAsync( Core.Constants.AutofillNeedsIdentityReplacementKey); @@ -44,6 +59,16 @@ namespace Bit.iOS.Autofill { await ASHelpers.ReplaceAllIdentities(); } + + _accountSwitchingOverlayHelper = new AccountSwitchingOverlayHelper(); + AccountSwitchingBarButton.Image = await _accountSwitchingOverlayHelper.CreateAvatarImageAsync(); + + _accountSwitchingOverlayView = _accountSwitchingOverlayHelper.CreateAccountSwitchingOverlayView(OverlayView); + } + + partial void AccountSwitchingBarButton_Activated(UIBarButtonItem sender) + { + _accountSwitchingOverlayHelper.OnToolbarItemActivated(_accountSwitchingOverlayView, OverlayView); } partial void CancelBarButton_Activated(UIBarButtonItem sender) @@ -88,6 +113,35 @@ namespace Bit.iOS.Autofill } } + private void SubscribeSyncCompleted() + { + _broadcasterService.Value.Subscribe(nameof(LoginListViewController), message => + { + if (message.Command == "syncCompleted" && _alreadyLoadItemsOnce) + { + DispatchQueue.MainQueue.DispatchAsync(async () => + { + try + { + await ((TableSource)TableView.Source).LoadItemsAsync(); + TableView.ReloadData(); + } + catch (Exception ex) + { + _logger.Value.Exception(ex); + } + }); + } + }); + } + + public override void ViewDidUnload() + { + base.ViewDidUnload(); + + _broadcasterService.Value.Unsubscribe(nameof(LoginListViewController)); + } + public void DismissModal() { DismissViewController(true, async () => @@ -99,13 +153,11 @@ namespace Bit.iOS.Autofill public class TableSource : ExtensionTableSource { - private Context _context; private LoginListViewController _controller; public TableSource(LoginListViewController controller) : base(controller.Context, controller) { - _context = controller.Context; _controller = controller; } diff --git a/src/iOS.Autofill/LoginListViewController.designer.cs b/src/iOS.Autofill/LoginListViewController.designer.cs index baaf3e88e..8bdd8059c 100644 --- a/src/iOS.Autofill/LoginListViewController.designer.cs +++ b/src/iOS.Autofill/LoginListViewController.designer.cs @@ -1,59 +1,89 @@ // WARNING // -// This file has been generated automatically by Visual Studio from the outlets and -// actions declared in your storyboard file. -// Manual changes to this file will not be maintained. +// This file has been generated automatically by Visual Studio to store outlets and +// actions made in the UI designer. If it is removed, they will be lost. +// Manual changes to this file may not be handled correctly. // using Foundation; -using System; using System.CodeDom.Compiler; -using UIKit; namespace Bit.iOS.Autofill { - [Register ("LoginListViewController")] - partial class LoginListViewController - { - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UIBarButtonItem AddBarButton { get; set; } + [Register ("LoginListViewController")] + partial class LoginListViewController + { + [Outlet] + UIKit.UIBarButtonItem AccountSwitchingBarButton { get; set; } - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UIBarButtonItem CancelBarButton { get; set; } + [Outlet] + [GeneratedCode ("iOS Designer", "1.0")] + UIKit.UIBarButtonItem AddBarButton { get; set; } - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UINavigationItem NavItem { get; set; } + [Outlet] + [GeneratedCode ("iOS Designer", "1.0")] + UIKit.UIBarButtonItem CancelBarButton { get; set; } - [Action ("AddBarButton_Activated:")] - [GeneratedCode ("iOS Designer", "1.0")] - partial void AddBarButton_Activated (UIKit.UIBarButtonItem sender); + [Outlet] + UIKit.UIView MainView { get; set; } - [Action ("CancelBarButton_Activated:")] - [GeneratedCode ("iOS Designer", "1.0")] - partial void CancelBarButton_Activated (UIKit.UIBarButtonItem sender); + [Outlet] + [GeneratedCode ("iOS Designer", "1.0")] + UIKit.UINavigationItem NavItem { get; set; } - [Action ("SearchBarButton_Activated:")] - [GeneratedCode ("iOS Designer", "1.0")] - partial void SearchBarButton_Activated (UIKit.UIBarButtonItem sender); + [Outlet] + UIKit.UIView OverlayView { get; set; } - void ReleaseDesignerOutlets () - { - if (AddBarButton != null) { - AddBarButton.Dispose (); - AddBarButton = null; - } + [Outlet] + UIKit.UITableView TableView { get; set; } - if (CancelBarButton != null) { - CancelBarButton.Dispose (); - CancelBarButton = null; - } + [Action ("AccountSwitchingBarButton_Activated:")] + partial void AccountSwitchingBarButton_Activated (UIKit.UIBarButtonItem sender); - if (NavItem != null) { - NavItem.Dispose (); - NavItem = null; - } - } - } -} \ No newline at end of file + [Action ("AddBarButton_Activated:")] + partial void AddBarButton_Activated (UIKit.UIBarButtonItem sender); + + [Action ("CancelBarButton_Activated:")] + partial void CancelBarButton_Activated (UIKit.UIBarButtonItem sender); + + [Action ("SearchBarButton_Activated:")] + partial void SearchBarButton_Activated (UIKit.UIBarButtonItem sender); + + void ReleaseDesignerOutlets () + { + if (AddBarButton != null) { + AddBarButton.Dispose (); + AddBarButton = null; + } + + if (CancelBarButton != null) { + CancelBarButton.Dispose (); + CancelBarButton = null; + } + + if (MainView != null) { + MainView.Dispose (); + MainView = null; + } + + if (NavItem != null) { + NavItem.Dispose (); + NavItem = null; + } + + if (OverlayView != null) { + OverlayView.Dispose (); + OverlayView = null; + } + + if (TableView != null) { + TableView.Dispose (); + TableView = null; + } + + if (AccountSwitchingBarButton != null) { + AccountSwitchingBarButton.Dispose (); + AccountSwitchingBarButton = null; + } + } + } +} diff --git a/src/iOS.Autofill/MainInterface.storyboard b/src/iOS.Autofill/MainInterface.storyboard index a5e219e6d..c2da85eff 100644 --- a/src/iOS.Autofill/MainInterface.storyboard +++ b/src/iOS.Autofill/MainInterface.storyboard @@ -1,7 +1,11 @@ - - + + + - + + + + @@ -9,30 +13,27 @@ - - - - - + - + + - + + - @@ -45,7 +46,7 @@ - + @@ -67,7 +68,7 @@ - + @@ -87,7 +88,7 @@ - + @@ -125,50 +126,79 @@ - - - + + + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - + + + + + + + + + + + + + + @@ -186,13 +216,17 @@ + + + + - + @@ -202,7 +236,7 @@ - + @@ -222,37 +256,34 @@ - - - - - + - + + - + - - - - - + + + + + @@ -287,7 +318,7 @@ - + @@ -301,15 +332,11 @@ - - - - - + @@ -318,88 +345,139 @@ - + - + - + - - - + + + - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - + - - - - + + + + + + - - + + - + - - - - - + + - - - - - + + + + + - + @@ -429,7 +507,7 @@ - + @@ -450,7 +528,7 @@ - + @@ -463,10 +541,10 @@ - + - + @@ -506,7 +584,7 @@ - + @@ -522,8 +600,19 @@ + + + + + + + + + + + - \ No newline at end of file + diff --git a/src/iOS.Autofill/Utilities/AutofillHelpers.cs b/src/iOS.Autofill/Utilities/AutofillHelpers.cs index 2f274b913..a2da552b4 100644 --- a/src/iOS.Autofill/Utilities/AutofillHelpers.cs +++ b/src/iOS.Autofill/Utilities/AutofillHelpers.cs @@ -15,7 +15,7 @@ namespace Bit.iOS.Autofill.Utilities { public async static Task TableRowSelectedAsync(UITableView tableView, NSIndexPath indexPath, ExtensionTableSource tableSource, CredentialProviderViewController cpViewController, - UITableViewController controller, IPasswordRepromptService passwordRepromptService, + UIViewController controller, IPasswordRepromptService passwordRepromptService, string loginAddSegue) { tableView.DeselectRow(indexPath, true); diff --git a/src/iOS.Core/Controllers/BaseLockPasswordViewController.cs b/src/iOS.Core/Controllers/BaseLockPasswordViewController.cs new file mode 100644 index 000000000..2dc1744c1 --- /dev/null +++ b/src/iOS.Core/Controllers/BaseLockPasswordViewController.cs @@ -0,0 +1,509 @@ +using System; +using UIKit; +using Foundation; +using Bit.iOS.Core.Views; +using Bit.App.Resources; +using Bit.iOS.Core.Utilities; +using Bit.App.Abstractions; +using Bit.Core.Abstractions; +using Bit.Core.Utilities; +using System.Threading.Tasks; +using Bit.App.Utilities; +using Bit.Core.Models.Domain; +using Bit.Core.Enums; +using Bit.App.Pages; +using Bit.App.Models; +using Xamarin.Forms; +using Bit.Core; + +namespace Bit.iOS.Core.Controllers +{ + public abstract class BaseLockPasswordViewController : ExtendedUIViewController + { + private IVaultTimeoutService _vaultTimeoutService; + private ICryptoService _cryptoService; + private IDeviceActionService _deviceActionService; + private IStateService _stateService; + private IStorageService _secureStorageService; + private IPlatformUtilsService _platformUtilsService; + private IBiometricService _biometricService; + private IKeyConnectorService _keyConnectorService; + private bool _isPinProtected; + private bool _isPinProtectedWithKey; + private bool _pinLock; + private bool _biometricLock; + private bool _biometricIntegrityValid = true; + private bool _passwordReprompt = false; + private bool _usesKeyConnector; + private bool _biometricUnlockOnly = false; + + protected bool autofillExtension = false; + + public BaseLockPasswordViewController(IntPtr handle) + : base(handle) + { } + + public abstract UINavigationItem BaseNavItem { get; } + public abstract UIBarButtonItem BaseCancelButton { get; } + public abstract UIBarButtonItem BaseSubmitButton { get; } + public abstract Action Success { get; } + public abstract Action Cancel { get; } + + public FormEntryTableViewCell MasterPasswordCell { get; set; } = new FormEntryTableViewCell( + AppResources.MasterPassword, useButton: true); + + public string BiometricIntegrityKey { get; set; } + + public UITableViewCell BiometricCell + { + get + { + var cell = new UITableViewCell(); + cell.BackgroundColor = ThemeHelpers.BackgroundColor; + if (_biometricIntegrityValid) + { + var biometricButtonText = _deviceActionService.SupportsFaceBiometric() ? + AppResources.UseFaceIDToUnlock : AppResources.UseFingerprintToUnlock; + cell.TextLabel.TextColor = ThemeHelpers.PrimaryColor; + cell.TextLabel.Text = biometricButtonText; + } + else + { + cell.TextLabel.TextColor = ThemeHelpers.DangerColor; + cell.TextLabel.Font = ThemeHelpers.GetDangerFont(); + cell.TextLabel.Lines = 0; + cell.TextLabel.LineBreakMode = UILineBreakMode.WordWrap; + cell.TextLabel.Text = AppResources.BiometricInvalidatedExtension; + } + return cell; + } + } + + public abstract UITableView TableView { get; } + + public override async void ViewDidLoad() + { + _vaultTimeoutService = ServiceContainer.Resolve("vaultTimeoutService"); + _cryptoService = ServiceContainer.Resolve("cryptoService"); + _deviceActionService = ServiceContainer.Resolve("deviceActionService"); + _stateService = ServiceContainer.Resolve("stateService"); + _secureStorageService = ServiceContainer.Resolve("secureStorageService"); + _platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); + _biometricService = ServiceContainer.Resolve("biometricService"); + _keyConnectorService = ServiceContainer.Resolve("keyConnectorService"); + + // We re-use the lock screen for autofill extension to verify master password + // when trying to access protected items. + if (autofillExtension && await _stateService.GetPasswordRepromptAutofillAsync()) + { + _passwordReprompt = true; + _isPinProtected = false; + _isPinProtectedWithKey = false; + _pinLock = false; + _biometricLock = false; + } + else + { + (_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; + } + + if (_pinLock) + { + BaseNavItem.Title = AppResources.VerifyPIN; + } + else if (_usesKeyConnector) + { + BaseNavItem.Title = AppResources.UnlockVault; + } + else + { + BaseNavItem.Title = AppResources.VerifyMasterPassword; + } + + BaseCancelButton.Title = AppResources.Cancel; + + if (_biometricUnlockOnly) + { + BaseSubmitButton.Title = null; + BaseSubmitButton.Enabled = false; + } + else + { + BaseSubmitButton.Title = AppResources.Submit; + } + + var descriptor = UIFontDescriptor.PreferredBody; + + if (!_biometricUnlockOnly) + { + MasterPasswordCell.Label.Text = _pinLock ? AppResources.PIN : AppResources.MasterPassword; + MasterPasswordCell.TextField.SecureTextEntry = true; + MasterPasswordCell.TextField.ReturnKeyType = UIReturnKeyType.Go; + MasterPasswordCell.TextField.ShouldReturn += (UITextField tf) => + { + CheckPasswordAsync().GetAwaiter().GetResult(); + return true; + }; + if (_pinLock) + { + MasterPasswordCell.TextField.KeyboardType = UIKeyboardType.NumberPad; + } + MasterPasswordCell.Button.TitleLabel.Font = UIFont.FromName("bwi-font", 28f); + MasterPasswordCell.Button.SetTitle(BitwardenIcons.Eye, UIControlState.Normal); + MasterPasswordCell.Button.TouchUpInside += (sender, e) => + { + MasterPasswordCell.TextField.SecureTextEntry = !MasterPasswordCell.TextField.SecureTextEntry; + MasterPasswordCell.Button.SetTitle(MasterPasswordCell.TextField.SecureTextEntry ? BitwardenIcons.Eye : BitwardenIcons.EyeSlash, UIControlState.Normal); + }; + } + + if (TableView != null) + { + TableView.BackgroundColor = ThemeHelpers.BackgroundColor; + TableView.SeparatorColor = ThemeHelpers.SeparatorColor; + } + + TableView.RowHeight = UITableView.AutomaticDimension; + TableView.EstimatedRowHeight = 70; + TableView.Source = new TableSource(this); + TableView.AllowsSelection = true; + + base.ViewDidLoad(); + + if (_biometricLock) + { + if (!_biometricIntegrityValid) + { + return; + } + var tasks = Task.Run(async () => + { + await Task.Delay(500); + NSRunLoop.Main.BeginInvokeOnMainThread(async () => await PromptBiometricAsync()); + }); + } + } + + public override async void ViewDidAppear(bool animated) + { + base.ViewDidAppear(animated); + + // Users with key connector and without biometric or pin has no MP to unlock with + if (_usesKeyConnector) + { + if (!(_pinLock || _biometricLock) || + (_biometricLock && !_biometricIntegrityValid)) + { + PromptSSO(); + } + } + else if (!_biometricLock || !_biometricIntegrityValid) + { + MasterPasswordCell.TextField.BecomeFirstResponder(); + } + } + + protected async Task CheckPasswordAsync() + { + if (string.IsNullOrWhiteSpace(MasterPasswordCell.TextField.Text)) + { + var alert = Dialogs.CreateAlert(AppResources.AnErrorHasOccurred, + string.Format(AppResources.ValidationFieldRequired, + _pinLock ? AppResources.PIN : AppResources.MasterPassword), + AppResources.Ok); + PresentViewController(alert, true, null); + return; + } + + var email = await _stateService.GetEmailAsync(); + var kdf = await _stateService.GetKdfTypeAsync(); + var kdfIterations = await _stateService.GetKdfIterationsAsync(); + var inputtedValue = MasterPasswordCell.TextField.Text; + + if (_pinLock) + { + var failed = true; + try + { + if (_isPinProtected) + { + var key = await _cryptoService.MakeKeyFromPinAsync(inputtedValue, email, + kdf.GetValueOrDefault(KdfType.PBKDF2_SHA256), kdfIterations.GetValueOrDefault(5000), + await _stateService.GetPinProtectedKeyAsync()); + var encKey = await _cryptoService.GetEncKeyAsync(key); + var protectedPin = await _stateService.GetProtectedPinAsync(); + var decPin = await _cryptoService.DecryptToUtf8Async(new EncString(protectedPin), encKey); + failed = decPin != inputtedValue; + if (!failed) + { + await AppHelpers.ResetInvalidUnlockAttemptsAsync(); + await SetKeyAndContinueAsync(key); + } + } + else + { + var key2 = await _cryptoService.MakeKeyFromPinAsync(inputtedValue, email, + kdf.GetValueOrDefault(KdfType.PBKDF2_SHA256), kdfIterations.GetValueOrDefault(5000)); + failed = false; + await AppHelpers.ResetInvalidUnlockAttemptsAsync(); + await SetKeyAndContinueAsync(key2); + } + } + catch + { + failed = true; + } + if (failed) + { + var invalidUnlockAttempts = await AppHelpers.IncrementInvalidUnlockAttemptsAsync(); + if (invalidUnlockAttempts >= 5) + { + await LogOutAsync(); + return; + } + InvalidValue(); + } + } + else + { + var key2 = await _cryptoService.MakeKeyAsync(inputtedValue, email, kdf, kdfIterations); + + var storedKeyHash = await _cryptoService.GetKeyHashAsync(); + if (storedKeyHash == null) + { + var oldKey = await _secureStorageService.GetAsync("oldKey"); + if (key2.KeyB64 == oldKey) + { + var localKeyHash = await _cryptoService.HashPasswordAsync(inputtedValue, key2, HashPurpose.LocalAuthorization); + await _secureStorageService.RemoveAsync("oldKey"); + await _cryptoService.SetKeyHashAsync(localKeyHash); + } + } + var passwordValid = await _cryptoService.CompareAndUpdateKeyHashAsync(inputtedValue, key2); + if (passwordValid) + { + if (_isPinProtected) + { + 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)); + await _stateService.SetPinProtectedKeyAsync(await _cryptoService.EncryptAsync(key2.Key, pinKey)); + } + await AppHelpers.ResetInvalidUnlockAttemptsAsync(); + await SetKeyAndContinueAsync(key2, true); + } + else + { + var invalidUnlockAttempts = await AppHelpers.IncrementInvalidUnlockAttemptsAsync(); + if (invalidUnlockAttempts >= 5) + { + await LogOutAsync(); + return; + } + InvalidValue(); + } + } + } + + public async Task PromptBiometricAsync() + { + if (!_biometricLock || !_biometricIntegrityValid) + { + return; + } + var success = await _platformUtilsService.AuthenticateBiometricAsync(null, + _pinLock ? AppResources.PIN : AppResources.MasterPassword, + () => MasterPasswordCell.TextField.BecomeFirstResponder()); + await _stateService.SetBiometricLockedAsync(!success); + if (success) + { + DoContinue(); + } + } + + public void PromptSSO() + { + var loginPage = new LoginSsoPage(); + var app = new App.App(new AppOptions { IosExtension = true }); + ThemeManager.SetTheme(app.Resources); + ThemeManager.ApplyResourcesToPage(loginPage); + if (loginPage.BindingContext is LoginSsoPageViewModel vm) + { + vm.SsoAuthSuccessAction = () => DoContinue(); + vm.CloseAction = Cancel; + } + + var navigationPage = new NavigationPage(loginPage); + var loginController = navigationPage.CreateViewController(); + loginController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen; + PresentViewController(loginController, true, null); + } + + private async Task SetKeyAndContinueAsync(SymmetricCryptoKey key, bool masterPassword = false) + { + var hasKey = await _cryptoService.HasKeyAsync(); + if (!hasKey) + { + await _cryptoService.SetKeyAsync(key); + } + DoContinue(masterPassword); + } + + private async void DoContinue(bool masterPassword = false) + { + if (masterPassword) + { + await _stateService.SetPasswordVerifiedAutofillAsync(true); + } + await EnableBiometricsIfNeeded(); + await _stateService.SetBiometricLockedAsync(false); + MasterPasswordCell.TextField.ResignFirstResponder(); + Success(); + } + + private async Task EnableBiometricsIfNeeded() + { + // Re-enable biometrics if initial use + if (_biometricLock & !_biometricIntegrityValid) + { + await _biometricService.SetupBiometricAsync(BiometricIntegrityKey); + } + } + + private void InvalidValue() + { + var alert = Dialogs.CreateAlert(AppResources.AnErrorHasOccurred, + string.Format(null, _pinLock ? AppResources.PIN : AppResources.InvalidMasterPassword), + AppResources.Ok, (a) => + { + + MasterPasswordCell.TextField.Text = string.Empty; + MasterPasswordCell.TextField.BecomeFirstResponder(); + }); + PresentViewController(alert, true, null); + } + + private async Task LogOutAsync() + { + await AppHelpers.LogOutAsync(await _stateService.GetActiveUserIdAsync()); + var authService = ServiceContainer.Resolve("authService"); + authService.LogOut(() => + { + Cancel?.Invoke(); + }); + } + + public class TableSource : ExtendedUITableViewSource + { + private readonly BaseLockPasswordViewController _controller; + + public TableSource(BaseLockPasswordViewController controller) + { + _controller = controller; + } + + public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath) + { + if (indexPath.Section == 0) + { + if (indexPath.Row == 0) + { + if (_controller._biometricUnlockOnly) + { + return _controller.BiometricCell; + } + else + { + return _controller.MasterPasswordCell; + } + } + } + else if (indexPath.Section == 1) + { + if (indexPath.Row == 0) + { + if (_controller._passwordReprompt) + { + var cell = new ExtendedUITableViewCell(); + cell.TextLabel.TextColor = ThemeHelpers.DangerColor; + cell.TextLabel.Font = ThemeHelpers.GetDangerFont(); + cell.TextLabel.Lines = 0; + cell.TextLabel.LineBreakMode = UILineBreakMode.WordWrap; + cell.TextLabel.Text = AppResources.PasswordConfirmationDesc; + return cell; + } + else if (!_controller._biometricUnlockOnly) + { + return _controller.BiometricCell; + } + } + } + return new ExtendedUITableViewCell(); + } + + public override nfloat GetHeightForRow(UITableView tableView, NSIndexPath indexPath) + { + return UITableView.AutomaticDimension; + } + + public override nint NumberOfSections(UITableView tableView) + { + return (!_controller._biometricUnlockOnly && _controller._biometricLock) || + _controller._passwordReprompt + ? 2 + : 1; + } + + public override nint RowsInSection(UITableView tableview, nint section) + { + if (section <= 1) + { + return 1; + } + return 0; + } + + public override nfloat GetHeightForHeader(UITableView tableView, nint section) + { + return section == 1 ? 0.00001f : UITableView.AutomaticDimension; + } + + public override string TitleForHeader(UITableView tableView, nint section) + { + return null; + } + + public override void RowSelected(UITableView tableView, NSIndexPath indexPath) + { + tableView.DeselectRow(indexPath, true); + tableView.EndEditing(true); + if (indexPath.Row == 0 && + ((_controller._biometricUnlockOnly && indexPath.Section == 0) || + indexPath.Section == 1)) + { + var task = _controller.PromptBiometricAsync(); + return; + } + var cell = tableView.CellAt(indexPath); + if (cell == null) + { + return; + } + if (cell is ISelectable selectableCell) + { + selectableCell.Select(); + } + } + } + } +} + diff --git a/src/iOS.Core/Controllers/LockPasswordViewController.cs b/src/iOS.Core/Controllers/LockPasswordViewController.cs index 5f4bfcee4..fbce117ca 100644 --- a/src/iOS.Core/Controllers/LockPasswordViewController.cs +++ b/src/iOS.Core/Controllers/LockPasswordViewController.cs @@ -18,6 +18,8 @@ using Bit.Core; namespace Bit.iOS.Core.Controllers { + // TODO: Leaving this here until all inheritance is changed to use BaseLockPasswordViewController instead of UITableViewController + [Obsolete("Use BaseLockPasswordViewController instead")] public abstract class LockPasswordViewController : ExtendedUITableViewController { private IVaultTimeoutService _vaultTimeoutService; diff --git a/src/iOS.Core/Utilities/AccountSwitchingOverlayHelper.cs b/src/iOS.Core/Utilities/AccountSwitchingOverlayHelper.cs new file mode 100644 index 000000000..2f771677b --- /dev/null +++ b/src/iOS.Core/Utilities/AccountSwitchingOverlayHelper.cs @@ -0,0 +1,69 @@ +using System.Threading.Tasks; +using Bit.App.Controls; +using Bit.Core.Abstractions; +using Bit.Core.Utilities; +using UIKit; +using Xamarin.Forms; +using Xamarin.Forms.Platform.iOS; + +namespace Bit.iOS.Core.Utilities +{ + public class AccountSwitchingOverlayHelper + { + IStateService _stateService; + IMessagingService _messagingService; + ILogger _logger; + + public AccountSwitchingOverlayHelper() + { + _stateService = ServiceContainer.Resolve("stateService"); + _messagingService = ServiceContainer.Resolve("messagingService"); + _logger = ServiceContainer.Resolve("logger"); + } + + public async Task CreateAvatarImageAsync() + { + var avatarImageSource = new AvatarImageSource(await _stateService.GetNameAsync(), await _stateService.GetEmailAsync()); + var avatarUIImage = await avatarImageSource.GetNativeImageAsync(); + return avatarUIImage.ImageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal); + } + + public AccountSwitchingOverlayView CreateAccountSwitchingOverlayView(UIView containerView) + { + var overlay = new AccountSwitchingOverlayView() + { + LongPressAccountEnabled = false + }; + + var vm = new AccountSwitchingOverlayViewModel(_stateService, _messagingService, _logger); + overlay.BindingContext = vm; + + var renderer = Platform.CreateRenderer(overlay.Content); + renderer.SetElementSize(new Size(containerView.Frame.Size.Width, containerView.Frame.Size.Height)); + + var view = renderer.NativeView; + view.TranslatesAutoresizingMaskIntoConstraints = false; + + containerView.AddSubview(view); + containerView.AddConstraints(new NSLayoutConstraint[] + { + NSLayoutConstraint.Create(containerView, NSLayoutAttribute.Trailing, NSLayoutRelation.Equal, view, NSLayoutAttribute.Trailing, 1f, 0f), + NSLayoutConstraint.Create(containerView, NSLayoutAttribute.Leading, NSLayoutRelation.Equal, view, NSLayoutAttribute.Leading, 1f, 0f), + NSLayoutConstraint.Create(containerView, NSLayoutAttribute.Top, NSLayoutRelation.Equal, view, NSLayoutAttribute.Top, 1f, 0f), + NSLayoutConstraint.Create(containerView, NSLayoutAttribute.Bottom, NSLayoutRelation.Equal, view, NSLayoutAttribute.Bottom, 1f, 0f) + }); + containerView.Hidden = true; + + return overlay; + } + + public void OnToolbarItemActivated(AccountSwitchingOverlayView accountSwitchingOverlayView, UIView containerView) + { + var overlayVisible = accountSwitchingOverlayView.IsVisible; + accountSwitchingOverlayView.ToggleVisibililtyCommand.Execute(null); + containerView.Hidden = false; + containerView.UserInteractionEnabled = !overlayVisible; + containerView.Subviews[0].UserInteractionEnabled = !overlayVisible; + } + } +} diff --git a/src/iOS.Core/Utilities/ImageSourceExtensions.cs b/src/iOS.Core/Utilities/ImageSourceExtensions.cs new file mode 100644 index 000000000..25941a0bd --- /dev/null +++ b/src/iOS.Core/Utilities/ImageSourceExtensions.cs @@ -0,0 +1,44 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using UIKit; +using Xamarin.Forms; +using Xamarin.Forms.Internals; +using Xamarin.Forms.Platform.iOS; + +namespace Bit.iOS.Core.Utilities +{ + public static class ImageSourceExtensions + { + /// + /// Gets the native image from the ImageSource. + /// Taken from https://github.com/xamarin/Xamarin.Forms/blob/02dee20dfa1365d0104758e534581d1fa5958990/Xamarin.Forms.Platform.iOS/Renderers/ImageElementManager.cs#L264 + /// + public static async Task GetNativeImageAsync(this ImageSource source, CancellationToken cancellationToken = default(CancellationToken)) + { + if (source == null || source.IsEmpty) + return null; + + var handler = Xamarin.Forms.Internals.Registrar.Registered.GetHandlerForObject(source); + if (handler == null) + return null; + + try + { + float scale = (float)UIScreen.MainScreen.Scale; + + return await handler.LoadImageAsync(source, scale: scale, cancelationToken: cancellationToken); + } + catch (OperationCanceledException) + { + Log.Warning("Image loading", "Image load cancelled"); + } + catch (Exception ex) + { + Log.Warning("Image loading", $"Image load failed: {ex}"); + } + + return null; + } + } +} diff --git a/src/iOS.Core/Utilities/iOSCoreHelpers.cs b/src/iOS.Core/Utilities/iOSCoreHelpers.cs index c1b7988bc..29cd91d67 100644 --- a/src/iOS.Core/Utilities/iOSCoreHelpers.cs +++ b/src/iOS.Core/Utilities/iOSCoreHelpers.cs @@ -7,6 +7,7 @@ using Bit.App.Pages; using Bit.App.Resources; using Bit.App.Services; using Bit.App.Utilities; +using Bit.App.Utilities.AccountManagement; using Bit.Core.Abstractions; using Bit.Core.Services; using Bit.Core.Utilities; @@ -172,6 +173,15 @@ namespace Bit.iOS.Core.Utilities ServiceContainer.Resolve("cryptoService")); ServiceContainer.Register("verificationActionsFlowHelper", verificationActionsFlowHelper); + var accountsManager = new AccountsManager( + ServiceContainer.Resolve("broadcasterService"), + ServiceContainer.Resolve("vaultTimeoutService"), + ServiceContainer.Resolve("secureStorageService"), + ServiceContainer.Resolve("stateService"), + ServiceContainer.Resolve("platformUtilsService"), + ServiceContainer.Resolve("authService")); + ServiceContainer.Register("accountsManager", accountsManager); + if (postBootstrapFunc != null) { await postBootstrapFunc.Invoke(); diff --git a/src/iOS.Core/Views/ExtensionTableSource.cs b/src/iOS.Core/Views/ExtensionTableSource.cs index 4bd856b1e..a548cd818 100644 --- a/src/iOS.Core/Views/ExtensionTableSource.cs +++ b/src/iOS.Core/Views/ExtensionTableSource.cs @@ -1,4 +1,10 @@ -using Bit.App.Resources; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Bit.App.Resources; using Bit.Core.Abstractions; using Bit.Core.Models.View; using Bit.Core.Utilities; @@ -6,12 +12,6 @@ using Bit.iOS.Core.Controllers; using Bit.iOS.Core.Models; using Bit.iOS.Core.Utilities; using Foundation; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; using UIKit; namespace Bit.iOS.Core.Views @@ -122,7 +122,10 @@ namespace Bit.iOS.Core.Views public override void WillDisplay(UITableView tableView, UITableViewCell cell, NSIndexPath indexPath) { - if (Items == null || Items.Count() == 0 || cell == null) + if (Items == null + || !Items.Any() + || cell?.TextLabel == null + || cell.DetailTextLabel == null) { return; } diff --git a/src/iOS.Core/iOS.Core.csproj b/src/iOS.Core/iOS.Core.csproj index a0e0967da..4344ddb6d 100644 --- a/src/iOS.Core/iOS.Core.csproj +++ b/src/iOS.Core/iOS.Core.csproj @@ -195,6 +195,9 @@ + + +