From f237fa98d2e84f23109dda90a0e923ce95acc2e0 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 28 Jun 2019 08:21:44 -0400 Subject: [PATCH] ios autofill extension implemented --- .../CredentialProviderViewController.cs | 260 ++++++++ ...edentialProviderViewController.designer.cs | 21 + .../LockPasswordViewController.cs | 29 + .../LockPasswordViewController.designer.cs | 64 ++ src/iOS.Autofill/LoginAddViewController.cs | 48 ++ .../LoginAddViewController.designer.cs | 55 ++ src/iOS.Autofill/LoginListViewController.cs | 100 ++++ .../LoginListViewController.designer.cs | 59 ++ src/iOS.Autofill/LoginSearchViewController.cs | 92 +++ .../LoginSearchViewController.designer.cs | 55 ++ src/iOS.Autofill/MainInterface.storyboard | 553 ++++++++++++++++-- .../PasswordGeneratorViewController.cs | 28 + ...asswordGeneratorViewController.designer.cs | 82 +++ src/iOS.Autofill/SetupViewController.cs | 46 ++ .../SetupViewController.designer.cs | 69 +++ src/iOS.Autofill/Utilities/AutofillHelpers.cs | 43 +- src/iOS.Autofill/iOS.Autofill.csproj | 32 + .../Controllers/LockPasswordViewController.cs | 192 +++++- src/iOS.Core/Utilities/iOSCoreHelpers.cs | 109 ++++ src/iOS.Core/Views/ExtensionTableSource.cs | 13 +- src/iOS.Core/iOS.Core.csproj | 1 + src/iOS/AppDelegate.cs | 120 +--- 22 files changed, 1865 insertions(+), 206 deletions(-) create mode 100644 src/iOS.Autofill/CredentialProviderViewController.cs create mode 100644 src/iOS.Autofill/CredentialProviderViewController.designer.cs create mode 100644 src/iOS.Autofill/LockPasswordViewController.cs create mode 100644 src/iOS.Autofill/LockPasswordViewController.designer.cs create mode 100644 src/iOS.Autofill/LoginAddViewController.cs create mode 100644 src/iOS.Autofill/LoginAddViewController.designer.cs create mode 100644 src/iOS.Autofill/LoginListViewController.cs create mode 100644 src/iOS.Autofill/LoginListViewController.designer.cs create mode 100644 src/iOS.Autofill/LoginSearchViewController.cs create mode 100644 src/iOS.Autofill/LoginSearchViewController.designer.cs create mode 100644 src/iOS.Autofill/PasswordGeneratorViewController.cs create mode 100644 src/iOS.Autofill/PasswordGeneratorViewController.designer.cs create mode 100644 src/iOS.Autofill/SetupViewController.cs create mode 100644 src/iOS.Autofill/SetupViewController.designer.cs create mode 100644 src/iOS.Core/Utilities/iOSCoreHelpers.cs diff --git a/src/iOS.Autofill/CredentialProviderViewController.cs b/src/iOS.Autofill/CredentialProviderViewController.cs new file mode 100644 index 000000000..80b99a2ac --- /dev/null +++ b/src/iOS.Autofill/CredentialProviderViewController.cs @@ -0,0 +1,260 @@ +using AuthenticationServices; +using Bit.App.Resources; +using Bit.Core.Abstractions; +using Bit.Core.Utilities; +using Bit.iOS.Autofill.Models; +using Bit.iOS.Core.Utilities; +using Foundation; +using System; +using System.Threading.Tasks; +using UIKit; + +namespace Bit.iOS.Autofill +{ + public partial class CredentialProviderViewController : ASCredentialProviderViewController + { + private Context _context; + + public CredentialProviderViewController(IntPtr handle) + : base(handle) + { } + + public override void ViewDidLoad() + { + InitApp(); + base.ViewDidLoad(); + _context = new Context + { + ExtContext = ExtensionContext + }; + } + + public override void PrepareCredentialList(ASCredentialServiceIdentifier[] serviceIdentifiers) + { + _context.ServiceIdentifiers = serviceIdentifiers; + if(serviceIdentifiers.Length > 0) + { + var uri = serviceIdentifiers[0].Identifier; + if(serviceIdentifiers[0].Type == ASCredentialServiceIdentifierType.Domain) + { + uri = string.Concat("https://", uri); + } + _context.UrlString = uri; + } + if(!CheckAuthed()) + { + return; + } + if(IsLocked()) + { + PerformSegue("lockPasswordSegue", this); + } + else + { + if(_context.ServiceIdentifiers == null || _context.ServiceIdentifiers.Length == 0) + { + PerformSegue("loginSearchSegue", this); + } + else + { + PerformSegue("loginListSegue", this); + } + } + } + + public override void ProvideCredentialWithoutUserInteraction(ASPasswordCredentialIdentity credentialIdentity) + { + if(!IsAuthed() || IsLocked()) + { + var err = new NSError(new NSString("ASExtensionErrorDomain"), + Convert.ToInt32(ASExtensionErrorCode.UserInteractionRequired), null); + ExtensionContext.CancelRequest(err); + return; + } + _context.CredentialIdentity = credentialIdentity; + ProvideCredentialAsync().GetAwaiter().GetResult(); + } + + public override void PrepareInterfaceToProvideCredential(ASPasswordCredentialIdentity credentialIdentity) + { + if(!CheckAuthed()) + { + return; + } + _context.CredentialIdentity = credentialIdentity; + CheckLock(async () => await ProvideCredentialAsync()); + } + + public override void PrepareInterfaceForExtensionConfiguration() + { + _context.Configuring = true; + if(!CheckAuthed()) + { + return; + } + CheckLock(() => PerformSegue("setupSegue", this)); + } + + public void CompleteRequest(string username = null, string password = null, string totp = null) + { + if((_context?.Configuring ?? true) && string.IsNullOrWhiteSpace(password)) + { + ExtensionContext?.CompleteExtensionConfigurationRequest(); + return; + } + + if(_context == null || string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password)) + { + var err = new NSError(new NSString("ASExtensionErrorDomain"), + Convert.ToInt32(ASExtensionErrorCode.UserCanceled), null); + NSRunLoop.Main.BeginInvokeOnMainThread(() => ExtensionContext?.CancelRequest(err)); + return; + } + + if(!string.IsNullOrWhiteSpace(totp)) + { + UIPasteboard.General.String = totp; + } + + var cred = new ASPasswordCredential(username, password); + NSRunLoop.Main.BeginInvokeOnMainThread(() => ExtensionContext?.CompleteRequest(cred, null)); + } + + public override void PrepareForSegue(UIStoryboardSegue segue, NSObject sender) + { + var navController = segue.DestinationViewController as UINavigationController; + if(navController != null) + { + var listLoginController = navController.TopViewController as LoginListViewController; + var listSearchController = navController.TopViewController as LoginSearchViewController; + var passwordViewController = navController.TopViewController as LockPasswordViewController; + var setupViewController = navController.TopViewController as SetupViewController; + + if(listLoginController != null) + { + listLoginController.Context = _context; + listLoginController.CPViewController = this; + } + else if(listSearchController != null) + { + listSearchController.Context = _context; + listSearchController.CPViewController = this; + } + else if(passwordViewController != null) + { + passwordViewController.CPViewController = this; + } + else if(setupViewController != null) + { + setupViewController.CPViewController = this; + } + } + } + + public void DismissLockAndContinue() + { + DismissViewController(false, async () => + { + if(_context.CredentialIdentity != null) + { + await ProvideCredentialAsync(); + return; + } + if(_context.Configuring) + { + PerformSegue("setupSegue", this); + return; + } + if(_context.ServiceIdentifiers == null || _context.ServiceIdentifiers.Length == 0) + { + PerformSegue("loginSearchSegue", this); + } + else + { + PerformSegue("loginListSegue", this); + } + }); + } + + private async Task ProvideCredentialAsync() + { + var cipherService = ServiceContainer.Resolve("cipherService"); + var cipher = await cipherService.GetAsync(_context.CredentialIdentity.RecordIdentifier); + if(cipher == null || cipher.Type != Bit.Core.Enums.CipherType.Login) + { + var err = new NSError(new NSString("ASExtensionErrorDomain"), + Convert.ToInt32(ASExtensionErrorCode.CredentialIdentityNotFound), null); + ExtensionContext.CancelRequest(err); + return; + } + + var storageService = ServiceContainer.Resolve("storageService"); + var decCipher = await cipher.DecryptAsync(); + string totpCode = null; + var disableTotpCopy = await storageService.GetAsync(Bit.Core.Constants.DisableAutoTotpCopyKey); + if(!disableTotpCopy.GetValueOrDefault(false)) + { + var userService = ServiceContainer.Resolve("userService"); + var canAccessPremiumAsync = await userService.CanAccessPremiumAsync(); + if(!string.IsNullOrWhiteSpace(decCipher.Login.Totp) && + (canAccessPremiumAsync || cipher.OrganizationUseTotp)) + { + var totpService = ServiceContainer.Resolve("totpService"); + totpCode = await totpService.GetCodeAsync(decCipher.Login.Totp); + } + } + + CompleteRequest(decCipher.Login.Username, decCipher.Login.Password, totpCode); + } + + private void CheckLock(Action notLockedAction) + { + if(IsLocked()) + { + PerformSegue("lockPasswordSegue", this); + } + else + { + notLockedAction(); + } + } + + private bool CheckAuthed() + { + if(!IsAuthed()) + { + var alert = Dialogs.CreateAlert(null, AppResources.MustLogInMainAppAutofill, AppResources.Ok, (a) => + { + CompleteRequest(); + }); + PresentViewController(alert, true, null); + return false; + } + return true; + } + + private bool IsLocked() + { + var lockService = ServiceContainer.Resolve("lockService"); + return lockService.IsLockedAsync().GetAwaiter().GetResult(); + } + + private bool IsAuthed() + { + var userService = ServiceContainer.Resolve("userService"); + return userService.IsAuthenticatedAsync().GetAwaiter().GetResult(); + } + + private void InitApp() + { + if(ServiceContainer.RegisteredServices.Count > 0) + { + return; + } + iOSCoreHelpers.RegisterLocalServices(); + ServiceContainer.Init(); + iOSCoreHelpers.RegisterHockeyApp(); + iOSCoreHelpers.Bootstrap(); + } + } +} \ No newline at end of file diff --git a/src/iOS.Autofill/CredentialProviderViewController.designer.cs b/src/iOS.Autofill/CredentialProviderViewController.designer.cs new file mode 100644 index 000000000..cd537d30c --- /dev/null +++ b/src/iOS.Autofill/CredentialProviderViewController.designer.cs @@ -0,0 +1,21 @@ +// 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. +// +using Foundation; +using System; +using System.CodeDom.Compiler; +using UIKit; + +namespace Bit.iOS.Autofill +{ + [Register ("CredentialProviderViewController")] + partial class CredentialProviderViewController + { + void ReleaseDesignerOutlets () + { + } + } +} \ No newline at end of file diff --git a/src/iOS.Autofill/LockPasswordViewController.cs b/src/iOS.Autofill/LockPasswordViewController.cs new file mode 100644 index 000000000..9b975473e --- /dev/null +++ b/src/iOS.Autofill/LockPasswordViewController.cs @@ -0,0 +1,29 @@ +using System; +using UIKit; + +namespace Bit.iOS.Autofill +{ + public partial class LockPasswordViewController : Core.Controllers.LockPasswordViewController + { + public LockPasswordViewController(IntPtr handle) + : base(handle) + { } + + public CredentialProviderViewController CPViewController { get; set; } + public override UINavigationItem BaseNavItem => NavItem; + public override UIBarButtonItem BaseCancelButton => CancelButton; + public override UIBarButtonItem BaseSubmitButton => SubmitButton; + public override Action Success => () => CPViewController.DismissLockAndContinue(); + public override Action Cancel => () => CPViewController.CompleteRequest(); + + partial void SubmitButton_Activated(UIBarButtonItem sender) + { + var task = CheckPasswordAsync(); + } + + partial void CancelButton_Activated(UIBarButtonItem sender) + { + Cancel(); + } + } +} diff --git a/src/iOS.Autofill/LockPasswordViewController.designer.cs b/src/iOS.Autofill/LockPasswordViewController.designer.cs new file mode 100644 index 000000000..3b37484c6 --- /dev/null +++ b/src/iOS.Autofill/LockPasswordViewController.designer.cs @@ -0,0 +1,64 @@ +// 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. +// +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; } + + [Outlet] + [GeneratedCode ("iOS Designer", "1.0")] + UIKit.UITableView MainTableView { get; set; } + + [Outlet] + [GeneratedCode ("iOS Designer", "1.0")] + UIKit.UINavigationItem NavItem { get; set; } + + [Outlet] + [GeneratedCode ("iOS Designer", "1.0")] + UIKit.UIBarButtonItem SubmitButton { get; set; } + + [Action ("CancelButton_Activated:")] + [GeneratedCode ("iOS Designer", "1.0")] + partial void CancelButton_Activated (UIKit.UIBarButtonItem sender); + + [Action ("SubmitButton_Activated:")] + [GeneratedCode ("iOS Designer", "1.0")] + partial void SubmitButton_Activated (UIKit.UIBarButtonItem sender); + + void ReleaseDesignerOutlets () + { + 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; + } + } + } +} \ No newline at end of file diff --git a/src/iOS.Autofill/LoginAddViewController.cs b/src/iOS.Autofill/LoginAddViewController.cs new file mode 100644 index 000000000..c697a3f7a --- /dev/null +++ b/src/iOS.Autofill/LoginAddViewController.cs @@ -0,0 +1,48 @@ +using System; +using Foundation; +using UIKit; + +namespace Bit.iOS.Autofill +{ + public partial class LoginAddViewController : Core.Controllers.LoginAddViewController + { + public LoginAddViewController(IntPtr handle) + : base(handle) + { } + + public LoginListViewController LoginListController { get; set; } + public LoginSearchViewController LoginSearchController { get; set; } + + public override UINavigationItem BaseNavItem => NavItem; + public override UIBarButtonItem BaseCancelButton => CancelBarButton; + public override UIBarButtonItem BaseSaveButton => SaveBarButton; + + public override Action Success => () => + { + LoginListController?.DismissModal(); + LoginSearchController?.DismissModal(); + }; + + partial void CancelBarButton_Activated(UIBarButtonItem sender) + { + DismissViewController(true, null); + } + + async partial void SaveBarButton_Activated(UIBarButtonItem sender) + { + await SaveAsync(); + } + + public override void PrepareForSegue(UIStoryboardSegue segue, NSObject sender) + { + if(segue.DestinationViewController is UINavigationController navController) + { + if(navController.TopViewController is PasswordGeneratorViewController passwordGeneratorController) + { + passwordGeneratorController.PasswordOptions = Context.PasswordOptions; + passwordGeneratorController.Parent = this; + } + } + } + } +} diff --git a/src/iOS.Autofill/LoginAddViewController.designer.cs b/src/iOS.Autofill/LoginAddViewController.designer.cs new file mode 100644 index 000000000..63aa2e69d --- /dev/null +++ b/src/iOS.Autofill/LoginAddViewController.designer.cs @@ -0,0 +1,55 @@ +// 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. +// +using Foundation; +using System; +using System.CodeDom.Compiler; +using UIKit; + +namespace Bit.iOS.Autofill +{ + [Register ("LoginAddViewController")] + partial class LoginAddViewController + { + [Outlet] + [GeneratedCode ("iOS Designer", "1.0")] + UIKit.UIBarButtonItem CancelBarButton { get; set; } + + [Outlet] + [GeneratedCode ("iOS Designer", "1.0")] + UIKit.UINavigationItem NavItem { get; set; } + + [Outlet] + [GeneratedCode ("iOS Designer", "1.0")] + UIKit.UIBarButtonItem SaveBarButton { get; set; } + + [Action ("CancelBarButton_Activated:")] + [GeneratedCode ("iOS Designer", "1.0")] + partial void CancelBarButton_Activated (UIKit.UIBarButtonItem sender); + + [Action ("SaveBarButton_Activated:")] + [GeneratedCode ("iOS Designer", "1.0")] + partial void SaveBarButton_Activated (UIKit.UIBarButtonItem sender); + + void ReleaseDesignerOutlets () + { + if (CancelBarButton != null) { + CancelBarButton.Dispose (); + CancelBarButton = null; + } + + if (NavItem != null) { + NavItem.Dispose (); + NavItem = null; + } + + if (SaveBarButton != null) { + SaveBarButton.Dispose (); + SaveBarButton = null; + } + } + } +} \ No newline at end of file diff --git a/src/iOS.Autofill/LoginListViewController.cs b/src/iOS.Autofill/LoginListViewController.cs new file mode 100644 index 000000000..cf22c4452 --- /dev/null +++ b/src/iOS.Autofill/LoginListViewController.cs @@ -0,0 +1,100 @@ +using System; +using Bit.iOS.Autofill.Models; +using Foundation; +using UIKit; +using Bit.iOS.Core.Controllers; +using Bit.App.Resources; +using Bit.iOS.Core.Views; +using Bit.iOS.Autofill.Utilities; + +namespace Bit.iOS.Autofill +{ + public partial class LoginListViewController : ExtendedUITableViewController + { + public LoginListViewController(IntPtr handle) + : base(handle) + { } + + public Context Context { get; set; } + public CredentialProviderViewController CPViewController { get; set; } + + public override void ViewWillAppear(bool animated) + { + UINavigationBar.Appearance.ShadowImage = new UIImage(); + UINavigationBar.Appearance.SetBackgroundImage(new UIImage(), UIBarMetrics.Default); + base.ViewWillAppear(animated); + } + + public async override void ViewDidLoad() + { + base.ViewDidLoad(); + NavItem.Title = AppResources.Items; + CancelBarButton.Title = AppResources.Cancel; + + TableView.RowHeight = UITableView.AutomaticDimension; + TableView.EstimatedRowHeight = 44; + TableView.Source = new TableSource(this); + await ((TableSource)TableView.Source).LoadItemsAsync(); + } + + partial void CancelBarButton_Activated(UIBarButtonItem sender) + { + CPViewController.CompleteRequest(); + } + + partial void AddBarButton_Activated(UIBarButtonItem sender) + { + PerformSegue("loginAddSegue", this); + } + + partial void SearchBarButton_Activated(UIBarButtonItem sender) + { + PerformSegue("loginSearchFromListSegue", this); + } + + public override void PrepareForSegue(UIStoryboardSegue segue, NSObject sender) + { + if(segue.DestinationViewController is UINavigationController navController) + { + if(navController.TopViewController is LoginAddViewController addLoginController) + { + addLoginController.Context = Context; + addLoginController.LoginListController = this; + } + if(navController.TopViewController is LoginSearchViewController searchLoginController) + { + searchLoginController.Context = Context; + searchLoginController.CPViewController = CPViewController; + } + } + } + + public void DismissModal() + { + DismissViewController(true, async () => + { + await ((TableSource)TableView.Source).LoadItemsAsync(); + TableView.ReloadData(); + }); + } + + public class TableSource : ExtensionTableSource + { + private Context _context; + private LoginListViewController _controller; + + public TableSource(LoginListViewController controller) + : base(controller.Context, controller) + { + _context = controller.Context; + _controller = controller; + } + + public async override void RowSelected(UITableView tableView, NSIndexPath indexPath) + { + await AutofillHelpers.TableRowSelectedAsync(tableView, indexPath, this, + _controller.CPViewController, _controller, "loginAddSegue"); + } + } + } +} diff --git a/src/iOS.Autofill/LoginListViewController.designer.cs b/src/iOS.Autofill/LoginListViewController.designer.cs new file mode 100644 index 000000000..baaf3e88e --- /dev/null +++ b/src/iOS.Autofill/LoginListViewController.designer.cs @@ -0,0 +1,59 @@ +// 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. +// +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; } + + [Outlet] + [GeneratedCode ("iOS Designer", "1.0")] + UIKit.UIBarButtonItem CancelBarButton { get; set; } + + [Outlet] + [GeneratedCode ("iOS Designer", "1.0")] + UIKit.UINavigationItem NavItem { get; set; } + + [Action ("AddBarButton_Activated:")] + [GeneratedCode ("iOS Designer", "1.0")] + partial void AddBarButton_Activated (UIKit.UIBarButtonItem sender); + + [Action ("CancelBarButton_Activated:")] + [GeneratedCode ("iOS Designer", "1.0")] + partial void CancelBarButton_Activated (UIKit.UIBarButtonItem sender); + + [Action ("SearchBarButton_Activated:")] + [GeneratedCode ("iOS Designer", "1.0")] + partial void SearchBarButton_Activated (UIKit.UIBarButtonItem sender); + + void ReleaseDesignerOutlets () + { + if (AddBarButton != null) { + AddBarButton.Dispose (); + AddBarButton = null; + } + + if (CancelBarButton != null) { + CancelBarButton.Dispose (); + CancelBarButton = null; + } + + if (NavItem != null) { + NavItem.Dispose (); + NavItem = null; + } + } + } +} \ No newline at end of file diff --git a/src/iOS.Autofill/LoginSearchViewController.cs b/src/iOS.Autofill/LoginSearchViewController.cs new file mode 100644 index 000000000..c240e4895 --- /dev/null +++ b/src/iOS.Autofill/LoginSearchViewController.cs @@ -0,0 +1,92 @@ +using System; +using Bit.iOS.Autofill.Models; +using Foundation; +using UIKit; +using Bit.iOS.Core.Controllers; +using Bit.App.Resources; +using Bit.iOS.Core.Views; +using Bit.iOS.Autofill.Utilities; + +namespace Bit.iOS.Autofill +{ + public partial class LoginSearchViewController : ExtendedUITableViewController + { + public LoginSearchViewController(IntPtr handle) + : base(handle) + { } + + public Context Context { get; set; } + public CredentialProviderViewController CPViewController { get; set; } + + public override void ViewWillAppear(bool animated) + { + UINavigationBar.Appearance.ShadowImage = new UIImage(); + UINavigationBar.Appearance.SetBackgroundImage(new UIImage(), UIBarMetrics.Default); + base.ViewWillAppear(animated); + } + + public async override void ViewDidLoad() + { + base.ViewDidLoad(); + NavItem.Title = AppResources.SearchVault; + CancelBarButton.Title = AppResources.Cancel; + SearchBar.Placeholder = AppResources.Search; + + TableView.RowHeight = UITableView.AutomaticDimension; + TableView.EstimatedRowHeight = 44; + TableView.Source = new TableSource(this); + SearchBar.Delegate = new ExtensionSearchDelegate(TableView); + await ((TableSource)TableView.Source).LoadItemsAsync(false, SearchBar.Text); + } + + partial void CancelBarButton_Activated(UIBarButtonItem sender) + { + CPViewController.CompleteRequest(); + } + + partial void AddBarButton_Activated(UIBarButtonItem sender) + { + PerformSegue("loginAddFromSearchSegue", this); + } + + public override void PrepareForSegue(UIStoryboardSegue segue, NSObject sender) + { + if(segue.DestinationViewController is UINavigationController navController) + { + if(navController.TopViewController is LoginAddViewController addLoginController) + { + addLoginController.Context = Context; + addLoginController.LoginSearchController = this; + } + } + } + + public void DismissModal() + { + DismissViewController(true, async () => + { + await ((TableSource)TableView.Source).LoadItemsAsync(false, SearchBar.Text); + TableView.ReloadData(); + }); + } + + public class TableSource : ExtensionTableSource + { + private Context _context; + private LoginSearchViewController _controller; + + public TableSource(LoginSearchViewController controller) + : base(controller.Context, controller) + { + _context = controller.Context; + _controller = controller; + } + + public async override void RowSelected(UITableView tableView, NSIndexPath indexPath) + { + await AutofillHelpers.TableRowSelectedAsync(tableView, indexPath, this, + _controller.CPViewController, _controller, "loginAddFromSearchSegue"); + } + } + } +} diff --git a/src/iOS.Autofill/LoginSearchViewController.designer.cs b/src/iOS.Autofill/LoginSearchViewController.designer.cs new file mode 100644 index 000000000..8579298c0 --- /dev/null +++ b/src/iOS.Autofill/LoginSearchViewController.designer.cs @@ -0,0 +1,55 @@ +// 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. +// +using Foundation; +using System; +using System.CodeDom.Compiler; +using UIKit; + +namespace Bit.iOS.Autofill +{ + [Register ("LoginSearchViewController")] + partial class LoginSearchViewController + { + [Outlet] + [GeneratedCode ("iOS Designer", "1.0")] + UIKit.UIBarButtonItem CancelBarButton { get; set; } + + [Outlet] + [GeneratedCode ("iOS Designer", "1.0")] + UIKit.UINavigationItem NavItem { get; set; } + + [Outlet] + [GeneratedCode ("iOS Designer", "1.0")] + UIKit.UISearchBar SearchBar { get; set; } + + [Action ("AddBarButton_Activated:")] + [GeneratedCode ("iOS Designer", "1.0")] + partial void AddBarButton_Activated (UIKit.UIBarButtonItem sender); + + [Action ("CancelBarButton_Activated:")] + [GeneratedCode ("iOS Designer", "1.0")] + partial void CancelBarButton_Activated (UIKit.UIBarButtonItem sender); + + void ReleaseDesignerOutlets () + { + if (CancelBarButton != null) { + CancelBarButton.Dispose (); + CancelBarButton = null; + } + + if (NavItem != null) { + NavItem.Dispose (); + NavItem = null; + } + + if (SearchBar != null) { + SearchBar.Dispose (); + SearchBar = null; + } + } + } +} \ No newline at end of file diff --git a/src/iOS.Autofill/MainInterface.storyboard b/src/iOS.Autofill/MainInterface.storyboard index 76eede89d..08b5e4368 100644 --- a/src/iOS.Autofill/MainInterface.storyboard +++ b/src/iOS.Autofill/MainInterface.storyboard @@ -1,63 +1,528 @@ - + - + + - - + + - + - - + + - - - + + + - - + + - - - - - - - - - - - - - + - - - - - - - + + - - - - + + + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - + + + + + \ No newline at end of file diff --git a/src/iOS.Autofill/PasswordGeneratorViewController.cs b/src/iOS.Autofill/PasswordGeneratorViewController.cs new file mode 100644 index 000000000..fbb1ff8fb --- /dev/null +++ b/src/iOS.Autofill/PasswordGeneratorViewController.cs @@ -0,0 +1,28 @@ +using System; +using UIKit; + +namespace Bit.iOS.Autofill +{ + public partial class PasswordGeneratorViewController : Core.Controllers.PasswordGeneratorViewController + { + public PasswordGeneratorViewController(IntPtr handle) + : base(handle) + { } + + public LoginAddViewController Parent { get; set; } + public override UINavigationItem BaseNavItem => NavItem; + public override UIBarButtonItem BaseCancelButton => CancelBarButton; + public override UIBarButtonItem BaseSelectBarButton => SelectBarButton; + public override UILabel BasePasswordLabel => PasswordLabel; + + partial void SelectBarButton_Activated(UIBarButtonItem sender) + { + DismissViewController(true, () => Parent.PasswordCell.TextField.Text = PasswordLabel.Text); + } + + partial void CancelBarButton_Activated(UIBarButtonItem sender) + { + DismissViewController(true, null); + } + } +} diff --git a/src/iOS.Autofill/PasswordGeneratorViewController.designer.cs b/src/iOS.Autofill/PasswordGeneratorViewController.designer.cs new file mode 100644 index 000000000..54ea722a4 --- /dev/null +++ b/src/iOS.Autofill/PasswordGeneratorViewController.designer.cs @@ -0,0 +1,82 @@ +// 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. +// +using Foundation; +using System; +using System.CodeDom.Compiler; +using UIKit; + +namespace Bit.iOS.Autofill +{ + [Register ("PasswordGeneratorViewController")] + partial class PasswordGeneratorViewController + { + [Outlet] + [GeneratedCode ("iOS Designer", "1.0")] + UIKit.UIView BaseView { get; set; } + + [Outlet] + [GeneratedCode ("iOS Designer", "1.0")] + UIKit.UIBarButtonItem CancelBarButton { get; set; } + + [Outlet] + [GeneratedCode ("iOS Designer", "1.0")] + UIKit.UINavigationItem NavItem { get; set; } + + [Outlet] + [GeneratedCode ("iOS Designer", "1.0")] + UIKit.UIView OptionsContainer { get; set; } + + [Outlet] + [GeneratedCode ("iOS Designer", "1.0")] + UIKit.UILabel PasswordLabel { get; set; } + + [Outlet] + [GeneratedCode ("iOS Designer", "1.0")] + UIKit.UIBarButtonItem SelectBarButton { get; set; } + + [Action ("CancelBarButton_Activated:")] + [GeneratedCode ("iOS Designer", "1.0")] + partial void CancelBarButton_Activated (UIKit.UIBarButtonItem sender); + + [Action ("SelectBarButton_Activated:")] + [GeneratedCode ("iOS Designer", "1.0")] + partial void SelectBarButton_Activated (UIKit.UIBarButtonItem sender); + + void ReleaseDesignerOutlets () + { + if (BaseView != null) { + BaseView.Dispose (); + BaseView = null; + } + + if (CancelBarButton != null) { + CancelBarButton.Dispose (); + CancelBarButton = null; + } + + if (NavItem != null) { + NavItem.Dispose (); + NavItem = null; + } + + if (OptionsContainer != null) { + OptionsContainer.Dispose (); + OptionsContainer = null; + } + + if (PasswordLabel != null) { + PasswordLabel.Dispose (); + PasswordLabel = null; + } + + if (SelectBarButton != null) { + SelectBarButton.Dispose (); + SelectBarButton = null; + } + } + } +} \ No newline at end of file diff --git a/src/iOS.Autofill/SetupViewController.cs b/src/iOS.Autofill/SetupViewController.cs new file mode 100644 index 000000000..42af991fc --- /dev/null +++ b/src/iOS.Autofill/SetupViewController.cs @@ -0,0 +1,46 @@ +using System; +using UIKit; +using Bit.iOS.Core.Controllers; +using Bit.App.Resources; +using Bit.iOS.Core.Utilities; + +namespace Bit.iOS.Autofill +{ + public partial class SetupViewController : ExtendedUIViewController + { + public SetupViewController(IntPtr handle) : base(handle) + { } + + public CredentialProviderViewController CPViewController { get; set; } + + public override void ViewWillAppear(bool animated) + { + UINavigationBar.Appearance.ShadowImage = new UIImage(); + UINavigationBar.Appearance.SetBackgroundImage(new UIImage(), UIBarMetrics.Default); + base.ViewWillAppear(animated); + } + + public override void ViewDidLoad() + { + View.BackgroundColor = new UIColor(red: 0.94f, green: 0.94f, blue: 0.96f, alpha: 1.0f); + var descriptor = UIFontDescriptor.PreferredBody; + DescriptionLabel.Text = $@"{AppResources.AutofillSetup} + +{AppResources.AutofillSetup2}"; + DescriptionLabel.Font = UIFont.FromDescriptor(descriptor, descriptor.PointSize); + DescriptionLabel.TextColor = new UIColor(red: 0.47f, green: 0.47f, blue: 0.47f, alpha: 1.0f); + + ActivatedLabel.Text = AppResources.AutofillActivated; + ActivatedLabel.Font = UIFont.FromDescriptor(descriptor, descriptor.PointSize * 1.3f); + + BackButton.Title = AppResources.Back; + base.ViewDidLoad(); + var task = ASHelpers.ReplaceAllIdentities(); + } + + partial void BackButton_Activated(UIBarButtonItem sender) + { + CPViewController.CompleteRequest(); + } + } +} diff --git a/src/iOS.Autofill/SetupViewController.designer.cs b/src/iOS.Autofill/SetupViewController.designer.cs new file mode 100644 index 000000000..052173cec --- /dev/null +++ b/src/iOS.Autofill/SetupViewController.designer.cs @@ -0,0 +1,69 @@ +// 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. +// +using Foundation; +using System; +using System.CodeDom.Compiler; +using UIKit; + +namespace Bit.iOS.Autofill +{ + [Register ("SetupViewController")] + partial class SetupViewController + { + [Outlet] + [GeneratedCode ("iOS Designer", "1.0")] + UIKit.UILabel ActivatedLabel { get; set; } + + [Outlet] + [GeneratedCode ("iOS Designer", "1.0")] + UIKit.UIBarButtonItem BackButton { get; set; } + + [Outlet] + [GeneratedCode ("iOS Designer", "1.0")] + UIKit.UILabel DescriptionLabel { get; set; } + + [Outlet] + [GeneratedCode ("iOS Designer", "1.0")] + UIKit.UIImageView IconImage { get; set; } + + [Outlet] + [GeneratedCode ("iOS Designer", "1.0")] + UIKit.UINavigationItem NavItem { get; set; } + + [Action ("BackButton_Activated:")] + [GeneratedCode ("iOS Designer", "1.0")] + partial void BackButton_Activated (UIKit.UIBarButtonItem sender); + + void ReleaseDesignerOutlets () + { + if (ActivatedLabel != null) { + ActivatedLabel.Dispose (); + ActivatedLabel = null; + } + + if (BackButton != null) { + BackButton.Dispose (); + BackButton = null; + } + + if (DescriptionLabel != null) { + DescriptionLabel.Dispose (); + DescriptionLabel = null; + } + + if (IconImage != null) { + IconImage.Dispose (); + IconImage = null; + } + + if (NavItem != null) { + NavItem.Dispose (); + NavItem = null; + } + } + } +} \ No newline at end of file diff --git a/src/iOS.Autofill/Utilities/AutofillHelpers.cs b/src/iOS.Autofill/Utilities/AutofillHelpers.cs index d61f92575..6671d4e33 100644 --- a/src/iOS.Autofill/Utilities/AutofillHelpers.cs +++ b/src/iOS.Autofill/Utilities/AutofillHelpers.cs @@ -1,6 +1,8 @@ -using System; -using System.Linq; +using System.Linq; +using System.Threading.Tasks; using Bit.App.Resources; +using Bit.Core.Abstractions; +using Bit.Core.Utilities; using Bit.iOS.Core.Utilities; using Bit.iOS.Core.Views; using Foundation; @@ -10,10 +12,9 @@ namespace Bit.iOS.Autofill.Utilities { public static class AutofillHelpers { - /* - public static void TableRowSelected(UITableView tableView, NSIndexPath indexPath, + public async static Task TableRowSelectedAsync(UITableView tableView, NSIndexPath indexPath, ExtensionTableSource tableSource, CredentialProviderViewController cpViewController, - UITableViewController controller, ISettings settings, string loginAddSegue) + UITableViewController controller, string loginAddSegue) { tableView.DeselectRow(indexPath, true); tableView.EndEditing(true); @@ -23,7 +24,6 @@ namespace Bit.iOS.Autofill.Utilities controller.PerformSegue(loginAddSegue, tableSource); return; } - var item = tableSource.Items.ElementAt(indexPath.Row); if(item == null) { @@ -34,15 +34,23 @@ namespace Bit.iOS.Autofill.Utilities if(!string.IsNullOrWhiteSpace(item.Username) && !string.IsNullOrWhiteSpace(item.Password)) { string totp = null; - if(!settings.GetValueOrDefault(App.Constants.SettingDisableTotpCopy, false)) + var storageService = ServiceContainer.Resolve("storageService"); + var disableTotpCopy = await storageService.GetAsync(Bit.Core.Constants.DisableAutoTotpCopyKey); + if(!disableTotpCopy.GetValueOrDefault(false)) { - totp = tableSource.GetTotp(item); + var userService = ServiceContainer.Resolve("userService"); + var canAccessPremiumAsync = await userService.CanAccessPremiumAsync(); + if(!string.IsNullOrWhiteSpace(item.Totp) && + (canAccessPremiumAsync || item.CipherView.OrganizationUseTotp)) + { + var totpService = ServiceContainer.Resolve("totpService"); + totp = await totpService.GetCodeAsync(item.Totp); + } } - cpViewController.CompleteRequest(item.Username, item.Password, totp); } else if(!string.IsNullOrWhiteSpace(item.Username) || !string.IsNullOrWhiteSpace(item.Password) || - !string.IsNullOrWhiteSpace(item.Totp.Value)) + !string.IsNullOrWhiteSpace(item.Totp)) { var sheet = Dialogs.CreateActionSheet(item.Name, controller); if(!string.IsNullOrWhiteSpace(item.Username)) @@ -65,7 +73,8 @@ namespace Bit.iOS.Autofill.Utilities { UIPasteboard clipboard = UIPasteboard.General; clipboard.String = item.Password; - var alert = Dialogs.CreateMessageAlert(AppResources.CopiedPassword); + var alert = Dialogs.CreateMessageAlert( + string.Format(AppResources.ValueHasBeenCopied, AppResources.Password)); controller.PresentViewController(alert, true, () => { controller.DismissViewController(true, null); @@ -73,26 +82,25 @@ namespace Bit.iOS.Autofill.Utilities })); } - if(!string.IsNullOrWhiteSpace(item.Totp.Value)) + if(!string.IsNullOrWhiteSpace(item.Totp)) { - sheet.AddAction(UIAlertAction.Create(AppResources.CopyTotp, UIAlertActionStyle.Default, a => + sheet.AddAction(UIAlertAction.Create(AppResources.CopyTotp, UIAlertActionStyle.Default, async a => { - var totp = tableSource.GetTotp(item); + var totp = await tableSource.GetTotpAsync(item); if(string.IsNullOrWhiteSpace(totp)) { return; } - UIPasteboard clipboard = UIPasteboard.General; clipboard.String = totp; - var alert = Dialogs.CreateMessageAlert(AppResources.CopiedTotp); + var alert = Dialogs.CreateMessageAlert( + string.Format(AppResources.ValueHasBeenCopied, AppResources.VerificationCodeTotp)); controller.PresentViewController(alert, true, () => { controller.DismissViewController(true, null); }); })); } - sheet.AddAction(UIAlertAction.Create(AppResources.Cancel, UIAlertActionStyle.Cancel, null)); controller.PresentViewController(sheet, true, null); } @@ -102,6 +110,5 @@ namespace Bit.iOS.Autofill.Utilities controller.PresentViewController(alert, true, null); } } - */ } } \ No newline at end of file diff --git a/src/iOS.Autofill/iOS.Autofill.csproj b/src/iOS.Autofill/iOS.Autofill.csproj index ac3e04f28..1a9b658b3 100644 --- a/src/iOS.Autofill/iOS.Autofill.csproj +++ b/src/iOS.Autofill/iOS.Autofill.csproj @@ -64,8 +64,36 @@ + + + CredentialProviderViewController.cs + + + + LockPasswordViewController.cs + + + + LoginAddViewController.cs + + + + LoginListViewController.cs + + + + LoginSearchViewController.cs + + + + PasswordGeneratorViewController.cs + + + + SetupViewController.cs + @@ -90,6 +118,10 @@ {ee44c6a1-2a85-45fe-8d9b-bf1d5f88809c} App + + {4b8a8c41-9820-4341-974c-41e65b7f4366} + Core + {e71f3053-056c-4381-9638-048ed73bdff6} iOS.Core diff --git a/src/iOS.Core/Controllers/LockPasswordViewController.cs b/src/iOS.Core/Controllers/LockPasswordViewController.cs index 919f8f01e..ce45cf7d5 100644 --- a/src/iOS.Core/Controllers/LockPasswordViewController.cs +++ b/src/iOS.Core/Controllers/LockPasswordViewController.cs @@ -5,23 +5,38 @@ using Bit.iOS.Core.Views; using Bit.App.Resources; using Bit.iOS.Core.Utilities; using Bit.App.Abstractions; -using System.Linq; -using Bit.iOS.Core.Controllers; +using Bit.Core.Abstractions; +using Bit.Core.Utilities; +using System.Threading.Tasks; +using Bit.Core.Models.Domain; +using Bit.Core.Enums; namespace Bit.iOS.Core.Controllers { public abstract class LockPasswordViewController : ExtendedUITableViewController { - //private IAuthService _authService; - //private ICryptoService _cryptoService; + private ILockService _lockService; + private ICryptoService _cryptoService; + private IDeviceActionService _deviceActionService; + private IUserService _userService; + private IStorageService _storageService; + private IStorageService _secureStorageService; + private IPlatformUtilsService _platformUtilsService; + private Tuple _pinSet; + private bool _hasKey; + private bool _pinLock; + private bool _fingerprintLock; + private int _invalidPinAttempts; - public LockPasswordViewController(IntPtr handle) : base(handle) + public LockPasswordViewController(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, useLabelAsPlaceholder: true); @@ -35,21 +50,32 @@ namespace Bit.iOS.Core.Controllers public override void ViewDidLoad() { - // _authService = Resolver.Resolve(); - // _cryptoService = Resolver.Resolve(); + _lockService = ServiceContainer.Resolve("lockService"); + _cryptoService = ServiceContainer.Resolve("cryptoService"); + _deviceActionService = ServiceContainer.Resolve("deviceActionService"); + _userService = ServiceContainer.Resolve("userService"); + _storageService = ServiceContainer.Resolve("storageService"); + _secureStorageService = ServiceContainer.Resolve("secureStorageService"); + _platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); - BaseNavItem.Title = AppResources.VerifyMasterPassword; + _pinSet = _lockService.IsPinLockSetAsync().GetAwaiter().GetResult(); + _hasKey = _cryptoService.HasKeyAsync().GetAwaiter().GetResult(); + _pinLock = (_pinSet.Item1 && _hasKey) || _pinSet.Item2; + _fingerprintLock = _lockService.IsFingerprintLockSetAsync().GetAwaiter().GetResult(); + + BaseNavItem.Title = _pinLock ? AppResources.VerifyPIN : AppResources.VerifyMasterPassword; BaseCancelButton.Title = AppResources.Cancel; BaseSubmitButton.Title = AppResources.Submit; View.BackgroundColor = new UIColor(red: 0.94f, green: 0.94f, blue: 0.96f, alpha: 1.0f); var descriptor = UIFontDescriptor.PreferredBody; + MasterPasswordCell.TextField.Placeholder = _pinLock ? AppResources.PIN : AppResources.MasterPassword; MasterPasswordCell.TextField.SecureTextEntry = true; MasterPasswordCell.TextField.ReturnKeyType = UIReturnKeyType.Go; MasterPasswordCell.TextField.ShouldReturn += (UITextField tf) => { - // CheckPassword(); + CheckPasswordAsync().GetAwaiter().GetResult(); return true; }; @@ -59,49 +85,156 @@ namespace Bit.iOS.Core.Controllers TableView.AllowsSelection = true; base.ViewDidLoad(); + + if(_fingerprintLock) + { + var fingerprintButtonText = _deviceActionService.SupportsFaceId() ? AppResources.UseFaceIDToUnlock : + AppResources.UseFingerprintToUnlock; + // TODO: set button text + var tasks = Task.Run(async () => + { + await Task.Delay(500); + PromptFingerprintAsync().GetAwaiter().GetResult(); + }); + } } public override void ViewDidAppear(bool animated) { base.ViewDidAppear(animated); - MasterPasswordCell.TextField.BecomeFirstResponder(); + if(!_fingerprintLock) + { + MasterPasswordCell.TextField.BecomeFirstResponder(); + } } - /* - protected void CheckPassword() + // TODO: Try fingerprint again button action + + protected async Task CheckPasswordAsync() { if(string.IsNullOrWhiteSpace(MasterPasswordCell.TextField.Text)) { var alert = Dialogs.CreateAlert(AppResources.AnErrorHasOccurred, - string.Format(AppResources.ValidationFieldRequired, AppResources.MasterPassword), AppResources.Ok); + string.Format(AppResources.ValidationFieldRequired, + _pinLock ? AppResources.PIN : AppResources.MasterPassword), + AppResources.Ok); PresentViewController(alert, true, null); return; } - var key = _cryptoService.MakeKeyFromPassword(MasterPasswordCell.TextField.Text, _authService.Email, - _authService.Kdf, _authService.KdfIterations); - if(key.Key.SequenceEqual(_cryptoService.Key.Key)) + var email = await _userService.GetEmailAsync(); + var kdf = await _userService.GetKdfAsync(); + var kdfIterations = await _userService.GetKdfIterationsAsync(); + var inputtedValue = MasterPasswordCell.TextField.Text; + + if(_pinLock) { - _appSettingsService.Locked = false; - MasterPasswordCell.TextField.ResignFirstResponder(); - Success(); + var failed = true; + try + { + if(_pinSet.Item1) + { + var protectedPin = await _storageService.GetAsync(Bit.Core.Constants.ProtectedPin); + var decPin = await _cryptoService.DecryptToUtf8Async(new CipherString(protectedPin)); + failed = decPin != inputtedValue; + _lockService.PinLocked = failed; + if(!failed) + { + DoContinue(); + } + } + else + { + var key2 = await _cryptoService.MakeKeyFromPinAsync(inputtedValue, email, + kdf.GetValueOrDefault(KdfType.PBKDF2_SHA256), kdfIterations.GetValueOrDefault(5000)); + failed = false; + await SetKeyAndContinueAsync(key2); + } + } + catch + { + failed = true; + } + if(failed) + { + _invalidPinAttempts++; + if(_invalidPinAttempts >= 5) + { + Cancel?.Invoke(); + return; + } + InvalidValue(); + } } else { - // TODO: keep track of invalid attempts and logout? + var key2 = await _cryptoService.MakeKeyAsync(inputtedValue, email, kdf, kdfIterations); + var keyHash = await _cryptoService.HashPasswordAsync(inputtedValue, key2); + var storedKeyHash = await _cryptoService.GetKeyHashAsync(); + if(storedKeyHash == null) + { + var oldKey = await _secureStorageService.GetAsync("oldKey"); + if(key2.KeyB64 == oldKey) + { + await _secureStorageService.RemoveAsync("oldKey"); + await _cryptoService.SetKeyHashAsync(keyHash); + storedKeyHash = keyHash; + } + } + if(storedKeyHash != null && keyHash != null && storedKeyHash == keyHash) + { + await SetKeyAndContinueAsync(key2); + } + else + { + InvalidValue(); + } + } + } - var alert = Dialogs.CreateAlert(AppResources.AnErrorHasOccurred, - string.Format(null, AppResources.InvalidMasterPassword), AppResources.Ok, (a) => + private async Task SetKeyAndContinueAsync(SymmetricCryptoKey key) + { + if(!_hasKey) + { + await _cryptoService.SetKeyAsync(key); + } + DoContinue(); + } + + private void DoContinue() + { + MasterPasswordCell.TextField.ResignFirstResponder(); + Success(); + } + + public async Task PromptFingerprintAsync() + { + if(!_fingerprintLock) + { + return; + } + var success = await _platformUtilsService.AuthenticateFingerprintAsync(null, + _pinLock ? AppResources.PIN : AppResources.MasterPassword, + () => MasterPasswordCell.TextField.BecomeFirstResponder()); + _lockService.FingerprintLocked = !success; + if(success) + { + DoContinue(); + } + } + + 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); - } + PresentViewController(alert, true, null); } - */ public class TableSource : UITableViewSource { @@ -121,7 +254,6 @@ namespace Bit.iOS.Core.Controllers return _controller.MasterPasswordCell; } } - return new UITableViewCell(); } @@ -141,7 +273,6 @@ namespace Bit.iOS.Core.Controllers { return 1; } - return 0; } @@ -159,15 +290,12 @@ namespace Bit.iOS.Core.Controllers { tableView.DeselectRow(indexPath, true); tableView.EndEditing(true); - var cell = tableView.CellAt(indexPath); if(cell == null) { return; } - - var selectableCell = cell as ISelectable; - if(selectableCell != null) + if(cell is ISelectable selectableCell) { selectableCell.Select(); } diff --git a/src/iOS.Core/Utilities/iOSCoreHelpers.cs b/src/iOS.Core/Utilities/iOSCoreHelpers.cs new file mode 100644 index 000000000..280bbc871 --- /dev/null +++ b/src/iOS.Core/Utilities/iOSCoreHelpers.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Bit.App.Abstractions; +using Bit.App.Services; +using Bit.App.Utilities; +using Bit.Core.Abstractions; +using Bit.Core.Services; +using Bit.Core.Utilities; +using Bit.iOS.Core.Services; +using Foundation; +using HockeyApp.iOS; +using UIKit; + +namespace Bit.iOS.Core.Utilities +{ + public static class iOSCoreHelpers + { + public static string AppId = "com.8bit.bitwarden"; + public static string AppGroupId = "group.com.8bit.bitwarden"; + public static string AccessGroup = "LTZ2PFU5D6.com.8bit.bitwarden"; + + public static void RegisterHockeyApp() + { + var crashManagerDelegate = new HockeyAppCrashManagerDelegate( + ServiceContainer.Resolve("appIdService"), + ServiceContainer.Resolve("userService")); + var manager = BITHockeyManager.SharedHockeyManager; + manager.Configure("51f96ae568ba45f699a18ad9f63046c3", crashManagerDelegate); + manager.CrashManager.CrashManagerStatus = BITCrashManagerStatus.AutoSend; + manager.StartManager(); + manager.Authenticator.AuthenticateInstallation(); + manager.DisableMetricsManager = manager.DisableFeedbackManager = manager.DisableUpdateManager = true; + var task = crashManagerDelegate.InitAsync(manager); + } + + public static void RegisterLocalServices() + { + if(ServiceContainer.Resolve("logService", true) == null) + { + ServiceContainer.Register("logService", new ConsoleLogService()); + } + + // Note: This might cause a race condition. Investigate more. + Task.Run(() => + { + FFImageLoading.Forms.Platform.CachedImageRenderer.Init(); + FFImageLoading.ImageService.Instance.Initialize(new FFImageLoading.Config.Configuration + { + FadeAnimationEnabled = false, + FadeAnimationForCachedImages = false + }); + }); + + var preferencesStorage = new PreferencesStorageService(AppGroupId); + var appGroupContainer = new NSFileManager().GetContainerUrl(AppGroupId); + var liteDbStorage = new LiteDbStorageService( + Path.Combine(appGroupContainer.Path, "Library", "bitwarden.db")); + liteDbStorage.InitAsync(); + var localizeService = new LocalizeService(); + var broadcasterService = new BroadcasterService(); + var messagingService = new MobileBroadcasterMessagingService(broadcasterService); + var i18nService = new MobileI18nService(localizeService.GetCurrentCultureInfo()); + var secureStorageService = new KeyChainStorageService(AppId, AccessGroup); + var cryptoPrimitiveService = new CryptoPrimitiveService(); + var mobileStorageService = new MobileStorageService(preferencesStorage, liteDbStorage); + var deviceActionService = new DeviceActionService(mobileStorageService, messagingService); + var platformUtilsService = new MobilePlatformUtilsService(deviceActionService, messagingService, + broadcasterService); + + ServiceContainer.Register("broadcasterService", broadcasterService); + ServiceContainer.Register("messagingService", messagingService); + ServiceContainer.Register("localizeService", localizeService); + ServiceContainer.Register("i18nService", i18nService); + ServiceContainer.Register("cryptoPrimitiveService", cryptoPrimitiveService); + ServiceContainer.Register("storageService", mobileStorageService); + ServiceContainer.Register("secureStorageService", secureStorageService); + ServiceContainer.Register("deviceActionService", deviceActionService); + ServiceContainer.Register("platformUtilsService", platformUtilsService); + } + + public static void Bootstrap() + { + (ServiceContainer.Resolve("i18nService") as MobileI18nService).Init(); + ServiceContainer.Resolve("authService").Init(); + // Note: This is not awaited + var bootstrapTask = BootstrapAsync(); + } + + public static void AppearanceAdjustments() + { + ThemeHelpers.SetAppearance(ThemeManager.GetTheme(false)); + UIApplication.SharedApplication.StatusBarHidden = false; + UIApplication.SharedApplication.StatusBarStyle = UIStatusBarStyle.LightContent; + } + + private static async Task BootstrapAsync() + { + 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(); + } + } +} \ No newline at end of file diff --git a/src/iOS.Core/Views/ExtensionTableSource.cs b/src/iOS.Core/Views/ExtensionTableSource.cs index 9d3fff2ec..4beb22f60 100644 --- a/src/iOS.Core/Views/ExtensionTableSource.cs +++ b/src/iOS.Core/Views/ExtensionTableSource.cs @@ -22,6 +22,7 @@ namespace Bit.iOS.Core.Views protected ICipherService _cipherService; protected ITotpService _totpService; protected IUserService _userService; + protected ISearchService _searchService; private AppExtensionContext _context; private UIViewController _controller; @@ -30,6 +31,7 @@ namespace Bit.iOS.Core.Views _cipherService = ServiceContainer.Resolve("cipherService"); _totpService = ServiceContainer.Resolve("totpService"); _userService = ServiceContainer.Resolve("userService"); + _searchService = ServiceContainer.Resolve("searchService"); _context = context; _controller = controller; } @@ -61,8 +63,6 @@ namespace Bit.iOS.Core.Views _allItems = combinedLogins .Where(c => c.Type == Bit.Core.Enums.CipherType.Login) .Select(s => new CipherViewModel(s)) - .OrderBy(s => s.Name) - .ThenBy(s => s.Username) .ToList() ?? new List(); FilterResults(searchFilter, new CancellationToken()); } @@ -78,12 +78,9 @@ namespace Bit.iOS.Core.Views else { searchFilter = searchFilter.ToLower(); - Items = _allItems - .Where(s => s.Name?.ToLower().Contains(searchFilter) ?? false || - (s.Username?.ToLower().Contains(searchFilter) ?? false) || - (s.Uris?.FirstOrDefault()?.Uri?.ToLower().Contains(searchFilter) ?? false)) - .TakeWhile(s => !ct.IsCancellationRequested) - .ToArray(); + var results = _searchService.SearchCiphersAsync(searchFilter, + c => c.Type == Bit.Core.Enums.CipherType.Login, null, ct).GetAwaiter().GetResult(); + Items = results.Select(s => new CipherViewModel(s)).ToArray(); } } diff --git a/src/iOS.Core/iOS.Core.csproj b/src/iOS.Core/iOS.Core.csproj index 436698af3..9cefcf134 100644 --- a/src/iOS.Core/iOS.Core.csproj +++ b/src/iOS.Core/iOS.Core.csproj @@ -70,6 +70,7 @@ + diff --git a/src/iOS/AppDelegate.cs b/src/iOS/AppDelegate.cs index 739c968f0..c42e30173 100644 --- a/src/iOS/AppDelegate.cs +++ b/src/iOS/AppDelegate.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; using AuthenticationServices; using Bit.App.Abstractions; using Bit.App.Resources; @@ -10,12 +8,10 @@ using Bit.App.Utilities; using Bit.Core.Abstractions; using Bit.Core.Services; using Bit.Core.Utilities; -using Bit.iOS.Core.Services; using Bit.iOS.Core.Utilities; using Bit.iOS.Services; using CoreNFC; using Foundation; -using HockeyApp.iOS; using UIKit; using Xamarin.Forms; using Xamarin.Forms.Platform.iOS; @@ -23,12 +19,8 @@ using Xamarin.Forms.Platform.iOS; namespace Bit.iOS { [Register("AppDelegate")] - public partial class AppDelegate : Xamarin.Forms.Platform.iOS.FormsApplicationDelegate + public partial class AppDelegate : FormsApplicationDelegate { - private const string AppId = "com.8bit.bitwarden"; - private const string AppGroupId = "group.com.8bit.bitwarden"; - private const string AccessGroup = "LTZ2PFU5D6.com.8bit.bitwarden"; - private NFCNdefReaderSession _nfcSession = null; private iOSPushNotificationHandler _pushHandler = null; private NFCReaderDelegate _nfcDelegate = null; @@ -42,14 +34,13 @@ namespace Bit.iOS { Forms.Init(); InitApp(); - Bootstrap(); _deviceActionService = ServiceContainer.Resolve("deviceActionService"); _messagingService = ServiceContainer.Resolve("messagingService"); _broadcasterService = ServiceContainer.Resolve("broadcasterService"); _storageService = ServiceContainer.Resolve("storageService"); LoadApplication(new App.App(null)); - AppearanceAdjustments(); + iOSCoreHelpers.AppearanceAdjustments(); ZXing.Net.Mobile.Forms.iOS.Platform.Init(); _broadcasterService.Subscribe(nameof(AppDelegate), async (message) => @@ -226,33 +217,14 @@ namespace Bit.iOS _pushHandler?.OnMessageReceived(userInfo); } - private void InitApp() + public void InitApp() { if(ServiceContainer.RegisteredServices.Count > 0) { return; } - RegisterLocalServices(); - ServiceContainer.Init(); - _pushHandler = new iOSPushNotificationHandler( - ServiceContainer.Resolve("pushNotificationListenerService")); - _nfcDelegate = new NFCReaderDelegate((success, message) => - _messagingService.Send("gotYubiKeyOTP", message)); - var crashManagerDelegate = new HockeyAppCrashManagerDelegate( - ServiceContainer.Resolve("appIdService"), - ServiceContainer.Resolve("userService")); - var manager = BITHockeyManager.SharedHockeyManager; - manager.Configure("51f96ae568ba45f699a18ad9f63046c3", crashManagerDelegate); - manager.CrashManager.CrashManagerStatus = BITCrashManagerStatus.AutoSend; - manager.StartManager(); - manager.Authenticator.AuthenticateInstallation(); - manager.DisableMetricsManager = manager.DisableFeedbackManager = manager.DisableUpdateManager = true; - var task = crashManagerDelegate.InitAsync(manager); - } - - private void RegisterLocalServices() - { + // Migration services ServiceContainer.Register("logService", new ConsoleLogService()); ServiceContainer.Register("settingsShim", new App.Migration.SettingsShim()); if(false && App.Migration.MigrationHelpers.NeedsMigration()) @@ -261,44 +233,20 @@ namespace Bit.iOS "oldSecureStorageService", new Migration.KeyChainStorageService()); } - // Note: This might cause a race condition. Investigate more. - Task.Run(() => - { - FFImageLoading.Forms.Platform.CachedImageRenderer.Init(); - FFImageLoading.ImageService.Instance.Initialize(new FFImageLoading.Config.Configuration - { - FadeAnimationEnabled = false, - FadeAnimationForCachedImages = false - }); - }); + iOSCoreHelpers.RegisterLocalServices(); + RegisterPush(); + ServiceContainer.Init(); + iOSCoreHelpers.RegisterHockeyApp(); + _pushHandler = new iOSPushNotificationHandler( + ServiceContainer.Resolve("pushNotificationListenerService")); + _nfcDelegate = new NFCReaderDelegate((success, message) => + _messagingService.Send("gotYubiKeyOTP", message)); - var preferencesStorage = new PreferencesStorageService(AppGroupId); - var appGroupContainer = new NSFileManager().GetContainerUrl(AppGroupId); - var liteDbStorage = new LiteDbStorageService( - Path.Combine(appGroupContainer.Path, "Library", "bitwarden.db")); - liteDbStorage.InitAsync(); - var localizeService = new LocalizeService(); - var broadcasterService = new BroadcasterService(); - var messagingService = new MobileBroadcasterMessagingService(broadcasterService); - var i18nService = new MobileI18nService(localizeService.GetCurrentCultureInfo()); - var secureStorageService = new KeyChainStorageService(AppId, AccessGroup); - var cryptoPrimitiveService = new CryptoPrimitiveService(); - var mobileStorageService = new MobileStorageService(preferencesStorage, liteDbStorage); - var deviceActionService = new DeviceActionService(mobileStorageService, messagingService); - var platformUtilsService = new MobilePlatformUtilsService(deviceActionService, messagingService, - broadcasterService); + iOSCoreHelpers.Bootstrap(); + } - ServiceContainer.Register("broadcasterService", broadcasterService); - ServiceContainer.Register("messagingService", messagingService); - ServiceContainer.Register("localizeService", localizeService); - ServiceContainer.Register("i18nService", i18nService); - ServiceContainer.Register("cryptoPrimitiveService", cryptoPrimitiveService); - ServiceContainer.Register("storageService", mobileStorageService); - ServiceContainer.Register("secureStorageService", secureStorageService); - ServiceContainer.Register("deviceActionService", deviceActionService); - ServiceContainer.Register("platformUtilsService", platformUtilsService); - - // Push + private void RegisterPush() + { var notificationListenerService = new PushNotificationListenerService(); ServiceContainer.Register( "pushNotificationListenerService", notificationListenerService); @@ -307,42 +255,6 @@ namespace Bit.iOS "pushNotificationService", iosPushNotificationService); } - private void Bootstrap() - { - (ServiceContainer.Resolve("i18nService") as MobileI18nService).Init(); - ServiceContainer.Resolve("authService").Init(); - // Note: This is not awaited - var bootstrapTask = BootstrapAsync(); - } - - private async Task BootstrapAsync() - { - 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(); - } - - private void AppearanceAdjustments() - { - ThemeHelpers.SetAppearance(ThemeManager.GetTheme(false)); - /* - var primaryColor = new UIColor(red: 0.24f, green: 0.55f, blue: 0.74f, alpha: 1.0f); - var grayLight = new UIColor(red: 0.47f, green: 0.47f, blue: 0.47f, alpha: 1.0f); - UINavigationBar.Appearance.ShadowImage = new UIImage(); - UINavigationBar.Appearance.SetBackgroundImage(new UIImage(), UIBarMetrics.Default); - UIBarButtonItem.AppearanceWhenContainedIn(new Type[] { typeof(UISearchBar) }).TintColor = primaryColor; - UIButton.AppearanceWhenContainedIn(new Type[] { typeof(UISearchBar) }).SetTitleColor(primaryColor, - UIControlState.Normal); - UIButton.AppearanceWhenContainedIn(new Type[] { typeof(UISearchBar) }).TintColor = primaryColor; - UIStepper.Appearance.TintColor = grayLight; - UISlider.Appearance.TintColor = primaryColor; - */ - UIApplication.SharedApplication.StatusBarHidden = false; - UIApplication.SharedApplication.StatusBarStyle = UIStatusBarStyle.LightContent; - } - private void ListenYubiKey(bool listen) { if(_deviceActionService.SupportsNfc())