Updates Argon2 to use Spans/Stackalloc (#140)

This commit is contained in:
Kamron Batman 2020-05-24 18:45:30 -07:00 • committed by GitHub
parent 2770ab1d50
commit 5d51e97c52
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 115 additions and 230 deletions

View file

@ -1,10 +1,8 @@
using System;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
// using System.Text.RegularExpressions;
namespace Server.Accounting.Security
namespace System.Security.Cryptography
{
/// <summary>
/// PasswordHasher is a class for creating Argon2 hashes and verifying them. This is a wrapper around
@ -93,39 +91,11 @@ namespace Server.Accounting.Security
/// <param name="password">A string representing the password to be hashed. The password is first decoded into bytes using StringEncoding (default: Encoding.UTF8)</param>
/// <returns>A formatted string representing the hashed password, encoded with the parameters used to perform the hash</returns>
/// </summary>
public string Hash(string password)
public string Hash(ReadOnlySpan<char> password)
{
CheckNull("Hash", "password", password);
return Hash(StringEncoding.GetBytes(password));
}
/// <summary>
/// Hash the raw password bytes using Argon2 with a cryptographically-secure, random, 16-byte salt.
/// Do not compare two Argon2 hashes directly. Instead, use the Verify or VerifyAndUpdate methods.
/// <param name="password">The raw bytes of the password to be hashed</param>
/// <returns>A formatted string representing the hashed password, encoded with the parameters used to perform the hash</returns>
/// </summary>
public string Hash(byte[] password)
{
CheckNull("Hash", "password", password);
return Hash(password, GenerateSalt());
}
/// <summary>
/// Hash the password using Argon2 with the specified salt.
/// Unless you need to specify your own salt for interoperability purposes, prefer the Hash(string password) overload instead.
/// Do not compare two Argon2 hashes directly. Instead, use the Verify or VerifyAndUpdate methods.
/// <param name="password">A string representing the password to be hashed. The password is first decoded into bytes using StringEncoding (default: Encoding.UTF8)</param>
/// <param name="salt">A string representing the salt to be used for the hash. The salt must be at least 8 bytes. The salt is first decoded into bytes using StringEncoding (default: Encoding.UTF8)</param>
/// <returns>A formatted string representing the hashed password, encoded with the parameters used to perform the hash</returns>
/// </summary>
public string Hash(string password, string salt)
{
CheckNull("Hash", "password", password, "salt", salt);
return Hash(StringEncoding.GetBytes(password), StringEncoding.GetBytes(salt));
Span<byte> salt = stackalloc byte[16];
Rng.GetBytes(salt);
return Hash(password, salt);
}
/// <summary>
@ -136,17 +106,18 @@ namespace Server.Accounting.Security
/// <param name="salt">The raw salt bytes to be used for the hash. The salt must be at least 8 bytes.</param>
/// <returns>A formatted string representing the hashed password, encoded with the parameters used to perform the hash</returns>
/// </summary>
public string Hash(byte[] password, byte[] salt)
public string Hash(ReadOnlySpan<char> password, ReadOnlySpan<byte> salt)
{
CheckNull("Hash", "password", password, "salt", salt);
Span<byte> hash = stackalloc byte[(int)HashLength];
Span<byte> encoded = stackalloc byte[(int)(39 + ((HashLength + salt.Length) * 4 + 3) / 3)];
Span<byte> passwordBytes = stackalloc byte[StringEncoding.GetByteCount(password)];
StringEncoding.GetBytes(password, passwordBytes);
byte[] hash = new byte[HashLength];
byte[] encoded = new byte[39 + ((HashLength + salt.Length) * 4 + 3) / 3];
var result = Argon2.Library.Hash(
TimeCost,
MemoryCost,
Parallelism,
password,
passwordBytes,
salt,
hash,
encoded,
@ -161,22 +132,7 @@ namespace Server.Accounting.Security
while (encoded[firstNonNull] == 0)
firstNonNull--;
return Encoding.ASCII.GetString(encoded, 0, firstNonNull + 1);
}
/// <summary>
/// Hash the password using Argon2 with the specified salt. The HashRaw methods may be used for password-based key derivation.
/// Unless you're using HashRaw for key deriviation or for interoperability purposes, the Hash methods should be used in favor of the HashRaw methods.
/// <param name="password">A string representing the password to be hashed. The password is first decoded into bytes using StringEncoding (default: Encoding.UTF8)</param>
/// <param name="salt">A string representing the salt to be used for the hash. The salt must be at least 8 bytes. The salt is first decoded into bytes using StringEncoding (default: Encoding.UTF8)</param>
/// <returns>A byte array containing only the resulting hash</returns>
/// </summary>
public byte[] HashRaw(string password, string salt)
{
CheckNull("HashRaw", "password", password, "salt", salt);
return HashRaw(StringEncoding.GetBytes(password), StringEncoding.GetBytes(salt));
return Encoding.ASCII.GetString(encoded.Slice(0, firstNonNull + 1));
}
/// <summary>
@ -186,15 +142,16 @@ namespace Server.Accounting.Security
/// <param name="salt">The raw salt bytes to be used for the hash. The salt must be at least 8 bytes.</param>
/// <returns>A byte array containing only the resulting hash</returns>
/// </summary>
public byte[] HashRaw(byte[] password, byte[] salt)
public void HashRaw(ReadOnlySpan<char> password, ReadOnlySpan<byte> salt, Span<byte> hash)
{
byte[] hash = new byte[(int)HashLength];
Span<byte> passwordBytes = stackalloc byte[StringEncoding.GetByteCount(password)];
StringEncoding.GetBytes(password, passwordBytes);
var result = Argon2.Library.Hash(
TimeCost,
MemoryCost,
Parallelism,
password,
passwordBytes,
salt,
hash,
null,
@ -204,8 +161,6 @@ namespace Server.Accounting.Security
if (result != Argon2Error.OK)
throw new Argon2Exception("raw hashing", result);
return hash;
}
@ -217,11 +172,13 @@ namespace Server.Accounting.Security
/// <param name="password">The password to hash and compare its result to expectedHash. The password is first decoded into bytes using StringEncoding (default: Encoding.UTF8)</param>
/// <returns>Whether the password results in the expectedHash when hashed</returns>
/// </summary>
public bool Verify(string expectedHash, string password)
public bool Verify(ReadOnlySpan<char> expectedHash, ReadOnlySpan<char> password)
{
CheckNull("Verify", "expectedHash", expectedHash, "password", password);
return Verify(expectedHash, StringEncoding.GetBytes(password));
Span<byte> expectedHashBytes = stackalloc byte[StringEncoding.GetByteCount(expectedHash)];
StringEncoding.GetBytes(expectedHash, expectedHashBytes);
Span<byte> passwordBytes = stackalloc byte[StringEncoding.GetByteCount(password)];
StringEncoding.GetBytes(password, passwordBytes);
return Verify(expectedHashBytes, passwordBytes);
}
/// <summary>
@ -232,11 +189,9 @@ namespace Server.Accounting.Security
/// <param name="password">The raw password bytes to hash and compare its result to expectedHash</param>
/// <returns>Whether the password results in the expectedHash when hashed</returns>
/// </summary>
public bool Verify(string expectedHash, byte[] password)
public bool Verify(ReadOnlySpan<byte> expectedHash, ReadOnlySpan<byte> password)
{
CheckNull("Verify", "expectedHash", expectedHash, "password", password);
var result = Argon2.Library.Verify(StringEncoding.GetBytes(expectedHash), password, password.Length, (int)ArgonType);
var result = Argon2.Library.Verify(expectedHash, password, password.Length, (int)ArgonType);
if (result == Argon2Error.OK || result == Argon2Error.VERIFY_MISMATCH || result == Argon2Error.DECODING_FAIL)
return result == Argon2Error.OK;
@ -244,26 +199,6 @@ namespace Server.Accounting.Security
throw new Argon2Exception("verifying", result);
}
/// <summary>
/// Hashes the password and verifies that the password results in the specified hash. (See Verify method)
/// If the password verification is successful, this method checks to see if the memory cost, time cost, and parallelism
/// match the parameters the PasswordHasher object was constructed with. If they do not much, then the password is rehashed
/// using the new parameters and the result is outputted via the newFormattedHash parameter.
/// <param name="expectedHash">Hashing the password should result in this hash</param>
/// <param name="password">The password to hash and compare its result to expectedHash. The password is first decoded into bytes using StringEncoding (default: Encoding.UTF8)</param>
/// <param name="isUpdated">Whether the cost parameters of expectedHash differ from the PasswordHasher object and if the password was rehashed using th new parameters. This is always false if the password was incorrect.</param>
/// <param name="newFormattedHash">If isUpdated is true, then newFormattedHash is the password hashed with the new cost parameters. If isUpdated is false, then newFormattedHash is expectedHash.</param>
/// <returns>Whether the password results in the expectedHash when hashed</returns>
/// </summary>
public bool VerifyAndUpdate(string expectedHash, string password, out bool isUpdated, out string newFormattedHash)
{
CheckNull("VerifyAndUpdate", "expectedHash", expectedHash, "password", password);
return VerifyAndUpdate(expectedHash, StringEncoding.GetBytes(password), out isUpdated, out newFormattedHash);
}
/// <summary>
/// Hashes the password and verifies that the password results in the specified hash. (See Verify method)
/// If the password verification is successful, this method checks to see if the memory cost, time cost, and parallelism
@ -275,11 +210,11 @@ namespace Server.Accounting.Security
/// <param name="newFormattedHash">If isUpdated is true, then newFormattedHash is the password hashed with the new cost parameters. If isUpdated is false, then newFormattedHash is expectedHash.</param>
/// <returns>Whether the password results in the expectedHash when hashed</returns>
/// </summary>
public bool VerifyAndUpdate(string expectedHash, byte[] password, out bool isUpdated, out string newFormattedHash)
public bool VerifyAndUpdate(ReadOnlySpan<char> expectedHash, ReadOnlySpan<char> password, out bool isUpdated, out string newFormattedHash)
{
CheckNull("VerifyAndUpdate", "expectedHash", expectedHash, "password", password);
bool verified = Verify(expectedHash, password);
if (Verify(expectedHash, password))
if (verified)
{
var hashMetadata = ExtractMetadata(expectedHash);
@ -288,44 +223,22 @@ namespace Server.Accounting.Security
isUpdated = true;
byte[] salt = hashMetadata.Salt;
newFormattedHash = Hash(password, salt);
return true;
}
else
{
isUpdated = false;
newFormattedHash = expectedHash;
}
return true;
}
isUpdated = false;
newFormattedHash = expectedHash;
return false;
newFormattedHash = expectedHash.ToString();
return verified;
}
/// <summary>
/// Generate salt using a Cryptographically-Secure Pseudo-Random Number Generator
/// <param name="byteLength">The number of bytes of salt to generate (default: 16)</param>
/// <returns>A array of randomly-generated bytes</returns>
/// </summary>
public static byte[] GenerateSalt(uint byteLength = 16)
{
var salt = new byte[byteLength];
Rng.GetBytes(salt);
return salt;
}
/// <summary>
/// Extracts the memory cost, time cost, etc. used to generate the Argon2 hash.
/// <param name="formattedHash">An encoded Argon2 hash created by the Hash method</param>
/// <returns>The hash metadata or null if the formattedHash was not a valid encoded Argon2 hash</returns>
/// </summary>
public static HashMetadata ExtractMetadata(string formattedHash)
public static HashMetadata ExtractMetadata(ReadOnlySpan<char> formattedHash)
{
CheckNull("ExtractMetadata", "formattedHash", formattedHash);
var context = new Argon2Context
{
Out = Marshal.AllocHGlobal(formattedHash.Length), // ensure the space to hold the hash is long enough
@ -347,7 +260,12 @@ namespace Server.Accounting.Security
try
{
var type = formattedHash.StartsWith("$argon2i") ? Argon2Type.Argon2i : Argon2Type.Argon2d;
var result = Argon2.Library.Decode(context, $"{formattedHash}\0", (int)type);
formattedHash = $"{formattedHash.ToString()}\0";
Span<byte> bytes = stackalloc byte[formattedHash.Length];
Encoding.ASCII.GetBytes(formattedHash, bytes);
var result = Argon2.Library.Decode(context, bytes, (int)type);
if (result != Argon2Error.OK)
return null;
@ -376,13 +294,5 @@ namespace Server.Accounting.Security
Marshal.FreeHGlobal(context.AssocData);
}
}
private static void CheckNull(string methodName, params object[] arguments)
{
for (var i = 0; i < arguments.Length; i += 2)
if (arguments[i + 1] == null)
throw new ArgumentNullException(arguments[i].ToString(), string.Format("Argument {0} to method PasswordHasher.{1} is null", arguments[i], methodName));
}
}
}