1
0
mirror of https://github.com/bitwarden/mobile.git synced 2024-06-24 10:14:55 +02:00

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
This commit is contained in:
Oscar Hinton 2021-05-21 15:13:54 +02:00 committed by GitHub
parent e61bcd2785
commit 976eeab6d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 401 additions and 55 deletions

View File

@ -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<FilledItem>();
}

View File

@ -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<IBroadcasterService>("broadcasterService", broadcasterService);
ServiceContainer.Register<IMessagingService>("messagingService", messagingService);
@ -110,6 +113,9 @@ namespace Bit.Droid
ServiceContainer.Register<IDeviceActionService>("deviceActionService", deviceActionService);
ServiceContainer.Register<IPlatformUtilsService>("platformUtilsService", platformUtilsService);
ServiceContainer.Register<IBiometricService>("biometricService", biometricService);
ServiceContainer.Register<ICryptoFunctionService>("cryptoFunctionService", cryptoFunctionService);
ServiceContainer.Register<ICryptoService>("cryptoService", cryptoService);
ServiceContainer.Register<IPasswordRepromptService>("passwordRepromptService", passwordRepromptService);
// Push
#if FDROID

View File

@ -307,7 +307,7 @@ namespace Bit.Droid.Services
public Task<string> 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;

View File

@ -19,7 +19,7 @@ namespace Bit.App.Abstractions
Task SelectFileAsync();
Task<string> 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<bool> SupportsFaceBiometricAsync();

View File

@ -0,0 +1,11 @@
using System.Threading.Tasks;
namespace Bit.App.Abstractions
{
public interface IPasswordRepromptService
{
string[] ProtectedFields { get; }
Task<bool> ShowPasswordPromptAsync();
}
}

View File

@ -219,15 +219,40 @@
Text="{Binding Cipher.Card.CardholderName}"
StyleClass="box-value" />
</StackLayout>
<StackLayout StyleClass="box-row, box-row-input">
<Grid StyleClass="box-row, box-row-input">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Label
Text="{u:I18n Number}"
StyleClass="box-label" />
<Entry
StyleClass="box-label"
Grid.Row="0"
Grid.Column="0" />
<controls:MonoEntry
x:Name="_cardNumberEntry"
Text="{Binding Cipher.Card.Number}"
StyleClass="box-value" />
</StackLayout>
StyleClass="box-value"
Grid.Row="1"
Grid.Column="0"
Keyboard="Numeric"
IsPassword="{Binding ShowCardNumber, Converter={StaticResource inverseBool}}"
IsSpellCheckEnabled="False"
IsTextPredictionEnabled="False" />
<controls:FaButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowCardNumberIcon}"
Command="{Binding ToggleCardNumberCommand}"
Grid.Row="0"
Grid.Column="1"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
</Grid>
<StackLayout StyleClass="box-row, box-row-input">
<Label
Text="{u:I18n Brand}"
@ -530,6 +555,17 @@
StyleClass="box-value"
HorizontalOptions="End" />
</StackLayout>
<StackLayout StyleClass="box-row, box-row-switch">
<Label
Text="{u:I18n PasswordPrompt}"
StyleClass="box-label-regular"
HorizontalOptions="StartAndExpand" />
<Switch
IsToggled="{Binding PasswordPrompt}"
Toggled="PasswordPrompt_Toggled"
StyleClass="box-value"
HorizontalOptions="End" />
</StackLayout>
<BoxView StyleClass="box-row-separator" />
</StackLayout>
<StackLayout StyleClass="box">

View File

@ -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;
}
}
}

View File

@ -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<IPolicyService>("policyService");
GeneratePasswordCommand = new Command(GeneratePassword);
TogglePasswordCommand = new Command(TogglePassword);
ToggleCardNumberCommand = new Command(ToggleCardNumber);
ToggleCardCodeCommand = new Command(ToggleCardCode);
CheckPasswordCommand = new Command(CheckPasswordAsync);
UriOptionsCommand = new Command<LoginUriView>(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;

View File

@ -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<ICipherService>("cipherService");
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
_passwordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService");
GroupedItems = new ExtendedObservableCollection<GroupingsPageListGroup>();
CipherOptionsCommand = new Command<CipherView>(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);
}
}
}

