Password generator policy enforcement (#741)

* Password generator policy enforcement

* Formatting

* Changed to simple cast (double unboxing for int64/long)

* Added ui indication of active policy on password generator page and fixed issue with switch enable logic
This commit is contained in:
Matt Portune 2020-02-27 19:53:02 -05:00 committed by GitHub
parent 02cffa01e2
commit f78f303a79
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 231 additions and 12 deletions

View file

@ -39,6 +39,29 @@
<ScrollView Padding="0, 0, 0, 20"> <ScrollView Padding="0, 0, 0, 20">
<StackLayout Spacing="0" Padding="0"> <StackLayout Spacing="0" Padding="0">
<StackLayout StyleClass="box"> <StackLayout StyleClass="box">
<Grid IsVisible="{Binding IsPolicyInEffect}"
Margin="0, 12, 0, 0"
RowSpacing="0"
ColumnSpacing="0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Frame Padding="10"
Margin="0"
HasShadow="False"
BackgroundColor="Transparent"
BorderColor="Accent">
<Label
Text="{u:I18n PasswordGeneratorPolicyInEffect}"
StyleClass="text-muted, text-sm, text-bold"
HorizontalTextAlignment="Center" />
</Frame>
</Grid>
<controls:MonoLabel <controls:MonoLabel
Text="{Binding ColoredPassword, Mode=OneWay}" Text="{Binding ColoredPassword, Mode=OneWay}"
TextType="Html" TextType="Html"
@ -152,6 +175,7 @@
HorizontalOptions="StartAndExpand" /> HorizontalOptions="StartAndExpand" />
<Switch <Switch
IsToggled="{Binding Uppercase}" IsToggled="{Binding Uppercase}"
IsEnabled="{Binding IsUppercaseSwitchEnabled}"
StyleClass="box-value" StyleClass="box-value"
HorizontalOptions="End" /> HorizontalOptions="End" />
</StackLayout> </StackLayout>
@ -163,6 +187,7 @@
HorizontalOptions="StartAndExpand" /> HorizontalOptions="StartAndExpand" />
<Switch <Switch
IsToggled="{Binding Lowercase}" IsToggled="{Binding Lowercase}"
IsEnabled="{Binding IsLowercaseSwitchEnabled}"
StyleClass="box-value" StyleClass="box-value"
HorizontalOptions="End" /> HorizontalOptions="End" />
</StackLayout> </StackLayout>
@ -174,6 +199,7 @@
HorizontalOptions="StartAndExpand" /> HorizontalOptions="StartAndExpand" />
<Switch <Switch
IsToggled="{Binding Number}" IsToggled="{Binding Number}"
IsEnabled="{Binding IsNumberSwitchEnabled}"
StyleClass="box-value" StyleClass="box-value"
HorizontalOptions="End" /> HorizontalOptions="End" />
</StackLayout> </StackLayout>
@ -185,6 +211,7 @@
HorizontalOptions="StartAndExpand" /> HorizontalOptions="StartAndExpand" />
<Switch <Switch
IsToggled="{Binding Special}" IsToggled="{Binding Special}"
IsEnabled="{Binding IsSpecialSwitchEnabled}"
StyleClass="box-value" StyleClass="box-value"
HorizontalOptions="End" /> HorizontalOptions="End" />
</StackLayout> </StackLayout>

View file

@ -15,6 +15,7 @@ namespace Bit.App.Pages
private readonly IPlatformUtilsService _platformUtilsService; private readonly IPlatformUtilsService _platformUtilsService;
private PasswordGenerationOptions _options; private PasswordGenerationOptions _options;
private PasswordGeneratorPolicyOptions _enforcedPolicyOptions;
private string _password; private string _password;
private bool _isPassword; private bool _isPassword;
private bool _uppercase; private bool _uppercase;
@ -221,7 +222,34 @@ namespace Bit.App.Pages
} }
} }
} }
public PasswordGeneratorPolicyOptions EnforcedPolicyOptions
{
get => _enforcedPolicyOptions;
set => SetProperty(ref _enforcedPolicyOptions, value,
additionalPropertyNames: new[]
{
nameof(IsPolicyInEffect),
nameof(IsUppercaseSwitchEnabled),
nameof(IsLowercaseSwitchEnabled),
nameof(IsNumberSwitchEnabled),
nameof(IsSpecialSwitchEnabled)
});
}
public bool IsPolicyInEffect => _enforcedPolicyOptions.MinLength > 0 || _enforcedPolicyOptions.UseUppercase ||
_enforcedPolicyOptions.UseLowercase || _enforcedPolicyOptions.UseNumbers ||
_enforcedPolicyOptions.NumberCount > 0 || _enforcedPolicyOptions.UseSpecial ||
_enforcedPolicyOptions.SpecialCount > 0;
public bool IsUppercaseSwitchEnabled => !IsPolicyInEffect || !EnforcedPolicyOptions.UseUppercase;
public bool IsLowercaseSwitchEnabled => !IsPolicyInEffect || !EnforcedPolicyOptions.UseLowercase;
public bool IsNumberSwitchEnabled => !IsPolicyInEffect || !EnforcedPolicyOptions.UseNumbers;
public bool IsSpecialSwitchEnabled => !IsPolicyInEffect || !EnforcedPolicyOptions.UseSpecial;
public int TypeSelectedIndex public int TypeSelectedIndex
{ {
get => _typeSelectedIndex; get => _typeSelectedIndex;
@ -237,7 +265,7 @@ namespace Bit.App.Pages
public async Task InitAsync() public async Task InitAsync()
{ {
_options = await _passwordGenerationService.GetOptionsAsync(); (_options, EnforcedPolicyOptions) = await _passwordGenerationService.GetOptionsAsync();
LoadFromOptions(); LoadFromOptions();
await RegenerateAsync(); await RegenerateAsync();
_doneIniting = true; _doneIniting = true;
@ -256,7 +284,7 @@ namespace Bit.App.Pages
return; return;
} }
SetOptions(); SetOptions();
_passwordGenerationService.NormalizeOptions(_options); _passwordGenerationService.NormalizeOptions(_options, _enforcedPolicyOptions);
await _passwordGenerationService.SaveOptionsAsync(_options); await _passwordGenerationService.SaveOptionsAsync(_options);
LoadFromOptions(); LoadFromOptions();
if(regenerate) if(regenerate)
@ -274,7 +302,7 @@ namespace Bit.App.Pages
public async Task SliderInputAsync() public async Task SliderInputAsync()
{ {
SetOptions(); SetOptions();
_passwordGenerationService.NormalizeOptions(_options); _passwordGenerationService.NormalizeOptions(_options, _enforcedPolicyOptions);
Password = await _passwordGenerationService.GeneratePasswordAsync(_options); Password = await _passwordGenerationService.GeneratePasswordAsync(_options);
} }

View file

@ -2835,5 +2835,11 @@ namespace Bit.App.Resources {
return ResourceManager.GetString("Clone", resourceCulture); return ResourceManager.GetString("Clone", resourceCulture);
} }
} }
public static string PasswordGeneratorPolicyInEffect {
get {
return ResourceManager.GetString("PasswordGeneratorPolicyInEffect", resourceCulture);
}
}
} }
} }

