Add credential verification helpers
This commit is contained in:
parent
40c4faf82b
commit
8b373ebfe8
2 changed files with 108 additions and 36 deletions
|
|
@ -15,9 +15,6 @@ namespace CapyKit.Helpers
|
|||
{
|
||||
#region Members
|
||||
|
||||
/// <summary> Default size to use when generating a new salt. </summary>
|
||||
private const int SALT_SIZE = 32;
|
||||
|
||||
/// <summary> A string of all the lower case characters. </summary>
|
||||
internal const string LOWER_CASE_CHARACTERS = "abcdefghijklmnopqrstuvwxyz";
|
||||
|
||||
|
|
@ -33,6 +30,14 @@ namespace CapyKit.Helpers
|
|||
#endregion Members
|
||||
|
||||
#region Methods
|
||||
|
||||
public static bool CompareHashedPassword<T>(Password existingPassword, string password, string salt,
|
||||
params object[] args)
|
||||
{
|
||||
var saltBytes = Convert.FromBase64String(salt);
|
||||
|
||||
return CompareHashedPassword<T>(existingPassword, password, saltBytes, args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares an unencrypted <paramref name="password"/> with a stored, encrypted <paramref name="existingPassword"/>.
|
||||
|
|
@ -47,7 +52,8 @@ namespace CapyKit.Helpers
|
|||
/// <returns>
|
||||
/// <see langword="true"/> if hash comparison succeeds, <see langword="false"/> if it fails.
|
||||
/// </returns>
|
||||
public static bool CompareHashedPassword<T>(Password existingPassword, string password, byte[] salt, params object[] args)
|
||||
public static bool CompareHashedPassword<T>(Password existingPassword, string password, byte[] salt,
|
||||
params object[] args)
|
||||
{
|
||||
var providedPassword = typeof(SecurityHelper)
|
||||
.GetMethod("GetPassword", new Type[] { typeof(string), typeof(byte[]), typeof(object[]) })
|
||||
|
|
@ -56,6 +62,14 @@ namespace CapyKit.Helpers
|
|||
|
||||
return existingPassword.Equals(providedPassword);
|
||||
}
|
||||
|
||||
public static bool CompareHashedPassword(Password existingPassword, string password, string salt,
|
||||
IPasswordAlgorithm algorithm, params object[] args)
|
||||
{
|
||||
var saltBytes = Convert.FromBase64String(salt);
|
||||
|
||||
return CompareHashedPassword(existingPassword, password, saltBytes, algorithm, args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares an unencrypted <paramref name="password"/> with a stored, encrypted <paramref name="existingPassword"/>.
|
||||
|
|
@ -70,18 +84,49 @@ namespace CapyKit.Helpers
|
|||
/// <returns>
|
||||
/// <see langword="true"/> if hash comparison succeeds, <see langword="false"/> if it fails.
|
||||
/// </returns>
|
||||
public static bool CompareHashedPassword(Password existingPassword, string password, byte[] salt, IPasswordAlgorithm algorithm, params object[] args)
|
||||
public static bool CompareHashedPassword(Password existingPassword, string password, byte[] salt,
|
||||
IPasswordAlgorithm algorithm, params object[] args)
|
||||
{
|
||||
var algorithmType = algorithm.GetType();
|
||||
|
||||
var providedPassword = typeof(SecurityHelper)
|
||||
.GetMethod("GetPassword", new Type[] {typeof(string), typeof(byte[]), typeof(object[])})
|
||||
.GetMethod("GetPassword", new Type[] { typeof(string), typeof(byte[]), typeof(object[]) })
|
||||
?.MakeGenericMethod(algorithmType)
|
||||
?.Invoke(null, new object[] { password, salt, args });
|
||||
|
||||
return existingPassword.Equals(providedPassword);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies a plaintext password against a persisted PBKDF2 hash and salt.
|
||||
/// </summary>
|
||||
/// <param name="password">The plaintext password supplied by the caller.</param>
|
||||
/// <param name="storedHash">The persisted PBKDF2 hash.</param>
|
||||
/// <param name="salt">The persisted PBKDF2 salt.</param>
|
||||
/// <returns><see langword="true"/> when the password matches the persisted hash.</returns>
|
||||
public static bool VerifyPbkdf2(string password, byte[] storedHash, byte[] salt)
|
||||
{
|
||||
if (string.IsNullOrEmpty(password) || storedHash == null || salt == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var candidateHash = Pbkdf2(password, salt).Hash;
|
||||
return CryptographicOperations.FixedTimeEquals(storedHash, candidateHash);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Produces a deterministic SHA-256 digest for an opaque secret that must be looked up
|
||||
/// without persisting the raw value.
|
||||
/// </summary>
|
||||
/// <param name="secret">The raw opaque secret.</param>
|
||||
/// <returns>The Base64-encoded digest.</returns>
|
||||
public static string HashOpaqueSecret(string secret)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(secret);
|
||||
return Convert.ToBase64String(SHA256.HashData(Encoding.UTF8.GetBytes(secret)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a <see cref="Password"/> object using the specified password and generates a random salt value.
|
||||
/// Then it uses that salt to call the overloaded <see cref="GetPassword{T}(string, byte[], object[])"/> method with the given password and
|
||||
|
|
@ -98,9 +143,9 @@ namespace CapyKit.Helpers
|
|||
/// instance of <typeparamref name="T"/> created using any optional constructor arguments provided.
|
||||
/// </returns>
|
||||
/// <seealso cref="SecurityHelper.SALT_SIZE"/>
|
||||
public static Password GetPassword<T>(string password, params object[] args)
|
||||
public static Password GetPassword<T>(string password, int saltSize, params object[] args) where T : IPasswordAlgorithm
|
||||
{
|
||||
var salt = SecurityHelper.GetSalt;
|
||||
var salt = GetRandomBytes(saltSize);
|
||||
return GetPassword<T>(password, salt, args);
|
||||
}
|
||||
|
||||
|
|
@ -129,14 +174,16 @@ namespace CapyKit.Helpers
|
|||
/// A new <see cref="Password"/> object with the given password and salt, as well as an instance
|
||||
/// of <typeparamref name="T"/> created using the provided constructor arguments.
|
||||
/// </returns>
|
||||
public static Password GetPassword<T>(string password, byte[] salt, params object[] args) where T : IPasswordAlgorithm
|
||||
public static Password GetPassword<T>(string password, byte[] salt, params object[] args)
|
||||
where T : IPasswordAlgorithm
|
||||
{
|
||||
//var allArgs = args.Prepend(salt).Prepend(password).ToArray(); // Prepend in reverse order so that password precedes salt.
|
||||
var argTypes = args.Select(arg => arg.GetType()).ToArray();
|
||||
var algorithmConstructor = typeof(T).GetConstructor(argTypes);
|
||||
if (algorithmConstructor == null)
|
||||
{
|
||||
CapyEventReporter.EmitEvent(EventLevel.Error, "Cannot find a constructor for {0} that matches the given arguments: {1}",
|
||||
CapyEventReporter.EmitEvent(EventLevel.Error,
|
||||
"Cannot find a constructor for {0} that matches the given arguments: {1}",
|
||||
args: new[]
|
||||
{
|
||||
typeof(T).Name,
|
||||
|
|
@ -149,7 +196,8 @@ namespace CapyKit.Helpers
|
|||
|
||||
if (passwordInstance == null)
|
||||
{
|
||||
CapyEventReporter.EmitEvent(EventLevel.Error, "There was an error invoking the constructor for {0} with the given arguments: {1}",
|
||||
CapyEventReporter.EmitEvent(EventLevel.Error,
|
||||
"There was an error invoking the constructor for {0} with the given arguments: {1}",
|
||||
args: new[]
|
||||
{
|
||||
typeof(T).Name,
|
||||
|
|
@ -198,9 +246,9 @@ namespace CapyKit.Helpers
|
|||
/// <returns>
|
||||
/// A new <see cref="Password"/> object containing the hashed password and a randomly generated salt.
|
||||
/// </returns>
|
||||
public static Password Pbkdf2(string password)
|
||||
public static Password Pbkdf2(string password, int saltSize)
|
||||
{
|
||||
var salt = SecurityHelper.GetRandomBytes(SALT_SIZE);
|
||||
var salt = SecurityHelper.GetRandomBytes(saltSize);
|
||||
var pwd = new Password(password, salt, Password.Pbkdf2Algorithm);
|
||||
|
||||
return pwd;
|
||||
|
|
@ -217,9 +265,15 @@ namespace CapyKit.Helpers
|
|||
{
|
||||
if (validCharacters.Length == 0)
|
||||
{
|
||||
CapyEventReporter.EmitEvent(EventLevel.Warning, "No valid characters were provided, so all valid caharacters will be assumed.");
|
||||
validCharacters = new[] { ValidCharacterCollection.Lowercase, ValidCharacterCollection.Uppercase, ValidCharacterCollection.Numbers, ValidCharacterCollection.Special };
|
||||
CapyEventReporter.EmitEvent(EventLevel.Warning,
|
||||
"No valid characters were provided, so all valid caharacters will be assumed.");
|
||||
validCharacters = new[]
|
||||
{
|
||||
ValidCharacterCollection.Lowercase, ValidCharacterCollection.Uppercase,
|
||||
ValidCharacterCollection.Numbers, ValidCharacterCollection.Special
|
||||
};
|
||||
}
|
||||
|
||||
return GetRandomString(length, validCharacters);
|
||||
}
|
||||
|
||||
|
|
@ -235,6 +289,7 @@ namespace CapyKit.Helpers
|
|||
{
|
||||
return CompareStrings(first, second);
|
||||
}
|
||||
|
||||
return CompareStrings(first.Trim(), second.Trim());
|
||||
}
|
||||
|
||||
|
|
@ -259,9 +314,12 @@ namespace CapyKit.Helpers
|
|||
public static string GetRandomString(int length, params ValidCharacterCollection[] validChars)
|
||||
{
|
||||
var buffer = new StringBuilder(length);
|
||||
var randomNumberBuffer = new byte[length * 3]; // Overprovision the buffer so we can discard a small percentage.
|
||||
var randomNumberBuffer =
|
||||
new byte[length * 3]; // Overprovision the buffer so we can discard a small percentage.
|
||||
var validCharacters = GetValidCharacterComposition(validChars);
|
||||
var validByteUpperLimit = (256 / validCharacters.Length) * validCharacters.Length - 1; // Maintains equal distribution of valid characters.
|
||||
var validByteUpperLimit =
|
||||
(256 / validCharacters.Length) * validCharacters.Length -
|
||||
1; // Maintains equal distribution of valid characters.
|
||||
|
||||
using (var rng = RandomNumberGenerator.Create())
|
||||
{
|
||||
|
|
@ -286,24 +344,13 @@ namespace CapyKit.Helpers
|
|||
return buffer.ToString();
|
||||
}
|
||||
|
||||
/// <summary> Generates a random byte array that can act as a salt. </summary>
|
||||
/// <remarks>
|
||||
/// A default length of <see cref="SALT_SIZE"/> is provided as a sane default. Larger values can be used for increased
|
||||
/// entropy.
|
||||
/// </remarks>
|
||||
/// <param name="length"> (Optional) The desired length of the generated byte array. </param>
|
||||
/// <returns> An array of byte. </returns>
|
||||
public static byte[] GetSalt(int length = SALT_SIZE)
|
||||
{
|
||||
return GetRandomBytes(length);
|
||||
}
|
||||
|
||||
/// <summary> Generates a new byte array of the specified length with random values. </summary>
|
||||
/// <param name="length"> The desired length of the generated byte array. </param>
|
||||
/// <returns> A new byte array of the specified length filled with random values. </returns>
|
||||
private static byte[] GetRandomBytes(int length)
|
||||
public static byte[] GetRandomBytes(int length)
|
||||
{
|
||||
if(length <= 0)
|
||||
if (length <= 0)
|
||||
{
|
||||
CapyEventReporter.EmitEvent(EventLevel.Error, "Length must be greater than 0.");
|
||||
return GetRandomBytes(16);
|
||||
|
|
@ -323,7 +370,7 @@ namespace CapyKit.Helpers
|
|||
/// </summary>
|
||||
/// <param name="validCharacters">An array of ValidCharacterCollection enumeration values representing the desired character sets.</param>
|
||||
/// <returns>A string containing all the characters from the specified character sets.</returns>
|
||||
private static string GetValidCharacterComposition(params ValidCharacterCollection[] validCharacters)
|
||||
public static string GetValidCharacterComposition(params ValidCharacterCollection[] validCharacters)
|
||||
{
|
||||
var composition = new StringBuilder();
|
||||
foreach (var c in validCharacters)
|
||||
|
|
@ -331,16 +378,16 @@ namespace CapyKit.Helpers
|
|||
switch (c)
|
||||
{
|
||||
case ValidCharacterCollection.Lowercase:
|
||||
composition.Append(LOWER_CASE_CHARACTERS);
|
||||
composition.Append(SecurityHelper.LOWER_CASE_CHARACTERS);
|
||||
break;
|
||||
case ValidCharacterCollection.Uppercase:
|
||||
composition.Append(LOWER_CASE_CHARACTERS);
|
||||
composition.Append(SecurityHelper.UPPER_CASE_CHARACTERS);
|
||||
break;
|
||||
case ValidCharacterCollection.Numbers:
|
||||
composition.Append(LOWER_CASE_CHARACTERS);
|
||||
composition.Append(SecurityHelper.NUMBER_CHARACTERS);
|
||||
break;
|
||||
case ValidCharacterCollection.Special:
|
||||
composition.Append(LOWER_CASE_CHARACTERS);
|
||||
composition.Append(SecurityHelper.SPECIAL_CHARACTERS);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
|
@ -379,16 +426,19 @@ namespace CapyKit.Helpers
|
|||
/// </summary>
|
||||
[EnumerationDescriptionAttribute(SecurityHelper.LOWER_CASE_CHARACTERS)]
|
||||
Lowercase,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that upper case characters should be included in the random string.
|
||||
/// </summary>
|
||||
[EnumerationDescriptionAttribute(SecurityHelper.UPPER_CASE_CHARACTERS)]
|
||||
Uppercase,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that numeric characters should be included in the random string.
|
||||
/// </summary>
|
||||
[EnumerationDescriptionAttribute(SecurityHelper.NUMBER_CHARACTERS)]
|
||||
Numbers,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that special characters should be included in the random string.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ namespace Tests.Helpers
|
|||
internal class SecurityHelperTests
|
||||
{
|
||||
private const string Password = "TestPassword";
|
||||
private static readonly byte[] Salt = SecurityHelper.GetSalt();
|
||||
private static readonly byte[] Salt = SecurityHelper.GetRandomBytes(32);
|
||||
|
||||
[Test]
|
||||
public void CompareHashedPassword_WithSamePassword_ShouldReturnTrue()
|
||||
|
|
@ -82,6 +82,28 @@ namespace Tests.Helpers
|
|||
Assert.AreEqual(password1.Hash, password2.Hash);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void VerifyPbkdf2_WithPersistedCredential_ReturnsExpectedResult()
|
||||
{
|
||||
var credential = SecurityHelper.Pbkdf2(Password, Salt);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(SecurityHelper.VerifyPbkdf2(Password, credential.Hash, credential.Salt), Is.True);
|
||||
Assert.That(SecurityHelper.VerifyPbkdf2("WrongPassword", credential.Hash, credential.Salt), Is.False);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HashOpaqueSecret_WithSameSecret_ReturnsStableDigest()
|
||||
{
|
||||
var first = SecurityHelper.HashOpaqueSecret("opaque-secret");
|
||||
var second = SecurityHelper.HashOpaqueSecret("opaque-secret");
|
||||
|
||||
Assert.That(first, Is.EqualTo(second));
|
||||
Assert.That(first, Is.Not.EqualTo("opaque-secret"));
|
||||
}
|
||||
|
||||
//[Test]
|
||||
//public void GetPassword_WithPBKDF2Algorithm_ShouldReturnValidPasswordObject()
|
||||
//{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue