[SG-460] Master Password security checks (mobile) (#2312)

* [SG-886] MasterPassword Strength Indicator (#2238)

* [SG-886] Add password strength indicator control

* [SG-570] Add weak password dialog check

* [SG-886] rename enum password strength

* [SG-886] Change control scale

* [SG-886] Move calculate user inputs to IPasswordGenerationService, refactor.

* [SG-886] Move formatted string to xaml. Move minimum chars to constant

* [SG-886] String to enum converter

* [SG-886] PR fixes. Code refactor control

* [SG-886] Update UI on OS theme change.

* [SG-886] Move colors to view

* [SG-886] Fixed password strength validation

* [SG-564][SG-565] Check Exposed Password (#2239)

* [SG-886] Add password strength indicator control

* [SG-570] Add weak password dialog check

* [SG-886] rename enum password strength

* [SG-564] [SG-565] Add check for exposed password and show dialog

* code format

* [SG-886] Change control scale

* [SG-886] Move calculate user inputs to IPasswordGenerationService, refactor.

* [SG-886] Move formatted string to xaml. Move minimum chars to constant

* [SG-886] String to enum converter

* [SG-886] Remove import

* [SG-886] Update UI on OS theme change.

* [SG-886] Move colors to view

* [SG-886] Fixed password strength validation
This commit is contained in:
André Bispo 2023-01-20 13:38:31 +00:00 committed by GitHub
parent 5aa1146657
commit d61bc4b5c1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 553 additions and 31 deletions

View file

@ -143,6 +143,7 @@
<Folder Include="Utilities\AccountManagement\" /> <Folder Include="Utilities\AccountManagement\" />
<Folder Include="Controls\DateTime\" /> <Folder Include="Controls\DateTime\" />
<Folder Include="Controls\IconLabelButton\" /> <Folder Include="Controls\IconLabelButton\" />
<Folder Include="Controls\PasswordStrengthProgressBar\" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -435,5 +436,6 @@
<None Remove="Utilities\AccountManagement\" /> <None Remove="Utilities\AccountManagement\" />
<None Remove="Controls\DateTime\" /> <None Remove="Controls\DateTime\" />
<None Remove="Controls\IconLabelButton\" /> <None Remove="Controls\IconLabelButton\" />
<None Remove="Controls\PasswordStrengthProgressBar\" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -0,0 +1,11 @@
using System.Collections.Generic;
namespace Bit.App.Controls
{
public interface IPasswordStrengthable
{
string Password { get; }
List<string> UserInputs { get; }
}
}

View file

@ -0,0 +1,17 @@
using Bit.Core.Attributes;
namespace Bit.App.Controls
{
public enum PasswordStrengthLevel
{
[LocalizableEnum("Weak")]
VeryWeak,
[LocalizableEnum("Weak")]
Weak,
[LocalizableEnum("Good")]
Good,
[LocalizableEnum("Strong")]
Strong
}
}

View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8" ?>
<StackLayout
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:u="clr-namespace:Bit.App.Utilities"
x:DataType="controls:PasswordStrengthViewModel"
x:Class="Bit.App.Controls.PasswordStrengthProgressBar"
StyleClass="box">
<StackLayout.Resources>
<ResourceDictionary>
<u:LocalizableEnumConverter x:Key="localizableEnum" />
</ResourceDictionary>
</StackLayout.Resources>
<ProgressBar
x:Name="_progressBar"
u:ProgressBarExtensions.AnimatedProgress="{Binding PasswordStrength}"
ScaleY="2" />
<Label
x:Name="_progressLabel"
Text="{Binding PasswordStrengthLevel, Converter={StaticResource localizableEnum}, TargetNullValue=' ' }"
StyleClass="box-footer-label" />
</StackLayout>

View file

@ -0,0 +1,107 @@
using System;
using Xamarin.Forms;
namespace Bit.App.Controls
{
public partial class PasswordStrengthProgressBar : StackLayout
{
public static readonly BindableProperty PasswordStrengthLevelProperty = BindableProperty.Create(
nameof(PasswordStrengthLevel),
typeof(PasswordStrengthLevel),
typeof(PasswordStrengthProgressBar),
propertyChanged: OnControlPropertyChanged);
public static readonly BindableProperty VeryWeakColorProperty = BindableProperty.Create(
nameof(VeryWeakColor),
typeof(Color),
typeof(PasswordStrengthProgressBar),
propertyChanged: OnControlPropertyChanged);
public static readonly BindableProperty WeakColorProperty = BindableProperty.Create(
nameof(WeakColor),
typeof(Color),
typeof(PasswordStrengthProgressBar),
propertyChanged: OnControlPropertyChanged);
public static readonly BindableProperty GoodColorProperty = BindableProperty.Create(
nameof(GoodColor),
typeof(Color),
typeof(PasswordStrengthProgressBar),
propertyChanged: OnControlPropertyChanged);
public static readonly BindableProperty StrongColorProperty = BindableProperty.Create(
nameof(StrongColor),
typeof(Color),
typeof(PasswordStrengthProgressBar),
propertyChanged: OnControlPropertyChanged);
public PasswordStrengthLevel? PasswordStrengthLevel
{
get { return (PasswordStrengthLevel?)GetValue(PasswordStrengthLevelProperty); }
set { SetValue(PasswordStrengthLevelProperty, value); }
}
public Color VeryWeakColor
{
get { return (Color)GetValue(VeryWeakColorProperty); }
set { SetValue(VeryWeakColorProperty, value); }
}
public Color WeakColor
{
get { return (Color)GetValue(WeakColorProperty); }
set { SetValue(WeakColorProperty, value); }
}
public Color GoodColor
{
get { return (Color)GetValue(GoodColorProperty); }
set { SetValue(GoodColorProperty, value); }
}
public Color StrongColor
{
get { return (Color)GetValue(StrongColorProperty); }
set { SetValue(StrongColorProperty, value); }
}
public PasswordStrengthProgressBar()
{
InitializeComponent();
SetBinding(PasswordStrengthProgressBar.PasswordStrengthLevelProperty, new Binding() { Path = nameof(PasswordStrengthViewModel.PasswordStrengthLevel) });
}
private static void OnControlPropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
(bindable as PasswordStrengthProgressBar)?.UpdateColors();
}
public void UpdateColors()
{
if (_progressBar == null || _progressLabel == null)
{
return;
}
_progressBar.ProgressColor = GetColorForStrength();
_progressLabel.TextColor = _progressBar.ProgressColor;
}
private Color GetColorForStrength()
{
switch (PasswordStrengthLevel)
{
case Controls.PasswordStrengthLevel.VeryWeak:
return VeryWeakColor;
case Controls.PasswordStrengthLevel.Weak:
return WeakColor;
case Controls.PasswordStrengthLevel.Good:
return GoodColor;
case Controls.PasswordStrengthLevel.Strong:
return StrongColor;
default:
return Color.Transparent;
}
}
}
}

View file

@ -0,0 +1,67 @@
using System.Collections.Generic;
using Bit.Core.Abstractions;
using Bit.Core.Utilities;
using Xamarin.Forms;
namespace Bit.App.Controls
{
public class PasswordStrengthViewModel : ExtendedViewModel
{
private readonly IPasswordGenerationService _passwordGenerationService;
private readonly IPasswordStrengthable _passwordStrengthable;
private double _passwordStrength;
private Color _passwordColor;
private PasswordStrengthLevel? _passwordStrengthLevel;
public PasswordStrengthViewModel(IPasswordStrengthable passwordStrengthable)
{
_passwordGenerationService = ServiceContainer.Resolve<IPasswordGenerationService>();
_passwordStrengthable = passwordStrengthable;
}
public double PasswordStrength
{
get => _passwordStrength;
set => SetProperty(ref _passwordStrength, value);
}
public PasswordStrengthLevel? PasswordStrengthLevel
{
get => _passwordStrengthLevel;
set => SetProperty(ref _passwordStrengthLevel, value);
}
public List<string> GetPasswordStrengthUserInput(string email) => _passwordGenerationService.GetPasswordStrengthUserInput(email);
public void CalculatePasswordStrength()
{
if (string.IsNullOrEmpty(_passwordStrengthable.Password))
{
PasswordStrength = 0;
PasswordStrengthLevel = null;
return;
}
var passwordStrength = _passwordGenerationService.PasswordStrength(_passwordStrengthable.Password, _passwordStrengthable.UserInputs);
// The passwordStrength.Score is 0..4, convertion was made to be used as a progress directly by the control 0..1
PasswordStrength = (passwordStrength.Score + 1f) / 5f;
if (PasswordStrength <= 0.4f)
{
PasswordStrengthLevel = Controls.PasswordStrengthLevel.VeryWeak;
}
else if (PasswordStrength <= 0.6f)
{
PasswordStrengthLevel = Controls.PasswordStrengthLevel.Weak;
}
else if (PasswordStrength <= 0.8f)
{
PasswordStrengthLevel = Controls.PasswordStrengthLevel.Good;
}
else
{
PasswordStrengthLevel = Controls.PasswordStrengthLevel.Strong;
}
}
}
}

View file

@ -4,6 +4,8 @@ using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.App.Abstractions; using Bit.App.Abstractions;
using Bit.App.Resources; using Bit.App.Resources;
using Bit.App.Utilities;
using Bit.Core;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.Core.Models.Domain; using Bit.Core.Models.Domain;
using Bit.Core.Utilities; using Bit.Core.Utilities;
@ -147,8 +149,8 @@ namespace Bit.App.Pages
} }
if (IsPolicyInEffect) if (IsPolicyInEffect)
{ {
var userInput = await GetPasswordStrengthUserInput(); var userInputs = _passwordGenerationService.GetPasswordStrengthUserInput(await _stateService.GetEmailAsync());
var passwordStrength = _passwordGenerationService.PasswordStrength(MasterPassword, userInput); var passwordStrength = _passwordGenerationService.PasswordStrength(MasterPassword, userInputs);
if (!await _policyService.EvaluateMasterPassword(passwordStrength.Score, MasterPassword, Policy)) if (!await _policyService.EvaluateMasterPassword(passwordStrength.Score, MasterPassword, Policy))
{ {
await _platformUtilsService.ShowDialogAsync(AppResources.MasterPasswordPolicyValidationMessage, await _platformUtilsService.ShowDialogAsync(AppResources.MasterPasswordPolicyValidationMessage,
@ -158,7 +160,7 @@ namespace Bit.App.Pages
} }
else else
{ {
if (MasterPassword.Length < 8) if (MasterPassword.Length < Constants.MasterPasswordMinimumChars)
{ {
await _platformUtilsService.ShowDialogAsync(AppResources.MasterPasswordLengthValMessage, await _platformUtilsService.ShowDialogAsync(AppResources.MasterPasswordLengthValMessage,
AppResources.MasterPasswordPolicyValidationTitle, AppResources.Ok); AppResources.MasterPasswordPolicyValidationTitle, AppResources.Ok);
@ -174,19 +176,5 @@ namespace Bit.App.Pages
return true; return true;
} }
private async Task<List<string>> GetPasswordStrengthUserInput()
{
var email = await _stateService.GetEmailAsync();
List<string> userInput = null;
var atPosition = email.IndexOf('@');
if (atPosition > -1)
{
var rx = new Regex("/[^A-Za-z0-9]/", RegexOptions.Compiled);
var data = rx.Split(email.Substring(0, atPosition).Trim().ToLower());
userInput = new List<string>(data);
}
return userInput;
}
} }
} }

View file

@ -25,7 +25,7 @@
</ContentPage.ToolbarItems> </ContentPage.ToolbarItems>
<ScrollView> <ScrollView>
<StackLayout Spacing="20"> <StackLayout Spacing="10">
<StackLayout StyleClass="box"> <StackLayout StyleClass="box">
<StackLayout StyleClass="box-row"> <StackLayout StyleClass="box-row">
<Label <Label
@ -72,8 +72,19 @@
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"/> AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"/>
</Grid> </Grid>
<Label <Label
Text="{u:I18n MasterPasswordDescription}" StyleClass="box-sub-label"
StyleClass="box-footer-label" /> Margin="0,0,0,10">
<Label.FormattedText>
<FormattedString>
<Span Text="{u:I18n Important}" TextColor="{DynamicResource InfoColor}"/>
<Span Text=": " TextColor="{DynamicResource InfoColor}"/>
<Span Text="{Binding MasterPasswordMininumCharactersDescription}" TextColor="{DynamicResource MutedColor}"/>
</FormattedString>
</Label.FormattedText>
</Label>
<controls:PasswordStrengthProgressBar
BindingContext="{Binding PasswordStrengthViewModel}"
Margin="0,0"/>
</StackLayout> </StackLayout>
<StackLayout StyleClass="box"> <StackLayout StyleClass="box">
<Grid StyleClass="box-row"> <Grid StyleClass="box-row">
@ -126,6 +137,17 @@
StyleClass="box-footer-label" /> StyleClass="box-footer-label" />
</StackLayout> </StackLayout>
<StackLayout StyleClass="box"> <StackLayout StyleClass="box">
<StackLayout StyleClass="box-row, box-row-switch">
<Switch
IsToggled="{Binding CheckExposedMasterPassword}"
StyleClass="box-value"
HorizontalOptions="Start"
Margin="0, 0, 10, 0"/>
<Label
Text="{u:I18n CheckKnownDataBreachesForThisPassword}"
StyleClass="box-footer-label"
VerticalOptions="Center"/>
</StackLayout>
<StackLayout StyleClass="box-row, box-row-switch" <StackLayout StyleClass="box-row, box-row-switch"
IsVisible="{Binding ShowTerms}"> IsVisible="{Binding ShowTerms}">
<Switch <Switch

View file

@ -1,28 +1,36 @@
using System; using System;
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Input; using System.Windows.Input;
using Bit.App.Abstractions; using Bit.App.Abstractions;
using Bit.App.Controls;
using Bit.App.Resources; using Bit.App.Resources;
using Bit.App.Utilities;
using Bit.Core; using Bit.Core;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Models.Request; using Bit.Core.Models.Request;
using Bit.Core.Services;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Xamarin.Forms; using Xamarin.Forms;
namespace Bit.App.Pages namespace Bit.App.Pages
{ {
public class RegisterPageViewModel : CaptchaProtectedViewModel public class RegisterPageViewModel : CaptchaProtectedViewModel, IPasswordStrengthable
{ {
private readonly IDeviceActionService _deviceActionService; private readonly IDeviceActionService _deviceActionService;
private readonly II18nService _i18nService; private readonly II18nService _i18nService;
private readonly IEnvironmentService _environmentService; private readonly IEnvironmentService _environmentService;
private readonly IAuditService _auditService;
private readonly IApiService _apiService; private readonly IApiService _apiService;
private readonly ICryptoService _cryptoService; private readonly ICryptoService _cryptoService;
private readonly IPlatformUtilsService _platformUtilsService; private readonly IPlatformUtilsService _platformUtilsService;
private string _email;
private string _masterPassword;
private bool _showPassword; private bool _showPassword;
private bool _acceptPolicies; private bool _acceptPolicies;
private bool _checkExposedMasterPassword;
public RegisterPageViewModel() public RegisterPageViewModel()
{ {
@ -32,12 +40,14 @@ namespace Bit.App.Pages
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService"); _platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService"); _i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
_environmentService = ServiceContainer.Resolve<IEnvironmentService>("environmentService"); _environmentService = ServiceContainer.Resolve<IEnvironmentService>("environmentService");
_auditService = ServiceContainer.Resolve<IAuditService>();
PageTitle = AppResources.CreateAccount; PageTitle = AppResources.CreateAccount;
TogglePasswordCommand = new Command(TogglePassword); TogglePasswordCommand = new Command(TogglePassword);
ToggleConfirmPasswordCommand = new Command(ToggleConfirmPassword); ToggleConfirmPasswordCommand = new Command(ToggleConfirmPassword);
SubmitCommand = new Command(async () => await SubmitAsync()); SubmitCommand = new Command(async () => await SubmitAsync());
ShowTerms = !_platformUtilsService.IsSelfHost(); ShowTerms = !_platformUtilsService.IsSelfHost();
PasswordStrengthViewModel = new PasswordStrengthViewModel(this);
} }
public ICommand PoliciesClickCommand => new Command<string>((url) => public ICommand PoliciesClickCommand => new Command<string>((url) =>
@ -61,6 +71,34 @@ namespace Bit.App.Pages
get => _acceptPolicies; get => _acceptPolicies;
set => SetProperty(ref _acceptPolicies, value); set => SetProperty(ref _acceptPolicies, value);
} }
public bool CheckExposedMasterPassword
{
get => _checkExposedMasterPassword;
set => SetProperty(ref _checkExposedMasterPassword, value);
}
public string MasterPassword
{
get => _masterPassword;
set
{
SetProperty(ref _masterPassword, value);
PasswordStrengthViewModel.CalculatePasswordStrength();
}
}
public string Email
{
get => _email;
set => SetProperty(ref _email, value);
}
public string Password => MasterPassword;
public List<string> UserInputs => PasswordStrengthViewModel.GetPasswordStrengthUserInput(Email);
public string MasterPasswordMininumCharactersDescription => string.Format(AppResources.YourMasterPasswordCannotBeRecoveredIfYouForgetItXCharactersMinimum,
Constants.MasterPasswordMinimumChars);
public PasswordStrengthViewModel PasswordStrengthViewModel { get; }
public bool ShowTerms { get; set; } public bool ShowTerms { get; set; }
public Command SubmitCommand { get; } public Command SubmitCommand { get; }
public Command TogglePasswordCommand { get; } public Command TogglePasswordCommand { get; }
@ -68,13 +106,10 @@ namespace Bit.App.Pages
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye; public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow; public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
public string Name { get; set; } public string Name { get; set; }
public string Email { get; set; }
public string MasterPassword { get; set; }
public string ConfirmMasterPassword { get; set; } public string ConfirmMasterPassword { get; set; }
public string Hint { get; set; } public string Hint { get; set; }
public Action RegistrationSuccess { get; set; } public Action RegistrationSuccess { get; set; }
public Action CloseAction { get; set; } public Action CloseAction { get; set; }
protected override II18nService i18nService => _i18nService; protected override II18nService i18nService => _i18nService;
protected override IEnvironmentService environmentService => _environmentService; protected override IEnvironmentService environmentService => _environmentService;
protected override IDeviceActionService deviceActionService => _deviceActionService; protected override IDeviceActionService deviceActionService => _deviceActionService;
@ -110,7 +145,7 @@ namespace Bit.App.Pages
AppResources.Ok); AppResources.Ok);
return; return;
} }
if (MasterPassword.Length < 8) if (MasterPassword.Length < Constants.MasterPasswordMinimumChars)
{ {
await _platformUtilsService.ShowDialogAsync(AppResources.MasterPasswordLengthValMessage, await _platformUtilsService.ShowDialogAsync(AppResources.MasterPasswordLengthValMessage,
AppResources.AnErrorHasOccurred, AppResources.Ok); AppResources.AnErrorHasOccurred, AppResources.Ok);
@ -128,8 +163,10 @@ namespace Bit.App.Pages
AppResources.AnErrorHasOccurred, AppResources.Ok); AppResources.AnErrorHasOccurred, AppResources.Ok);
return; return;
} }
if (await IsPasswordWeakOrExposed())
// TODO: Password strength check? {
return;
}
if (showLoading) if (showLoading)
{ {
@ -160,6 +197,7 @@ namespace Bit.App.Pages
}, },
CaptchaResponse = _captchaToken, CaptchaResponse = _captchaToken,
}; };
// TODO: org invite? // TODO: org invite?
try try
@ -208,5 +246,43 @@ namespace Bit.App.Pages
entry.Focus(); entry.Focus();
entry.CursorPosition = String.IsNullOrEmpty(ConfirmMasterPassword) ? 0 : ConfirmMasterPassword.Length; entry.CursorPosition = String.IsNullOrEmpty(ConfirmMasterPassword) ? 0 : ConfirmMasterPassword.Length;
} }
private async Task<bool> IsPasswordWeakOrExposed()
{
try
{
var title = string.Empty;
var message = string.Empty;
var exposedPassword = CheckExposedMasterPassword ? await _auditService.PasswordLeakedAsync(MasterPassword) > 0 : false;
var weakPassword = PasswordStrengthViewModel.PasswordStrengthLevel <= PasswordStrengthLevel.Weak;
if (exposedPassword && weakPassword)
{
title = AppResources.WeakAndExposedMasterPassword;
message = AppResources.WeakPasswordIdentifiedAndFoundInADataBreachAlertDescription;
}
else if (exposedPassword)
{
title = AppResources.ExposedMasterPassword;
message = AppResources.PasswordFoundInADataBreachAlertDescription;
}
else if (weakPassword)
{
title = AppResources.WeakMasterPassword;
message = AppResources.WeakPasswordIdentifiedUseAStrongPasswordToProtectYourAccount;
}
if (exposedPassword || weakPassword)
{
return !await _platformUtilsService.ShowDialogAsync(message, title, AppResources.Yes, AppResources.No);
}
}
catch (Exception ex)
{
HandleException(ex);
}
return false;
}
} }
} }

