From 55fb71744df8fe504b31ea12d0c5a8bcead366c6 Mon Sep 17 00:00:00 2001 From: mpbw2 <59324545+mpbw2@users.noreply.github.com> Date: Wed, 17 Jan 2024 13:03:11 -0500 Subject: [PATCH 01/50] Improve TOTP scan performance on Android --- src/Core/Pages/Vault/ScanPage.xaml.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Core/Pages/Vault/ScanPage.xaml.cs b/src/Core/Pages/Vault/ScanPage.xaml.cs index a3d78f293..fc71c96c5 100644 --- a/src/Core/Pages/Vault/ScanPage.xaml.cs +++ b/src/Core/Pages/Vault/ScanPage.xaml.cs @@ -54,6 +54,13 @@ namespace Bit.App.Pages { if (_cameraView == null) { return; } + if (DeviceInfo.Platform == DevicePlatform.Android) + { + // Reduce the size of the camera view to improve performance, scale it up to fill the space + _cameraView.WidthRequest = _cameraView.HeightRequest = 150; + _cameraView.Scale = 4; + } + ViewModel.StartCameraCommand?.Execute(this); _pageIsActive = true; From 9b2f596d1573aaf559ff60cd792e11cabf4fdd86 Mon Sep 17 00:00:00 2001 From: mpbw2 <59324545+mpbw2@users.noreply.github.com> Date: Thu, 18 Jan 2024 11:31:16 -0500 Subject: [PATCH 02/50] Move Android camera/scan changes to xaml --- src/Core/Pages/Vault/ScanPage.xaml | 3 +++ src/Core/Pages/Vault/ScanPage.xaml.cs | 7 ------- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/Core/Pages/Vault/ScanPage.xaml b/src/Core/Pages/Vault/ScanPage.xaml index 99c71665f..d7cc34de9 100644 --- a/src/Core/Pages/Vault/ScanPage.xaml +++ b/src/Core/Pages/Vault/ScanPage.xaml @@ -47,6 +47,9 @@ Camera="{Binding Camera}" AutoStartPreview="{Binding AutoStartPreview}" NumCamerasDetected="{Binding NumCameras, Mode=OneWayToSource}" + WidthRequest="{OnPlatform Android=150}" + HeightRequest="{OnPlatform Android=150}" + Scale="{OnPlatform Android=4}" Grid.Column="0" Grid.Row="0" Grid.RowSpan="3" /> diff --git a/src/Core/Pages/Vault/ScanPage.xaml.cs b/src/Core/Pages/Vault/ScanPage.xaml.cs index fc71c96c5..a3d78f293 100644 --- a/src/Core/Pages/Vault/ScanPage.xaml.cs +++ b/src/Core/Pages/Vault/ScanPage.xaml.cs @@ -54,13 +54,6 @@ namespace Bit.App.Pages { if (_cameraView == null) { return; } - if (DeviceInfo.Platform == DevicePlatform.Android) - { - // Reduce the size of the camera view to improve performance, scale it up to fill the space - _cameraView.WidthRequest = _cameraView.HeightRequest = 150; - _cameraView.Scale = 4; - } - ViewModel.StartCameraCommand?.Execute(this); _pageIsActive = true; From 75b4655f3845030c52495a9976dfd84922b06ddf Mon Sep 17 00:00:00 2001 From: Federico Maccaroni Date: Thu, 18 Jan 2024 16:32:00 -0300 Subject: [PATCH 03/50] PM-3350 Testing UseInterpreter false on CI build --- src/App/App.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/App/App.csproj b/src/App/App.csproj index 35458d047..ef430d7df 100644 --- a/src/App/App.csproj +++ b/src/App/App.csproj @@ -66,7 +66,7 @@ Automatic:AppStore iPhone Distribution Platforms\iOS\Entitlements.plist - true + -gcc_flags "-L$(ProjectDir)../../lib/ios -largon2 -force_load $(ProjectDir)../../lib/ios/libargon2.a" From 01ee1ff845cc2e2bf14cb61798d925584f2ba18f Mon Sep 17 00:00:00 2001 From: Federico Maccaroni Date: Thu, 18 Jan 2024 18:54:34 -0300 Subject: [PATCH 04/50] PM-3350 Enabled back UseInterpreter on iOS Release given that it crashes on startup without it. --- src/App/App.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/App/App.csproj b/src/App/App.csproj index ef430d7df..35458d047 100644 --- a/src/App/App.csproj +++ b/src/App/App.csproj @@ -66,7 +66,7 @@ Automatic:AppStore iPhone Distribution Platforms\iOS\Entitlements.plist - + true -gcc_flags "-L$(ProjectDir)../../lib/ios -largon2 -force_load $(ProjectDir)../../lib/ios/libargon2.a" From e66ac9dd44852ccb9d5a83d36705f842244d9d19 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 19 Jan 2024 09:45:12 +0000 Subject: [PATCH 05/50] Autosync the updated translations (#2944) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- src/App/Resources/AppResources.az.resx | 4 ++-- src/App/Resources/AppResources.cy.resx | 12 ++++++------ src/App/Resources/AppResources.zh-Hant.resx | 6 +++--- .../Localization/cy.lproj/Localizable.strings | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/App/Resources/AppResources.az.resx b/src/App/Resources/AppResources.az.resx index a30511650..57a001456 100644 --- a/src/App/Resources/AppResources.az.resx +++ b/src/App/Resources/AppResources.az.resx @@ -156,7 +156,7 @@ The button text that allows a user to copy the login's password to their clipboard. - İstifadəçi adını kopyalayın + İstifadəçi adını kopyala The button text that allows a user to copy the login's username to their clipboard. @@ -176,7 +176,7 @@ Confirmation alert message when deleteing something. - Redaktə edin + Düzəliş et Qovluğa düzəliş et diff --git a/src/App/Resources/AppResources.cy.resx b/src/App/Resources/AppResources.cy.resx index a7a01eb33..93c580219 100644 --- a/src/App/Resources/AppResources.cy.resx +++ b/src/App/Resources/AppResources.cy.resx @@ -265,7 +265,7 @@ The login button text (verb). - Login + Mewngofnodi Title for login page. (noun) @@ -308,7 +308,7 @@ Label for an entity name. - No + Na Nodiadau @@ -327,7 +327,7 @@ Button text for a save operation (verb). - Move + Symud Yn cadw... @@ -391,7 +391,7 @@ Fersiwn - View + Golwg Visit our website @@ -401,7 +401,7 @@ Label for a website. - Yes + Ydw Cyfrif @@ -715,7 +715,7 @@ Verification code - View item + Gweld yr Eitem Cell we Bitwarden diff --git a/src/App/Resources/AppResources.zh-Hant.resx b/src/App/Resources/AppResources.zh-Hant.resx index 1455e4e0b..65723cd56 100644 --- a/src/App/Resources/AppResources.zh-Hant.resx +++ b/src/App/Resources/AppResources.zh-Hant.resx @@ -2862,12 +2862,12 @@ 帳戶已登出。 - Your organization permissions were updated, requiring you to set a master password. + 您的組織權限已更新,需要您設定主密碼。 - Your organization requires you to set a master password. + 您的組織要求您設定主密碼。 - Set up an unlock option to change your vault timeout action. + 設定一個解鎖方式來變更您的密碼庫逾時動作。 diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Localization/cy.lproj/Localizable.strings b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Localization/cy.lproj/Localizable.strings index cad65e2b8..42c4107ef 100644 --- a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Localization/cy.lproj/Localizable.strings +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Localization/cy.lproj/Localizable.strings @@ -5,6 +5,6 @@ "SyncingItemsContainingVerificationCodes" = "Syncing items containing verification codes"; "UnlockBitwardenOnYourIPhoneToViewVerificationCodes" = "Unlock Bitwarden on your iPhone to view verification codes"; "SetUpBitwardenToViewItemsContainingVerificationCodes" = "Set up Bitwarden to view items containing verification codes"; -"Search" = "Search"; +"Search" = "Chwilio"; "NoItemsFound" = "No items found"; "SetUpAppleWatchPasscodeInOrderToUseBitwarden" = "Set up Apple Watch passcode in order to use Bitwarden"; From 4717f5e230f63138766ecbc07986258394e52cea Mon Sep 17 00:00:00 2001 From: Federico Maccaroni Date: Fri, 19 Jan 2024 15:01:31 -0300 Subject: [PATCH 06/50] PM-3349 PM-3350 Improved code safety with try...catch, better invoke on main thread and better null handling. --- .../Handlers/CustomTabbedPageHandler.cs | 13 +- src/App/Platforms/iOS/AppDelegate.cs | 415 +++++++++++------- src/Core/App.xaml.cs | 9 +- src/Core/Pages/Accounts/HomePage.xaml.cs | 2 +- src/Core/Pages/Accounts/LockPage.xaml.cs | 2 +- src/Core/Pages/Accounts/LockPageViewModel.cs | 7 +- src/Core/Pages/Accounts/LoginPage.xaml.cs | 19 +- .../LoginPasswordlessRequestPage.xaml.cs | 17 +- src/Core/Pages/Accounts/LoginSsoPage.xaml.cs | 48 +- .../Pages/Accounts/SetPasswordPage.xaml.cs | 16 +- src/Core/Pages/Accounts/TwoFactorPage.xaml.cs | 33 +- .../Pages/Accounts/TwoFactorPageViewModel.cs | 146 +++--- src/Core/Pages/BaseContentPage.cs | 11 +- src/iOS.Core/Handlers/CustomTabbedHandler.cs | 8 +- src/iOS.Core/Services/LocalizeService.cs | 9 +- .../Utilities/DictionaryExtensions.cs | 16 +- src/iOS.Core/Utilities/WCSessionManager.cs | 44 +- src/iOS.Core/Utilities/iOSCoreHelpers.cs | 92 ++-- src/iOS.Core/Views/ExtensionSearchDelegate.cs | 47 +- src/iOS.Extension/LoadingViewController.cs | 19 +- .../LoadingViewController.cs | 16 +- 21 files changed, 612 insertions(+), 377 deletions(-) diff --git a/src/App/Platforms/Android/Handlers/CustomTabbedPageHandler.cs b/src/App/Platforms/Android/Handlers/CustomTabbedPageHandler.cs index 8d863e003..1a734eeb6 100644 --- a/src/App/Platforms/Android/Handlers/CustomTabbedPageHandler.cs +++ b/src/App/Platforms/Android/Handlers/CustomTabbedPageHandler.cs @@ -1,5 +1,6 @@ using AndroidX.AppCompat.View.Menu; using Bit.Core.Abstractions; +using Bit.Core.Services; using Bit.Core.Utilities; using Google.Android.Material.BottomNavigation; using Microsoft.Maui.Handlers; @@ -90,7 +91,17 @@ namespace Bit.App.Handlers if(e.Item is MenuItemImpl item) { System.Diagnostics.Debug.WriteLine($"Tab '{item.Title}' was reselected so we'll PopToRoot."); - MainThread.BeginInvokeOnMainThread(async () => await _tabbedPage.CurrentPage.Navigation.PopToRootAsync()); + MainThread.BeginInvokeOnMainThread(async () => + { + try + { + await _tabbedPage.CurrentPage.Navigation.PopToRootAsync(); + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + } + }); } } diff --git a/src/App/Platforms/iOS/AppDelegate.cs b/src/App/Platforms/iOS/AppDelegate.cs index af4d6667b..b9f648805 100644 --- a/src/App/Platforms/iOS/AppDelegate.cs +++ b/src/App/Platforms/iOS/AppDelegate.cs @@ -15,6 +15,7 @@ using CoreNFC; using Foundation; using Microsoft.Maui.Platform; using UIKit; +using UserNotifications; using WatchConnectivity; namespace Bit.iOS @@ -41,73 +42,78 @@ namespace Bit.iOS private IStateService _stateService; private IEventService _eventService; - private LazyResolve _deepLinkContext = new LazyResolve(); + private readonly LazyResolve _deepLinkContext = new LazyResolve(); public override bool FinishedLaunching(UIApplication app, NSDictionary options) { - InitApp(); - - _deviceActionService = ServiceContainer.Resolve("deviceActionService"); - _messagingService = ServiceContainer.Resolve("messagingService"); - _broadcasterService = ServiceContainer.Resolve("broadcasterService"); - _storageService = ServiceContainer.Resolve("storageService"); - _stateService = ServiceContainer.Resolve("stateService"); - _eventService = ServiceContainer.Resolve("eventService"); - - ConnectToWatchIfNeededAsync().FireAndForget(); - - _broadcasterService.Subscribe(nameof(AppDelegate), async (message) => + try { - try + InitApp(); + + _deviceActionService = ServiceContainer.Resolve("deviceActionService"); + _messagingService = ServiceContainer.Resolve("messagingService"); + _broadcasterService = ServiceContainer.Resolve("broadcasterService"); + _storageService = ServiceContainer.Resolve("storageService"); + _stateService = ServiceContainer.Resolve("stateService"); + _eventService = ServiceContainer.Resolve("eventService"); + + ConnectToWatchIfNeededAsync().FireAndForget(); + + _broadcasterService.Subscribe(nameof(AppDelegate), async (message) => { - if (message.Command == "startEventTimer") + try { - StartEventTimer(); - } - else if (message.Command == "stopEventTimer") - { - var task = StopEventTimerAsync(); - } - else if (message.Command is ThemeManager.UPDATED_THEME_MESSAGE_KEY) - { - MainThread.BeginInvokeOnMainThread(() => + if (message.Command == "startEventTimer") { - iOSCoreHelpers.AppearanceAdjustments(); - }); - } - else if (message.Command == "listenYubiKeyOTP") - { - iOSCoreHelpers.ListenYubiKey((bool)message.Data, _deviceActionService, _nfcSession, _nfcDelegate); - } - else if (message.Command == "unlocked") - { - var needsAutofillReplacement = await _storageService.GetAsync( - Core.Constants.AutofillNeedsIdentityReplacementKey); - if (needsAutofillReplacement.GetValueOrDefault()) - { - await ASHelpers.ReplaceAllIdentities(); + StartEventTimer(); } - } - else if (message.Command == "showAppExtension") - { - MainThread.BeginInvokeOnMainThread(() => ShowAppExtension((ExtensionPageViewModel)message.Data)); - } - else if (message.Command == "syncCompleted") - { - if (message.Data is Dictionary data && data.ContainsKey("successfully")) + else if (message.Command == "stopEventTimer") { - var success = data["successfully"] as bool?; - if (success.GetValueOrDefault() && _deviceActionService.SystemMajorVersion() >= 12) + var task = StopEventTimerAsync(); + } + else if (message.Command is ThemeManager.UPDATED_THEME_MESSAGE_KEY) + { + await MainThread.InvokeOnMainThreadAsync(() => + { + iOSCoreHelpers.AppearanceAdjustments(); + }); + } + else if (message.Command == "listenYubiKeyOTP" && message.Data is bool listen) + { + iOSCoreHelpers.ListenYubiKey(listen, _deviceActionService, _nfcSession, _nfcDelegate); + } + else if (message.Command == "unlocked") + { + var needsAutofillReplacement = await _storageService.GetAsync( + Core.Constants.AutofillNeedsIdentityReplacementKey); + if (needsAutofillReplacement.GetValueOrDefault()) { await ASHelpers.ReplaceAllIdentities(); } } - } - else if (message.Command == "addedCipher" || message.Command == "editedCipher" || - message.Command == "restoredCipher") - { - if (_deviceActionService.SystemMajorVersion() >= 12) + else if (message.Command == "showAppExtension") { + await MainThread.InvokeOnMainThreadAsync(() => ShowAppExtension((ExtensionPageViewModel)message.Data)); + } + else if (message.Command == "syncCompleted") + { + if (message.Data is Dictionary data && data.TryGetValue("successfully", out var value)) + { + var success = value as bool?; + if (success.GetValueOrDefault() && _deviceActionService.SystemMajorVersion() >= 12) + { + await ASHelpers.ReplaceAllIdentities(); + } + } + } + else if (message.Command == "addedCipher" || message.Command == "editedCipher" || + message.Command == "restoredCipher") + { + if (!UIDevice.CurrentDevice.CheckSystemVersion(12, 0)) + { + return; + } + if (await ASHelpers.IdentitiesCanIncremental()) { var cipherId = message.Data as string; @@ -125,11 +131,13 @@ namespace Bit.iOS } await ASHelpers.ReplaceAllIdentities(); } - } - else if (message.Command == "deletedCipher" || message.Command == "softDeletedCipher") - { - if (_deviceActionService.SystemMajorVersion() >= 12) + else if (message.Command == "deletedCipher" || message.Command == "softDeletedCipher") { + if (!UIDevice.CurrentDevice.CheckSystemVersion(12, 0)) + { + return; + } + if (await ASHelpers.IdentitiesCanIncremental()) { var identity = ASHelpers.ToCredentialIdentity( @@ -144,101 +152,145 @@ namespace Bit.iOS } await ASHelpers.ReplaceAllIdentities(); } - } - else if (message.Command == "logout") - { - if (_deviceActionService.SystemMajorVersion() >= 12) + else if (message.Command == "logout" && UIDevice.CurrentDevice.CheckSystemVersion(12, 0)) { await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync(); } - } - else if ((message.Command == "softDeletedCipher" || message.Command == "restoredCipher") - && _deviceActionService.SystemMajorVersion() >= 12) - { - await ASHelpers.ReplaceAllIdentities(); - } - else if (message.Command == AppHelpers.VAULT_TIMEOUT_ACTION_CHANGED_MESSAGE_COMMAND) - { - var timeoutAction = await _stateService.GetVaultTimeoutActionAsync(); - if (timeoutAction == VaultTimeoutAction.Logout) - { - await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync(); - } - else + else if ((message.Command == "softDeletedCipher" || message.Command == "restoredCipher") + && UIDevice.CurrentDevice.CheckSystemVersion(12, 0)) { await ASHelpers.ReplaceAllIdentities(); } + else if (message.Command == AppHelpers.VAULT_TIMEOUT_ACTION_CHANGED_MESSAGE_COMMAND) + { + var timeoutAction = await _stateService.GetVaultTimeoutActionAsync(); + if (timeoutAction == VaultTimeoutAction.Logout) + { + if (UIDevice.CurrentDevice.CheckSystemVersion(12, 0)) + { + await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync(); + } + } + else + { + await ASHelpers.ReplaceAllIdentities(); + } + } } - } - catch (Exception ex) - { - LoggerHelper.LogEvenIfCantBeResolved(ex); - } - }); + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + } + }); - var finishedLaunching = base.FinishedLaunching(app, options); + var finishedLaunching = base.FinishedLaunching(app, options); - ThemeManager.SetTheme(Microsoft.Maui.Controls.Application.Current.Resources); - iOSCoreHelpers.AppearanceAdjustments(); + ThemeManager.SetTheme(Microsoft.Maui.Controls.Application.Current.Resources); + iOSCoreHelpers.AppearanceAdjustments(); - return finishedLaunching; + return finishedLaunching; + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + throw; + } } public override void OnResignActivation(UIApplication uiApplication) { - if (UIApplication.SharedApplication.KeyWindow != null) + try { - var view = new UIView(UIApplication.SharedApplication.KeyWindow.Frame) + if (UIApplication.SharedApplication.KeyWindow != null) { - Tag = SPLASH_VIEW_TAG - }; - var backgroundView = new UIView(UIApplication.SharedApplication.KeyWindow.Frame) - { - BackgroundColor = ThemeManager.GetResourceColor("SplashBackgroundColor").ToPlatform() - }; - var logo = new UIImage(!ThemeManager.UsingLightTheme ? "logo_white.png" : "logo.png"); - var frame = new CGRect(0, 0, 280, 100); //Setting image width to avoid it being larger and getting cropped on smaller devices. This harcoded size should be good even for very small devices. - var imageView = new UIImageView(frame) - { - Image = logo, - Center = new CGPoint(view.Center.X, view.Center.Y - 30), - ContentMode = UIViewContentMode.ScaleAspectFit - }; - view.AddSubview(backgroundView); - view.AddSubview(imageView); - UIApplication.SharedApplication.KeyWindow.AddSubview(view); - UIApplication.SharedApplication.KeyWindow.BringSubviewToFront(view); - UIApplication.SharedApplication.KeyWindow.EndEditing(true); + var view = new UIView(UIApplication.SharedApplication.KeyWindow.Frame) + { + Tag = SPLASH_VIEW_TAG + }; + var backgroundView = new UIView(UIApplication.SharedApplication.KeyWindow.Frame) + { + BackgroundColor = ThemeManager.GetResourceColor("SplashBackgroundColor").ToPlatform() + }; + var logo = new UIImage(!ThemeManager.UsingLightTheme ? "logo_white.png" : "logo.png"); + var frame = new CGRect(0, 0, 280, 100); //Setting image width to avoid it being larger and getting cropped on smaller devices. This harcoded size should be good even for very small devices. + var imageView = new UIImageView(frame) + { + Image = logo, + Center = new CGPoint(view.Center.X, view.Center.Y - 30), + ContentMode = UIViewContentMode.ScaleAspectFit + }; + view.AddSubview(backgroundView); + view.AddSubview(imageView); + UIApplication.SharedApplication.KeyWindow.AddSubview(view); + UIApplication.SharedApplication.KeyWindow.BringSubviewToFront(view); + UIApplication.SharedApplication.KeyWindow.EndEditing(true); + } + base.OnResignActivation(uiApplication); + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + throw; } - base.OnResignActivation(uiApplication); } public override void DidEnterBackground(UIApplication uiApplication) { - if (_stateService != null && _deviceActionService != null) + try { - _stateService.SetLastActiveTimeAsync(_deviceActionService.GetActiveTime()); - } + if (_stateService != null && _deviceActionService != null) + { + _stateService.SetLastActiveTimeAsync(_deviceActionService.GetActiveTime()); + } - _messagingService?.Send("slept"); - base.DidEnterBackground(uiApplication); + _messagingService?.Send("slept"); + base.DidEnterBackground(uiApplication); + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + throw; + } } - public override void OnActivated(UIApplication uiApplication) + public override async void OnActivated(UIApplication uiApplication) { - base.OnActivated(uiApplication); - UIApplication.SharedApplication.ApplicationIconBadgeNumber = 0; - UIApplication.SharedApplication.KeyWindow? - .ViewWithTag(SPLASH_VIEW_TAG)? - .RemoveFromSuperview(); + try + { + base.OnActivated(uiApplication); - ThemeManager.UpdateThemeOnPagesAsync(); + if (UIDevice.CurrentDevice.CheckSystemVersion(17, 0)) + { + await UNUserNotificationCenter.Current.SetBadgeCountAsync(0); + } + else + { + UIApplication.SharedApplication.ApplicationIconBadgeNumber = 0; + } + + UIApplication.SharedApplication.KeyWindow? + .ViewWithTag(SPLASH_VIEW_TAG)? + .RemoveFromSuperview(); + + ThemeManager.UpdateThemeOnPagesAsync(); + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + } } public override void WillEnterForeground(UIApplication uiApplication) { - _messagingService?.Send(AppHelpers.RESUMED_MESSAGE_COMMAND); - base.WillEnterForeground(uiApplication); + try + { + _messagingService?.Send(AppHelpers.RESUMED_MESSAGE_COMMAND); + base.WillEnterForeground(uiApplication); + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + } } [Export("application:openURL:sourceApplication:annotation:")] @@ -249,15 +301,30 @@ namespace Bit.iOS public override bool OpenUrl(UIApplication app, NSUrl url, NSDictionary options) { - return _deepLinkContext.Value.OnNewUri(url) || base.OpenUrl(app, url, options); + try + { + return _deepLinkContext.Value.OnNewUri(url) || base.OpenUrl(app, url, options); + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + return false; + } } public override bool ContinueUserActivity(UIApplication application, NSUserActivity userActivity, UIApplicationRestorationHandler completionHandler) { - if (Microsoft.Maui.ApplicationModel.Platform.ContinueUserActivity(application, userActivity, completionHandler)) + try { - return true; + if (Microsoft.Maui.ApplicationModel.Platform.ContinueUserActivity(application, userActivity, completionHandler)) + { + return true; + } + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); } return base.ContinueUserActivity(application, userActivity, completionHandler); } @@ -265,33 +332,68 @@ namespace Bit.iOS [Export("application:didFailToRegisterForRemoteNotificationsWithError:")] public void FailedToRegisterForRemoteNotifications(UIApplication application, NSError error) { - _pushHandler?.OnErrorReceived(error); + try + { + _pushHandler?.OnErrorReceived(error); + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + } } [Export("application:didRegisterForRemoteNotificationsWithDeviceToken:")] public void RegisteredForRemoteNotifications(UIApplication application, NSData deviceToken) { - _pushHandler?.OnRegisteredSuccess(deviceToken); + try + { + _pushHandler?.OnRegisteredSuccess(deviceToken); + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + } } [Export("application:didRegisterUserNotificationSettings:")] public void DidRegisterUserNotificationSettings(UIApplication application, UIUserNotificationSettings notificationSettings) { - application.RegisterForRemoteNotifications(); + try + { + application.RegisterForRemoteNotifications(); + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + } } [Export("application:didReceiveRemoteNotification:fetchCompletionHandler:")] public void DidReceiveRemoteNotification(UIApplication application, NSDictionary userInfo, Action completionHandler) { - _pushHandler?.OnMessageReceived(userInfo); + try + { + _pushHandler?.OnMessageReceived(userInfo); + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + } } [Export("application:didReceiveRemoteNotification:")] public void ReceivedRemoteNotification(UIApplication application, NSDictionary userInfo) { - _pushHandler?.OnMessageReceived(userInfo); + try + { + _pushHandler?.OnMessageReceived(userInfo); + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + } } public void InitApp() @@ -304,17 +406,6 @@ namespace Bit.iOS // Migration services ServiceContainer.Register("nativeLogService", 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 - // }); - //}); - iOSCoreHelpers.RegisterLocalServices(); RegisterPush(); var deviceActionService = ServiceContainer.Resolve("deviceActionService"); @@ -328,7 +419,7 @@ namespace Bit.iOS _nfcDelegate = new Core.NFCReaderDelegate((success, message) => _messagingService.Send("gotYubiKeyOTP", message)); - iOSCoreHelpers.Bootstrap(async () => await ApplyManagedSettingsAsync()); + iOSCoreHelpers.Bootstrap(ApplyManagedSettingsAsync); } private void RegisterPush() @@ -373,31 +464,45 @@ namespace Bit.iOS _eventTimer = null; MainThread.BeginInvokeOnMainThread(() => { - _eventTimer = NSTimer.CreateScheduledTimer(60, true, timer => + try { - var task = Task.Run(() => _eventService.UploadEventsAsync()); - }); + _eventTimer = NSTimer.CreateScheduledTimer(60, true, timer => + { + _eventService?.UploadEventsAsync().FireAndForget(); + }); + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + } }); } private async Task StopEventTimerAsync() { - _eventTimer?.Invalidate(); - _eventTimer?.Dispose(); - _eventTimer = null; - if (_eventBackgroundTaskId > 0) + try { + _eventTimer?.Invalidate(); + _eventTimer?.Dispose(); + _eventTimer = null; + if (_eventBackgroundTaskId > 0) + { + UIApplication.SharedApplication.EndBackgroundTask(_eventBackgroundTaskId); + _eventBackgroundTaskId = 0; + } + _eventBackgroundTaskId = UIApplication.SharedApplication.BeginBackgroundTask(() => + { + UIApplication.SharedApplication.EndBackgroundTask(_eventBackgroundTaskId); + _eventBackgroundTaskId = 0; + }); + await _eventService.UploadEventsAsync(); UIApplication.SharedApplication.EndBackgroundTask(_eventBackgroundTaskId); _eventBackgroundTaskId = 0; } - _eventBackgroundTaskId = UIApplication.SharedApplication.BeginBackgroundTask(() => + catch (Exception ex) { - UIApplication.SharedApplication.EndBackgroundTask(_eventBackgroundTaskId); - _eventBackgroundTaskId = 0; - }); - await _eventService.UploadEventsAsync(); - UIApplication.SharedApplication.EndBackgroundTask(_eventBackgroundTaskId); - _eventBackgroundTaskId = 0; + LoggerHelper.LogEvenIfCantBeResolved(ex); + } } private async Task ApplyManagedSettingsAsync() diff --git a/src/Core/App.xaml.cs b/src/Core/App.xaml.cs index 7deed659d..4f2fa1b3a 100644 --- a/src/Core/App.xaml.cs +++ b/src/Core/App.xaml.cs @@ -81,7 +81,7 @@ namespace Bit.App { get { - return Application.Current.Windows.OfType().FirstOrDefault(w => w.IsActive); + return Application.Current?.Windows.OfType().FirstOrDefault(w => w.IsActive); } } @@ -145,11 +145,14 @@ namespace Bit.App { get { - return Application.Current.MainPage; + return Application.Current?.MainPage; } set { - Application.Current.MainPage = value; + if (Application.Current != null) + { + Application.Current.MainPage = value; + } } } #endif diff --git a/src/Core/Pages/Accounts/HomePage.xaml.cs b/src/Core/Pages/Accounts/HomePage.xaml.cs index 11c5b36ba..9a418ac4d 100644 --- a/src/Core/Pages/Accounts/HomePage.xaml.cs +++ b/src/Core/Pages/Accounts/HomePage.xaml.cs @@ -153,7 +153,7 @@ namespace Bit.App.Pages private async Task StartEnvironmentAsync() { - await _accountListOverlay.HideAsync(); + await _accountListOverlay.HideAsync(); var page = new EnvironmentPage(); await Navigation.PushModalAsync(new NavigationPage(page)); } diff --git a/src/Core/Pages/Accounts/LockPage.xaml.cs b/src/Core/Pages/Accounts/LockPage.xaml.cs index 6861f7bc2..bd3be1858 100644 --- a/src/Core/Pages/Accounts/LockPage.xaml.cs +++ b/src/Core/Pages/Accounts/LockPage.xaml.cs @@ -81,7 +81,7 @@ namespace Bit.App.Pages { if (message.Command == Constants.ClearSensitiveFields) { - MainThread.BeginInvokeOnMainThread(_vm.ResetPinPasswordFields); + MainThread.BeginInvokeOnMainThread(() => _vm?.ResetPinPasswordFields()); } }); if (_appeared) diff --git a/src/Core/Pages/Accounts/LockPageViewModel.cs b/src/Core/Pages/Accounts/LockPageViewModel.cs index 2464524cf..b3bd61015 100644 --- a/src/Core/Pages/Accounts/LockPageViewModel.cs +++ b/src/Core/Pages/Accounts/LockPageViewModel.cs @@ -245,9 +245,9 @@ namespace Bit.App.Pages public async Task SubmitAsync() { - ShowPassword = false; try { + ShowPassword = false; var kdfConfig = await _stateService.GetActiveUserCustomDataAsync(a => new KdfConfig(a?.Profile)); if (PinEnabled) { @@ -257,12 +257,15 @@ namespace Bit.App.Pages { await UnlockWithMasterPasswordAsync(kdfConfig); } - } catch (LegacyUserException) { await HandleLegacyUserAsync(); } + catch (Exception ex) + { + HandleException(ex); + } } private async Task UnlockWithPinAsync(KdfConfig kdfConfig) diff --git a/src/Core/Pages/Accounts/LoginPage.xaml.cs b/src/Core/Pages/Accounts/LoginPage.xaml.cs index 84f62879f..7139e57b1 100644 --- a/src/Core/Pages/Accounts/LoginPage.xaml.cs +++ b/src/Core/Pages/Accounts/LoginPage.xaml.cs @@ -3,6 +3,7 @@ using Bit.App.Utilities; using Bit.Core; using Bit.Core.Abstractions; using Bit.Core.Enums; +using Bit.Core.Services; using Bit.Core.Utilities; namespace Bit.App.Pages @@ -74,7 +75,7 @@ namespace Bit.App.Pages { if (message.Command == Constants.ClearSensitiveFields) { - MainThread.BeginInvokeOnMainThread(_vm.ResetPasswordField); + MainThread.BeginInvokeOnMainThread(() => _vm?.ResetPasswordField()); } }); _mainContent.Content = _mainLayout; @@ -188,12 +189,20 @@ namespace Bit.App.Pages private async Task LogInSuccessAsync() { - if (AppHelpers.SetAlternateMainPage(_appOptions)) + try { - return; + if (AppHelpers.SetAlternateMainPage(_appOptions)) + { + return; + } + var previousPage = await AppHelpers.ClearPreviousPage(); + App.MainPage = new TabsPage(_appOptions, previousPage); + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + throw; } - var previousPage = await AppHelpers.ClearPreviousPage(); - App.MainPage = new TabsPage(_appOptions, previousPage); } private async Task UpdateTempPasswordAsync() diff --git a/src/Core/Pages/Accounts/LoginPasswordlessRequestPage.xaml.cs b/src/Core/Pages/Accounts/LoginPasswordlessRequestPage.xaml.cs index ad5e3aba3..a88f27a03 100644 --- a/src/Core/Pages/Accounts/LoginPasswordlessRequestPage.xaml.cs +++ b/src/Core/Pages/Accounts/LoginPasswordlessRequestPage.xaml.cs @@ -1,6 +1,7 @@ using Bit.App.Models; using Bit.App.Utilities; using Bit.Core.Enums; +using Bit.Core.Services; namespace Bit.App.Pages { @@ -48,12 +49,20 @@ namespace Bit.App.Pages private async Task LogInSuccessAsync() { - if (AppHelpers.SetAlternateMainPage(_appOptions)) + try { - return; + if (AppHelpers.SetAlternateMainPage(_appOptions)) + { + return; + } + var previousPage = await AppHelpers.ClearPreviousPage(); + App.MainPage = new TabsPage(_appOptions, previousPage); + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + throw; } - var previousPage = await AppHelpers.ClearPreviousPage(); - App.MainPage = new TabsPage(_appOptions, previousPage); } private async Task UpdateTempPasswordAsync() diff --git a/src/Core/Pages/Accounts/LoginSsoPage.xaml.cs b/src/Core/Pages/Accounts/LoginSsoPage.xaml.cs index 3760f961e..1c1402a8b 100644 --- a/src/Core/Pages/Accounts/LoginSsoPage.xaml.cs +++ b/src/Core/Pages/Accounts/LoginSsoPage.xaml.cs @@ -1,6 +1,7 @@ using Bit.App.Models; using Bit.App.Utilities; using Bit.Core.Abstractions; +using Bit.Core.Services; using Bit.Core.Utilities; namespace Bit.App.Pages @@ -89,16 +90,30 @@ namespace Bit.App.Pages private async Task StartTwoFactorAsync() { - RestoreAppOptionsFromCopy(); - var page = new TwoFactorPage(true, _appOptions, _vm.OrgIdentifier); - await Navigation.PushModalAsync(new NavigationPage(page)); + try + { + RestoreAppOptionsFromCopy(); + var page = new TwoFactorPage(true, _appOptions, _vm.OrgIdentifier); + await Navigation.PushModalAsync(new NavigationPage(page)); + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + } } private async Task StartSetPasswordAsync() { - RestoreAppOptionsFromCopy(); - var page = new SetPasswordPage(_appOptions, _vm.OrgIdentifier); - await Navigation.PushModalAsync(new NavigationPage(page)); + try + { + RestoreAppOptionsFromCopy(); + var page = new SetPasswordPage(_appOptions, _vm.OrgIdentifier); + await Navigation.PushModalAsync(new NavigationPage(page)); + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + } } private async Task UpdateTempPasswordAsync() @@ -115,16 +130,23 @@ namespace Bit.App.Pages private async Task SsoAuthSuccessAsync() { - RestoreAppOptionsFromCopy(); - await AppHelpers.ClearPreviousPage(); + try + { + RestoreAppOptionsFromCopy(); + await AppHelpers.ClearPreviousPage(); - if (await _vaultTimeoutService.IsLockedAsync()) - { - App.MainPage = new NavigationPage(new LockPage(_appOptions)); + if (await _vaultTimeoutService.IsLockedAsync()) + { + App.MainPage = new NavigationPage(new LockPage(_appOptions)); + } + else + { + App.MainPage = new TabsPage(_appOptions, null); + } } - else + catch (Exception ex) { - App.MainPage = new TabsPage(_appOptions, null); + LoggerHelper.LogEvenIfCantBeResolved(ex); } } } diff --git a/src/Core/Pages/Accounts/SetPasswordPage.xaml.cs b/src/Core/Pages/Accounts/SetPasswordPage.xaml.cs index 5504b9d43..19013dba4 100644 --- a/src/Core/Pages/Accounts/SetPasswordPage.xaml.cs +++ b/src/Core/Pages/Accounts/SetPasswordPage.xaml.cs @@ -1,5 +1,6 @@ using Bit.App.Models; using Bit.App.Utilities; +using Bit.Core.Services; namespace Bit.App.Pages { @@ -64,12 +65,19 @@ namespace Bit.App.Pages private async Task SetPasswordSuccessAsync() { - if (AppHelpers.SetAlternateMainPage(_appOptions)) + try { - return; + if (AppHelpers.SetAlternateMainPage(_appOptions)) + { + return; + } + var previousPage = await AppHelpers.ClearPreviousPage(); + App.MainPage = new TabsPage(_appOptions, previousPage); + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); } - var previousPage = await AppHelpers.ClearPreviousPage(); - App.MainPage = new TabsPage(_appOptions, previousPage); } } } diff --git a/src/Core/Pages/Accounts/TwoFactorPage.xaml.cs b/src/Core/Pages/Accounts/TwoFactorPage.xaml.cs index 1b218dc3d..9322936ce 100644 --- a/src/Core/Pages/Accounts/TwoFactorPage.xaml.cs +++ b/src/Core/Pages/Accounts/TwoFactorPage.xaml.cs @@ -2,6 +2,7 @@ using Bit.App.Models; using Bit.App.Utilities; using Bit.Core.Abstractions; +using Bit.Core.Services; using Bit.Core.Utilities; namespace Bit.App.Pages @@ -63,11 +64,11 @@ namespace Bit.App.Pages if (_vm.YubikeyMethod && !string.IsNullOrWhiteSpace(token) && token.Length == 44 && !token.Contains(" ")) { - MainThread.BeginInvokeOnMainThread(async () => + MainThread.BeginInvokeOnMainThread(() => { _vm.Token = token; - await _vm.SubmitAsync(); }); + _vm.SubmitCommand.Execute(null); } } else if (message.Command == "resumeYubiKey") @@ -124,12 +125,9 @@ namespace Bit.App.Pages return base.OnBackButtonPressed(); } - private async void Continue_Clicked(object sender, EventArgs e) + private void Continue_Clicked(object sender, EventArgs e) { - if (DoOnce()) - { - await _vm.SubmitAsync(); - } + _vm.SubmitCommand.Execute(null); } private async void Methods_Clicked(object sender, EventArgs e) @@ -158,17 +156,24 @@ namespace Bit.App.Pages private async void TryAgain_Clicked(object sender, EventArgs e) { - if (DoOnce()) + try { - if (_vm.Fido2Method) + if (DoOnce()) { - await _vm.Fido2AuthenticateAsync(); - } - else if (_vm.YubikeyMethod) - { - _messagingService.Send("listenYubiKeyOTP", true); + if (_vm.Fido2Method) + { + await _vm.Fido2AuthenticateAsync(); + } + else if (_vm.YubikeyMethod) + { + _messagingService.Send("listenYubiKeyOTP", true); + } } } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + } } private async Task StartSetPasswordAsync() diff --git a/src/Core/Pages/Accounts/TwoFactorPageViewModel.cs b/src/Core/Pages/Accounts/TwoFactorPageViewModel.cs index 0952c5be4..319d55160 100644 --- a/src/Core/Pages/Accounts/TwoFactorPageViewModel.cs +++ b/src/Core/Pages/Accounts/TwoFactorPageViewModel.cs @@ -1,25 +1,16 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Threading.Tasks; +using System.Net; using System.Windows.Input; using Bit.App.Abstractions; -using Bit.Core.Resources.Localization; using Bit.App.Utilities; using Bit.Core.Abstractions; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Domain; using Bit.Core.Models.Request; -using Bit.Core.Services; +using Bit.Core.Resources.Localization; using Bit.Core.Utilities; using Newtonsoft.Json; -using Microsoft.Maui.Authentication; -using Microsoft.Maui.Controls; -using Microsoft.Maui; - namespace Bit.App.Pages { public class TwoFactorPageViewModel : CaptchaProtectedViewModel @@ -62,7 +53,7 @@ namespace Bit.App.Pages _deviceTrustCryptoService = ServiceContainer.Resolve(); PageTitle = AppResources.TwoStepLogin; - SubmitCommand = new Command(async () => await SubmitAsync()); + SubmitCommand = CreateDefaultAsyncRelayCommand(() => MainThread.InvokeOnMainThreadAsync(async () => await SubmitAsync()), allowsMultipleExecutions: false); MoreCommand = CreateDefaultAsyncRelayCommand(MoreAsync, onException: _logger.Exception, allowsMultipleExecutions: false); } @@ -91,8 +82,7 @@ namespace Bit.App.Pages public bool TotpMethod => AuthenticatorMethod || EmailMethod; - public bool ShowTryAgain => (YubikeyMethod && // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes -Device.RuntimePlatform == Device.iOS) || Fido2Method; + public bool ShowTryAgain => (YubikeyMethod && DeviceInfo.Platform == DevicePlatform.iOS) || Fido2Method; public bool ShowContinue { @@ -106,9 +96,11 @@ Device.RuntimePlatform == Device.iOS) || Fido2Method; set => SetProperty(ref _enableContinue, value); } - public string YubikeyInstruction => // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes -Device.RuntimePlatform == Device.iOS ? AppResources.YubiKeyInstructionIos : - AppResources.YubiKeyInstruction; +#if IOS + public string YubikeyInstruction => AppResources.YubiKeyInstructionIos; +#else + public string YubikeyInstruction => AppResources.YubiKeyInstruction; +#endif public TwoFactorProviderType? SelectedProviderType { @@ -124,7 +116,7 @@ Device.RuntimePlatform == Device.iOS ? AppResources.YubiKeyInstructionIos : nameof(ShowTryAgain), }); } - public Command SubmitCommand { get; } + public ICommand SubmitCommand { get; } public ICommand MoreCommand { get; } public Action TwoFactorAuthSuccessAction { get; set; } public Action LockAction { get; set; } @@ -186,7 +178,7 @@ Device.RuntimePlatform == Device.iOS ? AppResources.YubiKeyInstructionIos : page.DuoWebView.RegisterAction(sig => { Token = sig; - Device.BeginInvokeOnMainThread(async () => await SubmitAsync()); + SubmitCommand.Execute(null); }); break; case TwoFactorProviderType.Email: @@ -213,68 +205,76 @@ Device.RuntimePlatform == Device.iOS ? AppResources.YubiKeyInstructionIos : public async Task Fido2AuthenticateAsync(Dictionary providerData = null) { - await _deviceActionService.ShowLoadingAsync(AppResources.Validating); - - if (providerData == null) - { - providerData = _authService.TwoFactorProvidersData[TwoFactorProviderType.Fido2WebAuthn]; - } - - var callbackUri = "bitwarden://webauthn-callback"; - var data = AppHelpers.EncodeDataParameter(new - { - callbackUri = callbackUri, - data = JsonConvert.SerializeObject(providerData), - headerText = AppResources.Fido2Title, - btnText = AppResources.Fido2AuthenticateWebAuthn, - btnReturnText = AppResources.Fido2ReturnToApp, - }); - - var url = _webVaultUrl + "/webauthn-mobile-connector.html?" + "data=" + data + - "&parent=" + Uri.EscapeDataString(callbackUri) + "&v=2"; - - WebAuthenticatorResult authResult = null; try { - var options = new WebAuthenticatorOptions - { - Url = new Uri(url), - CallbackUrl = new Uri(callbackUri), - PrefersEphemeralWebBrowserSession = true, - }; - authResult = await WebAuthenticator.AuthenticateAsync(options); - } - catch (TaskCanceledException) - { - // user canceled - await _deviceActionService.HideLoadingAsync(); - return; - } + await _deviceActionService.ShowLoadingAsync(AppResources.Validating); - string response = null; - if (authResult != null && authResult.Properties.TryGetValue("data", out var resultData)) - { - response = Uri.UnescapeDataString(resultData); - } - if (!string.IsNullOrWhiteSpace(response)) - { - Token = response; - await SubmitAsync(false); - } - else - { - await _deviceActionService.HideLoadingAsync(); - if (authResult != null && authResult.Properties.TryGetValue("error", out var resultError)) + if (providerData == null) { - var message = AppResources.Fido2CheckBrowser + "\n\n" + resultError; - await _platformUtilsService.ShowDialogAsync(message, AppResources.AnErrorHasOccurred, - AppResources.Ok); + providerData = _authService.TwoFactorProvidersData[TwoFactorProviderType.Fido2WebAuthn]; + } + + var callbackUri = "bitwarden://webauthn-callback"; + var data = AppHelpers.EncodeDataParameter(new + { + callbackUri = callbackUri, + data = JsonConvert.SerializeObject(providerData), + headerText = AppResources.Fido2Title, + btnText = AppResources.Fido2AuthenticateWebAuthn, + btnReturnText = AppResources.Fido2ReturnToApp, + }); + + var url = _webVaultUrl + "/webauthn-mobile-connector.html?" + "data=" + data + + "&parent=" + Uri.EscapeDataString(callbackUri) + "&v=2"; + + WebAuthenticatorResult authResult = null; + try + { + var options = new WebAuthenticatorOptions + { + Url = new Uri(url), + CallbackUrl = new Uri(callbackUri), + PrefersEphemeralWebBrowserSession = true, + }; + authResult = await WebAuthenticator.AuthenticateAsync(options); + } + catch (TaskCanceledException) + { + // user canceled + await _deviceActionService.HideLoadingAsync(); + return; + } + + string response = null; + if (authResult != null && authResult.Properties.TryGetValue("data", out var resultData)) + { + response = Uri.UnescapeDataString(resultData); + } + if (!string.IsNullOrWhiteSpace(response)) + { + Token = response; + await SubmitAsync(false); } else { - await _platformUtilsService.ShowDialogAsync(AppResources.Fido2CheckBrowser, - AppResources.AnErrorHasOccurred, AppResources.Ok); + await _deviceActionService.HideLoadingAsync(); + if (authResult != null && authResult.Properties.TryGetValue("error", out var resultError)) + { + var message = AppResources.Fido2CheckBrowser + "\n\n" + resultError; + await _platformUtilsService.ShowDialogAsync(message, AppResources.AnErrorHasOccurred, + AppResources.Ok); + } + else + { + await _platformUtilsService.ShowDialogAsync(AppResources.Fido2CheckBrowser, + AppResources.AnErrorHasOccurred, AppResources.Ok); + } } + + } + catch (Exception ex) + { + HandleException(ex); } } diff --git a/src/Core/Pages/BaseContentPage.cs b/src/Core/Pages/BaseContentPage.cs index d04c8a852..8aa3b453c 100644 --- a/src/Core/Pages/BaseContentPage.cs +++ b/src/Core/Pages/BaseContentPage.cs @@ -170,8 +170,15 @@ namespace Bit.App.Pages { Task.Run(async () => { - await Task.Delay(ShowModalAnimationDelay); - MainThread.BeginInvokeOnMainThread(() => input.Focus()); + try + { + await Task.Delay(ShowModalAnimationDelay); + MainThread.BeginInvokeOnMainThread(() => input.Focus()); + } + catch (Exception ex) + { + _logger.Value.Exception(ex); + } }); } diff --git a/src/iOS.Core/Handlers/CustomTabbedHandler.cs b/src/iOS.Core/Handlers/CustomTabbedHandler.cs index aa3a6f4ad..694720534 100644 --- a/src/iOS.Core/Handlers/CustomTabbedHandler.cs +++ b/src/iOS.Core/Handlers/CustomTabbedHandler.cs @@ -1,5 +1,4 @@ -using Bit.App.Abstractions; -using Bit.App.Pages; +using Bit.App.Pages; using Bit.App.Utilities; using Bit.Core.Abstractions; using Bit.Core.Utilities; @@ -13,7 +12,7 @@ namespace Bit.iOS.Core.Handlers public partial class CustomTabbedHandler : TabbedRenderer { private IBroadcasterService _broadcasterService; - private UITabBarItem _previousSelectedItem; + private UITabBarItem? _previousSelectedItem; public CustomTabbedHandler() { @@ -73,8 +72,7 @@ namespace Bit.iOS.Core.Handlers private void UpdateTabBarAppearance() { // https://developer.apple.com/forums/thread/682420 - var deviceActionService = ServiceContainer.Resolve("deviceActionService"); - if (deviceActionService.SystemMajorVersion() >= 15) + if (UIDevice.CurrentDevice.CheckSystemVersion(15,0)) { var appearance = new UITabBarAppearance(); appearance.ConfigureWithOpaqueBackground(); diff --git a/src/iOS.Core/Services/LocalizeService.cs b/src/iOS.Core/Services/LocalizeService.cs index 88f8dde94..287e44e2a 100644 --- a/src/iOS.Core/Services/LocalizeService.cs +++ b/src/iOS.Core/Services/LocalizeService.cs @@ -1,5 +1,4 @@ -using System; -using System.Diagnostics; +using System.Diagnostics; using System.Globalization; using Bit.App.Abstractions; using Bit.App.Models; @@ -20,7 +19,7 @@ namespace Bit.iOS.Core.Services } // This gets called a lot - try/catch can be expensive so consider caching or something - CultureInfo ci = null; + CultureInfo? ci; try { ci = new CultureInfo(netLanguage); @@ -108,7 +107,7 @@ namespace Bit.iOS.Core.Services { df.Locale = NSLocale.CurrentLocale; df.DateStyle = NSDateFormatterStyle.Short; - return df.StringFor((NSDate)date); + return df.StringFor((NSDate?)date); } } @@ -118,7 +117,7 @@ namespace Bit.iOS.Core.Services { df.Locale = NSLocale.CurrentLocale; df.TimeStyle = NSDateFormatterStyle.Short; - return df.StringFor((NSDate)time); + return df.StringFor((NSDate?)time); } } } diff --git a/src/iOS.Core/Utilities/DictionaryExtensions.cs b/src/iOS.Core/Utilities/DictionaryExtensions.cs index de3387237..b88e7411d 100644 --- a/src/iOS.Core/Utilities/DictionaryExtensions.cs +++ b/src/iOS.Core/Utilities/DictionaryExtensions.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Bit.Core.Models.Domain; -using Foundation; +using Foundation; using Newtonsoft.Json; namespace Bit.iOS.Core.Utilities @@ -15,6 +11,7 @@ namespace Bit.iOS.Core.Utilities } public static NSDictionary ToNSDictionary(this Dictionary dict, Func keyConverter, Func valueConverter) + where KFrom : notnull where KTo : NSObject where VTo : NSObject { @@ -23,19 +20,20 @@ namespace Bit.iOS.Core.Utilities return NSDictionary.FromObjectsAndKeys(NSValues, NSKeys, NSKeys.Count()); } - public static Dictionary ToDictionary(this NSDictionary nsDict) + public static Dictionary ToDictionary(this NSDictionary nsDict) { - return nsDict.ToDictionary(v => v?.ToString() as object); + return nsDict.ToDictionary(v => v?.ToString()); } - public static Dictionary ToDictionary(this NSDictionary nsDict, Func valueTransformer) + public static Dictionary ToDictionary(this NSDictionary nsDict, Func valueTransformer) { return nsDict.ToDictionary(k => k.ToString(), v => valueTransformer(v)); } - public static Dictionary ToDictionary(this NSDictionary nsDict, Func keyConverter, Func valueConverter) + public static Dictionary ToDictionary(this NSDictionary nsDict, Func keyConverter, Func valueConverter) where KFrom : NSObject where VFrom : NSObject + where KTo : notnull { var keys = nsDict.Keys.Select(k => keyConverter(k)).ToArray(); var values = nsDict.Values.Select(v => valueConverter(v)).ToArray(); diff --git a/src/iOS.Core/Utilities/WCSessionManager.cs b/src/iOS.Core/Utilities/WCSessionManager.cs index e3e0375cd..d4d116b04 100644 --- a/src/iOS.Core/Utilities/WCSessionManager.cs +++ b/src/iOS.Core/Utilities/WCSessionManager.cs @@ -1,14 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Threading.Tasks; -using Bit.Core.Models.Domain; +using System.Diagnostics; using Bit.Core.Services; using Bit.iOS.Core.Utilities; using Foundation; using Newtonsoft.Json; -using ObjCRuntime; namespace WatchConnectivity { @@ -17,35 +11,45 @@ namespace WatchConnectivity // Setup is converted from https://www.natashatherobot.com/watchconnectivity-say-hello-to-wcsession/ // with some extra bits private static readonly WCSessionManager sharedManager = new WCSessionManager(); - private static WCSession session = WCSession.IsSupported ? WCSession.DefaultSession : null; + private static WCSession? session = WCSession.IsSupported ? WCSession.DefaultSession : null; - public event WCSessionReceiveDataHandler OnApplicationContextUpdated; - public event WCSessionReceiveDataHandler OnMessagedReceived; - public delegate void WCSessionReceiveDataHandler(WCSession session, Dictionary data); + public event WCSessionReceiveDataHandler? OnApplicationContextUpdated; + public event WCSessionReceiveDataHandler? OnMessagedReceived; + public delegate void WCSessionReceiveDataHandler(WCSession session, Dictionary data); - WCSessionUserInfoTransfer _transf; + WCSessionUserInfoTransfer? _transf; - private WCSession validSession + private WCSession? validSession { get { + if (session is null) + { + return null; + } + Debug.WriteLine($"Paired status:{(session.Paired ? '✓' : '✗')}\n"); Debug.WriteLine($"Watch App Installed status:{(session.WatchAppInstalled ? '✓' : '✗')}\n"); return (session.Paired && session.WatchAppInstalled) ? session : null; } } - private WCSession validReachableSession + private WCSession? validReachableSession { get { + if (session is null) + { + return null; + } + return session.Reachable ? validSession : null; } } public bool IsValidSession => validSession != null; - public bool IsSessionReachable => session.Reachable; + public bool IsSessionReachable => session?.Reachable ?? false; public bool IsSessionActivated => validSession?.ActivationState == WCSessionActivationState.Activated; @@ -71,7 +75,7 @@ namespace WatchConnectivity public override void SessionReachabilityDidChange(WCSession session) { - Debug.WriteLine($"Watch connectivity Reachable:{(session.Reachable ? '✓' : '✗')}"); + Debug.WriteLine($"Watch connectivity Reachable:{(session?.Reachable == true ? '✓' : '✗')}"); } public void SendBackgroundHighPriorityMessage(NSDictionary applicationContext) @@ -102,7 +106,7 @@ namespace WatchConnectivity public void SendBackgroundFifoHighPriorityMessage(Dictionary message) { - if(validSession is null || validSession.ActivationState != WCSessionActivationState.Activated) + if (session is null || validSession is null || validSession.ActivationState != WCSessionActivationState.Activated) { return; } @@ -112,6 +116,10 @@ namespace WatchConnectivity Debug.WriteLine("Started transferring user info"); _transf = session.TransferUserInfo(message.ToNSDictionary()); + if (_transf is null) + { + return; + } Task.Run(async () => { @@ -136,7 +144,7 @@ namespace WatchConnectivity if (OnApplicationContextUpdated != null) { var keys = applicationContext.Keys.Select(k => k.ToString()).ToArray(); - var values = applicationContext.Values.Select(v => JsonConvert.DeserializeObject(v.ToString())).ToArray(); + var values = applicationContext.Values.Select(v => v != null ? JsonConvert.DeserializeObject(v.ToString()) : null).ToArray(); var dictionary = keys.Zip(values, (k, v) => new { Key = k, Value = v }) .ToDictionary(x => x.Key, x => x.Value); diff --git a/src/iOS.Core/Utilities/iOSCoreHelpers.cs b/src/iOS.Core/Utilities/iOSCoreHelpers.cs index dd2efa399..a9efe2f03 100644 --- a/src/iOS.Core/Utilities/iOSCoreHelpers.cs +++ b/src/iOS.Core/Utilities/iOSCoreHelpers.cs @@ -116,9 +116,13 @@ namespace Bit.iOS.Core.Utilities ServiceContainer.Register("nativeLogService", new ConsoleLogService()); } - ILogger logger = null; - if (ServiceContainer.Resolve("logger", true) == null) + ILogger? logger = null; + if (ServiceContainer.TryResolve(out var resolvedLogger)) { + logger = resolvedLogger; + } + else + { #if DEBUG logger = DebugLogger.Instance; #else @@ -129,6 +133,12 @@ namespace Bit.iOS.Core.Utilities var preferencesStorage = new PreferencesStorageService(AppGroupId); var appGroupContainer = new NSFileManager().GetContainerUrl(AppGroupId); + if (appGroupContainer?.Path is null) + { + var nreAppGroupContainer = new NullReferenceException("appGroupContainer or its Path is null when registering local services"); + logger!.Exception(nreAppGroupContainer); + throw nreAppGroupContainer; + } var liteDbStorage = new LiteDbStorageService( Path.Combine(appGroupContainer.Path, "Library", "bitwarden.db")); var localizeService = new LocalizeService(); @@ -187,14 +197,14 @@ namespace Bit.iOS.Core.Utilities ServiceContainer.Resolve())); } - public static void Bootstrap(Func postBootstrapFunc = null) + public static void Bootstrap(Func? postBootstrapFunc = null) { var locale = ServiceContainer.Resolve().GetLocale(); (ServiceContainer.Resolve("i18nService") as MobileI18nService) - .Init(locale != null ? new System.Globalization.CultureInfo(locale) : null); + ?.Init(locale != null ? new System.Globalization.CultureInfo(locale) : null); ServiceContainer.Resolve("authService").Init(); (ServiceContainer. - Resolve("platformUtilsService") as MobilePlatformUtilsService).Init(); + Resolve("platformUtilsService") as MobilePlatformUtilsService)?.Init(); var accountsManager = new AccountsManager( ServiceContainer.Resolve("broadcasterService"), @@ -231,20 +241,31 @@ namespace Bit.iOS.Core.Utilities if (message.Command == "showDialog") { var details = message.Data as DialogDetails; + if (details is null) + { + return; + } var confirmText = string.IsNullOrWhiteSpace(details.ConfirmText) ? AppResources.Ok : details.ConfirmText; NSRunLoop.Main.BeginInvokeOnMainThread(async () => { - var result = await deviceActionService.DisplayAlertAsync(details.Title, details.Text, - details.CancelText, confirmText); - var confirmed = result == details.ConfirmText; - messagingService.Send("showDialogResolve", new Tuple(details.DialogId, confirmed)); + try + { + var result = await deviceActionService.DisplayAlertAsync(details.Title, details.Text, + details.CancelText, confirmText); + var confirmed = result == details.ConfirmText; + messagingService.Send("showDialogResolve", new Tuple(details.DialogId, confirmed)); + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + } }); } - else if (message.Command == "listenYubiKeyOTP") + else if (message.Command == "listenYubiKeyOTP" && message.Data is bool listen) { - ListenYubiKey((bool)message.Data, deviceActionService, nfcSession, nfcDelegate); + ListenYubiKey(listen, deviceActionService, nfcSession, nfcDelegate); } }); } @@ -268,29 +289,36 @@ namespace Bit.iOS.Core.Utilities } } - private static async Task BootstrapAsync(Func postBootstrapFunc = null) + private static async Task BootstrapAsync(Func? postBootstrapFunc = null) { - await ServiceContainer.Resolve("environmentService").SetUrlsFromStorageAsync(); - - InitializeAppSetup(); - // TODO: Update when https://github.com/bitwarden/mobile/pull/1662 gets merged - var deleteAccountActionFlowExecutioner = new DeleteAccountActionFlowExecutioner( - ServiceContainer.Resolve("apiService"), - ServiceContainer.Resolve("messagingService"), - ServiceContainer.Resolve("platformUtilsService"), - ServiceContainer.Resolve("deviceActionService"), - ServiceContainer.Resolve("logger")); - ServiceContainer.Register("deleteAccountActionFlowExecutioner", deleteAccountActionFlowExecutioner); - - var verificationActionsFlowHelper = new VerificationActionsFlowHelper( - ServiceContainer.Resolve("passwordRepromptService"), - ServiceContainer.Resolve("cryptoService"), - ServiceContainer.Resolve()); - ServiceContainer.Register("verificationActionsFlowHelper", verificationActionsFlowHelper); - - if (postBootstrapFunc != null) + try { - await postBootstrapFunc.Invoke(); + await ServiceContainer.Resolve("environmentService").SetUrlsFromStorageAsync(); + + InitializeAppSetup(); + // TODO: Update when https://github.com/bitwarden/mobile/pull/1662 gets merged + var deleteAccountActionFlowExecutioner = new DeleteAccountActionFlowExecutioner( + ServiceContainer.Resolve("apiService"), + ServiceContainer.Resolve("messagingService"), + ServiceContainer.Resolve("platformUtilsService"), + ServiceContainer.Resolve("deviceActionService"), + ServiceContainer.Resolve("logger")); + ServiceContainer.Register("deleteAccountActionFlowExecutioner", deleteAccountActionFlowExecutioner); + + var verificationActionsFlowHelper = new VerificationActionsFlowHelper( + ServiceContainer.Resolve("passwordRepromptService"), + ServiceContainer.Resolve("cryptoService"), + ServiceContainer.Resolve()); + ServiceContainer.Register("verificationActionsFlowHelper", verificationActionsFlowHelper); + + if (postBootstrapFunc != null) + { + await postBootstrapFunc.Invoke(); + } + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); } } diff --git a/src/iOS.Core/Views/ExtensionSearchDelegate.cs b/src/iOS.Core/Views/ExtensionSearchDelegate.cs index 3fa4ff64a..b3753d511 100644 --- a/src/iOS.Core/Views/ExtensionSearchDelegate.cs +++ b/src/iOS.Core/Views/ExtensionSearchDelegate.cs @@ -1,6 +1,4 @@ -using System; -using System.Threading; -using System.Threading.Tasks; +using Bit.Core.Services; using Foundation; using UIKit; @@ -9,7 +7,7 @@ namespace Bit.iOS.Core.Views public class ExtensionSearchDelegate : UISearchBarDelegate { private readonly UITableView _tableView; - private CancellationTokenSource _filterResultsCancellationTokenSource; + private CancellationTokenSource? _filterResultsCancellationTokenSource; public ExtensionSearchDelegate(UITableView tableView) { @@ -23,25 +21,34 @@ namespace Bit.iOS.Core.Views { NSRunLoop.Main.BeginInvokeOnMainThread(async () => { - if (!string.IsNullOrWhiteSpace(searchText)) - { - await Task.Delay(300); - if (searchText != searchBar.Text) - { - return; - } - else - { - _filterResultsCancellationTokenSource?.Cancel(); - } - } try { - ((ExtensionTableSource)_tableView.Source).FilterResults(searchText, cts.Token); - _tableView.ReloadData(); + if (!string.IsNullOrWhiteSpace(searchText)) + { + await Task.Delay(300); + if (searchText != searchBar.Text) + { + return; + } + else + { + _filterResultsCancellationTokenSource?.Cancel(); + } + } + try + { + ((ExtensionTableSource)_tableView.Source).FilterResults(searchText, cts.Token); + _tableView.ReloadData(); + } + catch (OperationCanceledException) { } + _filterResultsCancellationTokenSource = cts; + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + _filterResultsCancellationTokenSource?.Cancel(); + cts?.Cancel(); } - catch (OperationCanceledException) { } - _filterResultsCancellationTokenSource = cts; }); }, cts.Token); } diff --git a/src/iOS.Extension/LoadingViewController.cs b/src/iOS.Extension/LoadingViewController.cs index 4daeb7670..85f77329a 100644 --- a/src/iOS.Extension/LoadingViewController.cs +++ b/src/iOS.Extension/LoadingViewController.cs @@ -479,16 +479,23 @@ namespace Bit.iOS.Extension { NSRunLoop.Main.BeginInvokeOnMainThread(async () => { - if (await IsAuthed()) + try { - var stateService = ServiceContainer.Resolve("stateService"); - await AppHelpers.LogOutAsync(await stateService.GetActiveUserIdAsync()); - var deviceActionService = ServiceContainer.Resolve("deviceActionService"); - if (deviceActionService.SystemMajorVersion() >= 12) + if (await IsAuthed()) { - await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync(); + var stateService = ServiceContainer.Resolve("stateService"); + await AppHelpers.LogOutAsync(await stateService.GetActiveUserIdAsync()); + if (UIDevice.CurrentDevice.CheckSystemVersion(12, 0)) + { + await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync(); + } } } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + throw; + } }); } diff --git a/src/iOS.ShareExtension/LoadingViewController.cs b/src/iOS.ShareExtension/LoadingViewController.cs index c77582ca1..fc3ae0e2d 100644 --- a/src/iOS.ShareExtension/LoadingViewController.cs +++ b/src/iOS.ShareExtension/LoadingViewController.cs @@ -274,14 +274,22 @@ namespace Bit.iOS.ShareExtension { NSRunLoop.Main.BeginInvokeOnMainThread(async () => { - if (await IsAuthed()) + try { - await AppHelpers.LogOutAsync(await _stateService.Value.GetActiveUserIdAsync()); - if (UIDevice.CurrentDevice.CheckSystemVersion(12, 0)) + if (await IsAuthed()) { - await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync(); + await AppHelpers.LogOutAsync(await _stateService.Value.GetActiveUserIdAsync()); + if (UIDevice.CurrentDevice.CheckSystemVersion(12, 0)) + { + await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync(); + } } } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + throw; + } }); } From 8e1a8b5f0ef8e9d28c32d224058bf616e4ad4eb7 Mon Sep 17 00:00:00 2001 From: Federico Maccaroni Date: Fri, 19 Jan 2024 15:02:53 -0300 Subject: [PATCH 07/50] PM-3349 PM-3350 Updated XCode version on build.yml to 15.1 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fe6f6a176..7e7680d5f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -495,7 +495,7 @@ jobs: - name: Set XCode version uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd # v1.6.0 with: - xcode-version: 15.0.1 + xcode-version: 15.1 - name: Setup NuGet uses: nuget/setup-nuget@296fd3ccf8528660c91106efefe2364482f86d6f # v1.2.0 From c6544b49e93d7eb8597952d740748326ef16ae15 Mon Sep 17 00:00:00 2001 From: Federico Maccaroni Date: Fri, 19 Jan 2024 15:14:22 -0300 Subject: [PATCH 08/50] PM-3350 Removed TapGesture Window MAUI hack from iOS.Extension and iOS.ShareExtension --- src/iOS.Core/Handlers/CustomWindowHandler.cs | 24 --------- src/iOS.Core/Utilities/iOSCoreHelpers.cs | 2 - .../LoadingViewController.TapGestureHack.cs | 54 ------------------- src/iOS.Extension/LoadingViewController.cs | 4 +- src/iOS.Extension/iOS.Extension.csproj | 3 -- .../LoadingViewController.TapGestureHack.cs | 22 -------- .../LoadingViewController.cs | 7 --- .../iOS.ShareExtension.csproj | 3 -- 8 files changed, 2 insertions(+), 117 deletions(-) delete mode 100644 src/iOS.Core/Handlers/CustomWindowHandler.cs delete mode 100644 src/iOS.Extension/LoadingViewController.TapGestureHack.cs delete mode 100644 src/iOS.ShareExtension/LoadingViewController.TapGestureHack.cs diff --git a/src/iOS.Core/Handlers/CustomWindowHandler.cs b/src/iOS.Core/Handlers/CustomWindowHandler.cs deleted file mode 100644 index 6927c7a9b..000000000 --- a/src/iOS.Core/Handlers/CustomWindowHandler.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Microsoft.Maui.Handlers; -using UIKit; - -namespace Bit.iOS.Core.Handlers -{ - public class CustomWindowHandler : ElementHandler, IWindowHandler - { - public static IPropertyMapper Mapper = new PropertyMapper(ElementHandler.ElementMapper) - { - }; - - public CustomWindowHandler() : base(Mapper) - { - } - - protected override UIWindow CreatePlatformElement() - { - // Haven't tested - //return UIApplication.SharedApplication.Delegate.GetWindow(); - return Platform.GetCurrentUIViewController().View.Window; - } - } -} - diff --git a/src/iOS.Core/Utilities/iOSCoreHelpers.cs b/src/iOS.Core/Utilities/iOSCoreHelpers.cs index a9efe2f03..c2efab343 100644 --- a/src/iOS.Core/Utilities/iOSCoreHelpers.cs +++ b/src/iOS.Core/Utilities/iOSCoreHelpers.cs @@ -29,8 +29,6 @@ namespace Bit.iOS.Core.Utilities { var builder = Bit.Core.MauiProgram.ConfigureMauiAppBuilder(ConfigureMAUIEffects, handlers => { - // WORKAROUND: This is needed to make TapGestureRecognizer work on extensions. - handlers.AddHandler(typeof(Window), typeof(Handlers.CustomWindowHandler)); ConfigureMAUIHandlers(handlers); }) .UseMauiEmbedding(); diff --git a/src/iOS.Extension/LoadingViewController.TapGestureHack.cs b/src/iOS.Extension/LoadingViewController.TapGestureHack.cs deleted file mode 100644 index f5e09ca22..000000000 --- a/src/iOS.Extension/LoadingViewController.TapGestureHack.cs +++ /dev/null @@ -1,54 +0,0 @@ -#if ENABLED_TAP_GESTURE_RECOGNIZER_MAUI_EMBEDDED_WORKAROUND - -using System; -using System.Linq; -using Bit.iOS.Core.Utilities; -using Bit.iOS.Extension.Models; -using Microsoft.Maui.Controls; -using Microsoft.Maui.Platform; -using UIKit; - -namespace Bit.iOS.Extension -{ - public partial class LoadingViewController : UIViewController - { - const string STORYBOARD_NAME = "MainInterface"; - Lazy _storyboard = new Lazy(() => UIStoryboard.FromName(STORYBOARD_NAME, null)); - - public void InitWithContext(Context context) - { - _context = context; - _shouldInitialize = false; - } - - public void DismissLockAndContinue() - { - if (UIApplication.SharedApplication.KeyWindow is null) - { - return; - } - - UIApplication.SharedApplication.KeyWindow.RootViewController = _storyboard.Value.InstantiateInitialViewController(); - - if (UIApplication.SharedApplication.KeyWindow?.RootViewController is UINavigationController navContr) - { - var rootVC = navContr.ViewControllers.FirstOrDefault(); - if (rootVC is LoadingViewController loadingVC) - { - loadingVC.InitWithContext(_context); - loadingVC.ContinueOn(); - } - } - } - - private void NavigateToPage(ContentPage page) - { - var navigationPage = new NavigationPage(page); - - var window = new Window(navigationPage); - window.ToHandler(MauiContextSingleton.Instance.MauiContext); - } - } -} - -#endif \ No newline at end of file diff --git a/src/iOS.Extension/LoadingViewController.cs b/src/iOS.Extension/LoadingViewController.cs index 85f77329a..b3874fa64 100644 --- a/src/iOS.Extension/LoadingViewController.cs +++ b/src/iOS.Extension/LoadingViewController.cs @@ -18,6 +18,8 @@ using Bit.iOS.Core.Views; using Bit.iOS.Extension.Models; using CoreNFC; using Foundation; +using Microsoft.Maui.Controls; +using Microsoft.Maui.Platform; using MobileCoreServices; using UIKit; @@ -151,7 +153,6 @@ namespace Bit.iOS.Extension } } -#if !ENABLED_TAP_GESTURE_RECOGNIZER_MAUI_EMBEDDED_WORKAROUND public void DismissLockAndContinue() { Debug.WriteLine("BW Log, Dismissing lock controller."); @@ -166,7 +167,6 @@ namespace Bit.iOS.Extension PresentViewController(uiController, true, null); } -#endif private void ContinueOn() { diff --git a/src/iOS.Extension/iOS.Extension.csproj b/src/iOS.Extension/iOS.Extension.csproj index f374b47be..8829d9423 100644 --- a/src/iOS.Extension/iOS.Extension.csproj +++ b/src/iOS.Extension/iOS.Extension.csproj @@ -9,8 +9,6 @@ 1 False - - $(DefineConstants);ENABLED_TAP_GESTURE_RECOGNIZER_MAUI_EMBEDDED_WORKAROUND 11.0 Bit.iOS.Extension @@ -75,7 +73,6 @@ - diff --git a/src/iOS.ShareExtension/LoadingViewController.TapGestureHack.cs b/src/iOS.ShareExtension/LoadingViewController.TapGestureHack.cs deleted file mode 100644 index 7dc69428c..000000000 --- a/src/iOS.ShareExtension/LoadingViewController.TapGestureHack.cs +++ /dev/null @@ -1,22 +0,0 @@ -#if ENABLED_TAP_GESTURE_RECOGNIZER_MAUI_EMBEDDED_WORKAROUND - -using Bit.iOS.Core.Utilities; -using Microsoft.Maui.Controls; -using Microsoft.Maui.Platform; -using UIKit; - -namespace Bit.iOS.ShareExtension -{ - public partial class LoadingViewController : UIViewController - { - private void NavigateToPage(ContentPage page) - { - var navigationPage = new NavigationPage(page); - - var window = new Window(navigationPage); - window.ToHandler(MauiContextSingleton.Instance.MauiContext); - } - } -} - -#endif diff --git a/src/iOS.ShareExtension/LoadingViewController.cs b/src/iOS.ShareExtension/LoadingViewController.cs index fc3ae0e2d..22cb86fb8 100644 --- a/src/iOS.ShareExtension/LoadingViewController.cs +++ b/src/iOS.ShareExtension/LoadingViewController.cs @@ -137,23 +137,16 @@ namespace Bit.iOS.ShareExtension } } -#if !ENABLED_TAP_GESTURE_RECOGNIZER_MAUI_EMBEDDED_WORKAROUND - private void NavigateToPage(ContentPage page) { var navigationPage = new NavigationPage(page); - var window = new Window(navigationPage); - window.ToHandler(MauiContextSingleton.Instance.MauiContext); - _currentModalController = navigationPage.ToUIViewController(MauiContextSingleton.Instance.MauiContext); _currentModalController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen; _presentingOnNavigationPage = true; PresentViewController(_currentModalController, true, null); } -#endif - public void DismissLockAndContinue() { Debug.WriteLine("BW Log, Dismissing lock controller."); diff --git a/src/iOS.ShareExtension/iOS.ShareExtension.csproj b/src/iOS.ShareExtension/iOS.ShareExtension.csproj index b8718e428..1f357ce2b 100644 --- a/src/iOS.ShareExtension/iOS.ShareExtension.csproj +++ b/src/iOS.ShareExtension/iOS.ShareExtension.csproj @@ -9,8 +9,6 @@ 1 False - - $(DefineConstants);ENABLED_TAP_GESTURE_RECOGNIZER_MAUI_EMBEDDED_WORKAROUND 11.0 Bit.iOS.ShareExtension @@ -67,7 +65,6 @@ ExtensionNavigationController.cs - From 7558f60a44ffb9a31e0319141b113f3de38674ff Mon Sep 17 00:00:00 2001 From: Matt Bishop Date: Fri, 19 Jan 2024 17:04:54 -0500 Subject: [PATCH 09/50] Fix Renovate config (#2945) --- .github/renovate.json | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/.github/renovate.json b/.github/renovate.json index 52e80afcf..cd01c8c00 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -8,16 +8,14 @@ ":pinAllExceptPeerDependencies", ":prConcurrentLimit10", ":rebaseStalePrs", - "schedule:weekends", - ":separateMajorReleases" + ":separateMajorReleases", + "group:monorepos", + "schedule:weekends" ], - "enabledManagers": ["cargo", "github-actions", "npm", "nuget"], + "enabledManagers": ["github-actions", "npm", "nuget"], + "commitMessagePrefix": "[deps]:", + "commitMessageTopic": "{{depName}}", "packageRules": [ - { - "groupName": "cargo minor", - "matchManagers": ["cargo"], - "matchUpdateTypes": ["minor", "patch"] - }, { "groupName": "gh minor", "matchManagers": ["github-actions"], @@ -32,6 +30,6 @@ "groupName": "nuget minor", "matchManagers": ["nuget"], "matchUpdateTypes": ["minor", "patch"] - }, + } ] } From 56b9e3f615d82c89d5e8051cd4a82acace911f02 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 19 Jan 2024 17:05:36 -0500 Subject: [PATCH 10/50] Pin dependency gh-pages to 3.2.3 (#2542) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 32f51b928..b92128bf8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "bitwarden-mobile", "version": "0.0.0", "devDependencies": { - "gh-pages": "^3.2.3" + "gh-pages": "3.2.3" } }, "node_modules/array-union": { diff --git a/package.json b/package.json index 37d87bd2e..acaf22c51 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,6 @@ "clean:l10n": "git push origin --delete l10n_master" }, "devDependencies": { - "gh-pages": "^3.2.3" + "gh-pages": "3.2.3" } } From 64c694e5932ac74ceb6a3d328773e771368631d7 Mon Sep 17 00:00:00 2001 From: Matt Bishop Date: Fri, 19 Jan 2024 17:31:32 -0500 Subject: [PATCH 11/50] Fix code ownership (#2946) --- .github/CODEOWNERS | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7dad4798d..8c0bb6a69 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,12 +1,14 @@ -# Please sort lines alphabetically, this will ensure we don't accidentally add duplicates. +# Please sort into logical groups with comment headers. Sort groups in order of specificity. +# For example, default owners should always be the first group. +# Sort lines alphabetically within these groups to avoid accidentally adding duplicates. # # https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners -# The following owners will be the default owners for everything in the repo. -# Unless a later match takes precedence -# @bitwarden/tech-leads +# Default file owners +* @bitwarden/dept-development-mobile -@bitwarden/dept-development-mobile +# DevOps for Actions and other workflow changes +.github/workflows @bitwarden/dept-devops ## Auth team files ## From 133a80acef80943f1b4cc4e3ae5081d55b2645a8 Mon Sep 17 00:00:00 2001 From: Federico Maccaroni Date: Mon, 22 Jan 2024 12:30:29 -0300 Subject: [PATCH 12/50] PM-3350 Fixed CancellationTokenSource proper disposal --- src/Core/Pages/Accounts/LoginPasswordlessViewModel.cs | 1 + src/Core/Utilities/TimerTask.cs | 9 ++------- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/Core/Pages/Accounts/LoginPasswordlessViewModel.cs b/src/Core/Pages/Accounts/LoginPasswordlessViewModel.cs index a5809659c..95033c7e1 100644 --- a/src/Core/Pages/Accounts/LoginPasswordlessViewModel.cs +++ b/src/Core/Pages/Accounts/LoginPasswordlessViewModel.cs @@ -77,6 +77,7 @@ namespace Bit.App.Pages { _requestTimeCts?.Cancel(); _requestTimeCts?.Dispose(); + _requestTimeCts = null; } catch (Exception ex) { diff --git a/src/Core/Utilities/TimerTask.cs b/src/Core/Utilities/TimerTask.cs index 27e0b81af..f2ac98778 100644 --- a/src/Core/Utilities/TimerTask.cs +++ b/src/Core/Utilities/TimerTask.cs @@ -1,9 +1,4 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Bit.Core.Abstractions; -using Microsoft.Maui.Controls; -using Microsoft.Maui; +using Bit.Core.Abstractions; namespace Bit.App.Utilities { @@ -37,7 +32,7 @@ namespace Bit.App.Utilities { while (!_cancellationTokenSource.IsCancellationRequested) { - await Device.InvokeOnMainThreadAsync(async () => + await MainThread.InvokeOnMainThreadAsync(async () => { if (!_cancellationTokenSource.IsCancellationRequested) { From b2f93d3d4b3e6aeb361225059587892d7f4af10f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 22 Jan 2024 10:31:17 -0500 Subject: [PATCH 13/50] [deps]: Update actions/setup-dotnet action to v4 (#2947) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 42f4a9ee5..320683cfa 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -70,7 +70,7 @@ jobs: nuget-version: 5.9.0 - name: Set up .NET - uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0 + uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0 with: dotnet-version: '3.1.x' From 983937c9ebe55c43f4319f0ffba30a0155121b0f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 23 Jan 2024 15:02:04 -0500 Subject: [PATCH 14/50] [deps]: Update actions/setup-node action to v4 (#2868) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8f5e1f757..c080d2752 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -147,7 +147,7 @@ jobs: name: com.x8bit.bitwarden-fdroid.apk - name: Set up Node - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 + uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1 with: node-version: '16.x' From 63904fd30337f15790b2cfb6a3edfa0a4dd5afe8 Mon Sep 17 00:00:00 2001 From: Federico Maccaroni Date: Tue, 23 Jan 2024 17:34:27 -0300 Subject: [PATCH 15/50] PM-3350 Fix Avatar toolbar icon on extensions to load properly and to take advantage of using directly SkiaSharp to do the native conversion to UIImage. Also improved the toolbar item so that size is set appropriately. --- src/Core/Controls/Avatar/AvatarImageSource.cs | 62 ++++++ .../{ => Avatar}/AvatarImageSourcePool.cs | 0 src/Core/Controls/Avatar/AvatarInfo.cs | 63 ++++++ .../Controls/Avatar/SKAvatarImageHelper.cs | 63 ++++++ src/Core/Controls/AvatarImageSource.cs | 179 ------------------ src/Core/Core.csproj | 2 + .../LockPasswordViewController.cs | 38 +++- .../LockPasswordViewController.designer.cs | 33 +--- src/iOS.Autofill/LoginListViewController.cs | 44 ++++- .../LoginListViewController.designer.cs | 23 --- src/iOS.Autofill/MainInterface.storyboard | 38 +--- .../AccountSwitchingOverlayHelper.cs | 46 ++++- .../Utilities/ImageSourceExtensions.cs | 47 ----- .../LockPasswordViewController.cs | 48 +++-- .../LockPasswordViewController.designer.cs | 32 +--- .../MainInterface.storyboard | 32 +--- 16 files changed, 350 insertions(+), 400 deletions(-) create mode 100644 src/Core/Controls/Avatar/AvatarImageSource.cs rename src/Core/Controls/{ => Avatar}/AvatarImageSourcePool.cs (100%) create mode 100644 src/Core/Controls/Avatar/AvatarInfo.cs create mode 100644 src/Core/Controls/Avatar/SKAvatarImageHelper.cs delete mode 100644 src/Core/Controls/AvatarImageSource.cs delete mode 100644 src/iOS.Core/Utilities/ImageSourceExtensions.cs diff --git a/src/Core/Controls/Avatar/AvatarImageSource.cs b/src/Core/Controls/Avatar/AvatarImageSource.cs new file mode 100644 index 000000000..66682c7d0 --- /dev/null +++ b/src/Core/Controls/Avatar/AvatarImageSource.cs @@ -0,0 +1,62 @@ +using SkiaSharp; + +namespace Bit.App.Controls +{ + public class AvatarImageSource : StreamImageSource + { + private readonly string _text; + private readonly string _id; + private readonly string _color; + private readonly AvatarInfo _avatarInfo; + + public override bool Equals(object obj) + { + if (obj is null) + { + return false; + } + + if (obj is AvatarImageSource avatar) + { + return avatar._id == _id && avatar._text == _text && avatar._color == _color; + } + + return base.Equals(obj); + } + + public override int GetHashCode() => _id?.GetHashCode() ?? _text?.GetHashCode() ?? -1; + + public AvatarImageSource(string userId = null, string name = null, string email = null, string color = null) + { + _id = userId; + _text = name; + if (string.IsNullOrWhiteSpace(_text)) + { + _text = email; + } + _color = color; + + //Workaround: [MAUI-Migration] There is currently a bug in MAUI where the actual size of the image is used instead of the size it should occupy in the Toolbar. + //This causes some issues with the position of the icon. As a workaround we make the icon smaller until this is fixed. + //Github issues: https://github.com/dotnet/maui/issues/12359 and https://github.com/dotnet/maui/pull/17120 + _avatarInfo = new AvatarInfo(userId, name, email, color, DeviceInfo.Platform == DevicePlatform.iOS ? 20 : 50); + } + + public override Func> Stream => GetStreamAsync; + + private Task GetStreamAsync(CancellationToken userToken = new CancellationToken()) + { + var result = Draw(); + return Task.FromResult(result); + } + + private Stream Draw() + { + using (var img = SKAvatarImageHelper.Draw(_avatarInfo)) + { + var data = img.Encode(SKEncodedImageFormat.Png, 100); + return data?.AsStream(true); + } + } + } +} diff --git a/src/Core/Controls/AvatarImageSourcePool.cs b/src/Core/Controls/Avatar/AvatarImageSourcePool.cs similarity index 100% rename from src/Core/Controls/AvatarImageSourcePool.cs rename to src/Core/Controls/Avatar/AvatarImageSourcePool.cs diff --git a/src/Core/Controls/Avatar/AvatarInfo.cs b/src/Core/Controls/Avatar/AvatarInfo.cs new file mode 100644 index 000000000..0380e9d3d --- /dev/null +++ b/src/Core/Controls/Avatar/AvatarInfo.cs @@ -0,0 +1,63 @@ +using Bit.Core.Utilities; + +#nullable enable + +namespace Bit.App.Controls +{ + public struct AvatarInfo + { + private const string DEFAULT_BACKGROUND_COLOR = "#33ffffff"; + + public AvatarInfo(string? userId = null, string? name = null, string? email = null, string? color = null, int size = 50) + { + Size = size; + var text = string.IsNullOrWhiteSpace(name) ? email : name; + + string? upperCaseText = null; + + if (string.IsNullOrEmpty(text)) + { + CharsToDraw = ".."; + } + else if (text.Length > 1) + { + upperCaseText = text.ToUpper(); + CharsToDraw = GetFirstLetters(upperCaseText, 2); + } + else + { + CharsToDraw = upperCaseText = text.ToUpper(); + } + + BackgroundColor = color ?? CoreHelpers.StringToColor(userId ?? upperCaseText, DEFAULT_BACKGROUND_COLOR); + TextColor = CoreHelpers.TextColorFromBgColor(BackgroundColor); + } + + public string CharsToDraw { get; } + public string BackgroundColor { get; } + public string TextColor { get; } + public int Size { get; } + + private static string GetFirstLetters(string data, int charCount) + { + var sanitizedData = data.Trim(); + var parts = sanitizedData.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + + if (parts.Length > 1 && charCount <= 2) + { + var text = string.Empty; + for (var i = 0; i < charCount; i++) + { + text += parts[i][0]; + } + return text; + } + if (sanitizedData.Length > 2) + { + return sanitizedData.Substring(0, 2); + } + return sanitizedData; + } + } +} + diff --git a/src/Core/Controls/Avatar/SKAvatarImageHelper.cs b/src/Core/Controls/Avatar/SKAvatarImageHelper.cs new file mode 100644 index 000000000..3557e9a72 --- /dev/null +++ b/src/Core/Controls/Avatar/SKAvatarImageHelper.cs @@ -0,0 +1,63 @@ +using SkiaSharp; + +namespace Bit.App.Controls +{ + public static class SKAvatarImageHelper + { + public static SKImage Draw(AvatarInfo avatarInfo) + { + using (var bitmap = new SKBitmap(avatarInfo.Size * 2, + avatarInfo.Size * 2, + SKImageInfo.PlatformColorType, + SKAlphaType.Premul)) + { + using (var canvas = new SKCanvas(bitmap)) + { + canvas.Clear(SKColors.Transparent); + using (var paint = new SKPaint + { + IsAntialias = true, + Style = SKPaintStyle.Fill, + StrokeJoin = SKStrokeJoin.Miter, + Color = SKColor.Parse(avatarInfo.BackgroundColor) + }) + { + var midX = canvas.LocalClipBounds.Size.ToSizeI().Width / 2; + var midY = canvas.LocalClipBounds.Size.ToSizeI().Height / 2; + var radius = midX - midX / 5; + + using (var circlePaint = new SKPaint + { + IsAntialias = true, + Style = SKPaintStyle.Fill, + StrokeJoin = SKStrokeJoin.Miter, + Color = SKColor.Parse(avatarInfo.BackgroundColor) + }) + { + canvas.DrawCircle(midX, midY, radius, circlePaint); + + var typeface = SKTypeface.FromFamilyName("Arial", SKFontStyle.Normal); + var textSize = midX / 1.3f; + using (var textPaint = new SKPaint + { + IsAntialias = true, + Style = SKPaintStyle.Fill, + Color = SKColor.Parse(avatarInfo.TextColor), + TextSize = textSize, + TextAlign = SKTextAlign.Center, + Typeface = typeface + }) + { + var rect = new SKRect(); + textPaint.MeasureText(avatarInfo.CharsToDraw, ref rect); + canvas.DrawText(avatarInfo.CharsToDraw, midX, midY + rect.Height / 2, textPaint); + + return SKImage.FromBitmap(bitmap); + } + } + } + } + } + } + } +} diff --git a/src/Core/Controls/AvatarImageSource.cs b/src/Core/Controls/AvatarImageSource.cs deleted file mode 100644 index c20d61117..000000000 --- a/src/Core/Controls/AvatarImageSource.cs +++ /dev/null @@ -1,179 +0,0 @@ -using Bit.Core.Utilities; -using SkiaSharp; - -namespace Bit.App.Controls -{ - public class AvatarImageSource : StreamImageSource - { - private readonly string _text; - private readonly string _id; - private readonly string _color; - - public override bool Equals(object obj) - { - if (obj is null) - { - return false; - } - - if (obj is AvatarImageSource avatar) - { - return avatar._id == _id && avatar._text == _text && avatar._color == _color; - } - - return base.Equals(obj); - } - - public override int GetHashCode() => _id?.GetHashCode() ?? _text?.GetHashCode() ?? -1; - - public AvatarImageSource(string userId = null, string name = null, string email = null, string color = null) - { - _id = userId; - _text = name; - if (string.IsNullOrWhiteSpace(_text)) - { - _text = email; - } - _color = color; - } - - public override Func> Stream => GetStreamAsync; - - private Task GetStreamAsync(CancellationToken userToken = new CancellationToken()) - { - var result = Draw(); - return Task.FromResult(result); - } - - private Stream Draw() - { - string chars; - string upperCaseText = null; - - if (string.IsNullOrEmpty(_text)) - { - chars = ".."; - } - else if (_text?.Length > 1) - { - upperCaseText = _text.ToUpper(); - chars = GetFirstLetters(upperCaseText, 2); - } - else - { - chars = upperCaseText = _text.ToUpper(); - } - - var bgColor = _color ?? CoreHelpers.StringToColor(_id ?? upperCaseText, "#33ffffff"); - var textColor = CoreHelpers.TextColorFromBgColor(bgColor); - var size = 50; - - //Workaround: [MAUI-Migration] There is currently a bug in MAUI where the actual size of the image is used instead of the size it should occupy in the Toolbar. - //This causes some issues with the position of the icon. As a workaround we make the icon smaller until this is fixed. - //Github issues: https://github.com/dotnet/maui/issues/12359 and https://github.com/dotnet/maui/pull/17120 - if (DeviceInfo.Platform == DevicePlatform.iOS) - { - size = 20; - } - - using (var bitmap = new SKBitmap(size * 2, - size * 2, - SKImageInfo.PlatformColorType, - SKAlphaType.Premul)) - { - using (var canvas = new SKCanvas(bitmap)) - { - canvas.Clear(SKColors.Transparent); - using (var paint = new SKPaint - { - IsAntialias = true, - Style = SKPaintStyle.Fill, - StrokeJoin = SKStrokeJoin.Miter, - Color = SKColor.Parse(bgColor) - }) - { - var midX = canvas.LocalClipBounds.Size.ToSizeI().Width / 2; - var midY = canvas.LocalClipBounds.Size.ToSizeI().Height / 2; - var radius = midX - midX / 5; - - using (var circlePaint = new SKPaint - { - IsAntialias = true, - Style = SKPaintStyle.Fill, - StrokeJoin = SKStrokeJoin.Miter, - Color = SKColor.Parse(bgColor) - }) - { - canvas.DrawCircle(midX, midY, radius, circlePaint); - - var typeface = SKTypeface.FromFamilyName("Arial", SKFontStyle.Normal); - var textSize = midX / 1.3f; - using (var textPaint = new SKPaint - { - IsAntialias = true, - Style = SKPaintStyle.Fill, - Color = SKColor.Parse(textColor), - TextSize = textSize, - TextAlign = SKTextAlign.Center, - Typeface = typeface - }) - { - var rect = new SKRect(); - textPaint.MeasureText(chars, ref rect); - canvas.DrawText(chars, midX, midY + rect.Height / 2, textPaint); - - using (var img = SKImage.FromBitmap(bitmap)) - { - var data = img.Encode(SKEncodedImageFormat.Png, 100); - return data?.AsStream(true); - } - } - } - } - } - } - } - - private string GetFirstLetters(string data, int charCount) - { - var sanitizedData = data.Trim(); - var parts = sanitizedData.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); - - if (parts.Length > 1 && charCount <= 2) - { - var text = string.Empty; - for (var i = 0; i < charCount; i++) - { - text += parts[i][0]; - } - return text; - } - if (sanitizedData.Length > 2) - { - return sanitizedData.Substring(0, 2); - } - return sanitizedData; - } - - private Color StringToColor(string str) - { - if (str == null) - { - return Color.FromArgb("#33ffffff"); - } - var hash = 0; - for (var i = 0; i < str.Length; i++) - { - hash = str[i] + ((hash << 5) - hash); - } - var color = "#FF"; - for (var i = 0; i < 3; i++) - { - var value = (hash >> (i * 8)) & 0xff; - var base16 = "00" + Convert.ToString(value, 16); - color += base16.Substring(base16.Length - 2); - } - return Color.FromArgb(color); - } - } -} diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 82fad1073..b4abf4463 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -76,6 +76,7 @@ + @@ -105,5 +106,6 @@ + \ No newline at end of file diff --git a/src/iOS.Autofill/LockPasswordViewController.cs b/src/iOS.Autofill/LockPasswordViewController.cs index 7f5df4c7e..0d73e1fff 100644 --- a/src/iOS.Autofill/LockPasswordViewController.cs +++ b/src/iOS.Autofill/LockPasswordViewController.cs @@ -2,12 +2,15 @@ using System; using Bit.App.Controls; using Bit.Core.Utilities; using Bit.iOS.Core.Utilities; +using MapKit; using UIKit; namespace Bit.iOS.Autofill { public partial class LockPasswordViewController : Core.Controllers.BaseLockPasswordViewController { + UIBarButtonItem _cancelButton; + UIControl _accountSwitchButton; AccountSwitchingOverlayView _accountSwitchingOverlayView; AccountSwitchingOverlayHelper _accountSwitchingOverlayHelper; @@ -23,22 +26,37 @@ namespace Bit.iOS.Autofill public CredentialProviderViewController CPViewController { get; set; } public override UINavigationItem BaseNavItem => NavItem; - public override UIBarButtonItem BaseCancelButton => CancelButton; + public override UIBarButtonItem BaseCancelButton => _cancelButton; public override UIBarButtonItem BaseSubmitButton => SubmitButton; public override Action Success => () => CPViewController.DismissLockAndContinue(); public override Action Cancel => () => CPViewController.CompleteRequest(); public override async void ViewDidLoad() { + _cancelButton = new UIBarButtonItem(UIBarButtonSystemItem.Cancel, CancelButton_TouchUpInside); + base.ViewDidLoad(); _accountSwitchingOverlayHelper = new AccountSwitchingOverlayHelper(); - AccountSwitchingBarButton.Image = await _accountSwitchingOverlayHelper.CreateAvatarImageAsync(); + + _accountSwitchButton = await _accountSwitchingOverlayHelper.CreateAccountSwitchToolbarButtonItemCustomViewAsync(); + _accountSwitchButton.TouchUpInside += AccountSwitchedButton_TouchUpInside; + + NavItem.SetLeftBarButtonItems(new UIBarButtonItem[] + { + _cancelButton, + new UIBarButtonItem(_accountSwitchButton) + }, false); _accountSwitchingOverlayView = _accountSwitchingOverlayHelper.CreateAccountSwitchingOverlayView(OverlayView); } - partial void AccountSwitchingBarButton_Activated(UIBarButtonItem sender) + private void CancelButton_TouchUpInside(object sender, EventArgs e) + { + Cancel(); + } + + private void AccountSwitchedButton_TouchUpInside(object sender, EventArgs e) { _accountSwitchingOverlayHelper.OnToolbarItemActivated(_accountSwitchingOverlayView, OverlayView); } @@ -48,9 +66,19 @@ namespace Bit.iOS.Autofill CheckPasswordAsync().FireAndForget(); } - partial void CancelButton_Activated(UIBarButtonItem sender) + protected override void Dispose(bool disposing) { - Cancel(); + if (disposing) + { + if (_accountSwitchButton != null) + { + _accountSwitchingOverlayHelper.DisposeAccountSwitchToolbarButtonItemImage(_accountSwitchButton); + + _accountSwitchButton.TouchUpInside -= AccountSwitchedButton_TouchUpInside; + } + } + + base.Dispose(disposing); } } } diff --git a/src/iOS.Autofill/LockPasswordViewController.designer.cs b/src/iOS.Autofill/LockPasswordViewController.designer.cs index 4fa4f737e..a82cc209f 100644 --- a/src/iOS.Autofill/LockPasswordViewController.designer.cs +++ b/src/iOS.Autofill/LockPasswordViewController.designer.cs @@ -12,13 +12,6 @@ namespace Bit.iOS.Autofill [Register ("LockPasswordViewController")] partial class LockPasswordViewController { - [Outlet] - UIKit.UIBarButtonItem AccountSwitchingBarButton { get; set; } - - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UIBarButtonItem CancelButton { get; set; } - [Outlet] [GeneratedCode ("iOS Designer", "1.0")] UIKit.UITableView MainTableView { get; set; } @@ -34,27 +27,11 @@ namespace Bit.iOS.Autofill [GeneratedCode ("iOS Designer", "1.0")] UIKit.UIBarButtonItem SubmitButton { get; set; } - [Action ("AccountSwitchingBarButton_Activated:")] - partial void AccountSwitchingBarButton_Activated (UIKit.UIBarButtonItem sender); - - [Action ("CancelButton_Activated:")] - partial void CancelButton_Activated (UIKit.UIBarButtonItem sender); - [Action ("SubmitButton_Activated:")] partial void SubmitButton_Activated (UIKit.UIBarButtonItem sender); void ReleaseDesignerOutlets () { - if (AccountSwitchingBarButton != null) { - AccountSwitchingBarButton.Dispose (); - AccountSwitchingBarButton = null; - } - - if (CancelButton != null) { - CancelButton.Dispose (); - CancelButton = null; - } - if (MainTableView != null) { MainTableView.Dispose (); MainTableView = null; @@ -65,15 +42,15 @@ namespace Bit.iOS.Autofill NavItem = null; } - if (SubmitButton != null) { - SubmitButton.Dispose (); - SubmitButton = null; - } - if (OverlayView != null) { OverlayView.Dispose (); OverlayView = null; } + + if (SubmitButton != null) { + SubmitButton.Dispose (); + SubmitButton = null; + } } } } diff --git a/src/iOS.Autofill/LoginListViewController.cs b/src/iOS.Autofill/LoginListViewController.cs index f53f5cb0c..af17166db 100644 --- a/src/iOS.Autofill/LoginListViewController.cs +++ b/src/iOS.Autofill/LoginListViewController.cs @@ -17,6 +17,9 @@ namespace Bit.iOS.Autofill { public partial class LoginListViewController : ExtendedUIViewController { + UIBarButtonItem _cancelButton; + UIControl _accountSwitchButton; + public LoginListViewController(IntPtr handle) : base(handle) { @@ -37,12 +40,14 @@ namespace Bit.iOS.Autofill public async override void ViewDidLoad() { + _cancelButton = new UIBarButtonItem(UIBarButtonSystemItem.Cancel, CancelButton_TouchUpInside); + base.ViewDidLoad(); SubscribeSyncCompleted(); NavItem.Title = AppResources.Items; - CancelBarButton.Title = AppResources.Cancel; + _cancelButton.Title = AppResources.Cancel; TableView.RowHeight = UITableView.AutomaticDimension; TableView.EstimatedRowHeight = 44; @@ -61,21 +66,29 @@ namespace Bit.iOS.Autofill } _accountSwitchingOverlayHelper = new AccountSwitchingOverlayHelper(); - AccountSwitchingBarButton.Image = await _accountSwitchingOverlayHelper.CreateAvatarImageAsync(); + + _accountSwitchButton = await _accountSwitchingOverlayHelper.CreateAccountSwitchToolbarButtonItemCustomViewAsync(); + _accountSwitchButton.TouchUpInside += AccountSwitchedButton_TouchUpInside; + + NavItem.SetLeftBarButtonItems(new UIBarButtonItem[] + { + _cancelButton, + new UIBarButtonItem(_accountSwitchButton) + }, false); _accountSwitchingOverlayView = _accountSwitchingOverlayHelper.CreateAccountSwitchingOverlayView(OverlayView); } - partial void AccountSwitchingBarButton_Activated(UIBarButtonItem sender) - { - _accountSwitchingOverlayHelper.OnToolbarItemActivated(_accountSwitchingOverlayView, OverlayView); - } - - partial void CancelBarButton_Activated(UIBarButtonItem sender) + private void CancelButton_TouchUpInside(object sender, EventArgs e) { Cancel(); } + private void AccountSwitchedButton_TouchUpInside(object sender, EventArgs e) + { + _accountSwitchingOverlayHelper.OnToolbarItemActivated(_accountSwitchingOverlayView, OverlayView); + } + private void Cancel() { CPViewController.CompleteRequest(); @@ -151,6 +164,21 @@ namespace Bit.iOS.Autofill }); } + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (_accountSwitchButton != null) + { + _accountSwitchingOverlayHelper.DisposeAccountSwitchToolbarButtonItemImage(_accountSwitchButton); + + _accountSwitchButton.TouchUpInside -= AccountSwitchedButton_TouchUpInside; + } + } + + base.Dispose(disposing); + } + public class TableSource : ExtensionTableSource { private LoginListViewController _controller; diff --git a/src/iOS.Autofill/LoginListViewController.designer.cs b/src/iOS.Autofill/LoginListViewController.designer.cs index 8bdd8059c..6451849c8 100644 --- a/src/iOS.Autofill/LoginListViewController.designer.cs +++ b/src/iOS.Autofill/LoginListViewController.designer.cs @@ -12,17 +12,10 @@ namespace Bit.iOS.Autofill [Register ("LoginListViewController")] partial class LoginListViewController { - [Outlet] - UIKit.UIBarButtonItem AccountSwitchingBarButton { get; set; } - [Outlet] [GeneratedCode ("iOS Designer", "1.0")] UIKit.UIBarButtonItem AddBarButton { get; set; } - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UIBarButtonItem CancelBarButton { get; set; } - [Outlet] UIKit.UIView MainView { get; set; } @@ -36,15 +29,9 @@ namespace Bit.iOS.Autofill [Outlet] UIKit.UITableView TableView { get; set; } - [Action ("AccountSwitchingBarButton_Activated:")] - partial void AccountSwitchingBarButton_Activated (UIKit.UIBarButtonItem sender); - [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); @@ -55,11 +42,6 @@ namespace Bit.iOS.Autofill AddBarButton = null; } - if (CancelBarButton != null) { - CancelBarButton.Dispose (); - CancelBarButton = null; - } - if (MainView != null) { MainView.Dispose (); MainView = null; @@ -79,11 +61,6 @@ namespace Bit.iOS.Autofill 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 7bc66f197..4521ddbc8 100644 --- a/src/iOS.Autofill/MainInterface.storyboard +++ b/src/iOS.Autofill/MainInterface.storyboard @@ -1,9 +1,9 @@ - + - + @@ -185,20 +185,6 @@ - - - - - - - - - - - - - - @@ -216,9 +202,7 @@ - - @@ -410,19 +394,6 @@ - - - - - - - - - - - - - @@ -430,8 +401,6 @@ - - @@ -601,13 +570,12 @@ - + - diff --git a/src/iOS.Core/Utilities/AccountSwitchingOverlayHelper.cs b/src/iOS.Core/Utilities/AccountSwitchingOverlayHelper.cs index 7171e9f34..6257a6700 100644 --- a/src/iOS.Core/Utilities/AccountSwitchingOverlayHelper.cs +++ b/src/iOS.Core/Utilities/AccountSwitchingOverlayHelper.cs @@ -1,7 +1,9 @@ using Bit.App.Controls; using Bit.Core.Abstractions; using Bit.Core.Utilities; +using CoreGraphics; using Microsoft.Maui.Platform; +using SkiaSharp.Views.iOS; using UIKit; namespace Bit.iOS.Core.Utilities @@ -30,12 +32,19 @@ namespace Bit.iOS.Core.Utilities throw new NullReferenceException(nameof(_stateService)); } - var avatarImageSource = new AvatarImageSource(await _stateService.GetActiveUserIdAsync(), - await _stateService.GetNameAsync(), await _stateService.GetEmailAsync(), - await _stateService.GetAvatarColorAsync()); - using (var avatarUIImage = await avatarImageSource.GetNativeImageAsync()) + var avatarInfo = await _stateService.GetActiveUserCustomDataAsync(a => a?.Profile is null + ? null + : new AvatarInfo(a.Profile.UserId, a.Profile.Name, a.Profile.Email, a.Profile.AvatarColor)); + + if (!avatarInfo.HasValue) { - return avatarUIImage?.ImageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal) ?? UIImage.GetSystemImage(DEFAULT_SYSTEM_AVATAR_IMAGE); + return UIImage.GetSystemImage(DEFAULT_SYSTEM_AVATAR_IMAGE); + } + + using (var avatarUIImage = SKAvatarImageHelper.Draw(avatarInfo.Value)) + { + return avatarUIImage?.ToUIImage()?.ImageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal) + ?? UIImage.GetSystemImage(DEFAULT_SYSTEM_AVATAR_IMAGE); } } catch (Exception ex) @@ -100,5 +109,32 @@ namespace Bit.iOS.Core.Utilities containerView.UserInteractionEnabled = !overlayVisible; containerView.Subviews[0].UserInteractionEnabled = !overlayVisible; } + + public async Task CreateAccountSwitchToolbarButtonItemCustomViewAsync() + { + const float size = 40f; + var image = await CreateAvatarImageAsync(); + var accountSwitchButton = new UIControl(new CGRect(0, 0, size, size)); + if (image != null) + { + var accountSwitchAvatarImageView = new UIImageView(new CGRect(0, 0, size, size)) + { + Image = image + }; + accountSwitchButton.AddSubview(accountSwitchAvatarImageView); + } + + return accountSwitchButton; + } + + public void DisposeAccountSwitchToolbarButtonItemImage(UIControl accountSwitchButton) + { + if (accountSwitchButton?.Subviews?.FirstOrDefault() is UIImageView accountSwitchImageView && accountSwitchImageView.Image != null) + { + var img = accountSwitchImageView.Image; + accountSwitchImageView.Image = null; + img.Dispose(); + } + } } } diff --git a/src/iOS.Core/Utilities/ImageSourceExtensions.cs b/src/iOS.Core/Utilities/ImageSourceExtensions.cs deleted file mode 100644 index 15f3a7d9d..000000000 --- a/src/iOS.Core/Utilities/ImageSourceExtensions.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Bit.Core.Services; -using Microsoft.Maui.Controls.Compatibility.Platform.iOS; -using UIKit; - -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 = Microsoft.Maui.Controls.Internals.Registrar.Registered.GetHandlerForObject(source); - if (handler == null) - { - LoggerHelper.LogEvenIfCantBeResolved(new InvalidOperationException("GetNativeImageAsync failed cause IImageSourceHandler couldn't be found")); - return null; - } - - try - { - float scale = (float)UIScreen.MainScreen.Scale; - return await handler.LoadImageAsync(source, scale: scale, cancelationToken: cancellationToken); - } - catch (OperationCanceledException) - { - LoggerHelper.LogEvenIfCantBeResolved(new OperationCanceledException("GetNativeImageAsync was cancelled")); - } - catch (Exception ex) - { - LoggerHelper.LogEvenIfCantBeResolved(new InvalidOperationException("GetNativeImageAsync failed", ex)); - } - - return null; - } - } -} diff --git a/src/iOS.ShareExtension/LockPasswordViewController.cs b/src/iOS.ShareExtension/LockPasswordViewController.cs index a068c2ae7..4e468b1fd 100644 --- a/src/iOS.ShareExtension/LockPasswordViewController.cs +++ b/src/iOS.ShareExtension/LockPasswordViewController.cs @@ -1,15 +1,17 @@ +using System; using Bit.App.Controls; using Bit.Core.Utilities; using Bit.iOS.Core.Utilities; -using System; using UIKit; namespace Bit.iOS.ShareExtension { public partial class LockPasswordViewController : Core.Controllers.BaseLockPasswordViewController { + UIBarButtonItem _cancelButton; + UIControl _accountSwitchButton; AccountSwitchingOverlayView _accountSwitchingOverlayView; - AccountSwitchingOverlayHelper _accountSwitchingOverlayHelper; + private Lazy _accountSwitchingOverlayHelper = new Lazy(() => new AccountSwitchingOverlayHelper()); public LockPasswordViewController() { @@ -43,15 +45,33 @@ namespace Bit.iOS.ShareExtension public override async void ViewDidLoad() { + _cancelButton = new UIBarButtonItem(UIBarButtonSystemItem.Cancel, CancelButton_TouchUpInside); + base.ViewDidLoad(); _cancelButton.TintColor = ThemeHelpers.NavBarTextColor; _submitButton.TintColor = ThemeHelpers.NavBarTextColor; - _accountSwitchingOverlayHelper = new AccountSwitchingOverlayHelper(); - _accountSwitchingButton.Image = await _accountSwitchingOverlayHelper.CreateAvatarImageAsync(); + _accountSwitchButton = await _accountSwitchingOverlayHelper.Value.CreateAccountSwitchToolbarButtonItemCustomViewAsync(); + _accountSwitchButton.TouchUpInside += AccountSwitchedButton_TouchUpInside; - _accountSwitchingOverlayView = _accountSwitchingOverlayHelper.CreateAccountSwitchingOverlayView(_overlayView); + _navItem.SetLeftBarButtonItems(new UIBarButtonItem[] + { + _cancelButton, + new UIBarButtonItem(_accountSwitchButton) + }, false); + + _accountSwitchingOverlayView = _accountSwitchingOverlayHelper.Value.CreateAccountSwitchingOverlayView(_overlayView); + } + + private void CancelButton_TouchUpInside(object sender, EventArgs e) + { + Cancel(); + } + + private void AccountSwitchedButton_TouchUpInside(object sender, EventArgs e) + { + _accountSwitchingOverlayHelper.Value.OnToolbarItemActivated(_accountSwitchingOverlayView, _overlayView); } protected override void UpdateNavigationBarTheme() @@ -59,21 +79,11 @@ namespace Bit.iOS.ShareExtension UpdateNavigationBarTheme(_navBar); } - partial void AccountSwitchingButton_Activated(UIBarButtonItem sender) - { - _accountSwitchingOverlayHelper.OnToolbarItemActivated(_accountSwitchingOverlayView, _overlayView); - } - partial void SubmitButton_Activated(UIBarButtonItem sender) { CheckPasswordAsync().FireAndForget(); } - partial void CancelButton_Activated(UIBarButtonItem sender) - { - Cancel(); - } - protected override void Dispose(bool disposing) { if (disposing) @@ -82,11 +92,11 @@ namespace Bit.iOS.ShareExtension { TableView.Source?.Dispose(); } - if (_accountSwitchingButton?.Image != null) + if (_accountSwitchButton != null) { - var img = _accountSwitchingButton.Image; - _accountSwitchingButton.Image = null; - img.Dispose(); + _accountSwitchingOverlayHelper.Value.DisposeAccountSwitchToolbarButtonItemImage(_accountSwitchButton); + + _accountSwitchButton.TouchUpInside -= AccountSwitchedButton_TouchUpInside; } if (_accountSwitchingOverlayView != null && _overlayView?.Subviews != null) { diff --git a/src/iOS.ShareExtension/LockPasswordViewController.designer.cs b/src/iOS.ShareExtension/LockPasswordViewController.designer.cs index 6be61f9dc..d7ee87dc0 100644 --- a/src/iOS.ShareExtension/LockPasswordViewController.designer.cs +++ b/src/iOS.ShareExtension/LockPasswordViewController.designer.cs @@ -12,12 +12,6 @@ namespace Bit.iOS.ShareExtension [Register ("LockPasswordViewController")] partial class LockPasswordViewController { - [Outlet] - UIKit.UIBarButtonItem _accountSwitchingButton { get; set; } - - [Outlet] - UIKit.UIBarButtonItem _cancelButton { get; set; } - [Outlet] UIKit.UITableView _mainTableView { get; set; } @@ -33,32 +27,21 @@ namespace Bit.iOS.ShareExtension [Outlet] UIKit.UIBarButtonItem _submitButton { get; set; } - [Action ("AccountSwitchingButton_Activated:")] - partial void AccountSwitchingButton_Activated (UIKit.UIBarButtonItem sender); - - [Action ("CancelButton_Activated:")] - partial void CancelButton_Activated (UIKit.UIBarButtonItem sender); - [Action ("SubmitButton_Activated:")] partial void SubmitButton_Activated (UIKit.UIBarButtonItem sender); void ReleaseDesignerOutlets () { - if (_accountSwitchingButton != null) { - _accountSwitchingButton.Dispose (); - _accountSwitchingButton = null; - } - - if (_cancelButton != null) { - _cancelButton.Dispose (); - _cancelButton = null; - } - if (_mainTableView != null) { _mainTableView.Dispose (); _mainTableView = null; } + if (_navBar != null) { + _navBar.Dispose (); + _navBar = null; + } + if (_navItem != null) { _navItem.Dispose (); _navItem = null; @@ -73,11 +56,6 @@ namespace Bit.iOS.ShareExtension _submitButton.Dispose (); _submitButton = null; } - - if (_navBar != null) { - _navBar.Dispose (); - _navBar = null; - } } } } diff --git a/src/iOS.ShareExtension/MainInterface.storyboard b/src/iOS.ShareExtension/MainInterface.storyboard index 1bde113e1..08210eb34 100644 --- a/src/iOS.ShareExtension/MainInterface.storyboard +++ b/src/iOS.ShareExtension/MainInterface.storyboard @@ -1,9 +1,9 @@ - + - + @@ -14,11 +14,11 @@ - + - + @@ -41,7 +41,7 @@ @@ -61,30 +61,17 @@ - + - + - + - - - - - - - - - - - - - @@ -116,8 +103,6 @@ - - @@ -132,7 +117,6 @@ - From 4472d7f9a8a38a6de5173fd1abddc1b7596282a5 Mon Sep 17 00:00:00 2001 From: Federico Maccaroni Date: Tue, 23 Jan 2024 17:59:32 -0300 Subject: [PATCH 16/50] PM-3349 PM-3350 Fix external link icon --- src/Core/Controls/ExternalLinkItemView.xaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Core/Controls/ExternalLinkItemView.xaml b/src/Core/Controls/ExternalLinkItemView.xaml index 8a4252eea..9390d2ebb 100644 --- a/src/Core/Controls/ExternalLinkItemView.xaml +++ b/src/Core/Controls/ExternalLinkItemView.xaml @@ -1,4 +1,4 @@ - + Date: Wed, 24 Jan 2024 13:15:24 +0000 Subject: [PATCH 17/50] [PM-4739] Implement checksum uri validation (#2893) * PM-4739 Implement checksum uri validation * PM-4739 Add missing field * PM-4739 Fix PR comments * PM-4739 Remove unnecessary comment * PM-4739 Add try catch and log exception * PM-4739 Added missing files from last commit * PM-4739 Change arg name * [PM-5461] Fix item saving with blank URI (#2948) * PM-5461 Fix item saving with blank URI * PM-5461 Fix PR comment --- src/Android/MainApplication.cs | 2 +- src/Core/Abstractions/ICryptoService.cs | 2 ++ src/Core/Constants.cs | 2 +- src/Core/Models/Api/LoginUriApi.cs | 1 + src/Core/Models/Data/LoginUriData.cs | 2 ++ src/Core/Models/Domain/Cipher.cs | 2 +- src/Core/Models/Domain/Login.cs | 11 ++++++-- src/Core/Models/Domain/LoginUri.cs | 7 ++++-- src/Core/Models/Export/LoginUri.cs | 2 ++ src/Core/Models/Request/CipherRequest.cs | 2 +- src/Core/Services/CipherService.cs | 4 ++- src/Core/Services/CryptoService.cs | 32 +++++++++++++++++++++++- src/iOS.Core/Utilities/iOSCoreHelpers.cs | 2 +- 13 files changed, 60 insertions(+), 11 deletions(-) diff --git a/src/Android/MainApplication.cs b/src/Android/MainApplication.cs index 4407c1c31..0d672de99 100644 --- a/src/Android/MainApplication.cs +++ b/src/Android/MainApplication.cs @@ -157,7 +157,7 @@ namespace Bit.Droid var autofillHandler = new AutofillHandler(stateService, messagingService, clipboardService, platformUtilsService, new LazyResolve()); var cryptoFunctionService = new PclCryptoFunctionService(cryptoPrimitiveService); - var cryptoService = new CryptoService(stateService, cryptoFunctionService); + var cryptoService = new CryptoService(stateService, cryptoFunctionService, logger); var biometricService = new BiometricService(stateService, cryptoService); var userPinService = new UserPinService(stateService, cryptoService); var passwordRepromptService = new MobilePasswordRepromptService(platformUtilsService, cryptoService, stateService); diff --git a/src/Core/Abstractions/ICryptoService.cs b/src/Core/Abstractions/ICryptoService.cs index 427179776..ffb9f4c31 100644 --- a/src/Core/Abstractions/ICryptoService.cs +++ b/src/Core/Abstractions/ICryptoService.cs @@ -63,5 +63,7 @@ namespace Bit.Core.Abstractions Task DecryptAndMigrateOldPinKeyAsync(bool masterPasswordOnRestart, string pin, string email, KdfConfig kdfConfig, EncString oldPinKey); Task GetOrDeriveMasterKeyAsync(string password, string userId = null); Task UpdateMasterKeyAndUserKeyAsync(MasterKey masterKey); + Task HashAsync(string value, CryptoHashAlgorithm hashAlgorithm); + Task ValidateUriChecksumAsync(EncString remoteUriChecksum, string rawUri, string orgId, SymmetricCryptoKey key); } } diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 9b96deb4a..44de67eb0 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -70,7 +70,7 @@ namespace Bit.Core public const int Argon2Parallelism = 4; public const int MasterPasswordMinimumChars = 12; public const int CipherKeyRandomBytesLength = 64; - public const string CipherKeyEncryptionMinServerVersion = "2023.9.1"; + public const string CipherKeyEncryptionMinServerVersion = "2023.12.0"; public const string DefaultFido2CredentialType = "public-key"; public const string DefaultFido2CredentialAlgorithm = "ECDSA"; public const string DefaultFido2CredentialCurve = "P-256"; diff --git a/src/Core/Models/Api/LoginUriApi.cs b/src/Core/Models/Api/LoginUriApi.cs index d9e59c65d..a1253f36d 100644 --- a/src/Core/Models/Api/LoginUriApi.cs +++ b/src/Core/Models/Api/LoginUriApi.cs @@ -6,5 +6,6 @@ namespace Bit.Core.Models.Api { public string Uri { get; set; } public UriMatchType? Match { get; set; } + public string UriChecksum { get; set; } } } diff --git a/src/Core/Models/Data/LoginUriData.cs b/src/Core/Models/Data/LoginUriData.cs index 52294c523..a27344f43 100644 --- a/src/Core/Models/Data/LoginUriData.cs +++ b/src/Core/Models/Data/LoginUriData.cs @@ -11,9 +11,11 @@ namespace Bit.Core.Models.Data { Uri = data.Uri; Match = data.Match; + UriChecksum = data.UriChecksum; } public string Uri { get; set; } public UriMatchType? Match { get; set; } + public string UriChecksum { get; set; } } } diff --git a/src/Core/Models/Domain/Cipher.cs b/src/Core/Models/Domain/Cipher.cs index 48aa3a761..a7a2d6943 100644 --- a/src/Core/Models/Domain/Cipher.cs +++ b/src/Core/Models/Domain/Cipher.cs @@ -115,7 +115,7 @@ namespace Bit.Core.Models.Domain switch (Type) { case Enums.CipherType.Login: - model.Login = await Login.DecryptAsync(OrganizationId, model.Key); + model.Login = await Login.DecryptAsync(OrganizationId, Key == null, model.Key); break; case Enums.CipherType.SecureNote: model.SecureNote = await SecureNote.DecryptAsync(OrganizationId, model.Key); diff --git a/src/Core/Models/Domain/Login.cs b/src/Core/Models/Domain/Login.cs index ed1822c13..26aecf840 100644 --- a/src/Core/Models/Domain/Login.cs +++ b/src/Core/Models/Domain/Login.cs @@ -2,8 +2,10 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Bit.Core.Abstractions; using Bit.Core.Models.Data; using Bit.Core.Models.View; +using Bit.Core.Utilities; namespace Bit.Core.Models.Domain { @@ -31,7 +33,7 @@ namespace Bit.Core.Models.Domain public EncString Totp { get; set; } public List Fido2Credentials { get; set; } - public async Task DecryptAsync(string orgId, SymmetricCryptoKey key = null) + public async Task DecryptAsync(string orgId, bool bypassUriChecksumValidation, SymmetricCryptoKey key = null) { var view = await DecryptObjAsync(new LoginView(this), this, new HashSet { @@ -41,10 +43,15 @@ namespace Bit.Core.Models.Domain }, orgId, key); if (Uris != null) { + var cryptoService = ServiceContainer.Resolve(); view.Uris = new List(); foreach (var uri in Uris) { - view.Uris.Add(await uri.DecryptAsync(orgId, key)); + var loginUriView = await uri.DecryptAsync(orgId, key); + if (bypassUriChecksumValidation || await cryptoService.ValidateUriChecksumAsync(uri.UriChecksum, loginUriView.Uri, orgId, key)) + { + view.Uris.Add(loginUriView); + } } } if (Fido2Credentials != null) diff --git a/src/Core/Models/Domain/LoginUri.cs b/src/Core/Models/Domain/LoginUri.cs index e9b26d6d1..896570d73 100644 --- a/src/Core/Models/Domain/LoginUri.cs +++ b/src/Core/Models/Domain/LoginUri.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Bit.Core.Enums; using Bit.Core.Models.Data; @@ -10,7 +11,8 @@ namespace Bit.Core.Models.Domain { private HashSet _map = new HashSet { - "Uri" + nameof(Uri), + nameof(UriChecksum) }; public LoginUri() { } @@ -23,10 +25,11 @@ namespace Bit.Core.Models.Domain public EncString Uri { get; set; } public UriMatchType? Match { get; set; } + public EncString UriChecksum { get; set; } public Task DecryptAsync(string orgId, SymmetricCryptoKey key = null) { - return DecryptObjAsync(new LoginUriView(this), this, _map, orgId, key); + return DecryptObjAsync(new LoginUriView(this), this, _map.Where(m => m != nameof(UriChecksum)).ToHashSet(), orgId, key); } public LoginUriData ToLoginUriData() diff --git a/src/Core/Models/Export/LoginUri.cs b/src/Core/Models/Export/LoginUri.cs index e3f215ff7..a4e161fed 100644 --- a/src/Core/Models/Export/LoginUri.cs +++ b/src/Core/Models/Export/LoginUri.cs @@ -17,10 +17,12 @@ namespace Bit.Core.Models.Export { Match = obj.Match; Uri = obj.Uri?.EncryptedString; + UriChecksum = obj.UriChecksum?.EncryptedString; } public UriMatchType? Match { get; set; } public string Uri { get; set; } + public string UriChecksum { get; set; } public static LoginUriView ToView(LoginUri req, LoginUriView view = null) { diff --git a/src/Core/Models/Request/CipherRequest.cs b/src/Core/Models/Request/CipherRequest.cs index f0b44d402..ae48b83b5 100644 --- a/src/Core/Models/Request/CipherRequest.cs +++ b/src/Core/Models/Request/CipherRequest.cs @@ -27,7 +27,7 @@ namespace Bit.Core.Models.Request Login = new LoginApi { Uris = cipher.Login.Uris?.Select( - u => new LoginUriApi { Match = u.Match, Uri = u.Uri?.EncryptedString }).ToList(), + u => new LoginUriApi { Match = u.Match, Uri = u.Uri?.EncryptedString, UriChecksum = u.UriChecksum?.EncryptedString }).ToList(), Username = cipher.Login.Username?.EncryptedString, Password = cipher.Login.Password?.EncryptedString, PasswordRevisionDate = cipher.Login.PasswordRevisionDate, diff --git a/src/Core/Services/CipherService.cs b/src/Core/Services/CipherService.cs index c17c172e7..87ec048da 100644 --- a/src/Core/Services/CipherService.cs +++ b/src/Core/Services/CipherService.cs @@ -1146,13 +1146,15 @@ namespace Bit.Core.Services if (model.Login.Uris != null) { cipher.Login.Uris = new List(); - foreach (var uri in model.Login.Uris) + foreach (var uri in model.Login.Uris.Where(u => u.Uri != null)) { var loginUri = new LoginUri { Match = uri.Match }; await EncryptObjPropertyAsync(uri, loginUri, new HashSet { "Uri" }, key); + var uriHash = await _cryptoService.HashAsync(uri.Uri, CryptoHashAlgorithm.Sha256); + loginUri.UriChecksum = await _cryptoService.EncryptAsync(uriHash, key); cipher.Login.Uris.Add(loginUri); } } diff --git a/src/Core/Services/CryptoService.cs b/src/Core/Services/CryptoService.cs index 7388f8e9a..5027cc853 100644 --- a/src/Core/Services/CryptoService.cs +++ b/src/Core/Services/CryptoService.cs @@ -19,6 +19,7 @@ namespace Bit.Core.Services private readonly IStateService _stateService; private readonly ICryptoFunctionService _cryptoFunctionService; + private readonly ILogger _logger; private SymmetricCryptoKey _legacyEtmKey; private string _masterKeyHash; @@ -29,10 +30,12 @@ namespace Bit.Core.Services public CryptoService( IStateService stateService, - ICryptoFunctionService cryptoFunctionService) + ICryptoFunctionService cryptoFunctionService, + ILogger logger) { _stateService = stateService; _cryptoFunctionService = cryptoFunctionService; + _logger = logger; } public void ClearCache() @@ -730,6 +733,33 @@ namespace Bit.Core.Services } } + public async Task HashAsync(string value, CryptoHashAlgorithm hashAlgorithm) + { + var hashArray = await _cryptoFunctionService.HashAsync(value, hashAlgorithm); + return Convert.ToBase64String(hashArray); + } + + public async Task ValidateUriChecksumAsync(EncString remoteUriChecksum, string rawUri, string orgId, SymmetricCryptoKey key) + { + try + { + if (remoteUriChecksum == null) + { + return false; + } + + var localChecksum = await HashAsync(rawUri, CryptoHashAlgorithm.Sha256); + + var remoteChecksum = await remoteUriChecksum.DecryptAsync(orgId, key); + return remoteChecksum == localChecksum; + } + catch (Exception ex) + { + _logger.Exception(ex); + return false; + } + } + // --HELPER METHODS-- private async Task StoreAdditionalKeysAsync(UserKey userKey, string userId = null) diff --git a/src/iOS.Core/Utilities/iOSCoreHelpers.cs b/src/iOS.Core/Utilities/iOSCoreHelpers.cs index 2296bdfaf..b17e15f07 100644 --- a/src/iOS.Core/Utilities/iOSCoreHelpers.cs +++ b/src/iOS.Core/Utilities/iOSCoreHelpers.cs @@ -113,7 +113,7 @@ namespace Bit.iOS.Core.Utilities var platformUtilsService = new MobilePlatformUtilsService(deviceActionService, clipboardService, messagingService, broadcasterService); var cryptoFunctionService = new PclCryptoFunctionService(cryptoPrimitiveService); - var cryptoService = new CryptoService(stateService, cryptoFunctionService); + var cryptoService = new CryptoService(stateService, cryptoFunctionService, logger); var biometricService = new BiometricService(stateService, cryptoService); var userPinService = new UserPinService(stateService, cryptoService); var passwordRepromptService = new MobilePasswordRepromptService(platformUtilsService, cryptoService, stateService); From cd33c7f608d09fd5016c0ebc0316de29aa49be50 Mon Sep 17 00:00:00 2001 From: Federico Maccaroni Date: Wed, 24 Jan 2024 11:22:02 -0300 Subject: [PATCH 18/50] PM-3350 Added new style to prevent spell check and text prediction --- src/Core/Pages/Accounts/EnvironmentPage.xaml | 10 +++++----- src/Core/Pages/Accounts/HomePage.xaml | 5 ++--- src/Core/Resources/Styles/Base.xaml | 6 ++++++ 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/Core/Pages/Accounts/EnvironmentPage.xaml b/src/Core/Pages/Accounts/EnvironmentPage.xaml index 28bfcaece..855c345a8 100644 --- a/src/Core/Pages/Accounts/EnvironmentPage.xaml +++ b/src/Core/Pages/Accounts/EnvironmentPage.xaml @@ -37,7 +37,7 @@ Text="{Binding BaseUrl}" Keyboard="Url" Placeholder="ex. https://bitwarden.company.com" - StyleClass="box-value" + StyleClass="box-value, no-keyboard-auto-help" ReturnType="Go" ReturnCommand="{Binding SubmitCommand}" AutomationId="ServerUrlEntry"/> @@ -59,7 +59,7 @@ x:Name="_webVaultEntry" Text="{Binding WebVaultUrl}" Keyboard="Url" - StyleClass="box-value" + StyleClass="box-value, no-keyboard-auto-help" AutomationId="WebVaultUrlEntry"/> @@ -70,7 +70,7 @@ x:Name="_apiEntry" Text="{Binding ApiUrl}" Keyboard="Url" - StyleClass="box-value" + StyleClass="box-value, no-keyboard-auto-help" AutomationId="ApiUrlEntry"/> @@ -81,7 +81,7 @@ x:Name="_identityEntry" Text="{Binding IdentityUrl}" Keyboard="Url" - StyleClass="box-value" + StyleClass="box-value, no-keyboard-auto-help" AutomationId="IdentityUrlEntry"/> @@ -92,7 +92,7 @@ x:Name="_iconsEntry" Text="{Binding IconsUrl}" Keyboard="Url" - StyleClass="box-value" + StyleClass="box-value, no-keyboard-auto-help" ReturnType="Go" ReturnCommand="{Binding SubmitCommand}" AutomationId="IconsUrlEntry"/> diff --git a/src/Core/Pages/Accounts/HomePage.xaml b/src/Core/Pages/Accounts/HomePage.xaml index fe6992155..950f7d75c 100644 --- a/src/Core/Pages/Accounts/HomePage.xaml +++ b/src/Core/Pages/Accounts/HomePage.xaml @@ -50,11 +50,10 @@ x:Name="_email" Text="{Binding Email}" Keyboard="Email" - StyleClass="box-value" + StyleClass="box-value, no-keyboard-auto-help" ReturnType="Go" ReturnCommand="{Binding ContinueCommand}" - AutomationId="EmailAddressEntry" - > + AutomationId="EmailAddressEntry"> diff --git a/src/Core/Resources/Styles/Base.xaml b/src/Core/Resources/Styles/Base.xaml index 34302c4d9..1eaed8e11 100644 --- a/src/Core/Resources/Styles/Base.xaml +++ b/src/Core/Resources/Styles/Base.xaml @@ -557,6 +557,12 @@ +