View file

@ -1612,4 +1612,7 @@
<value>Clone</value> <value>Clone</value>
<comment>Clone an entity (verb).</comment> <comment>Clone an entity (verb).</comment>
</data> </data>
<data name="PasswordGeneratorPolicyInEffect" xml:space="preserve">
<value>One or more organization policies are affecting your generator settings</value>
</data>
</root> </root>

View file

@ -12,9 +12,9 @@ namespace Bit.Core.Abstractions
Task<string> GeneratePassphraseAsync(PasswordGenerationOptions options); Task<string> GeneratePassphraseAsync(PasswordGenerationOptions options);
Task<string> GeneratePasswordAsync(PasswordGenerationOptions options); Task<string> GeneratePasswordAsync(PasswordGenerationOptions options);
Task<List<GeneratedPasswordHistory>> GetHistoryAsync(); Task<List<GeneratedPasswordHistory>> GetHistoryAsync();
Task<PasswordGenerationOptions> GetOptionsAsync(); Task<(PasswordGenerationOptions, PasswordGeneratorPolicyOptions)> GetOptionsAsync();
Task<object> PasswordStrength(string password, List<string> userInputs = null); Task<object> PasswordStrength(string password, List<string> userInputs = null);
Task SaveOptionsAsync(PasswordGenerationOptions options); Task SaveOptionsAsync(PasswordGenerationOptions options);
void NormalizeOptions(PasswordGenerationOptions options); void NormalizeOptions(PasswordGenerationOptions options, PasswordGeneratorPolicyOptions enforcedPolicyOptions);
} }
} }

View file

@ -0,0 +1,13 @@
namespace Bit.Core.Models.Domain
{
public class PasswordGeneratorPolicyOptions
{
public int MinLength { get; set; }
public bool UseUppercase { get; set; }
public bool UseLowercase { get; set; }
public bool UseNumbers { get; set; }
public int NumberCount { get; set; }
public bool UseSpecial { get; set; }
public int SpecialCount { get; set; }
}
}

View file

