diff --git a/CapyKit/Helpers/SecurityHelper.cs b/CapyKit/Helpers/SecurityHelper.cs
index b0f80b5..78a0915 100644
--- a/CapyKit/Helpers/SecurityHelper.cs
+++ b/CapyKit/Helpers/SecurityHelper.cs
@@ -15,9 +15,6 @@ namespace CapyKit.Helpers
{
#region Members
- /// Default size to use when generating a new salt.
- private const int SALT_SIZE = 32;
-
/// A string of all the lower case characters.
internal const string LOWER_CASE_CHARACTERS = "abcdefghijklmnopqrstuvwxyz";
@@ -33,6 +30,14 @@ namespace CapyKit.Helpers
#endregion Members
#region Methods
+
+ public static bool CompareHashedPassword(Password existingPassword, string password, string salt,
+ params object[] args)
+ {
+ var saltBytes = Convert.FromBase64String(salt);
+
+ return CompareHashedPassword(existingPassword, password, saltBytes, args);
+ }
///
/// Compares an unencrypted with a stored, encrypted .
@@ -47,7 +52,8 @@ namespace CapyKit.Helpers
///
/// if hash comparison succeeds, if it fails.
///
- public static bool CompareHashedPassword(Password existingPassword, string password, byte[] salt, params object[] args)
+ public static bool CompareHashedPassword(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);
+ }
///
/// Compares an unencrypted with a stored, encrypted .
@@ -70,18 +84,49 @@ namespace CapyKit.Helpers
///
/// if hash comparison succeeds, if it fails.
///
- 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);
}
+ ///
+ /// Verifies a plaintext password against a persisted PBKDF2 hash and salt.
+ ///
+ /// The plaintext password supplied by the caller.
+ /// The persisted PBKDF2 hash.
+ /// The persisted PBKDF2 salt.
+ /// when the password matches the persisted hash.
+ 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);
+ }
+
+ ///
+ /// Produces a deterministic SHA-256 digest for an opaque secret that must be looked up
+ /// without persisting the raw value.
+ ///
+ /// The raw opaque secret.
+ /// The Base64-encoded digest.
+ public static string HashOpaqueSecret(string secret)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(secret);
+ return Convert.ToBase64String(SHA256.HashData(Encoding.UTF8.GetBytes(secret)));
+ }
+
///
/// Retrieves a object using the specified password and generates a random salt value.
/// Then it uses that salt to call the overloaded method with the given password and
@@ -98,9 +143,9 @@ namespace CapyKit.Helpers
/// instance of created using any optional constructor arguments provided.
///
///
- public static Password GetPassword(string password, params object[] args)
+ public static Password GetPassword(string password, int saltSize, params object[] args) where T : IPasswordAlgorithm
{
- var salt = SecurityHelper.GetSalt;
+ var salt = GetRandomBytes(saltSize);
return GetPassword(password, salt, args);
}
@@ -129,14 +174,16 @@ namespace CapyKit.Helpers
/// A new object with the given password and salt, as well as an instance
/// of created using the provided constructor arguments.
///
- public static Password GetPassword(string password, byte[] salt, params object[] args) where T : IPasswordAlgorithm
+ public static Password GetPassword(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
///
/// A new object containing the hashed password and a randomly generated salt.
///
- 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();
}
- /// Generates a random byte array that can act as a salt.
- ///
- /// A default length of is provided as a sane default. Larger values can be used for increased
- /// entropy.
- ///
- /// (Optional) The desired length of the generated byte array.
- /// An array of byte.
- public static byte[] GetSalt(int length = SALT_SIZE)
- {
- return GetRandomBytes(length);
- }
/// Generates a new byte array of the specified length with random values.
/// The desired length of the generated byte array.
/// A new byte array of the specified length filled with random values.
- 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
///
/// An array of ValidCharacterCollection enumeration values representing the desired character sets.
/// A string containing all the characters from the specified character sets.
- 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
///
[EnumerationDescriptionAttribute(SecurityHelper.LOWER_CASE_CHARACTERS)]
Lowercase,
+
///
/// Indicates that upper case characters should be included in the random string.
///
[EnumerationDescriptionAttribute(SecurityHelper.UPPER_CASE_CHARACTERS)]
Uppercase,
+
///
/// Indicates that numeric characters should be included in the random string.
///
[EnumerationDescriptionAttribute(SecurityHelper.NUMBER_CHARACTERS)]
Numbers,
+
///
/// Indicates that special characters should be included in the random string.
///
diff --git a/Tests/Helpers/SecurityHelperTests.cs b/Tests/Helpers/SecurityHelperTests.cs
index 73dff40..d35fc3c 100644
--- a/Tests/Helpers/SecurityHelperTests.cs
+++ b/Tests/Helpers/SecurityHelperTests.cs
@@ -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()
//{