View File

@ -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<ISearchService>("searchService");
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
_passwordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService");
Ciphers = new ExtendedObservableCollection<CipherView>();
CipherOptionsCommand = new Command<CipherView>(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);
}
}
}

View File

@ -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<IMessagingService>("messagingService");
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
_storageService = ServiceContainer.Resolve<IStorageService>("storageService");
_passwordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>("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);
}
}
}

View File

@ -224,24 +224,41 @@
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Label
Text="{u:I18n Number}"
StyleClass="box-label"
Grid.Row="0"
Grid.Column="0" />
<Label
<controls:MonoLabel
Text="{Binding Cipher.Card.MaskedNumber, Mode=OneWay}"
StyleClass="box-value"
Grid.Row="1"
Grid.Column="0"
IsVisible="{Binding ShowCardNumber, Converter={StaticResource inverseBool}}" />
<controls:MonoLabel
Text="{Binding Cipher.Card.Number, Mode=OneWay}"
StyleClass="box-value"
Grid.Row="1"
Grid.Column="0" />
Grid.Column="0"
IsVisible="{Binding ShowCardNumber}" />
<controls:FaButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowCardNumberIcon}"
Command="{Binding ToggleCardNumberCommand}"
Grid.Row="0"
Grid.Column="1"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
<controls:FaButton
StyleClass="box-row-button, box-row-button-platform"
Text="&#xf24d;"
Command="{Binding CopyCommand}"
CommandParameter="CardNumber"
Grid.Row="0"
Grid.Column="1"
Grid.Column="2"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n CopyNumber}" />

View File