@ -7,6 +7,7 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.Core.Enums;
namespace Bit.Core.Services namespace Bit.Core.Services
{ {
@ -23,6 +24,7 @@ namespace Bit.Core.Services
private readonly ICryptoService _cryptoService; private readonly ICryptoService _cryptoService;
private readonly IStorageService _storageService; private readonly IStorageService _storageService;
private readonly ICryptoFunctionService _cryptoFunctionService; private readonly ICryptoFunctionService _cryptoFunctionService;
private readonly IPolicyService _policyService;
private PasswordGenerationOptions _defaultOptions = new PasswordGenerationOptions(true); private PasswordGenerationOptions _defaultOptions = new PasswordGenerationOptions(true);
private PasswordGenerationOptions _optionsCache; private PasswordGenerationOptions _optionsCache;
private List<GeneratedPasswordHistory> _history; private List<GeneratedPasswordHistory> _history;
@ -30,11 +32,13 @@ namespace Bit.Core.Services
public PasswordGenerationService( public PasswordGenerationService(
ICryptoService cryptoService, ICryptoService cryptoService,
IStorageService storageService, IStorageService storageService,
ICryptoFunctionService cryptoFunctionService) ICryptoFunctionService cryptoFunctionService,
IPolicyService policyService)
{ {
_cryptoService = cryptoService; _cryptoService = cryptoService;
_storageService = storageService; _storageService = storageService;
_cryptoFunctionService = cryptoFunctionService; _cryptoFunctionService = cryptoFunctionService;
_policyService = policyService;
} }
public async Task<string> GeneratePasswordAsync(PasswordGenerationOptions options) public async Task<string> GeneratePasswordAsync(PasswordGenerationOptions options)
@ -240,7 +244,7 @@ namespace Bit.Core.Services
return string.Join(options.WordSeparator, wordList); return string.Join(options.WordSeparator, wordList);
} }
public async Task<PasswordGenerationOptions> GetOptionsAsync() public async Task<(PasswordGenerationOptions,PasswordGeneratorPolicyOptions)> GetOptionsAsync()
{ {
if(_optionsCache == null) if(_optionsCache == null)
{ {
@ -255,7 +259,129 @@ namespace Bit.Core.Services
_optionsCache = options; _optionsCache = options;
} }
} }
return _optionsCache;
var enforcedPolicyOptions = await GetPasswordGeneratorPolicyOptions();
if(enforcedPolicyOptions != null)
{
if(_optionsCache.Length < enforcedPolicyOptions.MinLength)
{
_optionsCache.Length = enforcedPolicyOptions.MinLength;
}
if(enforcedPolicyOptions.UseUppercase)
{
_optionsCache.Uppercase = true;
}
if(enforcedPolicyOptions.UseLowercase)
{
_optionsCache.Lowercase = true;
}
if(enforcedPolicyOptions.UseNumbers)
{
_optionsCache.Number = true;
}
if(_optionsCache.MinNumber < enforcedPolicyOptions.NumberCount)
{
_optionsCache.MinNumber = enforcedPolicyOptions.NumberCount;
}
if(enforcedPolicyOptions.UseSpecial)
{
_optionsCache.Special = true;
}
if(_optionsCache.MinSpecial < enforcedPolicyOptions.SpecialCount)
{
_optionsCache.MinSpecial = enforcedPolicyOptions.SpecialCount;
}
// Must normalize these fields because the receiving call expects all options to pass the current rules
if(_optionsCache.MinSpecial + _optionsCache.MinNumber > _optionsCache.Length)
{
_optionsCache.MinSpecial = _optionsCache.Length - _optionsCache.MinNumber;
}
}
else
{
// UI layer expects an instantiated object to prevent more explicit null checks
enforcedPolicyOptions = new PasswordGeneratorPolicyOptions();
}
return (_optionsCache, enforcedPolicyOptions);
}
public async Task<PasswordGeneratorPolicyOptions> GetPasswordGeneratorPolicyOptions()
{
var policies = await _policyService.GetAll(PolicyType.PasswordGenerator);
PasswordGeneratorPolicyOptions enforcedOptions = null;
if(policies == null || !policies.Any())
{
return enforcedOptions;
}
foreach(var currentPolicy in policies)
{
if(!currentPolicy.Enabled || currentPolicy.Data == null)
{
continue;
}
if(enforcedOptions == null)
{
enforcedOptions = new PasswordGeneratorPolicyOptions();
}
var currentPolicyMinLength = currentPolicy.Data["minLength"];
if(currentPolicyMinLength != null &&
(int)(long)currentPolicyMinLength > enforcedOptions.MinLength)
{
enforcedOptions.MinLength = (int)(long)currentPolicyMinLength;
}
var currentPolicyUseUpper = currentPolicy.Data["useUpper"];
if(currentPolicyUseUpper != null && (bool)currentPolicyUseUpper)
{
enforcedOptions.UseUppercase = true;
}
var currentPolicyUseLower = currentPolicy.Data["useLower"];
if(currentPolicyUseLower != null && (bool)currentPolicyUseLower)
{
enforcedOptions.UseLowercase = true;
}
var currentPolicyUseNumbers = currentPolicy.Data["useNumbers"];
if(currentPolicyUseNumbers != null && (bool)currentPolicyUseNumbers)
{
enforcedOptions.UseNumbers = true;
}
var currentPolicyMinNumbers = currentPolicy.Data["minNumbers"];
if(currentPolicyMinNumbers != null &&
(int)(long)currentPolicyMinNumbers > enforcedOptions.NumberCount)
{
enforcedOptions.NumberCount = (int)(long)currentPolicyMinNumbers;
}
var currentPolicyUseSpecial = currentPolicy.Data["useSpecial"];
if(currentPolicyUseSpecial != null && (bool)currentPolicyUseSpecial)
{
enforcedOptions.UseSpecial = true;
}
var currentPolicyMinSpecial = currentPolicy.Data["minSpecial"];
if(currentPolicyMinSpecial != null &&
(int)(long)currentPolicyMinSpecial > enforcedOptions.SpecialCount)
{
enforcedOptions.SpecialCount = (int)(long)currentPolicyMinSpecial;
}
}
return enforcedOptions;
} }
public async Task SaveOptionsAsync(PasswordGenerationOptions options) public async Task SaveOptionsAsync(PasswordGenerationOptions options)
@ -315,7 +441,8 @@ namespace Bit.Core.Services
throw new NotImplementedException(); throw new NotImplementedException();
} }
public void NormalizeOptions(PasswordGenerationOptions options) public void NormalizeOptions(PasswordGenerationOptions options,
PasswordGeneratorPolicyOptions enforcedPolicyOptions)
{ {
options.MinLowercase = 0; options.MinLowercase = 0;
options.MinUppercase = 0; options.MinUppercase = 0;
@ -336,6 +463,11 @@ namespace Bit.Core.Services
options.Length = 128; options.Length = 128;
} }
if(options.Length < enforcedPolicyOptions.MinLength)
{
options.Length = enforcedPolicyOptions.MinLength;
}
if(options.MinNumber == null) if(options.MinNumber == null)
{ {
options.MinNumber = 0; options.MinNumber = 0;
@ -348,6 +480,11 @@ namespace Bit.Core.Services
{ {
options.MinNumber = 9; options.MinNumber = 9;
} }
if(options.MinNumber < enforcedPolicyOptions.NumberCount)
{
options.MinNumber = enforcedPolicyOptions.NumberCount;
}
if(options.MinSpecial == null) if(options.MinSpecial == null)
{ {
@ -362,6 +499,11 @@ namespace Bit.Core.Services
options.MinSpecial = 9; options.MinSpecial = 9;
} }
if(options.MinSpecial < enforcedPolicyOptions.SpecialCount)
{
options.MinSpecial = enforcedPolicyOptions.SpecialCount;
}
if(options.MinSpecial + options.MinNumber > options.Length) if(options.MinSpecial + options.MinNumber > options.Length)
{ {
options.MinSpecial = options.Length - options.MinNumber; options.MinSpecial = options.Length - options.MinNumber;

View file

@ -56,7 +56,7 @@ namespace Bit.Core.Utilities
return Task.FromResult(0); return Task.FromResult(0);
}); });
var passwordGenerationService = new PasswordGenerationService(cryptoService, storageService, var passwordGenerationService = new PasswordGenerationService(cryptoService, storageService,
cryptoFunctionService); cryptoFunctionService, policyService);
var totpService = new TotpService(storageService, cryptoFunctionService); var totpService = new TotpService(storageService, cryptoFunctionService);
var authService = new AuthService(cryptoService, apiService, userService, tokenService, appIdService, var authService = new AuthService(cryptoService, apiService, userService, tokenService, appIdService,
i18nService, platformUtilsService, messagingService, lockService); i18nService, platformUtilsService, messagingService, lockService);

View file

@ -71,7 +71,7 @@ namespace Bit.iOS.Core.Controllers
OptionsTableViewController.TableView.SeparatorColor = ThemeHelpers.SeparatorColor; OptionsTableViewController.TableView.SeparatorColor = ThemeHelpers.SeparatorColor;
} }
var options = await _passwordGenerationService.GetOptionsAsync(); var (options, enforcedPolicyOptions) = await _passwordGenerationService.GetOptionsAsync();
UppercaseCell.Switch.On = options.Uppercase.GetValueOrDefault(); UppercaseCell.Switch.On = options.Uppercase.GetValueOrDefault();
LowercaseCell.Switch.On = options.Lowercase.GetValueOrDefault(true); LowercaseCell.Switch.On = options.Lowercase.GetValueOrDefault(true);
SpecialCell.Switch.On = options.Special.GetValueOrDefault(); SpecialCell.Switch.On = options.Special.GetValueOrDefault();