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(); var allCiphers = ciphers.Item1.ToList();
allCiphers.AddRange(ciphers.Item2.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) else if (parser.FieldCollection.FillableForCard)
{ {
var ciphers = await cipherService.GetAllDecryptedAsync(); 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>(); return new List<FilledItem>();
} }

View file

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

View file

@ -307,7 +307,7 @@ namespace Bit.Droid.Services
public Task<string> DisplayPromptAync(string title = null, string description = null, public Task<string> DisplayPromptAync(string title = null, string description = null,
string text = null, string okButtonText = null, string cancelButtonText = 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; var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
if (activity == null) if (activity == null)
@ -333,6 +333,10 @@ namespace Bit.Droid.Services
input.KeyListener = DigitsKeyListener.GetInstance(false, false); input.KeyListener = DigitsKeyListener.GetInstance(false, false);
#pragma warning restore CS0618 // Type or member is obsolete #pragma warning restore CS0618 // Type or member is obsolete
} }
if (password)
{
input.InputType = InputTypes.TextVariationPassword | InputTypes.ClassText;
}
input.ImeOptions = input.ImeOptions | (ImeAction)ImeFlags.NoPersonalizedLearning | input.ImeOptions = input.ImeOptions | (ImeAction)ImeFlags.NoPersonalizedLearning |
(ImeAction)ImeFlags.NoExtractUi; (ImeAction)ImeFlags.NoExtractUi;

View file