@ -128,6 +128,10 @@ namespace Bit.App.Pages
}
else
{
if (!await _vm.PromptPasswordAsync())
{
return;
}
await Navigation.PushModalAsync(new NavigationPage(new AddEditPage(_vm.CipherId)));
}
}
@ -142,6 +146,10 @@ namespace Bit.App.Pages
{
if (DoOnce())
{
if (!await _vm.PromptPasswordAsync())
{
return;
}
var page = new AttachmentsPage(_vm.CipherId);
await Navigation.PushModalAsync(new NavigationPage(page));
}
@ -151,6 +159,10 @@ namespace Bit.App.Pages
{
if (DoOnce())
{
if (!await _vm.PromptPasswordAsync())
{
return;
}
var page = new SharePage(_vm.CipherId);
await Navigation.PushModalAsync(new NavigationPage(page));
}
@ -160,6 +172,10 @@ namespace Bit.App.Pages
{
if (DoOnce())
{
if (!await _vm.PromptPasswordAsync())
{
return;
}
if (await _vm.DeleteAsync())
{
await Navigation.PopModalAsync();
@ -171,6 +187,10 @@ namespace Bit.App.Pages
{
if (DoOnce())
{
if (!await _vm.PromptPasswordAsync())
{
return;
}
var page = new CollectionsPage(_vm.CipherId);
await Navigation.PushModalAsync(new NavigationPage(page));
}
@ -180,6 +200,10 @@ namespace Bit.App.Pages
{
if (DoOnce())
{
if (!await _vm.PromptPasswordAsync())
{
return;
}
var page = new AddEditPage(_vm.CipherId, cloneMode: true, viewPage: this);
await Navigation.PushModalAsync(new NavigationPage(page));
}

View File

@ -2,6 +2,7 @@
using Bit.App.Resources;
using Bit.App.Utilities;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.View;
using Bit.Core.Utilities;
@ -23,10 +24,12 @@ namespace Bit.App.Pages
private readonly IAuditService _auditService;
private readonly IMessagingService _messagingService;
private readonly IEventService _eventService;
private readonly IPasswordRepromptService _passwordRepromptService;
private CipherView _cipher;
private List<ViewPageFieldViewModel> _fields;
private bool _canAccessPremium;
private bool _showPassword;
private bool _showCardNumber;
private bool _showCardCode;
private string _totpCode;
private string _totpCodeFormatted;
@ -36,6 +39,7 @@ namespace Bit.App.Pages
private string _previousCipherId;
private byte[] _attachmentData;
private string _attachmentFilename;
private bool _passwordReprompted;
public ViewPageViewModel()
{
@ -47,11 +51,13 @@ namespace Bit.App.Pages
_auditService = ServiceContainer.Resolve<IAuditService>("auditService");
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
_eventService = ServiceContainer.Resolve<IEventService>("eventService");
_passwordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService");
CopyCommand = new Command<string>((id) => CopyAsync(id, null));
CopyUriCommand = new Command<LoginUriView>(CopyUri);
CopyFieldCommand = new Command<FieldView>(CopyField);
LaunchUriCommand = new Command<LoginUriView>(LaunchUri);
TogglePasswordCommand = new Command(TogglePassword);
ToggleCardNumberCommand = new Command(ToggleCardNumber);
ToggleCardCodeCommand = new Command(ToggleCardCode);
CheckPasswordCommand = new Command(CheckPasswordAsync);
DownloadAttachmentCommand = new Command<AttachmentView>(DownloadAttachmentAsync);
@ -64,6 +70,7 @@ namespace Bit.App.Pages
public Command CopyFieldCommand { get; set; }
public Command LaunchUriCommand { get; set; }
public Command TogglePasswordCommand { get; set; }
public Command ToggleCardNumberCommand { get; set; }
public Command ToggleCardCodeCommand { get; set; }
public Command CheckPasswordCommand { get; set; }
public Command DownloadAttachmentCommand { get; set; }
@ -109,6 +116,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;
@ -188,6 +204,7 @@ namespace Bit.App.Pages
public bool ShowTotp => IsLogin && !string.IsNullOrWhiteSpace(Cipher.Login.Totp) &&
!string.IsNullOrWhiteSpace(TotpCodeFormatted);
public string ShowPasswordIcon => ShowPassword ? "" : "";
public string ShowCardNumberIcon => ShowCardNumber ? "" : "";
public string ShowCardCodeIcon => ShowCardCode ? "" : "";
public string TotpCodeFormatted
{
@ -226,7 +243,7 @@ namespace Bit.App.Pages
}
Cipher = await cipher.DecryptAsync();
CanAccessPremium = await _userService.CanAccessPremiumAsync();
Fields = Cipher.Fields?.Select(f => new ViewPageFieldViewModel(Cipher, f)).ToList();
Fields = Cipher.Fields?.Select(f => new ViewPageFieldViewModel(this, Cipher, f)).ToList();
if (Cipher.Type == Core.Enums.CipherType.Login && !string.IsNullOrWhiteSpace(Cipher.Login.Totp) &&
(Cipher.OrganizationUseTotp || CanAccessPremium))
@ -259,8 +276,13 @@ namespace Bit.App.Pages
_totpInterval = null;
}
public void TogglePassword()
public async void TogglePassword()
{
if (! await PromptPasswordAsync())
{
return;
}
ShowPassword = !ShowPassword;
if (ShowPassword)
{
@ -268,8 +290,26 @@ namespace Bit.App.Pages
}
}
public void ToggleCardCode()
public async void ToggleCardNumber()
{
if (!await PromptPasswordAsync())
{
return;
}
ShowCardNumber = !ShowCardNumber;
if (ShowCardNumber)
{
var task = _eventService.CollectAsync(
Core.Enums.EventType.Cipher_ClientToggledCardNumberVisible, CipherId);
}
}
public async void ToggleCardCode()
{
if (!await PromptPasswordAsync())
{
return;
}
ShowCardCode = !ShowCardCode;
if (ShowCardCode)
{
@ -564,6 +604,11 @@ namespace Bit.App.Pages
private async void CopyAsync(string id, string text = null)
{
if (_passwordRepromptService.ProtectedFields.Contains(id) && !await PromptPasswordAsync())
{
return;
}
string name = null;
if (id == "LoginUsername")
{
@ -638,16 +683,28 @@ namespace Bit.App.Pages
_platformUtilsService.LaunchUri(uri.LaunchUri);
}
}
internal async Task<bool> PromptPasswordAsync()
{
if (Cipher.Reprompt == CipherRepromptType.None || _passwordReprompted)
{
return true;
}
return _passwordReprompted = await _passwordRepromptService.ShowPasswordPromptAsync();
}
}
public class ViewPageFieldViewModel : ExtendedViewModel
{
private ViewPageViewModel _vm;
private FieldView _field;
private CipherView _cipher;
private bool _showHiddenValue;
public ViewPageFieldViewModel(CipherView cipher, FieldView field)
public ViewPageFieldViewModel(ViewPageViewModel vm, CipherView cipher, FieldView field)
{
_vm = vm;
_cipher = cipher;
Field = field;
ToggleHiddenValueCommand = new Command(ToggleHiddenValue);
@ -688,8 +745,12 @@ namespace Bit.App.Pages
public bool ShowCopyButton => _field.Type != Core.Enums.FieldType.Boolean &&
!string.IsNullOrWhiteSpace(_field.Value) && !(IsHiddenType && !_cipher.ViewPassword);
public void ToggleHiddenValue()
public async void ToggleHiddenValue()
{
if (!await _vm.PromptPasswordAsync())
{
return;
}
ShowHiddenValue = !ShowHiddenValue;
if (ShowHiddenValue)
{

View File

@ -3512,5 +3512,25 @@ namespace Bit.App.Resources {
return ResourceManager.GetString("SendFileEmailVerificationRequired", resourceCulture);
}
}
public static string PasswordPrompt
{
get
{
return ResourceManager.GetString("PasswordPrompt", resourceCulture);
}
}
public static string PasswordConfirmation
{
get
{
return ResourceManager.GetString("PasswordConfirmation", resourceCulture);
}
}public static string PasswordConfirmationDesc
{
get
{
return ResourceManager.GetString("PasswordConfirmationDesc", resourceCulture);
}
}
}
}

View File

@ -1991,4 +1991,13 @@
<value>You must verify your email to use files with Send. You can verify your email in the web vault.</value>
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
</data>
<data name="PasswordPrompt" xml:space="preserve">
<value>Master password re-prompt</value>
</data>
<data name="PasswordConfirmation" xml:space="preserve">
<value>Master password confirmation</value>
</data>
<data name="PasswordConfirmationDesc" xml:space="preserve">
<value>This action is protected, to continue please re-enter your master password to verify your identity.</value>
</data>
</root>

View File

@ -0,0 +1,46 @@
using System.Threading.Tasks;
using Bit.Core.Abstractions;
using Bit.App.Abstractions;
using Bit.App.Resources;
using System;
namespace Bit.App.Services
{
public class MobilePasswordRepromptService : IPasswordRepromptService
{
private readonly IPlatformUtilsService _platformUtilsService;
private readonly ICryptoService _cryptoService;
public MobilePasswordRepromptService(IPlatformUtilsService platformUtilsService, ICryptoService cryptoService)
{
_platformUtilsService = platformUtilsService;
_cryptoService = cryptoService;
}
public string[] ProtectedFields { get; } = { "LoginTotp", "LoginPassword", "H_FieldValue", "CardNumber", "CardCode" };
public async Task<bool> ShowPasswordPromptAsync()
{
Func<string, Task<bool>> validator = async (string password) =>
{
// Assume user has canceled.
if (string.IsNullOrWhiteSpace(password))
{
return false;
};
var keyHash = await _cryptoService.HashPasswordAsync(password, null);
var storedKeyHash = await _cryptoService.GetKeyHashAsync();
if (storedKeyHash == null || keyHash == null || storedKeyHash != keyHash)
{
return false;
}
return true;
};
return await _platformUtilsService.ShowPasswordDialogAsync(AppResources.PasswordConfirmation, AppResources.PasswordConfirmationDesc, validator);
}
}
}

View File

@ -171,6 +171,21 @@ namespace Bit.App.Services
return tcs.Task;
}
public async Task<bool> ShowPasswordDialogAsync(string title, string body, Func<string, Task<bool>> validator)
{
var password = await _deviceActionService.DisplayPromptAync(AppResources.PasswordConfirmation,
AppResources.PasswordConfirmationDesc, null, AppResources.Submit, AppResources.Cancel, password: true);
var valid = await validator(password);
if (!valid)
{
await ShowDialogAsync(AppResources.InvalidMasterPassword, null, AppResources.Ok);
}
return valid;
}
public bool IsDev()
{
return Core.Utilities.CoreHelpers.InDebugMode();

View File

@ -19,7 +19,7 @@ namespace Bit.App.Utilities
{
public static class AppHelpers
{
public static async Task<string> CipherListOptions(ContentPage page, CipherView cipher)
public static async Task<string> CipherListOptions(ContentPage page, CipherView cipher, IPasswordRepromptService passwordRepromptService)
{
var platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
var eventService = ServiceContainer.Resolve<IEventService>("eventService");
@ -92,20 +92,26 @@ namespace Bit.App.Utilities
}
else if (selection == AppResources.CopyPassword)
{
await platformUtilsService.CopyToClipboardAsync(cipher.Login.Password);
platformUtilsService.ShowToast("info", null,
string.Format(AppResources.ValueHasBeenCopied, AppResources.Password));
var task = eventService.CollectAsync(Core.Enums.EventType.Cipher_ClientCopiedPassword, cipher.Id);
if (cipher.Reprompt == CipherRepromptType.None || await passwordRepromptService.ShowPasswordPromptAsync())
{
await platformUtilsService.CopyToClipboardAsync(cipher.Login.Password);
platformUtilsService.ShowToast("info", null,
string.Format(AppResources.ValueHasBeenCopied, AppResources.Password));
var task = eventService.CollectAsync(Core.Enums.EventType.Cipher_ClientCopiedPassword, cipher.Id);
}
}
else if (selection == AppResources.CopyTotp)
{
var totpService = ServiceContainer.Resolve<ITotpService>("totpService");
var totp = await totpService.GetCodeAsync(cipher.Login.Totp);
if (!string.IsNullOrWhiteSpace(totp))
if (cipher.Reprompt == CipherRepromptType.None || await passwordRepromptService.ShowPasswordPromptAsync())
{
await platformUtilsService.CopyToClipboardAsync(totp);
platformUtilsService.ShowToast("info", null,
string.Format(AppResources.ValueHasBeenCopied, AppResources.VerificationCodeTotp));
var totpService = ServiceContainer.Resolve<ITotpService>("totpService");
var totp = await totpService.GetCodeAsync(cipher.Login.Totp);
if (!string.IsNullOrWhiteSpace(totp))
{
await platformUtilsService.CopyToClipboardAsync(totp);
platformUtilsService.ShowToast("info", null,
string.Format(AppResources.ValueHasBeenCopied, AppResources.VerificationCodeTotp));
}
}
}
else if (selection == AppResources.Launch)
@ -114,16 +120,22 @@ namespace Bit.App.Utilities
}
else if (selection == AppResources.CopyNumber)
{
await platformUtilsService.CopyToClipboardAsync(cipher.Card.Number);
platformUtilsService.ShowToast("info", null,
string.Format(AppResources.ValueHasBeenCopied, AppResources.Number));
if (cipher.Reprompt == CipherRepromptType.None || await passwordRepromptService.ShowPasswordPromptAsync())
{
await platformUtilsService.CopyToClipboardAsync(cipher.Card.Number);
platformUtilsService.ShowToast("info", null,
string.Format(AppResources.ValueHasBeenCopied, AppResources.Number));
}
}
else if (selection == AppResources.CopySecurityCode)
{
await platformUtilsService.CopyToClipboardAsync(cipher.Card.Code);
platformUtilsService.ShowToast("info", null,
string.Format(AppResources.ValueHasBeenCopied, AppResources.SecurityCode));
var task = eventService.CollectAsync(Core.Enums.EventType.Cipher_ClientCopiedCardCode, cipher.Id);
if (cipher.Reprompt == CipherRepromptType.None || await passwordRepromptService.ShowPasswordPromptAsync())
{
await platformUtilsService.CopyToClipboardAsync(cipher.Card.Code);
platformUtilsService.ShowToast("info", null,
string.Format(AppResources.ValueHasBeenCopied, AppResources.SecurityCode));
var task = eventService.CollectAsync(Core.Enums.EventType.Cipher_ClientCopiedCardCode, cipher.Id);
}
}
else if (selection == AppResources.CopyNotes)
{

View File

@ -22,6 +22,7 @@ namespace Bit.Core.Abstractions
void SaveFile();
Task<bool> ShowDialogAsync(string text, string title = null, string confirmText = null,
string cancelText = null, string type = null);
Task<bool> ShowPasswordDialogAsync(string title, string body, Func<string, Task<bool>> validator);
void ShowToast(string type, string title, string text, Dictionary<string, object> options = null);
void ShowToast(string type, string title, string[] text, Dictionary<string, object> options = null);
bool SupportsU2f();

View File

@ -0,0 +1,8 @@
namespace Bit.Core.Enums
{
public enum CipherRepromptType : byte
{
None = 0,
Password = 1,
}
}

View File

@ -28,6 +28,7 @@
Cipher_ClientAutofilled = 1114,
Cipher_SoftDeleted = 1115,
Cipher_Restored = 1116,
Cipher_ClientToggledCardNumberVisible = 1117,
Collection_Created = 1300,
Collection_Updated = 1301,

View File

@ -25,6 +25,7 @@ namespace Bit.Core.Models.Data
Name = response.Name;
Notes = response.Notes;
CollectionIds = collectionIds?.ToList() ?? response.CollectionIds;
Reprompt = response.Reprompt;
try // Added to address Issue (https://github.com/bitwarden/mobile/issues/1006)
{
@ -84,5 +85,6 @@ namespace Bit.Core.Models.Data
public List<PasswordHistoryData> PasswordHistory { get; set; }
public List<string> CollectionIds { get; set; }
public DateTime? DeletedDate { get; set; }
public Enums.CipherRepromptType Reprompt { get; set; }
}
}

View File

@ -1,4 +1,5 @@
using Bit.Core.Models.Data;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
using Bit.Core.Models.View;
using System;
using System.Collections.Generic;
@ -30,6 +31,7 @@ namespace Bit.Core.Models.Domain
RevisionDate = obj.RevisionDate;
CollectionIds = obj.CollectionIds != null ? new HashSet<string>(obj.CollectionIds) : null;
LocalData = localData;
Reprompt = obj.Reprompt;
switch (Type)
{
@ -75,8 +77,8 @@ namespace Bit.Core.Models.Domain
public List<Field> Fields { get; set; }
public List<PasswordHistory> PasswordHistory { get; set; }
public HashSet<string> CollectionIds { get; set; }
public DateTime? DeletedDate { get; set; }
public CipherRepromptType Reprompt { get; set; }
public async Task<CipherView> DecryptAsync()
{
@ -167,7 +169,8 @@ namespace Bit.Core.Models.Domain
RevisionDate = RevisionDate,
Type = Type,
CollectionIds = CollectionIds.ToList(),
DeletedDate = DeletedDate
DeletedDate = DeletedDate,
Reprompt = Reprompt,
};
BuildDataModel(this, c, new HashSet<string>
{

View File

@ -18,6 +18,7 @@ namespace Bit.Core.Models.Request
Notes = cipher.Notes?.EncryptedString;
Favorite = cipher.Favorite;
LastKnownRevisionDate = cipher.RevisionDate;
Reprompt = cipher.Reprompt;
switch (Type)
{
@ -121,5 +122,6 @@ namespace Bit.Core.Models.Request
public Dictionary<string, string> Attachments { get; set; }
public Dictionary<string, AttachmentRequest> Attachments2 { get; set; }
public DateTime LastKnownRevisionDate { get; set; }
public CipherRepromptType Reprompt { get; set; }
}
}

View File

@ -1,4 +1,5 @@
using Bit.Core.Models.Api;
using Bit.Core.Enums;
using Bit.Core.Models.Api;
using System;
using System.Collections.Generic;
@ -26,5 +27,6 @@ namespace Bit.Core.Models.Response
public List<PasswordHistoryResponse> PasswordHistory { get; set; }
public List<string> CollectionIds { get; set; }
public DateTime? DeletedDate { get; set; }
public CipherRepromptType Reprompt { get; set; }
}
}

View File

@ -18,6 +18,7 @@ namespace Bit.Core.Models.View
public string ExpYear { get; set; }
public string Code { get; set; }
public string MaskedCode => Code != null ? new string('•', Code.Length) : null;
public string MaskedNumber => Number != null ? new string('•', Number.Length) : null;
public string Brand
{

View File

@ -24,6 +24,7 @@ namespace Bit.Core.Models.View
CollectionIds = c.CollectionIds;
RevisionDate = c.RevisionDate;
DeletedDate = c.DeletedDate;
Reprompt = c.Reprompt;
}
public string Id { get; set; }
@ -47,7 +48,7 @@ namespace Bit.Core.Models.View
public HashSet<string> CollectionIds { get; set; }
public DateTime RevisionDate { get; set; }
public DateTime? DeletedDate { get; set; }
public CipherRepromptType Reprompt { get; set; }
public string SubTitle
{

View File

@ -180,7 +180,8 @@ namespace Bit.Core.Services
OrganizationId = model.OrganizationId,
Type = model.Type,
CollectionIds = model.CollectionIds,
RevisionDate = model.RevisionDate
RevisionDate = model.RevisionDate,
Reprompt = model.Reprompt
};
if (key == null && cipher.OrganizationId != null)

View File

@ -23,14 +23,13 @@ namespace Bit.Core.Utilities
var platformUtilsService = Resolve<IPlatformUtilsService>("platformUtilsService");
var storageService = Resolve<IStorageService>("storageService");
var secureStorageService = Resolve<IStorageService>("secureStorageService");
var cryptoPrimitiveService = Resolve<ICryptoPrimitiveService>("cryptoPrimitiveService");
var i18nService = Resolve<II18nService>("i18nService");
var messagingService = Resolve<IMessagingService>("messagingService");
var cryptoFunctionService = Resolve<ICryptoFunctionService>("cryptoFunctionService");
var cryptoService = Resolve<ICryptoService>("cryptoService");
SearchService searchService = null;
var stateService = new StateService();
var cryptoFunctionService = new PclCryptoFunctionService(cryptoPrimitiveService);
var cryptoService = new CryptoService(storageService, secureStorageService, cryptoFunctionService);
var tokenService = new TokenService(storageService);
var apiService = new ApiService(tokenService, platformUtilsService, (bool expired) =>
{
@ -75,8 +74,6 @@ namespace Bit.Core.Utilities
var eventService = new EventService(storageService, apiService, userService, cipherService);
Register<IStateService>("stateService", stateService);
Register<ICryptoFunctionService>("cryptoFunctionService", cryptoFunctionService);
Register<ICryptoService>("cryptoService", cryptoService);
Register<ITokenService>("tokenService", tokenService);
Register<IApiService>("apiService", apiService);
Register<IAppIdService>("appIdService", appIdService);

View File

@ -9,6 +9,7 @@ using Bit.iOS.Autofill.Utilities;
using Bit.iOS.Core.Utilities;
using Bit.Core.Utilities;
using Bit.Core.Abstractions;
using Bit.App.Abstractions;
namespace Bit.iOS.Autofill
{
@ -18,10 +19,12 @@ namespace Bit.iOS.Autofill
: base(handle)
{
DismissModalAction = Cancel;
PasswordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService");
}
public Context Context { get; set; }
public CredentialProviderViewController CPViewController { get; set; }
public IPasswordRepromptService PasswordRepromptService { get; private set; }
public async override void ViewDidLoad()
{
@ -109,7 +112,7 @@ namespace Bit.iOS.Autofill
public async override void RowSelected(UITableView tableView, NSIndexPath indexPath)
{
await AutofillHelpers.TableRowSelectedAsync(tableView, indexPath, this,
_controller.CPViewController, _controller, "loginAddSegue");
_controller.CPViewController, _controller, _controller.PasswordRepromptService, "loginAddSegue");
}
}
}

View File

@ -7,6 +7,8 @@ using Bit.App.Resources;
using Bit.iOS.Core.Views;
using Bit.iOS.Autofill.Utilities;
using Bit.iOS.Core.Utilities;
using Bit.App.Abstractions;
using Bit.Core.Utilities;
namespace Bit.iOS.Autofill
{
@ -16,11 +18,13 @@ namespace Bit.iOS.Autofill
: base(handle)
{
DismissModalAction = Cancel;
PasswordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService");
}
public Context Context { get; set; }
public CredentialProviderViewController CPViewController { get; set; }
public bool FromList { get; set; }
public IPasswordRepromptService PasswordRepromptService { get; private set; }
public async override void ViewDidLoad()
{
@ -107,7 +111,8 @@ namespace Bit.iOS.Autofill
public async override void RowSelected(UITableView tableView, NSIndexPath indexPath)
{
await AutofillHelpers.TableRowSelectedAsync(tableView, indexPath, this,
_controller.CPViewController, _controller, "loginAddFromSearchSegue");
_controller.CPViewController, _controller, _controller.PasswordRepromptService,
"loginAddFromSearchSegue");
}
}
}

View File

@ -1,5 +1,6 @@
using System.Linq;
using System.Threading.Tasks;
using Bit.App.Abstractions;
using Bit.App.Resources;
using Bit.Core.Abstractions;
using Bit.Core.Utilities;
@ -14,7 +15,8 @@ namespace Bit.iOS.Autofill.Utilities
{
public async static Task TableRowSelectedAsync(UITableView tableView, NSIndexPath indexPath,
ExtensionTableSource tableSource, CredentialProviderViewController cpViewController,
UITableViewController controller, string loginAddSegue)
UITableViewController controller, IPasswordRepromptService passwordRepromptService,
string loginAddSegue)
{
tableView.DeselectRow(indexPath, true);
tableView.EndEditing(true);
@ -31,6 +33,11 @@ namespace Bit.iOS.Autofill.Utilities
return;
}
if (item.Reprompt != Bit.Core.Enums.CipherRepromptType.None && !await passwordRepromptService.ShowPasswordPromptAsync())
{
return;
}
if (!string.IsNullOrWhiteSpace(item.Username) && !string.IsNullOrWhiteSpace(item.Password))
{
string totp = null;

View File

@ -18,6 +18,7 @@ namespace Bit.iOS.Core.Models
Totp = cipher.Login?.Totp;
Uris = cipher.Login?.Uris?.Select(u => new LoginUriModel(u)).ToList();
Fields = cipher.Fields?.Select(f => new Tuple<string, string>(f.Name, f.Value)).ToList();
Reprompt = cipher.Reprompt;
}
public string Id { get; set; }
@ -28,6 +29,7 @@ namespace Bit.iOS.Core.Models
public string Totp { get; set; }
public List<Tuple<string, string>> Fields { get; set; }
public CipherView CipherView { get; set; }
public CipherRepromptType Reprompt { get; set; }
public class LoginUriModel
{

View File

@ -200,7 +200,7 @@ namespace Bit.iOS.Core.Services
public Task<string> 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 result = new TaskCompletionSource<string>();
var alert = UIAlertController.Create(title ?? string.Empty, description, UIAlertControllerStyle.Alert);
@ -223,6 +223,9 @@ namespace Bit.iOS.Core.Services
{
input.KeyboardType = UIKeyboardType.NumberPad;
}
if (password) {
input.SecureTextEntry = true;
}
if (!ThemeHelpers.LightTheme)
{
input.KeyboardAppearance = UIKeyboardAppearance.Dark;

View File

@ -55,6 +55,9 @@ namespace Bit.iOS.Core.Utilities
var platformUtilsService = new MobilePlatformUtilsService(deviceActionService, messagingService,
broadcasterService);
var biometricService = new BiometricService(mobileStorageService);
var cryptoFunctionService = new PclCryptoFunctionService(cryptoPrimitiveService);
var cryptoService = new CryptoService(mobileStorageService, secureStorageService, cryptoFunctionService);
var passwordRepromptService = new MobilePasswordRepromptService(platformUtilsService, cryptoService);
ServiceContainer.Register<IBroadcasterService>("broadcasterService", broadcasterService);
ServiceContainer.Register<IMessagingService>("messagingService", messagingService);
@ -66,6 +69,9 @@ namespace Bit.iOS.Core.Utilities
ServiceContainer.Register<IDeviceActionService>("deviceActionService", deviceActionService);
ServiceContainer.Register<IPlatformUtilsService>("platformUtilsService", platformUtilsService);
ServiceContainer.Register<IBiometricService>("biometricService", biometricService);
ServiceContainer.Register<ICryptoFunctionService>("cryptoFunctionService", cryptoFunctionService);
ServiceContainer.Register<ICryptoService>("cryptoService", cryptoService);
ServiceContainer.Register<IPasswordRepromptService>("passwordRepromptService", passwordRepromptService);
}
public static void Bootstrap(Func<Task> postBootstrapFunc = null)