Adds Argon2 for password protection (#123)

This commit is contained in:
Kamron Batman 2020-05-01 22:42:39 -07:00 • committed by GitHub
parent 29467a3ed9
commit dad8a64fad
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
54 changed files with 1152 additions and 167 deletions

128
Projects/Argon2/Argon2.cs Normal file
View file

@ -0,0 +1,128 @@
using System;
using System.Runtime.InteropServices;
using System.Text;
namespace Server.Accounting.Security
{
internal interface IArgon2
{
public Argon2Error Hash(uint t_cost, uint m_cost, uint parallelism,
byte[] pwd,
byte[] salt,
byte[] hash,
byte[] encoded,
int type, int version);
Argon2Error Verify(byte[] encoded, byte[] pwd, int pwdlen, int type);
// TODO: Change str to use ReadOnlySpan<char> then convert to pointer later
Argon2Error Decode(Argon2Context ctx, string str, int type);
}
internal static class Argon2
{
internal static readonly IArgon2 Library;
static Argon2()
{
if (RuntimeUtility.Unix)
Library = new UnixArgon2();
else
Library = new WindowsArgon2();
}
}
internal class WindowsArgon2 : IArgon2
{
public Argon2Error Hash(uint t_cost, uint m_cost, uint parallelism,
byte[] pwd,
byte[] salt,
byte[] hash,
byte[] encoded,
int type, int version) =>
NativeMethods.crypto_argon2_hash(t_cost, m_cost, parallelism,
pwd, pwd.Length,
salt, salt.Length,
hash, hash.Length,
encoded, encoded.Length,
type, version
);
public Argon2Error Verify(byte[] encoded, byte[] pwd, int pwdlen, int type) =>
NativeMethods.crypto_argon2_verify(encoded, pwd, pwdlen, type);
public Argon2Error Decode(Argon2Context ctx, string str, int type)
{
byte[] bytes = new byte[str.Length];
Encoding.ASCII.GetBytes(str, bytes);
// TODO: Use pointers instead
return NativeMethods.crypto_decode_string(ctx, bytes, type);
}
internal static class NativeMethods
{
[DllImport("argon2.dll", EntryPoint = "crypto_argon2_hash", CallingConvention = CallingConvention.Cdecl)]
internal static extern Argon2Error crypto_argon2_hash(uint t_cost, uint m_cost, uint parallelism,
byte[] pwd, int pwdlen,
byte[] salt, int saltlen,
byte[] hash, int hashlen,
byte[] encoded, int encodedlen,
int type, int version
);
[DllImport("argon2.dll", EntryPoint = "crypto_argon2_verify", CallingConvention = CallingConvention.Cdecl)]
internal static extern Argon2Error crypto_argon2_verify(byte[] encoded, byte[] pwd, int pwdlen, int type);
[DllImport("argon2.dll", EntryPoint = "crypto_decode_string", CallingConvention = CallingConvention.Cdecl)]
internal static extern Argon2Error crypto_decode_string(Argon2Context ctx, byte[] str, int type);
}
}
internal class UnixArgon2 : IArgon2
{
public Argon2Error Hash(uint t_cost, uint m_cost, uint parallelism,
byte[] pwd,
byte[] salt,
byte[] hash,
byte[] encoded,
int type, int version) =>
NativeMethods.crypto_argon2_hash(t_cost, m_cost, parallelism,
pwd, pwd.Length,
salt, salt.Length,
hash, hash.Length,
encoded, encoded.Length,
type, version
);
public Argon2Error Verify(byte[] encoded, byte[] pwd, int pwdlen, int type) =>
NativeMethods.crypto_argon2_verify(encoded, pwd, pwdlen, type);
public Argon2Error Decode(Argon2Context ctx, string str, int type)
{
byte[] bytes = new byte[str.Length];
Encoding.ASCII.GetBytes(str, bytes);
// TODO: Use pointers instead
return NativeMethods.crypto_decode_string(ctx, bytes, type);
}
internal static class NativeMethods
{
[DllImport("libargon2", EntryPoint = "argon2_hash")]
internal static extern Argon2Error crypto_argon2_hash(uint t_cost, uint m_cost, uint parallelism,
byte[] pwd, int pwdlen,
byte[] salt, int saltlen,
byte[] hash, int hashlen,
byte[] encoded, int encodedlen,
int type, int version
);
[DllImport("libargon2", EntryPoint = "argon2_verify")]
internal static extern Argon2Error crypto_argon2_verify(byte[] encoded, byte[] pwd, int pwdlen, int type);
[DllImport("libargon2", EntryPoint = "decode_string")]
internal static extern Argon2Error crypto_decode_string(Argon2Context ctx, byte[] str, int type);
}
}
}

View file

@ -0,0 +1,42 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RuntimeIdentifiers>win-x64;linux-x64;osx-x64</RuntimeIdentifiers>
<PackageVersion>1.0.0</PackageVersion>
<RootNamespace>Server</RootNamespace>
<AssemblyName>Argon2.Bindings</AssemblyName>
<PlatformTarget>x64</PlatformTarget>
<LangVersion>8.0</LangVersion>
<TargetFramework>netcoreapp3.1</TargetFramework>
<IsPackable>true</IsPackable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<Configurations>Debug;Release;Analyze</Configurations>
<DefineConstants Condition="'$(RuntimeIdentifier)' == 'win-x64'">WINDOWS</DefineConstants>
<AssemblyVersion>1.0.0</AssemblyVersion>
<Platforms>x64</Platforms>
</PropertyGroup>
<ItemGroup>
<Content Include="runtimes\win-x64\native\argon2.dll">
<Pack>true</Pack>
<PackagePath>runtimes/win-x64/native</PackagePath>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="runtimes\osx-x64\native\libargon2.dylib">
<Pack>true</Pack>
<PackagePath>runtimes/osx-x64/native</PackagePath>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="runtimes\linux-x64\native\libargon2.so">
<Pack>true</Pack>
<PackagePath>runtimes/linux-x64/native</PackagePath>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<Target Name="CopyPackage" AfterTargets="Pack">
<Exec Command="dotnet nuget push $(OutputPath)..\..\$(PackageId).$(PackageVersion).nupkg -s local" />
</Target>
<!-- <Target Name="CopyPackage" AfterTargets="Pack">
<Exec Command="nuget add $(OutputPath)..\$(PackageId).$(PackageVersion).nupkg -Source ../../packages" />
</Target> -->
</Project>

View file

@ -0,0 +1,15 @@
<?xml version="1.0"?>
<package >
<metadata>
<id>Argon2.Bindings</id>
<version>1.0.0</version>
<authors>Kamron Batman</authors>
<owners>Kamron Batman</owners>
<license type="expression">MIT</license>
<projectUrl>https://github.com/modernUO/ModernUO</projectUrl>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<description>Argon2 C# Bindings for ModernUO</description>
<releaseNotes>Initial release</releaseNotes>
<copyright>Copyright 2020</copyright>
</metadata>
</package>

View file

@ -0,0 +1,34 @@
using System;
using System.Runtime.InteropServices;
namespace Server.Accounting.Security
{
[StructLayout(LayoutKind.Sequential)]
internal class Argon2Context
{
public IntPtr Out;
public uint OutLen;
public IntPtr Pwd;
public uint PwdLen;
public IntPtr Salt;
public uint SaltLen;
public IntPtr Secret;
public uint SecretLen;
public IntPtr AssocData;
public uint AssocDataLen;
public uint TimeCost;
public uint MemoryCost;
public uint Lanes;
public uint Threads;
public IntPtr AllocateCallback;
public IntPtr FreeCallback;
public uint Flags;
}
}

View file

@ -0,0 +1,77 @@
namespace Server.Accounting.Security
{
/// <summary>
/// An enumeration of the possible error codes which are returned from Daniel Dinu and
/// Dmitry Khovratovich's Argon2 library.
///
/// Some of these error conditions cannot be reached while using the C# PasswordHasher wrapper
/// </summary>
public enum Argon2Error
{
/// <summary>
/// The operation was successful
/// </summary>
OK = 0,
/// <summary>
/// The output hash length is less than 4 bytes
/// </summary>
OUTPUT_TOO_SHORT = -2,
/// <summary>
/// The salt is less than 8 bytes
/// </summary>
SALT_TOO_SHORT = -6,
/// <summary>
/// The time cost is less than 1
/// </summary>
TIME_TOO_SMALL = -12,
/// <summary>
/// The memory cost is less than 8 (KiB)
/// </summary>
MEMORY_TOO_LITTLE = -14,
/// <summary>
/// The memory cost is greater than 2^21 (KiB) (2 GiB)
/// </summary>
MEMORY_TOO_MUCH = -15,
/// <summary>
/// The parallelism is less than 1
/// </summary>
LANES_TOO_FEW = -16,
/// <summary>
/// The parallelism is greater than 16,777,215
/// </summary>
LANES_TOO_MANY = -17,
/// <summary>
/// Memory allocation failed
/// </summary>
MEMORY_ALLOCATION_ERROR = -22,
/// <summary>
/// The parallelism is less than 1
/// </summary>
THREADS_TOO_FEW = -28,
/// <summary>
/// The parallelism is greater than 16,777,215
/// </summary>
THREADS_TOO_MANY = -29,
/// <summary>
/// This will not be returned from the C# PasswordHasher wrapper
/// </summary>
DECODING_FAIL = -32,
/// <summary>
/// Unable to create the number of threads requested
/// </summary>
THREAD_FAIL = -33,
/// <summary>
/// This will not be returned from the C# PasswordHasher wrapper
/// </summary>
VERIFY_MISMATCH = -35
}
}

View file

@ -0,0 +1,20 @@
using System;
namespace Server.Accounting.Security
{
/// <summary>
/// An exception class to wrap the errors returned by Daniel Dinu and Dmitry Khovratovich's Argon2 library.
///
/// Except through very unusual conditions, the only exceptions which could be thrown from PasswordHasher
/// are Argon2Exception, ArgumentNullException, DllNotFoundException (if libargon2.dll is not found)
/// </summary>
public class Argon2Exception : Exception
{
/// <summary>
/// Construct an Argon2Exception with the specified Argon2 error code
/// <param name="action">Which method the Argon2Exception originated from</param>
/// <param name="error">The error returned from the Argon2 library</param>
/// </summary>
public Argon2Exception(string action, Argon2Error error) : base(string.Format("Error during Argon2 {0}: ({1}) {2}", action, (int)error, error)) {}
}
}

View file

@ -0,0 +1,388 @@
using System;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
// using System.Text.RegularExpressions;
namespace Server.Accounting.Security
{
/// <summary>
/// PasswordHasher is a class for creating Argon2 hashes and verifying them. This is a wrapper around
/// Daniel Dinu and Dmitry Khovratovich's Argon2 library.
/// </summary>
public class Argon2PasswordHasher
{
private static readonly RNGCryptoServiceProvider Rng = new RNGCryptoServiceProvider();
// private static readonly Regex HashRegex = new Regex(@"^\$argon2([di])\$v=(\d+)$m=(\d+),t=(\d+),p=(\d+)\$([A-Za-z0-9+/=]+)\$([A-Za-z0-9+/=]*)$", RegexOptions.Compiled);
/// <summary>
/// How many iterations of the Argon2 hash to perform
/// </summary>
public uint TimeCost { get; set; }
/// <summary>
/// How much memory to use while hashing in kibibytes (KiB)
/// </summary>
public uint MemoryCost { get; set; }
/// <summary>
/// How many threads to use while hashing
/// </summary>
public uint Parallelism { get; set; }
/// <summary>
/// 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)
/// </summary>
public Argon2Type ArgonType { get; set; }
/// <summary>
/// Length of the generated raw hash in bytes
/// </summary>
public uint HashLength { get; set; }
/// <summary>
/// How strings should be decoded when passed to the Hash method.
/// The default is Encoding.UTF8.
/// </summary>
public Encoding StringEncoding { get; set; }
/// <summary>
/// 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.
/// </summary>
public Argon2PasswordHasher()
{
TimeCost = 3;
MemoryCost = 8192;
Parallelism = 1;
ArgonType = Argon2Type.Argon2i;
HashLength = 32;
StringEncoding = Encoding.UTF8;
}
/// <summary>
/// Initialize the Argon2 PasswordHasher with the performance and algorithm settings to use while hashing
/// <param name="timeCost">How many iterations of the Argon2 hash to perform (default: 3, must be at least 1)</param>
/// <param name="memoryCost">How much memory to use while hashing in kibibytes (KiB) (default: 8192 KiB [8 MiB], must be at least 8 KiB)</param>
/// <param name="parallelism">How many threads to use while hashing (default: 1, must be at least 1)</param>
/// <param name="argonType">The type of Argon2 hashing algorithm to use (Independent [default] or Dependent)</param>
/// <param name="hashLength">The length of the resulting hash in bytes (default: 32)</param>
/// </summary>
public Argon2PasswordHasher(uint timeCost = 3, uint memoryCost = 8192, uint parallelism = 1, Argon2Type argonType = Argon2Type.Argon2i, uint hashLength = 32)
{
TimeCost = timeCost;
MemoryCost = memoryCost;
Parallelism = parallelism;
ArgonType = argonType;
HashLength = hashLength;
StringEncoding = Encoding.UTF8;
}
/// <summary>
/// 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.
/// <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)
{
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));
}
/// <summary>
/// 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.
/// <param name="password">The raw bytes of the password to be hashed</param>
/// <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)
{
CheckNull("Hash", "password", password, "salt", salt);
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,
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, 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));
}
/// <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">The raw bytes of the password to be hashed</param>
/// <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)
{
byte[] hash = new byte[(int)HashLength];
var result = Argon2.Library.Hash(
TimeCost,
MemoryCost,
Parallelism,
password,
salt,
hash,
null,
(int)ArgonType,
0x13
);
if (result != Argon2Error.OK)
throw new Argon2Exception("raw hashing", result);
return hash;
}
/// <summary>
/// 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.
/// <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>
/// <returns>Whether the password results in the expectedHash when hashed</returns>
/// </summary>
public bool Verify(string expectedHash, string password)
{
CheckNull("Verify", "expectedHash", expectedHash, "password", password);
return Verify(expectedHash, StringEncoding.GetBytes(password));
}
/// <summary>
/// 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.
/// <param name="expectedHash">Hashing the password should result in this hash</param>
/// <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)
{
CheckNull("Verify", "expectedHash", expectedHash, "password", password);
var result = Argon2.Library.Verify(StringEncoding.GetBytes(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);
}
/// <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
/// 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 raw password bytes to hash and compare its result to expectedHash</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, byte[] password, out bool isUpdated, out string newFormattedHash)
{
CheckNull("VerifyAndUpdate", "expectedHash", expectedHash, "password", password);
if (Verify(expectedHash, password))
{
var hashMetadata = ExtractMetadata(expectedHash);
if (hashMetadata.MemoryCost != MemoryCost || hashMetadata.TimeCost != TimeCost || hashMetadata.Parallelism != Parallelism)
{
isUpdated = true;
byte[] salt = hashMetadata.Salt;
newFormattedHash = Hash(password, salt);
}
else
{
isUpdated = false;
newFormattedHash = expectedHash;
}
return true;
}
isUpdated = false;
newFormattedHash = expectedHash;
return false;
}
/// <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)
{
CheckNull("ExtractMetadata", "formattedHash", 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;
var result = Argon2.Library.Decode(context, $"{formattedHash}\0", (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);
}
}
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));
}
}
}

