diff --git a/mRemoteNGTests/Security/PasswordCreation/PasswordIncludesSpecialCharactersConstraintTests.cs b/mRemoteNGTests/Security/PasswordCreation/PasswordIncludesSpecialCharactersConstraintTests.cs new file mode 100644 index 000000000..2ba1747eb --- /dev/null +++ b/mRemoteNGTests/Security/PasswordCreation/PasswordIncludesSpecialCharactersConstraintTests.cs @@ -0,0 +1,73 @@ +using System; +using mRemoteNG.Security; +using mRemoteNG.Security.PasswordCreation; +using NUnit.Framework; + + +namespace mRemoteNGTests.Security.PasswordCreation +{ + public class PasswordIncludesSpecialCharactersConstraintTests + { + private PasswordIncludesSpecialCharactersConstraint _specialCharactersConstraint; + + [Test] + public void PasswordWithMinimumSpecialCharsPassesValidation() + { + var password = "hello$".ConvertToSecureString(); + _specialCharactersConstraint = new PasswordIncludesSpecialCharactersConstraint(); + Assert.That(_specialCharactersConstraint.Validate(password), Is.True); + } + + [Test] + public void PasswordExceedingMinimumSpecialCharsPassesValidation() + { + var password = "hello!#%$".ConvertToSecureString(); + _specialCharactersConstraint = new PasswordIncludesSpecialCharactersConstraint(3); + Assert.That(_specialCharactersConstraint.Validate(password), Is.True); + } + + [Test] + public void PasswordUnderMinimumSpecialCharsFailsValidation() + { + var password = "hello!".ConvertToSecureString(); + _specialCharactersConstraint = new PasswordIncludesSpecialCharactersConstraint(2); + Assert.That(_specialCharactersConstraint.Validate(password), Is.False); + } + + [Test] + public void PasswordWithoutSpecialCharsFailsValidation() + { + var password = "hello".ConvertToSecureString(); + _specialCharactersConstraint = new PasswordIncludesSpecialCharactersConstraint(); + Assert.That(_specialCharactersConstraint.Validate(password), Is.False); + } + + [Test] + public void PasswordMatchingCustomCharsPassesValidation() + { + var password = "hello(".ConvertToSecureString(); + _specialCharactersConstraint = new PasswordIncludesSpecialCharactersConstraint(new[] {'('}); + Assert.That(_specialCharactersConstraint.Validate(password), Is.True); + } + + [Test] + public void PasswordWithoutCustomCharsFailsValidation() + { + var password = "hello!".ConvertToSecureString(); + _specialCharactersConstraint = new PasswordIncludesSpecialCharactersConstraint(new[] { '(' }); + Assert.That(_specialCharactersConstraint.Validate(password), Is.False); + } + + [Test] + public void CantProvideNullListOfCharacters() + { + Assert.Throws(() => new PasswordIncludesSpecialCharactersConstraint(null)); + } + + [Test] + public void MinimumCountMustBeAPositiveValue() + { + Assert.Throws(() => new PasswordIncludesSpecialCharactersConstraint(-1)); + } + } +} \ No newline at end of file diff --git a/mRemoteNGTests/mRemoteNGTests.csproj b/mRemoteNGTests/mRemoteNGTests.csproj index 5038277e5..5bde74f7d 100644 --- a/mRemoteNGTests/mRemoteNGTests.csproj +++ b/mRemoteNGTests/mRemoteNGTests.csproj @@ -140,6 +140,7 @@ + diff --git a/mRemoteV1/Resources/Language/Language.Designer.cs b/mRemoteV1/Resources/Language/Language.Designer.cs index 2c1e02936..ea56087ce 100644 --- a/mRemoteV1/Resources/Language/Language.Designer.cs +++ b/mRemoteV1/Resources/Language/Language.Designer.cs @@ -3820,6 +3820,15 @@ namespace mRemoteNG { } } + /// + /// Looks up a localized string similar to Password must contain at least {0} of the following characters: {1}. + /// + internal static string strPasswordConstainsSpecialCharactersConstraintHint { + get { + return ResourceManager.GetString("strPasswordConstainsSpecialCharactersConstraintHint", resourceCulture); + } + } + /// /// Looks up a localized string similar to Password must contain at least {0} lower case character(s). /// diff --git a/mRemoteV1/Resources/Language/Language.resx b/mRemoteV1/Resources/Language/Language.resx index 05a6a1b75..3e7eea97f 100644 --- a/mRemoteV1/Resources/Language/Language.resx +++ b/mRemoteV1/Resources/Language/Language.resx @@ -2463,6 +2463,9 @@ mRemoteNG will now quit and begin with the installation. Alert on Idle Disconnect + + Password must contain at least {0} of the following characters: {1} + Password must contain at least {0} lower case character(s) diff --git a/mRemoteV1/Security/PasswordCreation/PasswordIncludesSpecialCharactersConstraint.cs b/mRemoteV1/Security/PasswordCreation/PasswordIncludesSpecialCharactersConstraint.cs new file mode 100644 index 000000000..46bf16167 --- /dev/null +++ b/mRemoteV1/Security/PasswordCreation/PasswordIncludesSpecialCharactersConstraint.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Security; +using System.Text.RegularExpressions; + + +namespace mRemoteNG.Security.PasswordCreation +{ + public class PasswordIncludesSpecialCharactersConstraint : IPasswordConstraint + { + private readonly int _minimumCount; + + public IEnumerable SpecialCharacters { get; } = new []{'!','@','#','$','%','^','&','*'}; + + public string ConstraintHint { get; } + + public PasswordIncludesSpecialCharactersConstraint(int minimumCount = 1) + { + if (minimumCount < 0) + throw new ArgumentException($"{nameof(minimumCount)} must be a positive value"); + + _minimumCount = minimumCount; + } + + public PasswordIncludesSpecialCharactersConstraint(IEnumerable specialCharacters, int minimumCount = 1) + : this(minimumCount) + { + if (specialCharacters == null) + throw new ArgumentNullException(nameof(specialCharacters)); + + SpecialCharacters = specialCharacters; + ConstraintHint = string.Format(Language.strPasswordConstainsSpecialCharactersConstraintHint, _minimumCount, string.Concat(SpecialCharacters)); + } + + public bool Validate(SecureString password) + { + var regex = new Regex($"[{string.Concat(SpecialCharacters)}]"); + return regex.Matches(password.ConvertToUnsecureString()).Count >= _minimumCount; + } + } +} \ No newline at end of file diff --git a/mRemoteV1/mRemoteV1.csproj b/mRemoteV1/mRemoteV1.csproj index 1acf3e343..008e2aef9 100644 --- a/mRemoteV1/mRemoteV1.csproj +++ b/mRemoteV1/mRemoteV1.csproj @@ -226,6 +226,7 @@ +