@ -19,7 +19,7 @@ namespace Bit.App.Abstractions
Task SelectFileAsync(); Task SelectFileAsync();
Task<string> DisplayPromptAync(string title = null, string description = null, string text = null, Task<string> DisplayPromptAync(string title = null, string description = null, string text = null,
string okButtonText = null, string cancelButtonText = null, bool numericKeyboard = false, string okButtonText = null, string cancelButtonText = null, bool numericKeyboard = false,
bool autofocus = true); bool autofocus = true, bool password = false);
void RateApp(); void RateApp();
bool SupportsFaceBiometric(); bool SupportsFaceBiometric();
Task<bool> SupportsFaceBiometricAsync(); 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}" Text="{Binding Cipher.Card.CardholderName}"
StyleClass="box-value" /> StyleClass="box-value" />
</StackLayout> </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 <Label
Text="{u:I18n Number}" Text="{u:I18n Number}"
StyleClass="box-label" /> StyleClass="box-label"
<Entry Grid.Row="0"
Grid.Column="0" />
<controls:MonoEntry
x:Name="_cardNumberEntry" x:Name="_cardNumberEntry"
Text="{Binding Cipher.Card.Number}" Text="{Binding Cipher.Card.Number}"
StyleClass="box-value" /> StyleClass="box-value"
</StackLayout> 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"> <StackLayout StyleClass="box-row, box-row-input">
<Label <Label
Text="{u:I18n Brand}" Text="{u:I18n Brand}"
@ -530,6 +555,17 @@
StyleClass="box-value" StyleClass="box-value"
HorizontalOptions="End" /> HorizontalOptions="End" />
</StackLayout> </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" /> <BoxView StyleClass="box-row-separator" />
</StackLayout> </StackLayout>
<StackLayout StyleClass="box"> <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 CipherView _cipher;
private bool _showNotesSeparator; private bool _showNotesSeparator;
private bool _showPassword; private bool _showPassword;
private bool _showCardNumber;
private bool _showCardCode; private bool _showCardCode;
private int _typeSelectedIndex; private int _typeSelectedIndex;
private int _cardBrandSelectedIndex; private int _cardBrandSelectedIndex;
@ -82,6 +83,7 @@ namespace Bit.App.Pages
_policyService = ServiceContainer.Resolve<IPolicyService>("policyService"); _policyService = ServiceContainer.Resolve<IPolicyService>("policyService");
GeneratePasswordCommand = new Command(GeneratePassword); GeneratePasswordCommand = new Command(GeneratePassword);
TogglePasswordCommand = new Command(TogglePassword); TogglePasswordCommand = new Command(TogglePassword);
ToggleCardNumberCommand = new Command(ToggleCardNumber);
ToggleCardCodeCommand = new Command(ToggleCardCode); ToggleCardCodeCommand = new Command(ToggleCardCode);
CheckPasswordCommand = new Command(CheckPasswordAsync); CheckPasswordCommand = new Command(CheckPasswordAsync);
UriOptionsCommand = new Command<LoginUriView>(UriOptions); UriOptionsCommand = new Command<LoginUriView>(UriOptions);
@ -141,6 +143,7 @@ namespace Bit.App.Pages
public Command GeneratePasswordCommand { get; set; } public Command GeneratePasswordCommand { get; set; }
public Command TogglePasswordCommand { get; set; } public Command TogglePasswordCommand { get; set; }
public Command ToggleCardNumberCommand { get; set; }
public Command ToggleCardCodeCommand { get; set; } public Command ToggleCardCodeCommand { get; set; }
public Command CheckPasswordCommand { get; set; } public Command CheckPasswordCommand { get; set; }
public Command UriOptionsCommand { get; set; } public Command UriOptionsCommand { get; set; }
@ -246,6 +249,15 @@ namespace Bit.App.Pages
nameof(ShowPasswordIcon) nameof(ShowPasswordIcon)
}); });
} }
public bool ShowCardNumber
{
get => _showCardNumber;
set => SetProperty(ref _showCardNumber, value,
additionalPropertyNames: new string[]
{
nameof(ShowCardNumberIcon)
});
}
public bool ShowCardCode public bool ShowCardCode
{ {
get => _showCardCode; get => _showCardCode;
@ -277,10 +289,12 @@ namespace Bit.App.Pages
public bool ShowUris => IsLogin && Cipher.Login.HasUris; public bool ShowUris => IsLogin && Cipher.Login.HasUris;
public bool ShowAttachments => Cipher.HasAttachments; public bool ShowAttachments => Cipher.HasAttachments;
public string ShowPasswordIcon => ShowPassword ? "" : ""; public string ShowPasswordIcon => ShowPassword ? "" : "";
public string ShowCardNumberIcon => ShowCardNumber ? "" : "";
public string ShowCardCodeIcon => ShowCardCode ? "" : ""; public string ShowCardCodeIcon => ShowCardCode ? "" : "";
public int PasswordFieldColSpan => Cipher.ViewPassword ? 1 : 4; public int PasswordFieldColSpan => Cipher.ViewPassword ? 1 : 4;
public int TotpColumnSpan => Cipher.ViewPassword ? 1 : 2; public int TotpColumnSpan => Cipher.ViewPassword ? 1 : 2;
public bool AllowPersonal { get; set; } public bool AllowPersonal { get; set; }
public bool PasswordPrompt => Cipher.Reprompt != CipherRepromptType.None;
public void Init() 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() public void ToggleCardCode()
{ {
ShowCardCode = !ShowCardCode; ShowCardCode = !ShowCardCode;

View file

@ -23,6 +23,7 @@ namespace Bit.App.Pages
private readonly IDeviceActionService _deviceActionService; private readonly IDeviceActionService _deviceActionService;
private readonly ICipherService _cipherService; private readonly ICipherService _cipherService;
private readonly IStateService _stateService; private readonly IStateService _stateService;
private readonly IPasswordRepromptService _passwordRepromptService;
private AppOptions _appOptions; private AppOptions _appOptions;
private bool _showList; private bool _showList;
@ -35,6 +36,7 @@ namespace Bit.App.Pages
_cipherService = ServiceContainer.Resolve<ICipherService>("cipherService"); _cipherService = ServiceContainer.Resolve<ICipherService>("cipherService");
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService"); _deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
_stateService = ServiceContainer.Resolve<IStateService>("stateService"); _stateService = ServiceContainer.Resolve<IStateService>("stateService");
_passwordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService");
GroupedItems = new ExtendedObservableCollection<GroupingsPageListGroup>(); GroupedItems = new ExtendedObservableCollection<GroupingsPageListGroup>();
CipherOptionsCommand = new Command<CipherView>(CipherOptionsAsync); CipherOptionsCommand = new Command<CipherView>(CipherOptionsAsync);
@ -118,10 +120,14 @@ namespace Bit.App.Pages
} }
if (_deviceActionService.SystemMajorVersion() < 21) if (_deviceActionService.SystemMajorVersion() < 21)
{ {
await AppHelpers.CipherListOptions(Page, cipher); await AppHelpers.CipherListOptions(Page, cipher, _passwordRepromptService);
} }
else else
{ {
if (cipher.Reprompt != CipherRepromptType.None && !await _passwordRepromptService.ShowPasswordPromptAsync())
{
return;
}
var autofillResponse = AppResources.Yes; var autofillResponse = AppResources.Yes;
if (fuzzy) if (fuzzy)
{ {
@ -175,7 +181,7 @@ namespace Bit.App.Pages
{ {
if ((Page as BaseContentPage).DoOnce()) 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 ISearchService _searchService;
private readonly IDeviceActionService _deviceActionService; private readonly IDeviceActionService _deviceActionService;
private readonly IStateService _stateService; private readonly IStateService _stateService;
private readonly IPasswordRepromptService _passwordRepromptService;
private CancellationTokenSource _searchCancellationTokenSource; private CancellationTokenSource _searchCancellationTokenSource;
private bool _showNoData; private bool _showNoData;
@ -35,6 +36,7 @@ namespace Bit.App.Pages
_searchService = ServiceContainer.Resolve<ISearchService>("searchService"); _searchService = ServiceContainer.Resolve<ISearchService>("searchService");
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService"); _deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
_stateService = ServiceContainer.Resolve<IStateService>("stateService"); _stateService = ServiceContainer.Resolve<IStateService>("stateService");
_passwordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService");
Ciphers = new ExtendedObservableCollection<CipherView>(); Ciphers = new ExtendedObservableCollection<CipherView>();
CipherOptionsCommand = new Command<CipherView>(CipherOptionsAsync); CipherOptionsCommand = new Command<CipherView>(CipherOptionsAsync);
@ -182,7 +184,7 @@ namespace Bit.App.Pages
} }
if (_deviceActionService.SystemMajorVersion() < 21) if (_deviceActionService.SystemMajorVersion() < 21)
{ {
await Utilities.AppHelpers.CipherListOptions(Page, cipher); await Utilities.AppHelpers.CipherListOptions(Page, cipher, _passwordRepromptService);
} }
else else
{ {
@ -195,7 +197,7 @@ namespace Bit.App.Pages
{ {
if ((Page as BaseContentPage).DoOnce()) 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 IMessagingService _messagingService;
private readonly IStateService _stateService; private readonly IStateService _stateService;
private readonly IStorageService _storageService; private readonly IStorageService _storageService;
private readonly IPasswordRepromptService _passwordRepromptService;
public GroupingsPageViewModel() public GroupingsPageViewModel()
{ {
@ -60,6 +61,7 @@ namespace Bit.App.Pages
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService"); _messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
_stateService = ServiceContainer.Resolve<IStateService>("stateService"); _stateService = ServiceContainer.Resolve<IStateService>("stateService");
_storageService = ServiceContainer.Resolve<IStorageService>("storageService"); _storageService = ServiceContainer.Resolve<IStorageService>("storageService");
_passwordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService");
Loading = true; Loading = true;
PageTitle = AppResources.MyVault; PageTitle = AppResources.MyVault;
@ -514,7 +516,7 @@ namespace Bit.App.Pages
{ {
if ((Page as BaseContentPage).DoOnce()) if ((Page as BaseContentPage).DoOnce())
{ {
await AppHelpers.CipherListOptions(Page, cipher); await AppHelpers.CipherListOptions(Page, cipher, _passwordRepromptService);
} }
} }
} }

View file

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

View file

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

View file

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

View file

@ -3512,5 +3512,25 @@ namespace Bit.App.Resources {
return ResourceManager.GetString("SendFileEmailVerificationRequired", resourceCulture); 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> <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> <comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
</data> </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> </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; 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() public bool IsDev()
{ {
return Core.Utilities.CoreHelpers.InDebugMode(); return Core.Utilities.CoreHelpers.InDebugMode();

View file

@ -19,7 +19,7 @@ namespace Bit.App.Utilities
{ {
public static class AppHelpers 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 platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
var eventService = ServiceContainer.Resolve<IEventService>("eventService"); var eventService = ServiceContainer.Resolve<IEventService>("eventService");
@ -91,13 +91,18 @@ namespace Bit.App.Utilities
string.Format(AppResources.ValueHasBeenCopied, AppResources.Username)); string.Format(AppResources.ValueHasBeenCopied, AppResources.Username));
} }
else if (selection == AppResources.CopyPassword) else if (selection == AppResources.CopyPassword)
{
if (cipher.Reprompt == CipherRepromptType.None || await passwordRepromptService.ShowPasswordPromptAsync())
{ {
await platformUtilsService.CopyToClipboardAsync(cipher.Login.Password); await platformUtilsService.CopyToClipboardAsync(cipher.Login.Password);
platformUtilsService.ShowToast("info", null, platformUtilsService.ShowToast("info", null,
string.Format(AppResources.ValueHasBeenCopied, AppResources.Password)); string.Format(AppResources.ValueHasBeenCopied, AppResources.Password));
var task = eventService.CollectAsync(Core.Enums.EventType.Cipher_ClientCopiedPassword, cipher.Id); var task = eventService.CollectAsync(Core.Enums.EventType.Cipher_ClientCopiedPassword, cipher.Id);
} }
}
else if (selection == AppResources.CopyTotp) else if (selection == AppResources.CopyTotp)
{
if (cipher.Reprompt == CipherRepromptType.None || await passwordRepromptService.ShowPasswordPromptAsync())
{ {
var totpService = ServiceContainer.Resolve<ITotpService>("totpService"); var totpService = ServiceContainer.Resolve<ITotpService>("totpService");
var totp = await totpService.GetCodeAsync(cipher.Login.Totp); var totp = await totpService.GetCodeAsync(cipher.Login.Totp);
@ -108,23 +113,30 @@ namespace Bit.App.Utilities
string.Format(AppResources.ValueHasBeenCopied, AppResources.VerificationCodeTotp)); string.Format(AppResources.ValueHasBeenCopied, AppResources.VerificationCodeTotp));
} }
} }
}
else if (selection == AppResources.Launch) else if (selection == AppResources.Launch)
{ {
platformUtilsService.LaunchUri(cipher.Login.LaunchUri); platformUtilsService.LaunchUri(cipher.Login.LaunchUri);
} }
else if (selection == AppResources.CopyNumber) else if (selection == AppResources.CopyNumber)
{
if (cipher.Reprompt == CipherRepromptType.None || await passwordRepromptService.ShowPasswordPromptAsync())
{ {
await platformUtilsService.CopyToClipboardAsync(cipher.Card.Number); await platformUtilsService.CopyToClipboardAsync(cipher.Card.Number);
platformUtilsService.ShowToast("info", null, platformUtilsService.ShowToast("info", null,
string.Format(AppResources.ValueHasBeenCopied, AppResources.Number)); string.Format(AppResources.ValueHasBeenCopied, AppResources.Number));
} }
}
else if (selection == AppResources.CopySecurityCode) else if (selection == AppResources.CopySecurityCode)
{
if (cipher.Reprompt == CipherRepromptType.None || await passwordRepromptService.ShowPasswordPromptAsync())
{ {
await platformUtilsService.CopyToClipboardAsync(cipher.Card.Code); await platformUtilsService.CopyToClipboardAsync(cipher.Card.Code);
platformUtilsService.ShowToast("info", null, platformUtilsService.ShowToast("info", null,
string.Format(AppResources.ValueHasBeenCopied, AppResources.SecurityCode)); string.Format(AppResources.ValueHasBeenCopied, AppResources.SecurityCode));
var task = eventService.CollectAsync(Core.Enums.EventType.Cipher_ClientCopiedCardCode, cipher.Id); var task = eventService.CollectAsync(Core.Enums.EventType.Cipher_ClientCopiedCardCode, cipher.Id);
} }
}
else if (selection == AppResources.CopyNotes) else if (selection == AppResources.CopyNotes)
{ {
await platformUtilsService.CopyToClipboardAsync(cipher.Notes); await platformUtilsService.CopyToClipboardAsync(cipher.Notes);

View file

@ -22,6 +22,7 @@ namespace Bit.Core.Abstractions
void SaveFile(); void SaveFile();
Task<bool> ShowDialogAsync(string text, string title = null, string confirmText = null, Task<bool> ShowDialogAsync(string text, string title = null, string confirmText = null,
string cancelText = null, string type = 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);
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(); 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_ClientAutofilled = 1114,
Cipher_SoftDeleted = 1115, Cipher_SoftDeleted = 1115,
Cipher_Restored = 1116, Cipher_Restored = 1116,
Cipher_ClientToggledCardNumberVisible = 1117,
Collection_Created = 1300, Collection_Created = 1300,
Collection_Updated = 1301, Collection_Updated = 1301,

View file

@ -25,6 +25,7 @@ namespace Bit.Core.Models.Data
Name = response.Name; Name = response.Name;
Notes = response.Notes; Notes = response.Notes;
CollectionIds = collectionIds?.ToList() ?? response.CollectionIds; CollectionIds = collectionIds?.ToList() ?? response.CollectionIds;
Reprompt = response.Reprompt;
try // Added to address Issue (https://github.com/bitwarden/mobile/issues/1006) 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<PasswordHistoryData> PasswordHistory { get; set; }
public List<string> CollectionIds { get; set; } public List<string> CollectionIds { get; set; }
public DateTime? DeletedDate { 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 Bit.Core.Models.View;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@ -30,6 +31,7 @@ namespace Bit.Core.Models.Domain
RevisionDate = obj.RevisionDate; RevisionDate = obj.RevisionDate;
CollectionIds = obj.CollectionIds != null ? new HashSet<string>(obj.CollectionIds) : null; CollectionIds = obj.CollectionIds != null ? new HashSet<string>(obj.CollectionIds) : null;
LocalData = localData; LocalData = localData;
Reprompt = obj.Reprompt;
switch (Type) switch (Type)
{ {
@ -75,8 +77,8 @@ namespace Bit.Core.Models.Domain
public List<Field> Fields { get; set; } public List<Field> Fields { get; set; }
public List<PasswordHistory> PasswordHistory { get; set; } public List<PasswordHistory> PasswordHistory { get; set; }
public HashSet<string> CollectionIds { get; set; } public HashSet<string> CollectionIds { get; set; }
public DateTime? DeletedDate { get; set; } public DateTime? DeletedDate { get; set; }
public CipherRepromptType Reprompt { get; set; }
public async Task<CipherView> DecryptAsync() public async Task<CipherView> DecryptAsync()
{ {
@ -167,7 +169,8 @@ namespace Bit.Core.Models.Domain
RevisionDate = RevisionDate, RevisionDate = RevisionDate,
Type = Type, Type = Type,
CollectionIds = CollectionIds.ToList(), CollectionIds = CollectionIds.ToList(),
DeletedDate = DeletedDate DeletedDate = DeletedDate,
Reprompt = Reprompt,
}; };
BuildDataModel(this, c, new HashSet<string> BuildDataModel(this, c, new HashSet<string>
{ {

View file

@ -18,6 +18,7 @@ namespace Bit.Core.Models.Request
Notes = cipher.Notes?.EncryptedString; Notes = cipher.Notes?.EncryptedString;
Favorite = cipher.Favorite; Favorite = cipher.Favorite;
LastKnownRevisionDate = cipher.RevisionDate; LastKnownRevisionDate = cipher.RevisionDate;
Reprompt = cipher.Reprompt;
switch (Type) switch (Type)
{ {
@ -121,5 +122,6 @@ namespace Bit.Core.Models.Request
public Dictionary<string, string> Attachments { get; set; } public Dictionary<string, string> Attachments { get; set; }
public Dictionary<string, AttachmentRequest> Attachments2 { get; set; } public Dictionary<string, AttachmentRequest> Attachments2 { get; set; }
public DateTime LastKnownRevisionDate { 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;
using System.Collections.Generic; using System.Collections.Generic;
@ -26,5 +27,6 @@ namespace Bit.Core.Models.Response
public List<PasswordHistoryResponse> PasswordHistory { get; set; } public List<PasswordHistoryResponse> PasswordHistory { get; set; }
public List<string> CollectionIds { get; set; } public List<string> CollectionIds { get; set; }
public DateTime? DeletedDate { 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 ExpYear { get; set; }
public string Code { get; set; } public string Code { get; set; }
public string MaskedCode => Code != null ? new string('•', Code.Length) : null; public string MaskedCode => Code != null ? new string('•', Code.Length) : null;
public string MaskedNumber => Number != null ? new string('•', Number.Length) : null;
public string Brand public string Brand
{ {

View file

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

View file

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

View file

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

View file

@ -9,6 +9,7 @@ using Bit.iOS.Autofill.Utilities;
using Bit.iOS.Core.Utilities; using Bit.iOS.Core.Utilities;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.App.Abstractions;
namespace Bit.iOS.Autofill namespace Bit.iOS.Autofill
{ {
@ -18,10 +19,12 @@ namespace Bit.iOS.Autofill
: base(handle) : base(handle)
{ {
DismissModalAction = Cancel; DismissModalAction = Cancel;
PasswordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService");
} }
public Context Context { get; set; } public Context Context { get; set; }
public CredentialProviderViewController CPViewController { get; set; } public CredentialProviderViewController CPViewController { get; set; }
public IPasswordRepromptService PasswordRepromptService { get; private set; }
public async override void ViewDidLoad() public async override void ViewDidLoad()
{ {
@ -109,7 +112,7 @@ namespace Bit.iOS.Autofill
public async override void RowSelected(UITableView tableView, NSIndexPath indexPath) public async override void RowSelected(UITableView tableView, NSIndexPath indexPath)
{ {
await AutofillHelpers.TableRowSelectedAsync(tableView, indexPath, this, 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.Core.Views;
using Bit.iOS.Autofill.Utilities; using Bit.iOS.Autofill.Utilities;
using Bit.iOS.Core.Utilities; using Bit.iOS.Core.Utilities;
using Bit.App.Abstractions;
using Bit.Core.Utilities;
namespace Bit.iOS.Autofill namespace Bit.iOS.Autofill
{ {
@ -16,11 +18,13 @@ namespace Bit.iOS.Autofill
: base(handle) : base(handle)
{ {
DismissModalAction = Cancel; DismissModalAction = Cancel;
PasswordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService");
} }
public Context Context { get; set; } public Context Context { get; set; }
public CredentialProviderViewController CPViewController { get; set; } public CredentialProviderViewController CPViewController { get; set; }
public bool FromList { get; set; } public bool FromList { get; set; }
public IPasswordRepromptService PasswordRepromptService { get; private set; }
public async override void ViewDidLoad() public async override void ViewDidLoad()
{ {
@ -107,7 +111,8 @@ namespace Bit.iOS.Autofill
public async override void RowSelected(UITableView tableView, NSIndexPath indexPath) public async override void RowSelected(UITableView tableView, NSIndexPath indexPath)
{ {
await AutofillHelpers.TableRowSelectedAsync(tableView, indexPath, this, 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.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.App.Abstractions;
using Bit.App.Resources; using Bit.App.Resources;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.Core.Utilities; using Bit.Core.Utilities;
@ -14,7 +15,8 @@ namespace Bit.iOS.Autofill.Utilities
{ {
public async static Task TableRowSelectedAsync(UITableView tableView, NSIndexPath indexPath, public async static Task TableRowSelectedAsync(UITableView tableView, NSIndexPath indexPath,
ExtensionTableSource tableSource, CredentialProviderViewController cpViewController, ExtensionTableSource tableSource, CredentialProviderViewController cpViewController,
UITableViewController controller, string loginAddSegue) UITableViewController controller, IPasswordRepromptService passwordRepromptService,
string loginAddSegue)
{ {
tableView.DeselectRow(indexPath, true); tableView.DeselectRow(indexPath, true);
tableView.EndEditing(true); tableView.EndEditing(true);
@ -31,6 +33,11 @@ namespace Bit.iOS.Autofill.Utilities
return; return;
} }
if (item.Reprompt != Bit.Core.Enums.CipherRepromptType.None && !await passwordRepromptService.ShowPasswordPromptAsync())
{
return;
}
if (!string.IsNullOrWhiteSpace(item.Username) && !string.IsNullOrWhiteSpace(item.Password)) if (!string.IsNullOrWhiteSpace(item.Username) && !string.IsNullOrWhiteSpace(item.Password))
{ {
string totp = null; string totp = null;

View file

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

View file

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

View file

@ -55,6 +55,9 @@ namespace Bit.iOS.Core.Utilities
var platformUtilsService = new MobilePlatformUtilsService(deviceActionService, messagingService, var platformUtilsService = new MobilePlatformUtilsService(deviceActionService, messagingService,
broadcasterService); broadcasterService);
var biometricService = new BiometricService(mobileStorageService); 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<IBroadcasterService>("broadcasterService", broadcasterService);
ServiceContainer.Register<IMessagingService>("messagingService", messagingService); ServiceContainer.Register<IMessagingService>("messagingService", messagingService);
@ -66,6 +69,9 @@ namespace Bit.iOS.Core.Utilities
ServiceContainer.Register<IDeviceActionService>("deviceActionService", deviceActionService); ServiceContainer.Register<IDeviceActionService>("deviceActionService", deviceActionService);
ServiceContainer.Register<IPlatformUtilsService>("platformUtilsService", platformUtilsService); ServiceContainer.Register<IPlatformUtilsService>("platformUtilsService", platformUtilsService);
ServiceContainer.Register<IBiometricService>("biometricService", biometricService); 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) public static void Bootstrap(Func<Task> postBootstrapFunc = null)