From 976eeab6d710d03b3aef6b931c674949b0d9ceca Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Fri, 21 May 2021 15:13:54 +0200 Subject: [PATCH] Password reprompt (#1365) * Make card number hidden * Add support for password reprompt * Rename PasswordPrompt to Reprompt * Protect autofill * Use Enums.CipherRepromptType * Fix iOS not building * Protect iOS autofill * Update to match jslib * Fix failing build --- src/Android/Autofill/AutofillHelpers.cs | 5 +- src/Android/MainApplication.cs | 6 ++ src/Android/Services/DeviceActionService.cs | 6 +- src/App/Abstractions/IDeviceActionService.cs | 2 +- .../Abstractions/IPasswordRepromptService.cs | 11 +++ src/App/Pages/Vault/AddEditPage.xaml | 46 ++++++++++-- src/App/Pages/Vault/AddEditPage.xaml.cs | 5 ++ src/App/Pages/Vault/AddEditPageViewModel.cs | 24 +++++++ .../Vault/AutofillCiphersPageViewModel.cs | 10 ++- src/App/Pages/Vault/CiphersPageViewModel.cs | 6 +- .../GroupingsPage/GroupingsPageViewModel.cs | 4 +- src/App/Pages/Vault/ViewPage.xaml | 23 +++++- src/App/Pages/Vault/ViewPage.xaml.cs | 24 +++++++ src/App/Pages/Vault/ViewPageViewModel.cs | 71 +++++++++++++++++-- src/App/Resources/AppResources.Designer.cs | 20 ++++++ src/App/Resources/AppResources.resx | 9 +++ .../Services/MobilePasswordRepromptService.cs | 46 ++++++++++++ .../Services/MobilePlatformUtilsService.cs | 15 ++++ src/App/Utilities/AppHelpers.cs | 48 ++++++++----- .../Abstractions/IPlatformUtilsService.cs | 1 + src/Core/Enums/CipherRepromptType.cs | 8 +++ src/Core/Enums/EventType.cs | 1 + src/Core/Models/Data/CipherData.cs | 2 + src/Core/Models/Domain/Cipher.cs | 9 ++- src/Core/Models/Request/CipherRequest.cs | 2 + src/Core/Models/Response/CipherResponse.cs | 4 +- src/Core/Models/View/CardView.cs | 1 + src/Core/Models/View/CipherView.cs | 3 +- src/Core/Services/CipherService.cs | 3 +- src/Core/Utilities/ServiceContainer.cs | 7 +- src/iOS.Autofill/LoginListViewController.cs | 5 +- src/iOS.Autofill/LoginSearchViewController.cs | 7 +- src/iOS.Autofill/Utilities/AutofillHelpers.cs | 9 ++- src/iOS.Core/Models/CipherViewModel.cs | 2 + src/iOS.Core/Services/DeviceActionService.cs | 5 +- src/iOS.Core/Utilities/iOSCoreHelpers.cs | 6 ++ 36 files changed, 401 insertions(+), 55 deletions(-) create mode 100644 src/App/Abstractions/IPasswordRepromptService.cs create mode 100644 src/App/Services/MobilePasswordRepromptService.cs create mode 100644 src/Core/Enums/CipherRepromptType.cs diff --git a/src/Android/Autofill/AutofillHelpers.cs b/src/Android/Autofill/AutofillHelpers.cs index 900def5ee..8489858c9 100644 --- a/src/Android/Autofill/AutofillHelpers.cs +++ b/src/Android/Autofill/AutofillHelpers.cs @@ -140,13 +140,14 @@ namespace Bit.Droid.Autofill { var allCiphers = ciphers.Item1.ToList(); allCiphers.AddRange(ciphers.Item2.ToList()); - return allCiphers.Select(c => new FilledItem(c)).ToList(); + var nonPromptCiphers = allCiphers.Where(cipher => cipher.Reprompt == CipherRepromptType.None); + return nonPromptCiphers.Select(c => new FilledItem(c)).ToList(); } } else if (parser.FieldCollection.FillableForCard) { var ciphers = await cipherService.GetAllDecryptedAsync(); - return ciphers.Where(c => c.Type == CipherType.Card).Select(c => new FilledItem(c)).ToList(); + return ciphers.Where(c => c.Type == CipherType.Card && c.Reprompt == CipherRepromptType.None).Select(c => new FilledItem(c)).ToList(); } return new List(); } diff --git a/src/Android/MainApplication.cs b/src/Android/MainApplication.cs index 1962019ab..b0c825656 100644 --- a/src/Android/MainApplication.cs +++ b/src/Android/MainApplication.cs @@ -99,6 +99,9 @@ namespace Bit.Droid var platformUtilsService = new MobilePlatformUtilsService(deviceActionService, messagingService, broadcasterService); var biometricService = new BiometricService(); + var cryptoFunctionService = new PclCryptoFunctionService(cryptoPrimitiveService); + var cryptoService = new CryptoService(mobileStorageService, secureStorageService, cryptoFunctionService); + var passwordRepromptService = new MobilePasswordRepromptService(platformUtilsService, cryptoService); ServiceContainer.Register("broadcasterService", broadcasterService); ServiceContainer.Register("messagingService", messagingService); @@ -110,6 +113,9 @@ namespace Bit.Droid ServiceContainer.Register("deviceActionService", deviceActionService); ServiceContainer.Register("platformUtilsService", platformUtilsService); ServiceContainer.Register("biometricService", biometricService); + ServiceContainer.Register("cryptoFunctionService", cryptoFunctionService); + ServiceContainer.Register("cryptoService", cryptoService); + ServiceContainer.Register("passwordRepromptService", passwordRepromptService); // Push #if FDROID diff --git a/src/Android/Services/DeviceActionService.cs b/src/Android/Services/DeviceActionService.cs index 9afbcba7d..68523ca77 100644 --- a/src/Android/Services/DeviceActionService.cs +++ b/src/Android/Services/DeviceActionService.cs @@ -307,7 +307,7 @@ namespace Bit.Droid.Services public Task DisplayPromptAync(string title = null, string description = null, string text = null, string okButtonText = null, string cancelButtonText = null, - bool numericKeyboard = false, bool autofocus = true) + bool numericKeyboard = false, bool autofocus = true, bool password = false) { var activity = (MainActivity)CrossCurrentActivity.Current.Activity; if (activity == null) @@ -333,6 +333,10 @@ namespace Bit.Droid.Services input.KeyListener = DigitsKeyListener.GetInstance(false, false); #pragma warning restore CS0618 // Type or member is obsolete } + if (password) + { + input.InputType = InputTypes.TextVariationPassword | InputTypes.ClassText; + } input.ImeOptions = input.ImeOptions | (ImeAction)ImeFlags.NoPersonalizedLearning | (ImeAction)ImeFlags.NoExtractUi; diff --git a/src/App/Abstractions/IDeviceActionService.cs b/src/App/Abstractions/IDeviceActionService.cs index 8f1b03e64..2f32a7da9 100644 --- a/src/App/Abstractions/IDeviceActionService.cs +++ b/src/App/Abstractions/IDeviceActionService.cs @@ -19,7 +19,7 @@ namespace Bit.App.Abstractions Task SelectFileAsync(); Task DisplayPromptAync(string title = null, string description = null, string text = null, string okButtonText = null, string cancelButtonText = null, bool numericKeyboard = false, - bool autofocus = true); + bool autofocus = true, bool password = false); void RateApp(); bool SupportsFaceBiometric(); Task SupportsFaceBiometricAsync(); diff --git a/src/App/Abstractions/IPasswordRepromptService.cs b/src/App/Abstractions/IPasswordRepromptService.cs new file mode 100644 index 000000000..31323f1b3 --- /dev/null +++ b/src/App/Abstractions/IPasswordRepromptService.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; + +namespace Bit.App.Abstractions +{ + public interface IPasswordRepromptService + { + string[] ProtectedFields { get; } + + Task ShowPasswordPromptAsync(); + } +} diff --git a/src/App/Pages/Vault/AddEditPage.xaml b/src/App/Pages/Vault/AddEditPage.xaml index 0e137f2fd..247b026c3 100644 --- a/src/App/Pages/Vault/AddEditPage.xaml +++ b/src/App/Pages/Vault/AddEditPage.xaml @@ -219,15 +219,40 @@ Text="{Binding Cipher.Card.CardholderName}" StyleClass="box-value" /> - + + + + + + + + + + StyleClass="box-value" + Grid.Row="1" + Grid.Column="0" + Keyboard="Numeric" + IsPassword="{Binding ShowCardNumber, Converter={StaticResource inverseBool}}" + IsSpellCheckEnabled="False" + IsTextPredictionEnabled="False" /> + + + + diff --git a/src/App/Pages/Vault/AddEditPage.xaml.cs b/src/App/Pages/Vault/AddEditPage.xaml.cs index 3a8d9cc06..f8c7b53ac 100644 --- a/src/App/Pages/Vault/AddEditPage.xaml.cs +++ b/src/App/Pages/Vault/AddEditPage.xaml.cs @@ -376,5 +376,10 @@ namespace Bit.App.Pages } } } + + private void PasswordPrompt_Toggled(object sender, ToggledEventArgs e) + { + _vm.Cipher.Reprompt = e.Value ? CipherRepromptType.Password : CipherRepromptType.None; + } } } diff --git a/src/App/Pages/Vault/AddEditPageViewModel.cs b/src/App/Pages/Vault/AddEditPageViewModel.cs index 9d592a318..2642d723b 100644 --- a/src/App/Pages/Vault/AddEditPageViewModel.cs +++ b/src/App/Pages/Vault/AddEditPageViewModel.cs @@ -29,6 +29,7 @@ namespace Bit.App.Pages private CipherView _cipher; private bool _showNotesSeparator; private bool _showPassword; + private bool _showCardNumber; private bool _showCardCode; private int _typeSelectedIndex; private int _cardBrandSelectedIndex; @@ -82,6 +83,7 @@ namespace Bit.App.Pages _policyService = ServiceContainer.Resolve("policyService"); GeneratePasswordCommand = new Command(GeneratePassword); TogglePasswordCommand = new Command(TogglePassword); + ToggleCardNumberCommand = new Command(ToggleCardNumber); ToggleCardCodeCommand = new Command(ToggleCardCode); CheckPasswordCommand = new Command(CheckPasswordAsync); UriOptionsCommand = new Command(UriOptions); @@ -141,6 +143,7 @@ namespace Bit.App.Pages public Command GeneratePasswordCommand { get; set; } public Command TogglePasswordCommand { get; set; } + public Command ToggleCardNumberCommand { get; set; } public Command ToggleCardCodeCommand { get; set; } public Command CheckPasswordCommand { get; set; } public Command UriOptionsCommand { get; set; } @@ -246,6 +249,15 @@ namespace Bit.App.Pages nameof(ShowPasswordIcon) }); } + public bool ShowCardNumber + { + get => _showCardNumber; + set => SetProperty(ref _showCardNumber, value, + additionalPropertyNames: new string[] + { + nameof(ShowCardNumberIcon) + }); + } public bool ShowCardCode { get => _showCardCode; @@ -277,10 +289,12 @@ namespace Bit.App.Pages public bool ShowUris => IsLogin && Cipher.Login.HasUris; public bool ShowAttachments => Cipher.HasAttachments; public string ShowPasswordIcon => ShowPassword ? "" : ""; + public string ShowCardNumberIcon => ShowCardNumber ? "" : ""; public string ShowCardCodeIcon => ShowCardCode ? "" : ""; public int PasswordFieldColSpan => Cipher.ViewPassword ? 1 : 4; public int TotpColumnSpan => Cipher.ViewPassword ? 1 : 2; public bool AllowPersonal { get; set; } + public bool PasswordPrompt => Cipher.Reprompt != CipherRepromptType.None; public void Init() { @@ -691,6 +705,16 @@ namespace Bit.App.Pages } } + public void ToggleCardNumber() + { + ShowCardNumber = !ShowCardNumber; + if (EditMode && ShowCardNumber) + { + var task = _eventService.CollectAsync( + Core.Enums.EventType.Cipher_ClientToggledCardNumberVisible, CipherId); + } + } + public void ToggleCardCode() { ShowCardCode = !ShowCardCode; diff --git a/src/App/Pages/Vault/AutofillCiphersPageViewModel.cs b/src/App/Pages/Vault/AutofillCiphersPageViewModel.cs index 4c5b86aa2..6deb7710a 100644 --- a/src/App/Pages/Vault/AutofillCiphersPageViewModel.cs +++ b/src/App/Pages/Vault/AutofillCiphersPageViewModel.cs @@ -23,6 +23,7 @@ namespace Bit.App.Pages private readonly IDeviceActionService _deviceActionService; private readonly ICipherService _cipherService; private readonly IStateService _stateService; + private readonly IPasswordRepromptService _passwordRepromptService; private AppOptions _appOptions; private bool _showList; @@ -35,6 +36,7 @@ namespace Bit.App.Pages _cipherService = ServiceContainer.Resolve("cipherService"); _deviceActionService = ServiceContainer.Resolve("deviceActionService"); _stateService = ServiceContainer.Resolve("stateService"); + _passwordRepromptService = ServiceContainer.Resolve("passwordRepromptService"); GroupedItems = new ExtendedObservableCollection(); CipherOptionsCommand = new Command(CipherOptionsAsync); @@ -118,10 +120,14 @@ namespace Bit.App.Pages } if (_deviceActionService.SystemMajorVersion() < 21) { - await AppHelpers.CipherListOptions(Page, cipher); + await AppHelpers.CipherListOptions(Page, cipher, _passwordRepromptService); } else { + if (cipher.Reprompt != CipherRepromptType.None && !await _passwordRepromptService.ShowPasswordPromptAsync()) + { + return; + } var autofillResponse = AppResources.Yes; if (fuzzy) { @@ -175,7 +181,7 @@ namespace Bit.App.Pages { if ((Page as BaseContentPage).DoOnce()) { - await AppHelpers.CipherListOptions(Page, cipher); + await AppHelpers.CipherListOptions(Page, cipher, _passwordRepromptService); } } } diff --git a/src/App/Pages/Vault/CiphersPageViewModel.cs b/src/App/Pages/Vault/CiphersPageViewModel.cs index 1d3eb8434..4c11bd68c 100644 --- a/src/App/Pages/Vault/CiphersPageViewModel.cs +++ b/src/App/Pages/Vault/CiphersPageViewModel.cs @@ -22,6 +22,7 @@ namespace Bit.App.Pages private readonly ISearchService _searchService; private readonly IDeviceActionService _deviceActionService; private readonly IStateService _stateService; + private readonly IPasswordRepromptService _passwordRepromptService; private CancellationTokenSource _searchCancellationTokenSource; private bool _showNoData; @@ -35,6 +36,7 @@ namespace Bit.App.Pages _searchService = ServiceContainer.Resolve("searchService"); _deviceActionService = ServiceContainer.Resolve("deviceActionService"); _stateService = ServiceContainer.Resolve("stateService"); + _passwordRepromptService = ServiceContainer.Resolve("passwordRepromptService"); Ciphers = new ExtendedObservableCollection(); CipherOptionsCommand = new Command(CipherOptionsAsync); @@ -182,7 +184,7 @@ namespace Bit.App.Pages } if (_deviceActionService.SystemMajorVersion() < 21) { - await Utilities.AppHelpers.CipherListOptions(Page, cipher); + await Utilities.AppHelpers.CipherListOptions(Page, cipher, _passwordRepromptService); } else { @@ -195,7 +197,7 @@ namespace Bit.App.Pages { if ((Page as BaseContentPage).DoOnce()) { - await Utilities.AppHelpers.CipherListOptions(Page, cipher); + await Utilities.AppHelpers.CipherListOptions(Page, cipher, _passwordRepromptService); } } } diff --git a/src/App/Pages/Vault/GroupingsPage/GroupingsPageViewModel.cs b/src/App/Pages/Vault/GroupingsPage/GroupingsPageViewModel.cs index d56259910..31696a480 100644 --- a/src/App/Pages/Vault/GroupingsPage/GroupingsPageViewModel.cs +++ b/src/App/Pages/Vault/GroupingsPage/GroupingsPageViewModel.cs @@ -46,6 +46,7 @@ namespace Bit.App.Pages private readonly IMessagingService _messagingService; private readonly IStateService _stateService; private readonly IStorageService _storageService; + private readonly IPasswordRepromptService _passwordRepromptService; public GroupingsPageViewModel() { @@ -60,6 +61,7 @@ namespace Bit.App.Pages _messagingService = ServiceContainer.Resolve("messagingService"); _stateService = ServiceContainer.Resolve("stateService"); _storageService = ServiceContainer.Resolve("storageService"); + _passwordRepromptService = ServiceContainer.Resolve("passwordRepromptService"); Loading = true; PageTitle = AppResources.MyVault; @@ -514,7 +516,7 @@ namespace Bit.App.Pages { if ((Page as BaseContentPage).DoOnce()) { - await AppHelpers.CipherListOptions(Page, cipher); + await AppHelpers.CipherListOptions(Page, cipher, _passwordRepromptService); } } } diff --git a/src/App/Pages/Vault/ViewPage.xaml b/src/App/Pages/Vault/ViewPage.xaml index 27bde6b26..8a4e0c16d 100644 --- a/src/App/Pages/Vault/ViewPage.xaml +++ b/src/App/Pages/Vault/ViewPage.xaml @@ -224,24 +224,41 @@ +