using System.Runtime.InteropServices; using System.Text; namespace System.Security.Cryptography { /// /// PasswordHasher is a class for creating Argon2 hashes and verifying them. This is a wrapper around /// Daniel Dinu and Dmitry Khovratovich's Argon2 library. /// public class Argon2PasswordHasher { private static RandomNumberGenerator m_Rng; /// /// How many iterations of the Argon2 hash to perform /// public uint TimeCost { get; set; } /// /// How much memory to use while hashing in kibibytes (KiB) /// public uint MemoryCost { get; set; } /// /// How many threads to use while hashing /// public uint Parallelism { get; set; } /// /// The type of Argon2 hashing algorithm to use /// Argon2d - The memory access is dependent upon the hash value (vulnerable to side-channel attacks) /// Argon2i - The memory access is independent upon the hash value (safe from side-channel atacks) /// public Argon2Type ArgonType { get; set; } /// /// Length of the generated raw hash in bytes /// public uint HashLength { get; set; } /// /// How strings should be decoded when passed to the Hash method. /// The default is Encoding.UTF8. /// public Encoding StringEncoding { get; set; } /// /// Randomizer used to generate salts /// public RandomNumberGenerator Rng { get; set; } /// /// Initialize the Argon2 PasswordHasher with default performance and algorithm settings based upon the environment the hashing will be used in. /// You should perform your own profiling to determine what the parameters should be for your specific usage; however, this attempts to provide /// some reasonable defaults. /// public Argon2PasswordHasher(RandomNumberGenerator rng = null) { TimeCost = 3; MemoryCost = 8192; Parallelism = 1; ArgonType = Argon2Type.Argon2i; HashLength = 32; StringEncoding = Encoding.UTF8; Rng = rng ?? (m_Rng ??= new RNGCryptoServiceProvider()); } /// /// Hash the password using Argon2 with a cryptographically-secure, random, 16-byte salt. /// This is the only overload of the Hash method that the typical user will need to use for password storage. The other overloads are provided for interoperability purposes. /// Do not compare two Argon2 hashes directly. Instead, use the Verify or VerifyAndUpdate methods. /// A string representing the password to be hashed. The password is first decoded into bytes using StringEncoding (default: Encoding.UTF8) /// A formatted string representing the hashed password, encoded with the parameters used to perform the hash /// public string Hash(ReadOnlySpan password) { Span salt = stackalloc byte[16]; Rng.GetBytes(salt); return Hash(password, salt); } /// /// Hash the raw password bytes using Argon2 with the specified salt bytes. /// Unless you need to specify your own salt for interoperability purposes, prefer the Hash(byte[] password) overload instead. /// Do not compare two Argon2 hashes directly. Instead, use the Verify or VerifyAndUpdate methods. /// The raw bytes of the password to be hashed /// The raw salt bytes to be used for the hash. The salt must be at least 8 bytes. /// A formatted string representing the hashed password, encoded with the parameters used to perform the hash /// public string Hash(ReadOnlySpan password, ReadOnlySpan salt) { Span hash = stackalloc byte[(int)HashLength]; Span encoded = stackalloc byte[(int)(39 + ((HashLength + salt.Length) * 4 + 3) / 3)]; Span passwordBytes = stackalloc byte[StringEncoding.GetByteCount(password)]; StringEncoding.GetBytes(password, passwordBytes); var result = Argon2.Library.Hash( TimeCost, MemoryCost, Parallelism, passwordBytes, salt, hash, encoded, (int)ArgonType, 0x13 ); if (result != Argon2Error.OK) throw new Argon2Exception("hashing", result); var firstNonNull = encoded.Length - 2; while (encoded[firstNonNull] == 0) firstNonNull--; return Encoding.ASCII.GetString(encoded.Slice(0, firstNonNull + 1)); } /// /// 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. /// The raw bytes of the password to be hashed /// The raw salt bytes to be used for the hash. The salt must be at least 8 bytes. /// A byte array containing only the resulting hash /// public void HashRaw(ReadOnlySpan password, ReadOnlySpan salt, Span hash) { Span passwordBytes = stackalloc byte[StringEncoding.GetByteCount(password)]; StringEncoding.GetBytes(password, passwordBytes); var result = Argon2.Library.Hash( TimeCost, MemoryCost, Parallelism, passwordBytes, salt, hash, null, (int)ArgonType, 0x13 ); if (result != Argon2Error.OK) throw new Argon2Exception("raw hashing", result); } /// /// Hashes the password and verifies that the password results in the specified hash. /// The ArgonType must of this PasswordHasher object must match what was used to generate expectedHash. /// The other parameters (timeCost, etc.) do not need to match and the parameters embedded in the expectedHash will be used. /// Hashing the password should result in this hash /// The password to hash and compare its result to expectedHash. The password is first decoded into bytes using StringEncoding (default: Encoding.UTF8) /// Whether the password results in the expectedHash when hashed /// public bool Verify(ReadOnlySpan expectedHash, ReadOnlySpan password) { Span expectedHashBytes = stackalloc byte[StringEncoding.GetByteCount(expectedHash)]; StringEncoding.GetBytes(expectedHash, expectedHashBytes); Span passwordBytes = stackalloc byte[StringEncoding.GetByteCount(password)]; StringEncoding.GetBytes(password, passwordBytes); return Verify(expectedHashBytes, passwordBytes); } /// /// Hashes the raw password bytes and verifies that the password results in the specified hash. /// The ArgonType must of this PasswordHasher object must match what was used to generate expectedHash. /// The other parameters (timeCost, etc.) do not need to match and the parameters embedded in the expectedHash will be used. /// Hashing the password should result in this hash /// The raw password bytes to hash and compare its result to expectedHash /// Whether the password results in the expectedHash when hashed /// public bool Verify(ReadOnlySpan expectedHash, ReadOnlySpan password) { 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; throw new Argon2Exception("verifying", result); } /// /// 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. /// Hashing the password should result in this hash /// The raw password bytes to hash and compare its result to expectedHash /// 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. /// If isUpdated is true, then newFormattedHash is the password hashed with the new cost parameters. If isUpdated is false, then newFormattedHash is expectedHash. /// Whether the password results in the expectedHash when hashed /// public bool VerifyAndUpdate(ReadOnlySpan expectedHash, ReadOnlySpan password, out bool isUpdated, out string newFormattedHash) { bool verified = Verify(expectedHash, password); if (verified) { var hashMetadata = ExtractMetadata(expectedHash); if (hashMetadata.MemoryCost != MemoryCost || hashMetadata.TimeCost != TimeCost || hashMetadata.Parallelism != Parallelism) { isUpdated = true; byte[] salt = hashMetadata.Salt; newFormattedHash = Hash(password, salt); return true; } } isUpdated = false; newFormattedHash = expectedHash.ToString(); return verified; } /// /// Extracts the memory cost, time cost, etc. used to generate the Argon2 hash. /// An encoded Argon2 hash created by the Hash method /// The hash metadata or null if the formattedHash was not a valid encoded Argon2 hash /// public static HashMetadata ExtractMetadata(ReadOnlySpan formattedHash) { var context = new Argon2Context { Out = Marshal.AllocHGlobal(formattedHash.Length), // ensure the space to hold the hash is long enough OutLen = (uint)formattedHash.Length, Pwd = Marshal.AllocHGlobal(1), PwdLen = 1, Salt = Marshal.AllocHGlobal(formattedHash.Length), // ensure the space to hold the salt is long enough SaltLen = (uint)formattedHash.Length, Secret = Marshal.AllocHGlobal(1), SecretLen = 1, AssocData = Marshal.AllocHGlobal(1), AssocDataLen = 1, TimeCost = 0, MemoryCost = 0, Lanes = 0, Threads = 0 }; try { var type = formattedHash.StartsWith("$argon2i") ? Argon2Type.Argon2i : Argon2Type.Argon2d; formattedHash = $"{formattedHash.ToString()}\0"; Span 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; var salt = new byte[context.SaltLen]; var hash = new byte[context.OutLen]; Marshal.Copy(context.Salt, salt, 0, salt.Length); Marshal.Copy(context.Out, hash, 0, hash.Length); return new HashMetadata { ArgonType = type, MemoryCost = context.MemoryCost, TimeCost = context.TimeCost, Parallelism = context.Threads, Salt = salt, Hash = hash }; } finally { Marshal.FreeHGlobal(context.Out); Marshal.FreeHGlobal(context.Pwd); Marshal.FreeHGlobal(context.Salt); Marshal.FreeHGlobal(context.Secret); Marshal.FreeHGlobal(context.AssocData); } } } }