View file

@ -0,0 +1,18 @@
namespace Server.Accounting.Security
{
/// <summary>
/// The type of Argon2 hashing algorithm to use.
/// </summary>
public enum Argon2Type
{
/// <summary>
/// The memory access is dependent upon the hash value (vulnerable to side-channel attacks)
/// </summary>
Argon2d = 0,
/// <summary>
/// The memory access is independent upon the hash value (safe from side-channel atacks)
/// </summary>
Argon2i = 1
}
}

View file

@ -0,0 +1,60 @@
using System;
namespace Server.Accounting.Security
{
/// <summary>
/// HashMetadata represents the information stored in the encoded Argon2 format
/// </summary>
public class HashMetadata
{
/// <summary>
/// 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)
/// </summary>
public Argon2Type ArgonType { get; set; }
/// <summary>
/// How much memory to use while hashing in kibibytes (KiB)
/// </summary>
public uint MemoryCost { get; set; }
/// <summary>
/// How many iterations of the Argon2 hash to perform
/// </summary>
public uint TimeCost { get; set; }
/// <summary>
/// How many threads to use while hashing
/// </summary>
public uint Parallelism { get; set; }
/// <summary>
/// The raw bytes of the salt
/// </summary>
public byte[] Salt { get; set; }
/// <summary>
/// The raw bytes of the hash
/// </summary>
public byte[] Hash { get; set; }
/// <summary>
/// A base-64 encoded string of the salt, minus the padding (=) characters
/// </summary>
public string GetBase64Salt() => Convert.ToBase64String(Salt).Replace("=", "");
/// <summary>
/// A base-64 encoded string of the hash, minus the padding (=) characters
/// </summary>
public string GetBase64Hash() => Convert.ToBase64String(Hash).Replace("=", "");
/// <summary>
/// Converts HashMetadata back into the original Argon2 formatted string.
/// </summary>
public override string ToString() =>
$"$argon2{(ArgonType == Argon2Type.Argon2i ? "i" : "d")}$v=19$m={MemoryCost},t={TimeCost},p={Parallelism}${GetBase64Salt()}${GetBase64Hash()}";
}
}

View file

@ -0,0 +1,13 @@
using System.Runtime.InteropServices;
namespace Server
{
internal static class RuntimeUtility
{
public static bool IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
public static bool IsDarwin = RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
public static bool IsFreeBSD = RuntimeInformation.IsOSPlatform(OSPlatform.FreeBSD);
public static bool IsLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || IsFreeBSD;
public static bool Unix = IsDarwin || IsFreeBSD || IsLinux;
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.