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

View file

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Net;
using System.Xml;
using Server.Accounting.Security;
using Server.Misc;
using Server.Mobiles;
using Server.Multis;
@ -297,13 +298,9 @@ namespace Server.Accounting
public bool GetBanTags(out DateTime banTime, out TimeSpan banDuration)
{
string tagTime = GetTag("BanTime");
string tagDuration = GetTag("BanDuration");
if (tagTime != null)
banTime = Utility.GetXMLDateTime(tagTime, DateTime.MinValue);
else
banTime = DateTime.MinValue;
banTime = Utility.GetXMLDateTime(GetTag("BanTime"), DateTime.MinValue);
if (tagDuration == "Infinite")
banDuration = TimeSpan.MaxValue;
@ -323,7 +320,9 @@ namespace Server.Accounting
public bool CheckPassword(string plainPassword)
{
bool ok = AccountSecurity.GetPasswordProtection(m_PasswordAlgorithm).ValidatePassword(Password, plainPassword);
string phrase = m_PasswordAlgorithm == PasswordProtectionAlgorithm.SHA1 ? $"{Username}{plainPassword}" : plainPassword;
bool ok = AccountSecurity.GetPasswordProtection(m_PasswordAlgorithm).ValidatePassword(Password, phrase);
if (!ok)
return false;
@ -459,6 +458,24 @@ namespace Server.Accounting
// TODO: Offload passwords to its own module so it can be easily written/upgraded
Password = Utility.GetText(node["password"], null);
Enum.TryParse(Utility.GetText(node["passwordAlgorithm"], null), true, out m_PasswordAlgorithm);
if (m_PasswordAlgorithm == PasswordProtectionAlgorithm.None)
{
string md5Password = Utility.GetText(node["cryptPassword"], null);
string sha1Password = Utility.GetText(node["newCryptPassword"], null);
if (sha1Password != null)
{
Password = sha1Password;
m_PasswordAlgorithm = PasswordProtectionAlgorithm.SHA1;
}
else if (md5Password != null)
{
Password = md5Password;
m_PasswordAlgorithm = PasswordProtectionAlgorithm.MD5;
}
}
Enum.TryParse(Utility.GetText(node["accessLevel"], "Player"), true, out m_AccessLevel);
Flags = Utility.GetXMLInt32(Utility.GetText(node["flags"], "0"), 0);
Created = Utility.GetXMLDateTime(Utility.GetText(node["created"], null), DateTime.UtcNow);

View file

@ -1,26 +0,0 @@
namespace Server.Accounting
{
public enum PasswordProtectionAlgorithm
{
PBKDF2
}
public static class AccountSecurity
{
// TODO: Put it in a configuration
public const PasswordProtectionAlgorithm AlgorithmName = PasswordProtectionAlgorithm.PBKDF2;
public static readonly IPasswordProtection CurrentPasswordProtection = GetPasswordProtection(AlgorithmName);
public static IPasswordProtection GetPasswordProtection(PasswordProtectionAlgorithm algorithm)
{
var passwordProtection = algorithm switch
{
PasswordProtectionAlgorithm.PBKDF2 => PBKDF2PasswordProtection.Instance,
_ => null
};
return passwordProtection;
}
}
}

View file

@ -0,0 +1,45 @@
using System;
namespace Server.Accounting.Security
{
public enum PasswordProtectionAlgorithm
{
// Obsolete algorithms. These are not secure!
// They are included for password upgrades only.
None,
MD5,
SHA1,
// Support algorithms
PBKDF2,
Argon2 // Recommended algorithm for real security.
}
public static class AccountSecurity
{
// TODO: Put it in a configuration
public const PasswordProtectionAlgorithm AlgorithmName = PasswordProtectionAlgorithm.Argon2;
public static readonly IPasswordProtection CurrentPasswordProtection = GetPasswordProtection(AlgorithmName);
public static void Configure()
{
if (AlgorithmName < PasswordProtectionAlgorithm.PBKDF2)
throw new Exception($"Security: {AlgorithmName} is obselete and not secure. Do not use it.");
}
public static IPasswordProtection GetPasswordProtection(PasswordProtectionAlgorithm algorithm)
{
var passwordProtection = algorithm switch
{
PasswordProtectionAlgorithm.MD5 => MD5PasswordProtection.Instance,
PasswordProtectionAlgorithm.SHA1 => SHA1PasswordProtection.Instance,
PasswordProtectionAlgorithm.PBKDF2 => PBKDF2PasswordProtection.Instance,
PasswordProtectionAlgorithm.Argon2 => Argon2PasswordProtection.Instance,
_ => null
};
return passwordProtection;
}
}
}

View file

@ -0,0 +1,14 @@
namespace Server.Accounting.Security
{
public class Argon2PasswordProtection : IPasswordProtection
{
public static IPasswordProtection Instance = new Argon2PasswordProtection();
private Argon2PasswordHasher m_PasswordHasher = new Argon2PasswordHasher();
public string EncryptPassword(string plainPassword) =>
m_PasswordHasher.Hash(plainPassword);
public bool ValidatePassword(string encryptedPassword, string plainPassword) =>
m_PasswordHasher.Verify(encryptedPassword, plainPassword);
}
}

View file

@ -0,0 +1,25 @@
using System;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
namespace Server.Accounting.Security
{
public class MD5PasswordProtection : IPasswordProtection
{
public static IPasswordProtection Instance = new MD5PasswordProtection();
private MD5CryptoServiceProvider m_MD5HashProvider = new MD5CryptoServiceProvider();
public string EncryptPassword(string plainPassword)
{
ReadOnlySpan<char> password = plainPassword.AsSpan(0, Math.Min(256, plainPassword.Length));
byte[] bytes = new byte[Encoding.ASCII.GetByteCount(password)];
Encoding.ASCII.GetBytes(password, bytes);
return BitConverter.ToString(m_MD5HashProvider.ComputeHash(bytes));
}
public bool ValidatePassword(string encryptedPassword, string plainPassword) =>
EncryptPassword(plainPassword) == encryptedPassword;
}
}

View file

@ -3,11 +3,11 @@ using System.Buffers.Binary;
using System.Security.Cryptography;
using Server.Misc;
namespace Server.Accounting
namespace Server.Accounting.Security
{
public class PBKDF2PasswordProtection : IPasswordProtection
{
public static PBKDF2PasswordProtection Instance = new PBKDF2PasswordProtection();
public static IPasswordProtection Instance = new PBKDF2PasswordProtection();
private const ushort m_MinIterations = 1024;
private const ushort m_MaxIterations = 1536;

View file

@ -0,0 +1,25 @@
using System;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
namespace Server.Accounting.Security
{
public class SHA1PasswordProtection : IPasswordProtection
{
public static IPasswordProtection Instance = new SHA1PasswordProtection();
private SHA1CryptoServiceProvider m_SHA1HashProvider = new SHA1CryptoServiceProvider();
public string EncryptPassword(string plainPassword)
{
ReadOnlySpan<char> password = plainPassword.AsSpan(0, Math.Min(256, plainPassword.Length));
byte[] bytes = new byte[Encoding.ASCII.GetByteCount(password)];
Encoding.ASCII.GetBytes(password, bytes);
return BitConverter.ToString(m_SHA1HashProvider.ComputeHash(bytes));
}
public bool ValidatePassword(string encryptedPassword, string plainPassword) =>
EncryptPassword(plainPassword) == encryptedPassword;
}
}

View file

@ -19,6 +19,8 @@
<PublishDir>$(SolutionDir)\Distribution\Assemblies</PublishDir>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Configurations>Debug;Release;Analyze</Configurations>
<DefineConstants Condition="'$(RuntimeIdentifier)' == 'win-x64'">WINDOWS</DefineConstants>
<Platforms>x64</Platforms>
</PropertyGroup>
<Target Name="CleanPub" AfterTargets="Clean">
<Message Text="Removing distribution assemblies..." />
@ -38,14 +40,14 @@
<Delete Files="$(SolutionDir)\Distribution\Assemblies\$(AssemblyName).pdb" ContinueOnError="true" />
<Delete Files="$(SolutionDir)\Distribution\Assemblies\System.IO.Pipelines.dll" ContinueOnError="true" />
</Target>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DefineConstants>TRACE;DEBUG</DefineConstants>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<DefineConstants>TRACE;DEBUG;$(DefineConstants)</DefineConstants>
<Optimize>false</Optimize>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Optimize>true</Optimize>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Analyze|AnyCPU'">
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Analyze|x64'">
<Optimize>true</Optimize>
<RunCodeAnalysis>true</RunCodeAnalysis>
<RunAnalyzersDuringBuild>true</RunAnalyzersDuringBuild>
@ -64,6 +66,7 @@
<PackageReference Include="Microsoft.AspNetCore.Connections.Abstractions" Version="3.1.3" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.1.3" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.3" />
<PackageReference Include="Argon2.Bindings" Version="1.0.0" />
</ItemGroup>
<ItemGroup Condition="'$(Configuration)|$(Platform)'=='Analyze|AnyCPU'">
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
@ -74,4 +77,21 @@
</PackageReference>
<AdditionalFiles Include="$(SolutionDir)\Rules.ruleset" Link="Rules.rulest" />
</ItemGroup>
<ItemGroup Condition="'$(Configuration)|$(Platform)'=='Analyze|x64'">
<PackageReference Include="StyleCop.Analyzers">
<Version>1.1.118</Version>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<AdditionalFiles Include="$(SolutionDir)\stylecop.json">
<Link>stylecop.json</Link>
</AdditionalFiles>
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers">
<Version>2.9.8</Version>
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<AdditionalFiles Include="$(SolutionDir)\Rules.ruleset">
<Link>Rules.rulest</Link>
</AdditionalFiles>
</ItemGroup>
</Project>