View file

@ -5,6 +5,7 @@ using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.App.Abstractions; using Bit.App.Abstractions;
using Bit.App.Resources; using Bit.App.Resources;
using Bit.App.Utilities;
using Bit.Core; using Bit.Core;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.Core.Enums; using Bit.Core.Enums;
@ -137,8 +138,8 @@ namespace Bit.App.Pages
} }
if (IsPolicyInEffect) if (IsPolicyInEffect)
{ {
var userInput = await GetPasswordStrengthUserInput(); var userInputs = _passwordGenerationService.GetPasswordStrengthUserInput(await _stateService.GetEmailAsync());
var passwordStrength = _passwordGenerationService.PasswordStrength(MasterPassword, userInput); var passwordStrength = _passwordGenerationService.PasswordStrength(MasterPassword, userInputs);
if (!await _policyService.EvaluateMasterPassword(passwordStrength.Score, MasterPassword, Policy)) if (!await _policyService.EvaluateMasterPassword(passwordStrength.Score, MasterPassword, Policy))
{ {
await Page.DisplayAlert(AppResources.MasterPasswordPolicyValidationTitle, await Page.DisplayAlert(AppResources.MasterPasswordPolicyValidationTitle,
@ -148,7 +149,7 @@ namespace Bit.App.Pages
} }
else else
{ {
if (MasterPassword.Length < 8) if (MasterPassword.Length < Constants.MasterPasswordMinimumChars)
{ {
await Page.DisplayAlert(AppResources.MasterPasswordPolicyValidationTitle, await Page.DisplayAlert(AppResources.MasterPasswordPolicyValidationTitle,
AppResources.MasterPasswordLengthValMessage, AppResources.Ok); AppResources.MasterPasswordLengthValMessage, AppResources.Ok);

View file

@ -3,6 +3,7 @@ using Bit.App.Abstractions;
using Bit.App.Controls; using Bit.App.Controls;
using Bit.App.Resources; using Bit.App.Resources;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.Core.Exceptions;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Xamarin.Forms; using Xamarin.Forms;
@ -33,6 +34,11 @@ namespace Bit.App.Pages
protected void HandleException(Exception ex, string message = null) protected void HandleException(Exception ex, string message = null)
{ {
if (ex is ApiException apiException && apiException.Error != null)
{
message = apiException.Error.GetSingleMessage();
}
Xamarin.Essentials.MainThread.InvokeOnMainThreadAsync(async () => Xamarin.Essentials.MainThread.InvokeOnMainThreadAsync(async () =>
{ {
await _deviceActionService.Value.HideLoadingAsync(); await _deviceActionService.Value.HideLoadingAsync();

View file

@ -1372,6 +1372,15 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Check known data breaches for this password.
/// </summary>
public static string CheckKnownDataBreachesForThisPassword {
get {
return ResourceManager.GetString("CheckKnownDataBreachesForThisPassword", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Check if password has been exposed.. /// Looks up a localized string similar to Check if password has been exposed..
/// </summary> /// </summary>
@ -2416,6 +2425,15 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Exposed Master Password.
/// </summary>
public static string ExposedMasterPassword {
get {
return ResourceManager.GetString("ExposedMasterPassword", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Extension activated!. /// Looks up a localized string similar to Extension activated!.
/// </summary> /// </summary>
@ -2938,6 +2956,15 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Good.
/// </summary>
public static string Good {
get {
return ResourceManager.GetString("Good", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Go to my vault. /// Looks up a localized string similar to Go to my vault.
/// </summary> /// </summary>
@ -3073,6 +3100,15 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Important.
/// </summary>
public static string Important {
get {
return ResourceManager.GetString("Important", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Import items. /// Looks up a localized string similar to Import items.
/// </summary> /// </summary>
@ -4570,6 +4606,15 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Password found in a data breach. Use a unique password to protect your account. Are you sure you want to use an exposed password?.
/// </summary>
public static string PasswordFoundInADataBreachAlertDescription {
get {
return ResourceManager.GetString("PasswordFoundInADataBreachAlertDescription", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Password generated. /// Looks up a localized string similar to Password generated.
/// </summary> /// </summary>
@ -5669,6 +5714,15 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Strong.
/// </summary>
public static string Strong {
get {
return ResourceManager.GetString("Strong", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Submit. /// Looks up a localized string similar to Submit.
/// </summary> /// </summary>
@ -6650,6 +6704,51 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Weak.
/// </summary>
public static string Weak {
get {
return ResourceManager.GetString("Weak", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Weak and Exposed Master Password.
/// </summary>
public static string WeakAndExposedMasterPassword {
get {
return ResourceManager.GetString("WeakAndExposedMasterPassword", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Weak Master Password.
/// </summary>
public static string WeakMasterPassword {
get {
return ResourceManager.GetString("WeakMasterPassword", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Weak password identified and found in a data breach. Use a strong and unique password to protect your account. Are you sure you want to use this password?.
/// </summary>
public static string WeakPasswordIdentifiedAndFoundInADataBreachAlertDescription {
get {
return ResourceManager.GetString("WeakPasswordIdentifiedAndFoundInADataBreachAlertDescription", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Weak password identified. Use a strong password to protect your account. Are you sure you want to use a weak password?.
/// </summary>
public static string WeakPasswordIdentifiedUseAStrongPasswordToProtectYourAccount {
get {
return ResourceManager.GetString("WeakPasswordIdentifiedUseAStrongPasswordToProtectYourAccount", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Website. /// Looks up a localized string similar to Website.
/// </summary> /// </summary>
@ -6767,6 +6866,15 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Your master password cannot be recovered if you forget it! {0} characters minimum..
/// </summary>
public static string YourMasterPasswordCannotBeRecoveredIfYouForgetItXCharactersMinimum {
get {
return ResourceManager.GetString("YourMasterPasswordCannotBeRecoveredIfYouForgetItXCharactersMinimum", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to To continue, hold your YubiKey NEO against the back of the device or insert your YubiKey into your device&apos;s USB port, then touch its button.. /// Looks up a localized string similar to To continue, hold your YubiKey NEO against the back of the device or insert your YubiKey into your device&apos;s USB port, then touch its button..
/// </summary> /// </summary>

View file

@ -2515,4 +2515,40 @@ Do you want to switch to this account?</value>
<data name="EnableCamerPermissionToUseTheScanner" xml:space="preserve"> <data name="EnableCamerPermissionToUseTheScanner" xml:space="preserve">
<value>Enable camera permission to use the scanner</value> <value>Enable camera permission to use the scanner</value>
</data> </data>
<data name="Important" xml:space="preserve">
<value>Important</value>
</data>
<data name="YourMasterPasswordCannotBeRecoveredIfYouForgetItXCharactersMinimum" xml:space="preserve">
<value>Your master password cannot be recovered if you forget it! {0} characters minimum.</value>
</data>
<data name="WeakMasterPassword" xml:space="preserve">
<value>Weak Master Password</value>
</data>
<data name="WeakPasswordIdentifiedUseAStrongPasswordToProtectYourAccount" xml:space="preserve">
<value>Weak password identified. Use a strong password to protect your account. Are you sure you want to use a weak password?</value>
</data>
<data name="Weak" xml:space="preserve">
<value>Weak</value>
</data>
<data name="Good" xml:space="preserve">
<value>Good</value>
</data>
<data name="Strong" xml:space="preserve">
<value>Strong</value>
</data>
<data name="CheckKnownDataBreachesForThisPassword" xml:space="preserve">
<value>Check known data breaches for this password</value>
</data>
<data name="ExposedMasterPassword" xml:space="preserve">
<value>Exposed Master Password</value>
</data>
<data name="PasswordFoundInADataBreachAlertDescription" xml:space="preserve">
<value>Password found in a data breach. Use a unique password to protect your account. Are you sure you want to use an exposed password?</value>
</data>
<data name="WeakAndExposedMasterPassword" xml:space="preserve">
<value>Weak and Exposed Master Password</value>
</data>
<data name="WeakPasswordIdentifiedAndFoundInADataBreachAlertDescription" xml:space="preserve">
<value>Weak password identified and found in a data breach. Use a strong and unique password to protect your account. Are you sure you want to use this password?</value>
</data>
</root> </root>

View file

@ -562,4 +562,15 @@
</VisualStateGroupList> </VisualStateGroupList>
</Setter> </Setter>
</Style> </Style>
<Style TargetType="controls:PasswordStrengthProgressBar">
<Setter Property="VeryWeakColor"
Value="{DynamicResource DangerColor}" />
<Setter Property="WeakColor"
Value="{DynamicResource WarningColor}" />
<Setter Property="GoodColor"
Value="{DynamicResource PrimaryColor}" />
<Setter Property="StrongColor"
Value="{DynamicResource SuccessColor}" />
</Style>
</ResourceDictionary> </ResourceDictionary>

View file

@ -0,0 +1,25 @@
using Xamarin.Forms;
namespace Bit.App.Utilities
{
public static class ProgressBarExtensions
{
public static BindableProperty AnimatedProgressProperty =
BindableProperty.CreateAttached("AnimatedProgress",
typeof(double),
typeof(ProgressBar),
0.0d,
BindingMode.OneWay,
propertyChanged: (b, o, n) => ProgressBarProgressChanged((ProgressBar)b, (double)n));
public static double GetAnimatedProgress(BindableObject target) => (double)target.GetValue(AnimatedProgressProperty);
public static void SetAnimatedProgress(BindableObject target, double value) => target.SetValue(AnimatedProgressProperty, value);
private static void ProgressBarProgressChanged(ProgressBar progressBar, double progress)
{
ViewExtensions.CancelAnimations(progressBar);
progressBar.ProgressTo(progress, 500, Easing.SinIn);
}
}
}

View file

@ -2,6 +2,7 @@
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.Core.Models.Domain; using Bit.Core.Models.Domain;
using Zxcvbn;
namespace Bit.Core.Abstractions namespace Bit.Core.Abstractions
{ {
@ -16,8 +17,9 @@ namespace Bit.Core.Abstractions
Task<(PasswordGenerationOptions, PasswordGeneratorPolicyOptions)> GetOptionsAsync(); Task<(PasswordGenerationOptions, PasswordGeneratorPolicyOptions)> GetOptionsAsync();
Task<(PasswordGenerationOptions, PasswordGeneratorPolicyOptions)> Task<(PasswordGenerationOptions, PasswordGeneratorPolicyOptions)>
EnforcePasswordGeneratorPoliciesOnOptionsAsync(PasswordGenerationOptions options); EnforcePasswordGeneratorPoliciesOnOptionsAsync(PasswordGenerationOptions options);
Zxcvbn.Result PasswordStrength(string password, List<string> userInputs = null); Result PasswordStrength(string password, List<string> userInputs = null);
Task SaveOptionsAsync(PasswordGenerationOptions options); Task SaveOptionsAsync(PasswordGenerationOptions options);
void NormalizeOptions(PasswordGenerationOptions options, PasswordGeneratorPolicyOptions enforcedPolicyOptions); void NormalizeOptions(PasswordGenerationOptions options, PasswordGeneratorPolicyOptions enforcedPolicyOptions);
List<string> GetPasswordStrengthUserInput(string email);
} }
} }

View file

@ -46,6 +46,7 @@
public const int SaveFileRequestCode = 44; public const int SaveFileRequestCode = 44;
public const int TotpDefaultTimer = 30; public const int TotpDefaultTimer = 30;
public const int PasswordlessNotificationTimeoutInMinutes = 15; public const int PasswordlessNotificationTimeoutInMinutes = 15;
public const int MasterPasswordMinimumChars = 8;
public static readonly string[] AndroidAllClearCipherCacheKeys = public static readonly string[] AndroidAllClearCipherCacheKeys =
{ {

View file

@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Text.RegularExpressions;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
@ -395,6 +396,19 @@ namespace Bit.Core.Services
return enforcedOptions; return enforcedOptions;
} }
public List<string> GetPasswordStrengthUserInput(string email)
{
var atPosition = email?.IndexOf('@');
if (atPosition is null || atPosition < 0)
{
return null;
}
var rx = new Regex("/[^A-Za-z0-9]/", RegexOptions.Compiled);
var data = rx.Split(email.Substring(0, atPosition.Value).Trim().ToLower());
return new List<string>(data);
}
private int? GetPolicyInt(Policy policy, string key) private int? GetPolicyInt(Policy policy, string key)
{ {
if (policy.Data.ContainsKey(key)) if (policy.Data.ContainsKey(key))