diff --git a/src/App/Pages/Generator/GeneratorPage.xaml b/src/App/Pages/Generator/GeneratorPage.xaml index 82ecf6d7a..efecf5b68 100644 --- a/src/App/Pages/Generator/GeneratorPage.xaml +++ b/src/App/Pages/Generator/GeneratorPage.xaml @@ -39,6 +39,29 @@ + + + + + + + + + + + @@ -163,6 +187,7 @@ HorizontalOptions="StartAndExpand" /> @@ -174,6 +199,7 @@ HorizontalOptions="StartAndExpand" /> @@ -185,6 +211,7 @@ HorizontalOptions="StartAndExpand" /> diff --git a/src/App/Pages/Generator/GeneratorPageViewModel.cs b/src/App/Pages/Generator/GeneratorPageViewModel.cs index e4c6ca1c1..afe888ac7 100644 --- a/src/App/Pages/Generator/GeneratorPageViewModel.cs +++ b/src/App/Pages/Generator/GeneratorPageViewModel.cs @@ -15,6 +15,7 @@ namespace Bit.App.Pages private readonly IPlatformUtilsService _platformUtilsService; private PasswordGenerationOptions _options; + private PasswordGeneratorPolicyOptions _enforcedPolicyOptions; private string _password; private bool _isPassword; 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 { get => _typeSelectedIndex; @@ -237,7 +265,7 @@ namespace Bit.App.Pages public async Task InitAsync() { - _options = await _passwordGenerationService.GetOptionsAsync(); + (_options, EnforcedPolicyOptions) = await _passwordGenerationService.GetOptionsAsync(); LoadFromOptions(); await RegenerateAsync(); _doneIniting = true; @@ -256,7 +284,7 @@ namespace Bit.App.Pages return; } SetOptions(); - _passwordGenerationService.NormalizeOptions(_options); + _passwordGenerationService.NormalizeOptions(_options, _enforcedPolicyOptions); await _passwordGenerationService.SaveOptionsAsync(_options); LoadFromOptions(); if(regenerate) @@ -274,7 +302,7 @@ namespace Bit.App.Pages public async Task SliderInputAsync() { SetOptions(); - _passwordGenerationService.NormalizeOptions(_options); + _passwordGenerationService.NormalizeOptions(_options, _enforcedPolicyOptions); Password = await _passwordGenerationService.GeneratePasswordAsync(_options); } diff --git a/src/App/Resources/AppResources.Designer.cs b/src/App/Resources/AppResources.Designer.cs index 8918f9543..2caea6fab 100644 --- a/src/App/Resources/AppResources.Designer.cs +++ b/src/App/Resources/AppResources.Designer.cs @@ -2835,5 +2835,11 @@ namespace Bit.App.Resources { return ResourceManager.GetString("Clone", resourceCulture); } } + + public static string PasswordGeneratorPolicyInEffect { + get { + return ResourceManager.GetString("PasswordGeneratorPolicyInEffect", resourceCulture); + } + } } } diff --git a/src/App/Resources/AppResources.resx b/src/App/Resources/AppResources.resx index 758d48f51..3ae080eba 100644 --- a/src/App/Resources/AppResources.resx +++ b/src/App/Resources/AppResources.resx @@ -1612,4 +1612,7 @@ Clone Clone an entity (verb). + + One or more organization policies are affecting your generator settings + \ No newline at end of file diff --git a/src/Core/Abstractions/IPasswordGenerationService.cs b/src/Core/Abstractions/IPasswordGenerationService.cs index dc91d82d7..aee4cd506 100644 --- a/src/Core/Abstractions/IPasswordGenerationService.cs +++ b/src/Core/Abstractions/IPasswordGenerationService.cs @@ -12,9 +12,9 @@ namespace Bit.Core.Abstractions Task GeneratePassphraseAsync(PasswordGenerationOptions options); Task GeneratePasswordAsync(PasswordGenerationOptions options); Task> GetHistoryAsync(); - Task GetOptionsAsync(); + Task<(PasswordGenerationOptions, PasswordGeneratorPolicyOptions)> GetOptionsAsync(); Task PasswordStrength(string password, List userInputs = null); Task SaveOptionsAsync(PasswordGenerationOptions options); - void NormalizeOptions(PasswordGenerationOptions options); + void NormalizeOptions(PasswordGenerationOptions options, PasswordGeneratorPolicyOptions enforcedPolicyOptions); } -} \ No newline at end of file +} diff --git a/src/Core/Models/Domain/PasswordGeneratorPolicyOptions.cs b/src/Core/Models/Domain/PasswordGeneratorPolicyOptions.cs new file mode 100644 index 000000000..62a78756e --- /dev/null +++ b/src/Core/Models/Domain/PasswordGeneratorPolicyOptions.cs @@ -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; } + } +} diff --git a/src/Core/Services/PasswordGenerationService.cs b/src/Core/Services/PasswordGenerationService.cs index 4519230e4..eeb91cc8a 100644 --- a/src/Core/Services/PasswordGenerationService.cs +++ b/src/Core/Services/PasswordGenerationService.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; +using Bit.Core.Enums; namespace Bit.Core.Services { @@ -23,6 +24,7 @@ namespace Bit.Core.Services private readonly ICryptoService _cryptoService; private readonly IStorageService _storageService; private readonly ICryptoFunctionService _cryptoFunctionService; + private readonly IPolicyService _policyService; private PasswordGenerationOptions _defaultOptions = new PasswordGenerationOptions(true); private PasswordGenerationOptions _optionsCache; private List _history; @@ -30,11 +32,13 @@ namespace Bit.Core.Services public PasswordGenerationService( ICryptoService cryptoService, IStorageService storageService, - ICryptoFunctionService cryptoFunctionService) + ICryptoFunctionService cryptoFunctionService, + IPolicyService policyService) { _cryptoService = cryptoService; _storageService = storageService; _cryptoFunctionService = cryptoFunctionService; + _policyService = policyService; } public async Task GeneratePasswordAsync(PasswordGenerationOptions options) @@ -240,7 +244,7 @@ namespace Bit.Core.Services return string.Join(options.WordSeparator, wordList); } - public async Task GetOptionsAsync() + public async Task<(PasswordGenerationOptions,PasswordGeneratorPolicyOptions)> GetOptionsAsync() { if(_optionsCache == null) { @@ -255,7 +259,129 @@ namespace Bit.Core.Services _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 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) @@ -315,7 +441,8 @@ namespace Bit.Core.Services throw new NotImplementedException(); } - public void NormalizeOptions(PasswordGenerationOptions options) + public void NormalizeOptions(PasswordGenerationOptions options, + PasswordGeneratorPolicyOptions enforcedPolicyOptions) { options.MinLowercase = 0; options.MinUppercase = 0; @@ -336,6 +463,11 @@ namespace Bit.Core.Services options.Length = 128; } + if(options.Length < enforcedPolicyOptions.MinLength) + { + options.Length = enforcedPolicyOptions.MinLength; + } + if(options.MinNumber == null) { options.MinNumber = 0; @@ -348,6 +480,11 @@ namespace Bit.Core.Services { options.MinNumber = 9; } + + if(options.MinNumber < enforcedPolicyOptions.NumberCount) + { + options.MinNumber = enforcedPolicyOptions.NumberCount; + } if(options.MinSpecial == null) { @@ -362,6 +499,11 @@ namespace Bit.Core.Services options.MinSpecial = 9; } + if(options.MinSpecial < enforcedPolicyOptions.SpecialCount) + { + options.MinSpecial = enforcedPolicyOptions.SpecialCount; + } + if(options.MinSpecial + options.MinNumber > options.Length) { options.MinSpecial = options.Length - options.MinNumber; diff --git a/src/Core/Utilities/ServiceContainer.cs b/src/Core/Utilities/ServiceContainer.cs index 768fdab4e..10f2e58ef 100644 --- a/src/Core/Utilities/ServiceContainer.cs +++ b/src/Core/Utilities/ServiceContainer.cs @@ -56,7 +56,7 @@ namespace Bit.Core.Utilities return Task.FromResult(0); }); var passwordGenerationService = new PasswordGenerationService(cryptoService, storageService, - cryptoFunctionService); + cryptoFunctionService, policyService); var totpService = new TotpService(storageService, cryptoFunctionService); var authService = new AuthService(cryptoService, apiService, userService, tokenService, appIdService, i18nService, platformUtilsService, messagingService, lockService); diff --git a/src/iOS.Core/Controllers/PasswordGeneratorViewController.cs b/src/iOS.Core/Controllers/PasswordGeneratorViewController.cs index 6d967fc6a..16222c5b6 100644 --- a/src/iOS.Core/Controllers/PasswordGeneratorViewController.cs +++ b/src/iOS.Core/Controllers/PasswordGeneratorViewController.cs @@ -71,7 +71,7 @@ namespace Bit.iOS.Core.Controllers OptionsTableViewController.TableView.SeparatorColor = ThemeHelpers.SeparatorColor; } - var options = await _passwordGenerationService.GetOptionsAsync(); + var (options, enforcedPolicyOptions) = await _passwordGenerationService.GetOptionsAsync(); UppercaseCell.Switch.On = options.Uppercase.GetValueOrDefault(); LowercaseCell.Switch.On = options.Lowercase.GetValueOrDefault(true); SpecialCell.Switch.On = options.Special.GetValueOrDefault();