diff --git a/README.md b/README.md index b03ac86d9..3a268ac0b 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ The Bitwarden mobile application is written in C# with Xamarin Android, Xamarin # Build/Run -Please refer to the [Mobile section](https://contributing.bitwarden.com/clients/mobile) of the [Contributing Documentation](https://contributing.bitwarden.com/) for build instructions, recommended tooling, code style tips, and lots of other great information to get you started. +Please refer to the [Mobile section](https://contributing.bitwarden.com/mobile/) of the [Contributing Documentation](https://contributing.bitwarden.com/) for build instructions, recommended tooling, code style tips, and lots of other great information to get you started. # We're Hiring! diff --git a/src/Android/MainApplication.cs b/src/Android/MainApplication.cs index ecb9ca277..991aea474 100644 --- a/src/Android/MainApplication.cs +++ b/src/Android/MainApplication.cs @@ -99,12 +99,13 @@ namespace Bit.Droid { ServiceContainer.Register("nativeLogService", new AndroidLogService()); #if FDROID - ServiceContainer.Register("logger", new StubLogger()); + var logger = new StubLogger(); #elif DEBUG - ServiceContainer.Register("logger", DebugLogger.Instance); + var logger = DebugLogger.Instance; #else - ServiceContainer.Register("logger", Logger.Instance); + var logger = Logger.Instance; #endif + ServiceContainer.Register("logger", logger); // Note: This might cause a race condition. Investigate more. Task.Run(() => @@ -124,7 +125,7 @@ namespace Bit.Droid var documentsPath = System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal); var liteDbStorage = new LiteDbStorageService(Path.Combine(documentsPath, "bitwarden.db")); var localizeService = new LocalizeService(); - var broadcasterService = new BroadcasterService(); + var broadcasterService = new BroadcasterService(logger); var messagingService = new MobileBroadcasterMessagingService(broadcasterService); var i18nService = new MobileI18nService(localizeService.GetCurrentCultureInfo()); var secureStorageService = new SecureStorageService(); diff --git a/src/Android/Properties/AndroidManifest.xml b/src/Android/Properties/AndroidManifest.xml index f1d29b722..2f5780562 100644 --- a/src/Android/Properties/AndroidManifest.xml +++ b/src/Android/Properties/AndroidManifest.xml @@ -1,5 +1,5 @@ - + diff --git a/src/Android/Services/ClipboardService.cs b/src/Android/Services/ClipboardService.cs index 2abd8df45..7978e8523 100644 --- a/src/Android/Services/ClipboardService.cs +++ b/src/Android/Services/ClipboardService.cs @@ -28,17 +28,25 @@ namespace Bit.Droid.Services public async Task CopyTextAsync(string text, int expiresInMs = -1, bool isSensitive = true) { - // Xamarin.Essentials.Clipboard currently doesn't support the IS_SENSITIVE flag for API 33+ - if ((int)Build.VERSION.SdkInt < 33) + try { - await Clipboard.SetTextAsync(text); - } - else - { - CopyToClipboard(text, isSensitive); - } + // Xamarin.Essentials.Clipboard currently doesn't support the IS_SENSITIVE flag for API 33+ + if ((int)Build.VERSION.SdkInt < 33) + { + await Clipboard.SetTextAsync(text); + } + else + { + CopyToClipboard(text, isSensitive); + } - await ClearClipboardAlarmAsync(expiresInMs); + await ClearClipboardAlarmAsync(expiresInMs); + } + catch (Java.Lang.SecurityException ex) when (ex.Message.Contains("does not belong to")) + { + // #1962 Just ignore, the content is copied either way but there is some app interfiering in the process + // that the OS catches and just throws this exception. + } } public bool IsCopyNotificationHandledByPlatform() diff --git a/src/App/App.xaml.cs b/src/App/App.xaml.cs index 9e534cd72..3415fd7e9 100644 --- a/src/App/App.xaml.cs +++ b/src/App/App.xaml.cs @@ -10,6 +10,7 @@ using Bit.App.Utilities.AccountManagement; using Bit.Core.Abstractions; using Bit.Core.Enums; using Bit.Core.Models.Data; +using Bit.Core.Services; using Bit.Core.Utilities; using Xamarin.Forms; using Xamarin.Forms.Xaml; @@ -56,86 +57,93 @@ namespace Bit.App Bootstrap(); _broadcasterService.Subscribe(nameof(App), async (message) => { - if (message.Command == "showDialog") + try { - var details = message.Data as DialogDetails; - var confirmed = true; - var confirmText = string.IsNullOrWhiteSpace(details.ConfirmText) ? - AppResources.Ok : details.ConfirmText; - Device.BeginInvokeOnMainThread(async () => + if (message.Command == "showDialog") { - if (!string.IsNullOrWhiteSpace(details.CancelText)) + var details = message.Data as DialogDetails; + var confirmed = true; + var confirmText = string.IsNullOrWhiteSpace(details.ConfirmText) ? + AppResources.Ok : details.ConfirmText; + Device.BeginInvokeOnMainThread(async () => { - confirmed = await Current.MainPage.DisplayAlert(details.Title, details.Text, confirmText, - details.CancelText); - } - else - { - await Current.MainPage.DisplayAlert(details.Title, details.Text, confirmText); - } - _messagingService.Send("showDialogResolve", new Tuple(details.DialogId, confirmed)); - }); - } - else if (message.Command == "resumed") - { - if (Device.RuntimePlatform == Device.iOS) + if (!string.IsNullOrWhiteSpace(details.CancelText)) + { + confirmed = await Current.MainPage.DisplayAlert(details.Title, details.Text, confirmText, + details.CancelText); + } + else + { + await Current.MainPage.DisplayAlert(details.Title, details.Text, confirmText); + } + _messagingService.Send("showDialogResolve", new Tuple(details.DialogId, confirmed)); + }); + } + else if (message.Command == "resumed") { - ResumedAsync().FireAndForget(); + if (Device.RuntimePlatform == Device.iOS) + { + ResumedAsync().FireAndForget(); + } + } + else if (message.Command == "slept") + { + if (Device.RuntimePlatform == Device.iOS) + { + await SleptAsync(); + } + } + else if (message.Command == "migrated") + { + await Task.Delay(1000); + await _accountsManager.NavigateOnAccountChangeAsync(); + } + else if (message.Command == "popAllAndGoToTabGenerator" || + message.Command == "popAllAndGoToTabMyVault" || + message.Command == "popAllAndGoToTabSend" || + message.Command == "popAllAndGoToAutofillCiphers") + { + Device.BeginInvokeOnMainThread(async () => + { + if (Current.MainPage is TabsPage tabsPage) + { + while (tabsPage.Navigation.ModalStack.Count > 0) + { + await tabsPage.Navigation.PopModalAsync(false); + } + if (message.Command == "popAllAndGoToAutofillCiphers") + { + Current.MainPage = new NavigationPage(new AutofillCiphersPage(Options)); + } + else if (message.Command == "popAllAndGoToTabMyVault") + { + Options.MyVaultTile = false; + tabsPage.ResetToVaultPage(); + } + else if (message.Command == "popAllAndGoToTabGenerator") + { + Options.GeneratorTile = false; + tabsPage.ResetToGeneratorPage(); + } + else if (message.Command == "popAllAndGoToTabSend") + { + tabsPage.ResetToSendPage(); + } + } + }); + } + else if (message.Command == "convertAccountToKeyConnector") + { + Device.BeginInvokeOnMainThread(async () => + { + await Application.Current.MainPage.Navigation.PushModalAsync( + new NavigationPage(new RemoveMasterPasswordPage())); + }); } } - else if (message.Command == "slept") + catch (Exception ex) { - if (Device.RuntimePlatform == Device.iOS) - { - await SleptAsync(); - } - } - else if (message.Command == "migrated") - { - await Task.Delay(1000); - await _accountsManager.NavigateOnAccountChangeAsync(); - } - else if (message.Command == "popAllAndGoToTabGenerator" || - message.Command == "popAllAndGoToTabMyVault" || - message.Command == "popAllAndGoToTabSend" || - message.Command == "popAllAndGoToAutofillCiphers") - { - Device.BeginInvokeOnMainThread(async () => - { - if (Current.MainPage is TabsPage tabsPage) - { - while (tabsPage.Navigation.ModalStack.Count > 0) - { - await tabsPage.Navigation.PopModalAsync(false); - } - if (message.Command == "popAllAndGoToAutofillCiphers") - { - Current.MainPage = new NavigationPage(new AutofillCiphersPage(Options)); - } - else if (message.Command == "popAllAndGoToTabMyVault") - { - Options.MyVaultTile = false; - tabsPage.ResetToVaultPage(); - } - else if (message.Command == "popAllAndGoToTabGenerator") - { - Options.GeneratorTile = false; - tabsPage.ResetToGeneratorPage(); - } - else if (message.Command == "popAllAndGoToTabSend") - { - tabsPage.ResetToSendPage(); - } - } - }); - } - else if (message.Command == "convertAccountToKeyConnector") - { - Device.BeginInvokeOnMainThread(async () => - { - await Application.Current.MainPage.Navigation.PushModalAsync( - new NavigationPage(new RemoveMasterPasswordPage())); - }); + LoggerHelper.LogEvenIfCantBeResolved(ex); } }); } diff --git a/src/App/Pages/Accounts/HomePage.xaml.cs b/src/App/Pages/Accounts/HomePage.xaml.cs index d63413216..f01fccef0 100644 --- a/src/App/Pages/Accounts/HomePage.xaml.cs +++ b/src/App/Pages/Accounts/HomePage.xaml.cs @@ -54,7 +54,7 @@ namespace Bit.App.Pages { _vm.AvatarImageSource = await GetAvatarImageSourceAsync(); } - _broadcasterService.Subscribe(nameof(HomePage), async (message) => + _broadcasterService.Subscribe(nameof(HomePage), (message) => { if (message.Command == "updatedTheme") { diff --git a/src/App/Pages/Accounts/LockPage.xaml.cs b/src/App/Pages/Accounts/LockPage.xaml.cs index 354c45230..22f6416ef 100644 --- a/src/App/Pages/Accounts/LockPage.xaml.cs +++ b/src/App/Pages/Accounts/LockPage.xaml.cs @@ -25,8 +25,6 @@ namespace Bit.App.Pages _vm = BindingContext as LockPageViewModel; _vm.Page = this; _vm.UnlockedAction = () => Device.BeginInvokeOnMainThread(async () => await UnlockedAsync()); - MasterPasswordEntry = _masterPassword; - PinEntry = _pin; if (Device.RuntimePlatform == Device.iOS) { @@ -38,8 +36,17 @@ namespace Bit.App.Pages } } - public Entry MasterPasswordEntry { get; set; } - public Entry PinEntry { get; set; } + public Entry SecretEntry + { + get + { + if (_vm?.PinLock ?? false) + { + return _pin; + } + return _masterPassword; + } + } public async Task PromptBiometricAfterResumeAsync() { @@ -70,16 +77,12 @@ namespace Bit.App.Pages _vm.AvatarImageSource = await GetAvatarImageSourceAsync(); await _vm.InitAsync(); + + _vm.FocusSecretEntry += PerformFocusSecretEntry; + if (!_vm.BiometricLock) { - if (_vm.PinLock) - { - RequestFocus(PinEntry); - } - else - { - RequestFocus(MasterPasswordEntry); - } + RequestFocus(SecretEntry); } else { @@ -99,6 +102,18 @@ namespace Bit.App.Pages } } + private void PerformFocusSecretEntry(int? cursorPosition) + { + Device.BeginInvokeOnMainThread(() => + { + SecretEntry.Focus(); + if (cursorPosition.HasValue) + { + SecretEntry.CursorPosition = cursorPosition.Value; + } + }); + } + protected override bool OnBackButtonPressed() { if (_accountListOverlay.IsVisible) diff --git a/src/App/Pages/Accounts/LockPageViewModel.cs b/src/App/Pages/Accounts/LockPageViewModel.cs index 5b007be38..5419916e9 100644 --- a/src/App/Pages/Accounts/LockPageViewModel.cs +++ b/src/App/Pages/Accounts/LockPageViewModel.cs @@ -10,6 +10,7 @@ using Bit.Core.Enums; using Bit.Core.Models.Domain; using Bit.Core.Models.Request; using Bit.Core.Utilities; +using Xamarin.CommunityToolkit.Helpers; using Xamarin.Forms; namespace Bit.App.Pages @@ -27,6 +28,7 @@ namespace Bit.App.Pages private readonly IBiometricService _biometricService; private readonly IKeyConnectorService _keyConnectorService; private readonly ILogger _logger; + private readonly WeakEventManager _secretEntryFocusWeakEventManager = new WeakEventManager(); private string _email; private bool _showPassword; @@ -133,6 +135,11 @@ namespace Bit.App.Pages public string MasterPassword { get; set; } public string Pin { get; set; } public Action UnlockedAction { get; set; } + public event Action FocusSecretEntry + { + add => _secretEntryFocusWeakEventManager.AddEventHandler(value); + remove => _secretEntryFocusWeakEventManager.RemoveEventHandler(value); + } public async Task InitAsync() { @@ -346,11 +353,8 @@ namespace Bit.App.Pages public void TogglePassword() { ShowPassword = !ShowPassword; - var page = (Page as LockPage); - var entry = PinLock ? page.PinEntry : page.MasterPasswordEntry; - var str = PinLock ? Pin : MasterPassword; - entry.Focus(); - entry.CursorPosition = String.IsNullOrEmpty(str) ? 0 : str.Length; + var secret = PinLock ? Pin : MasterPassword; + _secretEntryFocusWeakEventManager.RaiseEvent(string.IsNullOrEmpty(secret) ? 0 : secret.Length, nameof(FocusSecretEntry)); } public async Task PromptBiometricAsync() @@ -361,18 +365,8 @@ namespace Bit.App.Pages return; } var success = await _platformUtilsService.AuthenticateBiometricAsync(null, - PinLock ? AppResources.PIN : AppResources.MasterPassword, () => - { - var page = Page as LockPage; - if (PinLock) - { - page.PinEntry.Focus(); - } - else - { - page.MasterPasswordEntry.Focus(); - } - }); + PinLock ? AppResources.PIN : AppResources.MasterPassword, + () => _secretEntryFocusWeakEventManager.RaiseEvent((int?)null, nameof(FocusSecretEntry))); await _stateService.SetBiometricLockedAsync(!success); if (success) { diff --git a/src/App/Pages/Accounts/LoginSsoPageViewModel.cs b/src/App/Pages/Accounts/LoginSsoPageViewModel.cs index 66145457a..61dad0114 100644 --- a/src/App/Pages/Accounts/LoginSsoPageViewModel.cs +++ b/src/App/Pages/Accounts/LoginSsoPageViewModel.cs @@ -81,10 +81,12 @@ namespace Bit.App.Pages } await _deviceActionService.ShowLoadingAsync(AppResources.LoggingIn); + string ssoToken; try { - await _apiService.PreValidateSso(OrgIdentifier); + var response = await _apiService.PreValidateSso(OrgIdentifier); + ssoToken = response.Token; } catch (ApiException e) { @@ -112,7 +114,8 @@ namespace Bit.App.Pages "response_type=code&scope=api%20offline_access&" + "state=" + state + "&code_challenge=" + codeChallenge + "&" + "code_challenge_method=S256&response_mode=query&" + - "domain_hint=" + Uri.EscapeDataString(OrgIdentifier); + "domain_hint=" + Uri.EscapeDataString(OrgIdentifier) + "&" + + "ssoToken=" + Uri.EscapeDataString(ssoToken); WebAuthenticatorResult authResult = null; try diff --git a/src/App/Pages/Accounts/SetPasswordPageViewModel.cs b/src/App/Pages/Accounts/SetPasswordPageViewModel.cs index fd4adfd05..b4c415959 100644 --- a/src/App/Pages/Accounts/SetPasswordPageViewModel.cs +++ b/src/App/Pages/Accounts/SetPasswordPageViewModel.cs @@ -219,7 +219,8 @@ namespace Bit.App.Pages // Request var resetRequest = new OrganizationUserResetPasswordEnrollmentRequest { - ResetPasswordKey = encryptedKey.EncryptedString + ResetPasswordKey = encryptedKey.EncryptedString, + MasterPasswordHash = masterPasswordHash, }; var userId = await _stateService.GetActiveUserIdAsync(); // Enroll user diff --git a/src/App/Pages/Send/SendGroupingsPage/SendGroupingsPage.xaml.cs b/src/App/Pages/Send/SendGroupingsPage/SendGroupingsPage.xaml.cs index 183a832d8..6e8acc1ca 100644 --- a/src/App/Pages/Send/SendGroupingsPage/SendGroupingsPage.xaml.cs +++ b/src/App/Pages/Send/SendGroupingsPage/SendGroupingsPage.xaml.cs @@ -5,6 +5,7 @@ using Bit.App.Controls; using Bit.App.Models; using Bit.Core.Abstractions; using Bit.Core.Enums; +using Bit.Core.Services; using Bit.Core.Utilities; using Xamarin.Forms; @@ -68,21 +69,28 @@ namespace Bit.App.Pages _broadcasterService.Subscribe(_pageName, async (message) => { - if (message.Command == "syncStarted") + try { - Device.BeginInvokeOnMainThread(() => IsBusy = true); - } - else if (message.Command == "syncCompleted" || message.Command == "sendUpdated") - { - await Task.Delay(500); - Device.BeginInvokeOnMainThread(() => + if (message.Command == "syncStarted") { - IsBusy = false; - if (_vm.LoadedOnce) + Device.BeginInvokeOnMainThread(() => IsBusy = true); + } + else if (message.Command == "syncCompleted" || message.Command == "sendUpdated") + { + await Task.Delay(500); + Device.BeginInvokeOnMainThread(() => { - var task = _vm.LoadAsync(); - } - }); + IsBusy = false; + if (_vm.LoadedOnce) + { + var task = _vm.LoadAsync(); + } + }); + } + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); } }); diff --git a/src/App/Pages/Vault/AutofillCiphersPage.xaml.cs b/src/App/Pages/Vault/AutofillCiphersPage.xaml.cs index aed80316f..35b85fc9c 100644 --- a/src/App/Pages/Vault/AutofillCiphersPage.xaml.cs +++ b/src/App/Pages/Vault/AutofillCiphersPage.xaml.cs @@ -6,6 +6,7 @@ using Bit.App.Models; using Bit.App.Utilities; using Bit.Core.Abstractions; using Bit.Core.Enums; +using Bit.Core.Services; using Bit.Core.Utilities; using Xamarin.Forms; @@ -55,21 +56,28 @@ namespace Bit.App.Pages _broadcasterService.Subscribe(nameof(AutofillCiphersPage), async (message) => { - if (message.Command == "syncStarted") + try { - Device.BeginInvokeOnMainThread(() => IsBusy = true); - } - else if (message.Command == "syncCompleted") - { - await Task.Delay(500); - Device.BeginInvokeOnMainThread(() => + if (message.Command == "syncStarted") { - IsBusy = false; - if (_vm.LoadedOnce) + Device.BeginInvokeOnMainThread(() => IsBusy = true); + } + else if (message.Command == "syncCompleted") + { + await Task.Delay(500); + Device.BeginInvokeOnMainThread(() => { - var task = _vm.LoadAsync(); - } - }); + IsBusy = false; + if (_vm.LoadedOnce) + { + var task = _vm.LoadAsync(); + } + }); + } + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); } }); diff --git a/src/App/Pages/Vault/GroupingsPage/GroupingsPage.xaml.cs b/src/App/Pages/Vault/GroupingsPage/GroupingsPage.xaml.cs index 8bb30c634..776e4434b 100644 --- a/src/App/Pages/Vault/GroupingsPage/GroupingsPage.xaml.cs +++ b/src/App/Pages/Vault/GroupingsPage/GroupingsPage.xaml.cs @@ -7,6 +7,7 @@ using Bit.App.Resources; using Bit.Core.Abstractions; using Bit.Core.Enums; using Bit.Core.Models.Data; +using Bit.Core.Services; using Bit.Core.Utilities; using Xamarin.Forms; @@ -95,21 +96,28 @@ namespace Bit.App.Pages _broadcasterService.Subscribe(_pageName, async (message) => { - if (message.Command == "syncStarted") + try { - Device.BeginInvokeOnMainThread(() => IsBusy = true); - } - else if (message.Command == "syncCompleted") - { - await Task.Delay(500); - Device.BeginInvokeOnMainThread(() => + if (message.Command == "syncStarted") { - IsBusy = false; - if (_vm.LoadedOnce) + Device.BeginInvokeOnMainThread(() => IsBusy = true); + } + else if (message.Command == "syncCompleted") + { + await Task.Delay(500); + Device.BeginInvokeOnMainThread(() => { - var task = _vm.LoadAsync(); - } - }); + IsBusy = false; + if (_vm.LoadedOnce) + { + var task = _vm.LoadAsync(); + } + }); + } + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); } }); diff --git a/src/App/Pages/Vault/ViewPage.xaml.cs b/src/App/Pages/Vault/ViewPage.xaml.cs index eb03551f1..93e8b5eff 100644 --- a/src/App/Pages/Vault/ViewPage.xaml.cs +++ b/src/App/Pages/Vault/ViewPage.xaml.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using Bit.App.Resources; using Bit.Core.Abstractions; +using Bit.Core.Services; using Bit.Core.Utilities; using Xamarin.Forms; @@ -56,37 +57,44 @@ namespace Bit.App.Pages _broadcasterService.Subscribe(nameof(ViewPage), async (message) => { - if (message.Command == "syncStarted") + try { - Device.BeginInvokeOnMainThread(() => IsBusy = true); - } - else if (message.Command == "syncCompleted") - { - await Task.Delay(500); - Device.BeginInvokeOnMainThread(() => + if (message.Command == "syncStarted") { - IsBusy = false; - if (message.Data is Dictionary data && data.ContainsKey("successfully")) + Device.BeginInvokeOnMainThread(() => IsBusy = true); + } + else if (message.Command == "syncCompleted") + { + await Task.Delay(500); + Device.BeginInvokeOnMainThread(() => { - var success = data["successfully"] as bool?; - if (success.GetValueOrDefault()) + IsBusy = false; + if (message.Data is Dictionary data && data.ContainsKey("successfully")) { - var task = _vm.LoadAsync(() => AdjustToolbar()); + var success = data["successfully"] as bool?; + if (success.GetValueOrDefault()) + { + var task = _vm.LoadAsync(() => AdjustToolbar()); + } } - } - }); - } - else if (message.Command == "selectSaveFileResult") - { - Device.BeginInvokeOnMainThread(() => + }); + } + else if (message.Command == "selectSaveFileResult") { - var data = message.Data as Tuple; - if (data == null) + Device.BeginInvokeOnMainThread(() => { - return; - } - _vm.SaveFileSelected(data.Item1, data.Item2); - }); + var data = message.Data as Tuple; + if (data == null) + { + return; + } + _vm.SaveFileSelected(data.Item1, data.Item2); + }); + } + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); } }); await LoadOnAppearedAsync(_scrollView, true, async () => diff --git a/src/App/Pages/Vault/ViewPageViewModel.cs b/src/App/Pages/Vault/ViewPageViewModel.cs index 938d60232..31ee9632f 100644 --- a/src/App/Pages/Vault/ViewPageViewModel.cs +++ b/src/App/Pages/Vault/ViewPageViewModel.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using System.Windows.Input; using Bit.App.Abstractions; using Bit.App.Resources; using Bit.App.Utilities; @@ -11,6 +12,7 @@ using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.View; using Bit.Core.Utilities; +using Xamarin.CommunityToolkit.ObjectModel; using Xamarin.Forms; namespace Bit.App.Pages @@ -28,6 +30,7 @@ namespace Bit.App.Pages private readonly IPasswordRepromptService _passwordRepromptService; private readonly ILocalizeService _localizeService; private readonly IClipboardService _clipboardService; + private readonly ILogger _logger; private CipherView _cipher; private List _fields; @@ -58,10 +61,11 @@ namespace Bit.App.Pages _passwordRepromptService = ServiceContainer.Resolve("passwordRepromptService"); _localizeService = ServiceContainer.Resolve("localizeService"); _clipboardService = ServiceContainer.Resolve("clipboardService"); + _logger = ServiceContainer.Resolve("logger"); - CopyCommand = new Command((id) => CopyAsync(id, null)); - CopyUriCommand = new Command(CopyUri); - CopyFieldCommand = new Command(CopyField); + CopyCommand = new AsyncCommand((id) => CopyAsync(id, null), onException: ex => _logger.Exception(ex), allowsMultipleExecutions: false); + CopyUriCommand = new AsyncCommand(uriView => CopyAsync("LoginUri", uriView.Uri), onException: ex => _logger.Exception(ex), allowsMultipleExecutions: false); + CopyFieldCommand = new AsyncCommand(field => CopyAsync(field.Type == FieldType.Hidden ? "H_FieldValue" : "FieldValue", field.Value), onException: ex => _logger.Exception(ex), allowsMultipleExecutions: false); LaunchUriCommand = new Command(LaunchUri); TogglePasswordCommand = new Command(TogglePassword); ToggleCardNumberCommand = new Command(ToggleCardNumber); @@ -72,9 +76,9 @@ namespace Bit.App.Pages PageTitle = AppResources.ViewItem; } - public Command CopyCommand { get; set; } - public Command CopyUriCommand { get; set; } - public Command CopyFieldCommand { get; set; } + public ICommand CopyCommand { get; set; } + public ICommand CopyUriCommand { get; set; } + public ICommand CopyFieldCommand { get; set; } public Command LaunchUriCommand { get; set; } public Command TogglePasswordCommand { get; set; } public Command ToggleCardNumberCommand { get; set; } @@ -623,7 +627,7 @@ namespace Bit.App.Pages _attachmentFilename = null; } - private async void CopyAsync(string id, string text = null) + private async Task CopyAsync(string id, string text = null) { if (_passwordRepromptService.ProtectedFields.Contains(id) && !await PromptPasswordAsync()) { @@ -687,16 +691,6 @@ namespace Bit.App.Pages } } - private void CopyUri(LoginUriView uri) - { - CopyAsync("LoginUri", uri.Uri); - } - - private void CopyField(FieldView field) - { - CopyAsync(field.Type == Core.Enums.FieldType.Hidden ? "H_FieldValue" : "FieldValue", field.Value); - } - private void LaunchUri(LoginUriView uri) { if (uri.CanLaunch && (Page as BaseContentPage).DoOnce()) diff --git a/src/Core/Abstractions/IApiService.cs b/src/Core/Abstractions/IApiService.cs index 48405d4e8..4ae00024a 100644 --- a/src/Core/Abstractions/IApiService.cs +++ b/src/Core/Abstractions/IApiService.cs @@ -44,7 +44,7 @@ namespace Bit.Core.Abstractions Task PutDeleteCipherAsync(string id); Task PutRestoreCipherAsync(string id); Task RefreshIdentityTokenAsync(); - Task PreValidateSso(string identifier); + Task PreValidateSso(string identifier); Task SendAsync(HttpMethod method, string path, TRequest body, bool authed, bool hasResponse, bool logoutOnUnauthorized = true); void SetUrls(EnvironmentUrls urls); diff --git a/src/Core/Abstractions/IBroadcasterService.cs b/src/Core/Abstractions/IBroadcasterService.cs index ac481b53d..0675aad18 100644 --- a/src/Core/Abstractions/IBroadcasterService.cs +++ b/src/Core/Abstractions/IBroadcasterService.cs @@ -5,7 +5,8 @@ namespace Bit.Core.Abstractions { public interface IBroadcasterService { - void Send(Message message, string id = null); + void Send(Message message); + void Send(Message message, string id); void Subscribe(string id, Action messageCallback); void Unsubscribe(string id); } diff --git a/src/Core/Models/Request/OrganizationUserResetPasswordEnrollmentRequest.cs b/src/Core/Models/Request/OrganizationUserResetPasswordEnrollmentRequest.cs index 267a425ec..663e0fc61 100644 --- a/src/Core/Models/Request/OrganizationUserResetPasswordEnrollmentRequest.cs +++ b/src/Core/Models/Request/OrganizationUserResetPasswordEnrollmentRequest.cs @@ -2,6 +2,7 @@ { public class OrganizationUserResetPasswordEnrollmentRequest { + public string MasterPasswordHash { get; set; } public string ResetPasswordKey { get; set; } } } diff --git a/src/Core/Models/Response/SsoPrevalidateResponse.cs b/src/Core/Models/Response/SsoPrevalidateResponse.cs new file mode 100644 index 000000000..589867a4b --- /dev/null +++ b/src/Core/Models/Response/SsoPrevalidateResponse.cs @@ -0,0 +1,7 @@ +namespace Bit.Core.Models.Response +{ + public class SsoPrevalidateResponse + { + public string Token { get; set; } + } +} diff --git a/src/Core/Services/ApiService.cs b/src/Core/Services/ApiService.cs index fdd880c59..ad462c058 100644 --- a/src/Core/Services/ApiService.cs +++ b/src/Core/Services/ApiService.cs @@ -547,7 +547,7 @@ namespace Bit.Core.Services return accessToken; } - public async Task PreValidateSso(string identifier) + public async Task PreValidateSso(string identifier) { var path = "/account/prevalidate?domainHint=" + WebUtility.UrlEncode(identifier); using (var requestMessage = new HttpRequestMessage()) @@ -571,7 +571,8 @@ namespace Bit.Core.Services var error = await HandleErrorAsync(response, false, true); throw new ApiException(error); } - return null; + var responseJsonString = await response.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject(responseJsonString); } } diff --git a/src/Core/Services/BroadcasterService.cs b/src/Core/Services/BroadcasterService.cs index 553ec49c9..782529335 100644 --- a/src/Core/Services/BroadcasterService.cs +++ b/src/Core/Services/BroadcasterService.cs @@ -8,24 +8,58 @@ namespace Bit.App.Services { public class BroadcasterService : IBroadcasterService { + private readonly ILogger _logger; private readonly Dictionary> _subscribers = new Dictionary>(); private object _myLock = new object(); - public void Send(Message message, string id = null) + public BroadcasterService(ILogger logger) + { + _logger = logger; + } + + public void Send(Message message) { lock (_myLock) { - if (!string.IsNullOrWhiteSpace(id)) - { - if (_subscribers.ContainsKey(id)) - { - Task.Run(() => _subscribers[id].Invoke(message)); - } - return; - } foreach (var sub in _subscribers) { - Task.Run(() => sub.Value.Invoke(message)); + Task.Run(() => + { + try + { + sub.Value(message); + } + catch (Exception ex) + { + _logger.Exception(ex); + } + }); + } + } + } + + public void Send(Message message, string id) + { + if (string.IsNullOrWhiteSpace(id)) + { + return; + } + + lock (_myLock) + { + if (_subscribers.TryGetValue(id, out var action)) + { + Task.Run(() => + { + try + { + action(message); + } + catch (Exception ex) + { + _logger.Exception(ex); + } + }); } } } @@ -34,14 +68,7 @@ namespace Bit.App.Services { lock (_myLock) { - if (_subscribers.ContainsKey(id)) - { - _subscribers[id] = messageCallback; - } - else - { - _subscribers.Add(id, messageCallback); - } + _subscribers[id] = messageCallback; } } diff --git a/src/Core/Utilities/ServiceContainer.cs b/src/Core/Utilities/ServiceContainer.cs index 19450173b..84dda33bd 100644 --- a/src/Core/Utilities/ServiceContainer.cs +++ b/src/Core/Utilities/ServiceContainer.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; +using System.Text; using System.Threading.Tasks; using Bit.Core.Abstractions; using Bit.Core.Services; @@ -8,7 +10,7 @@ namespace Bit.Core.Utilities { public static class ServiceContainer { - public static Dictionary RegisteredServices { get; set; } = new Dictionary(); + public static ConcurrentDictionary RegisteredServices { get; set; } = new ConcurrentDictionary(); public static bool Inited { get; set; } public static void Init(string customUserAgent = null, string clearCipherCacheKey = null, @@ -109,18 +111,17 @@ namespace Bit.Core.Utilities public static void Register(string serviceName, T obj) { - if (RegisteredServices.ContainsKey(serviceName)) + if (!RegisteredServices.TryAdd(serviceName, obj)) { throw new Exception($"Service {serviceName} has already been registered."); } - RegisteredServices.Add(serviceName, obj); } public static T Resolve(string serviceName, bool dontThrow = false) { - if (RegisteredServices.ContainsKey(serviceName)) + if (RegisteredServices.TryGetValue(serviceName, out var service)) { - return (T)RegisteredServices[serviceName]; + return (T)service; } if (dontThrow) { @@ -129,6 +130,59 @@ namespace Bit.Core.Utilities throw new Exception($"Service {serviceName} is not registered."); } + public static void Register(T obj) + where T : class + { + Register(typeof(T), obj); + } + + public static void Register(Type type, object obj) + { + var serviceName = GetServiceRegistrationName(type); + if (!RegisteredServices.TryAdd(serviceName, obj)) + { + throw new Exception($"Service {serviceName} has already been registered."); + } + } + + public static T Resolve() + where T : class + { + return (T)Resolve(typeof(T)); + } + + public static object Resolve(Type type) + { + var serviceName = GetServiceRegistrationName(type); + if (RegisteredServices.TryGetValue(serviceName, out var service)) + { + return service; + } + throw new Exception($"Service {serviceName} is not registered."); + } + + public static bool TryResolve(out T service) + where T : class + { + try + { + var toReturn = TryResolve(typeof(T), out var serviceObj); + service = (T)serviceObj; + return toReturn; + } + catch (Exception) + { + service = null; + return false; + } + } + + public static bool TryResolve(Type type, out object service) + { + var serviceName = GetServiceRegistrationName(type); + return RegisteredServices.TryGetValue(serviceName, out service); + } + public static void Reset() { foreach (var service in RegisteredServices) @@ -140,7 +194,33 @@ namespace Bit.Core.Utilities } Inited = false; RegisteredServices.Clear(); - RegisteredServices = new Dictionary(); + RegisteredServices = new ConcurrentDictionary(); + } + + /// + /// Gets the service registration name + /// + /// Type of the service + /// + /// In order to work with already register/resolve we need to maintain the naming convention + /// of camelCase without the first "I" on the services interfaces + /// e.g. "ITokenService" -> "tokenService" + /// + static string GetServiceRegistrationName(Type type) + { + var typeName = type.Name; + var sb = new StringBuilder(); + + var indexToLowerCase = 0; + if (typeName[0] == 'I' && char.IsUpper(typeName[1])) + { + // if it's an interface then we ignore the first char + // and lower case the 2nd one (index 1) + indexToLowerCase = 1; + } + sb.Append(char.ToLower(typeName[indexToLowerCase])); + sb.Append(typeName.Substring(++indexToLowerCase)); + return sb.ToString(); } } } diff --git a/src/iOS.Autofill/Info.plist b/src/iOS.Autofill/Info.plist index 306b1513c..d467245f3 100644 --- a/src/iOS.Autofill/Info.plist +++ b/src/iOS.Autofill/Info.plist @@ -11,7 +11,7 @@ CFBundleIdentifier com.8bit.bitwarden.autofill CFBundleShortVersionString - 2022.05.1 + 2022.6.1 CFBundleVersion 1 CFBundleLocalizations diff --git a/src/iOS.Core/Renderers/CustomTabbedRenderer.cs b/src/iOS.Core/Renderers/CustomTabbedRenderer.cs index a8904ef6b..60e59ba23 100644 --- a/src/iOS.Core/Renderers/CustomTabbedRenderer.cs +++ b/src/iOS.Core/Renderers/CustomTabbedRenderer.cs @@ -21,7 +21,7 @@ namespace Bit.iOS.Core.Renderers public CustomTabbedRenderer() { _broadcasterService = ServiceContainer.Resolve("broadcasterService"); - _broadcasterService.Subscribe(nameof(CustomTabbedRenderer), async (message) => + _broadcasterService.Subscribe(nameof(CustomTabbedRenderer), (message) => { if (message.Command == "updatedTheme") { diff --git a/src/iOS.Core/Utilities/iOSCoreHelpers.cs b/src/iOS.Core/Utilities/iOSCoreHelpers.cs index cfd0bf1bb..21321803e 100644 --- a/src/iOS.Core/Utilities/iOSCoreHelpers.cs +++ b/src/iOS.Core/Utilities/iOSCoreHelpers.cs @@ -38,13 +38,15 @@ namespace Bit.iOS.Core.Utilities ServiceContainer.Register("nativeLogService", new ConsoleLogService()); } + ILogger logger = null; if (ServiceContainer.Resolve("logger", true) == null) { #if DEBUG - ServiceContainer.Register("logger", DebugLogger.Instance); + logger = DebugLogger.Instance; #else - ServiceContainer.Register("logger", Logger.Instance); + logger = Logger.Instance; #endif + ServiceContainer.Register("logger", logger); } var preferencesStorage = new PreferencesStorageService(AppGroupId); @@ -52,7 +54,7 @@ namespace Bit.iOS.Core.Utilities var liteDbStorage = new LiteDbStorageService( Path.Combine(appGroupContainer.Path, "Library", "bitwarden.db")); var localizeService = new LocalizeService(); - var broadcasterService = new BroadcasterService(); + var broadcasterService = new BroadcasterService(logger); var messagingService = new MobileBroadcasterMessagingService(broadcasterService); var i18nService = new MobileI18nService(localizeService.GetCurrentCultureInfo()); var secureStorageService = new KeyChainStorageService(AppId, AccessGroup, diff --git a/src/iOS.Extension/Info.plist b/src/iOS.Extension/Info.plist index 9709f2139..7ef132a1a 100644 --- a/src/iOS.Extension/Info.plist +++ b/src/iOS.Extension/Info.plist @@ -11,7 +11,7 @@ CFBundleIdentifier com.8bit.bitwarden.find-login-action-extension CFBundleShortVersionString - 2022.05.1 + 2022.6.1 CFBundleLocalizations en diff --git a/src/iOS.ShareExtension/Info.plist b/src/iOS.ShareExtension/Info.plist index 9071b815b..64aee5209 100644 --- a/src/iOS.ShareExtension/Info.plist +++ b/src/iOS.ShareExtension/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 2022.05.1 + 2022.6.1 CFBundleVersion 1 MinimumOSVersion diff --git a/src/iOS/AppDelegate.cs b/src/iOS/AppDelegate.cs index 020a3803b..9686c8071 100644 --- a/src/iOS/AppDelegate.cs +++ b/src/iOS/AppDelegate.cs @@ -59,114 +59,121 @@ namespace Bit.iOS _broadcasterService.Subscribe(nameof(AppDelegate), async (message) => { - if (message.Command == "startEventTimer") + try { - StartEventTimer(); - } - else if (message.Command == "stopEventTimer") - { - var task = StopEventTimerAsync(); - } - else if (message.Command == "updatedTheme") - { - Device.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()) + StartEventTimer(); + } + else if (message.Command == "stopEventTimer") + { + var task = StopEventTimerAsync(); + } + else if (message.Command == "updatedTheme") + { + Device.BeginInvokeOnMainThread(() => + { + 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(); + } + } + else if (message.Command == "showAppExtension") + { + Device.BeginInvokeOnMainThread(() => ShowAppExtension((ExtensionPageViewModel)message.Data)); + } + else if (message.Command == "syncCompleted") + { + if (message.Data is Dictionary data && data.ContainsKey("successfully")) + { + var success = data["successfully"] as bool?; + if (success.GetValueOrDefault() && _deviceActionService.SystemMajorVersion() >= 12) + { + await ASHelpers.ReplaceAllIdentities(); + } + } + } + else if (message.Command == "addedCipher" || message.Command == "editedCipher" || + message.Command == "restoredCipher") + { + if (_deviceActionService.SystemMajorVersion() >= 12) + { + if (await ASHelpers.IdentitiesCanIncremental()) + { + var cipherId = message.Data as string; + if (message.Command == "addedCipher" && !string.IsNullOrWhiteSpace(cipherId)) + { + var identity = await ASHelpers.GetCipherIdentityAsync(cipherId); + if (identity == null) + { + return; + } + await ASCredentialIdentityStore.SharedStore?.SaveCredentialIdentitiesAsync( + new ASPasswordCredentialIdentity[] { identity }); + return; + } + } + await ASHelpers.ReplaceAllIdentities(); + } + } + else if (message.Command == "deletedCipher" || message.Command == "softDeletedCipher") + { + if (_deviceActionService.SystemMajorVersion() >= 12) + { + if (await ASHelpers.IdentitiesCanIncremental()) + { + var identity = ASHelpers.ToCredentialIdentity( + message.Data as Bit.Core.Models.View.CipherView); + if (identity == null) + { + return; + } + await ASCredentialIdentityStore.SharedStore?.RemoveCredentialIdentitiesAsync( + new ASPasswordCredentialIdentity[] { identity }); + return; + } + await ASHelpers.ReplaceAllIdentities(); + } + } + else if (message.Command == "logout") + { + if (_deviceActionService.SystemMajorVersion() >= 12) + { + await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync(); + } + } + else if ((message.Command == "softDeletedCipher" || message.Command == "restoredCipher") + && _deviceActionService.SystemMajorVersion() >= 12) { await ASHelpers.ReplaceAllIdentities(); } - } - else if (message.Command == "showAppExtension") - { - Device.BeginInvokeOnMainThread(() => ShowAppExtension((ExtensionPageViewModel)message.Data)); - } - else if (message.Command == "syncCompleted") - { - if (message.Data is Dictionary data && data.ContainsKey("successfully")) + else if (message.Command == "vaultTimeoutActionChanged") { - var success = data["successfully"] as bool?; - if (success.GetValueOrDefault() && _deviceActionService.SystemMajorVersion() >= 12) + var timeoutAction = await _stateService.GetVaultTimeoutActionAsync(); + if (timeoutAction == VaultTimeoutAction.Logout) + { + await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync(); + } + else { await ASHelpers.ReplaceAllIdentities(); } } } - else if (message.Command == "addedCipher" || message.Command == "editedCipher" || - message.Command == "restoredCipher") + catch (Exception ex) { - if (_deviceActionService.SystemMajorVersion() >= 12) - { - if (await ASHelpers.IdentitiesCanIncremental()) - { - var cipherId = message.Data as string; - if (message.Command == "addedCipher" && !string.IsNullOrWhiteSpace(cipherId)) - { - var identity = await ASHelpers.GetCipherIdentityAsync(cipherId); - if (identity == null) - { - return; - } - await ASCredentialIdentityStore.SharedStore?.SaveCredentialIdentitiesAsync( - new ASPasswordCredentialIdentity[] { identity }); - return; - } - } - await ASHelpers.ReplaceAllIdentities(); - } - } - else if (message.Command == "deletedCipher" || message.Command == "softDeletedCipher") - { - if (_deviceActionService.SystemMajorVersion() >= 12) - { - if (await ASHelpers.IdentitiesCanIncremental()) - { - var identity = ASHelpers.ToCredentialIdentity( - message.Data as Bit.Core.Models.View.CipherView); - if (identity == null) - { - return; - } - await ASCredentialIdentityStore.SharedStore?.RemoveCredentialIdentitiesAsync( - new ASPasswordCredentialIdentity[] { identity }); - return; - } - await ASHelpers.ReplaceAllIdentities(); - } - } - else if (message.Command == "logout") - { - if (_deviceActionService.SystemMajorVersion() >= 12) - { - await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync(); - } - } - else if ((message.Command == "softDeletedCipher" || message.Command == "restoredCipher") - && _deviceActionService.SystemMajorVersion() >= 12) - { - await ASHelpers.ReplaceAllIdentities(); - } - else if (message.Command == "vaultTimeoutActionChanged") - { - var timeoutAction = await _stateService.GetVaultTimeoutActionAsync(); - if (timeoutAction == VaultTimeoutAction.Logout) - { - await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync(); - } - else - { - await ASHelpers.ReplaceAllIdentities(); - } + LoggerHelper.LogEvenIfCantBeResolved(ex); } }); diff --git a/src/iOS/Info.plist b/src/iOS/Info.plist index 3cf733988..9bf586efb 100644 --- a/src/iOS/Info.plist +++ b/src/iOS/Info.plist @@ -11,7 +11,7 @@ CFBundleIdentifier com.8bit.bitwarden CFBundleShortVersionString - 2022.05.1 + 2022.6.1 CFBundleVersion 1 CFBundleIconName