diff --git a/.circleci/checksum.txt b/.circleci/checksum.txt new file mode 100644 index 000000000..5d4b569c0 --- /dev/null +++ b/.circleci/checksum.txt @@ -0,0 +1,13 @@ +95482048463cdc99c77a69951654de7b28fd6880 ../Projects/Argon2/Argon2.csproj +ebafae4e8160057cbf408cb13114479ca26e1b58 ../Projects/Scripts.Tests/Scripts.Tests.csproj +6c6a48220658bdfa7b2e1e173077fd139ed4af2c ../Projects/Scripts/Scripts.csproj +6213c10f697b45729bab15f7870c42dcf49b026b ../Projects/Server.Tests/Server.Tests.csproj +4b3626e04f76dff1ab445e207658a3b386fce4e5 ../Projects/Server/Server.csproj +a663d309b6b86c7632ea5cd175468dd5180808c1 ../Projects/Argon2/Argon2.cs +1e89625abe364f895346d0c5753f3ca6653cf7df ../Projects/Argon2/Argon2Context.cs +abbb34d85f55ca1d78a400b4d46afbd6965f642b ../Projects/Argon2/Argon2Error.cs +00ef35a6809d4959bdda1de01f45e39ff388270f ../Projects/Argon2/Argon2Exception.cs +53164d1621e24adefc6917379d36d7273551f457 ../Projects/Argon2/Argon2PasswordHasher.cs +a137d2fe0478d065d478366168f80ab9eaaa9c3a ../Projects/Argon2/Argon2Type.cs +c09f7a2f9690d56ca838aafb28640c09950c5a8c ../Projects/Argon2/HashMetadata.cs +b5da07548eb41ec9bd35bc2def006f23698b357e ../Projects/Argon2/Utility.cs diff --git a/.circleci/config.yml b/.circleci/config.yml index 53c4fa85c..7c8138fb3 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -19,17 +19,20 @@ jobs: - checkout - restore_cache: keys: - - dotnet-packages-linux-{{ checksum "Projects/Server/Server.csproj" }}-{{ checksum "Projects/Scripts/Scripts.csproj" }} + - dotnet-packages-v1-{{ arch }}-{{ checksum ".circleci/checksum.txt" }} + - run: + name: Build Argon2 + command: dotnet build -c Release -r linux-x64 Projects/Argon2/Argon2.csproj - run: name: Install ModernUO dependencies command: dotnet restore - save_cache: paths: - - ~/.nuget/packages - key: dotnet-packages-linux-{{ checksum "Projects/Server/Server.csproj" }}-{{ checksum "Projects/Scripts/Scripts.csproj" }} + - /mnt/ramdisk/packages + key: dotnet-packages-v1-{{ arch }}-{{ checksum ".circleci/checksum.txt" }} - run: name: Building ModernUO for Linux (Debug) - command: dotnet build -c Release /p:PublishProfile=Linux + command: dotnet build -c Debug -r linux-x64 build_windows: description: Build application executor: win/default @@ -37,17 +40,20 @@ jobs: - checkout - restore_cache: keys: - - dotnet-packages-windows-{{ checksum "Projects/Server/Server.csproj" }}-{{ checksum "Projects/Scripts/Scripts.csproj" }} + - dotnet-packages-v1-{{ arch }}-{{ checksum ".circleci/checksum.txt" }} + - run: + name: Build Argon2 + command: dotnet build -c Debug -r win-x64 Projects/Argon2/Argon2.csproj - run: name: Install ModernUO dependencies command: dotnet restore - save_cache: paths: - - ~/.nuget/packages - key: dotnet-packages-windows-{{ checksum "Projects/Server/Server.csproj" }}-{{ checksum "Projects/Scripts/Scripts.csproj" }} + - /mnt/ramdisk/packages + key: dotnet-packages-v1-{{ arch }}-{{ checksum ".circleci/checksum.txt" }} - run: name: Building ModernUO for Windows (Debug) - command: dotnet build -c Release /p:PublishProfile=Windows + command: dotnet build -c Debug -r win-x64 code_styling: description: Code analysis executor: linux-build @@ -56,17 +62,20 @@ jobs: - checkout - restore_cache: keys: - - dotnet-packages-linux-{{ checksum "Projects/Server/Server.csproj" }}-{{ checksum "Projects/Scripts/Scripts.csproj" }} + - dotnet-packages-v1-{{ arch }}-{{ checksum ".circleci/checksum.txt" }} + - run: + name: Build Argon2 + command: dotnet build -c Debug -r linux-x64 Projects/Argon2/Argon2.csproj - run: name: Install ModernUO dependencies command: dotnet restore - save_cache: paths: - - ~/.nuget/packages - key: dotnet-packages-linux-{{ checksum "Projects/Server/Server.csproj" }}-{{ checksum "Projects/Scripts/Scripts.csproj" }} + - /mnt/ramdisk/packages + key: dotnet-packages-v1-{{ arch }}-{{ checksum ".circleci/checksum.txt" }} - run: name: Building with Analyzers - command: dotnet build -c Analyze /p:PublishProfile=Linux + command: dotnet build -c Analyze -r linux-x64 tests: description: Unit Tests executor: linux-build @@ -75,17 +84,20 @@ jobs: - checkout - restore_cache: keys: - - dotnet-packages-linux-{{ checksum "Projects/Server/Server.csproj" }}-{{ checksum "Projects/Scripts/Scripts.csproj" }} + - dotnet-packages-v1-{{ arch }}-{{ checksum ".circleci/checksum.txt" }} + - run: + name: Build Argon2 + command: dotnet build -c Debug -r linux-x64 Projects/Argon2/Argon2.csproj - run: name: Install ModernUO dependencies command: dotnet restore - save_cache: paths: - - ~/.nuget/packages - key: dotnet-packages-linux-{{ checksum "Projects/Server/Server.csproj" }}-{{ checksum "Projects/Scripts/Scripts.csproj" }} + - /mnt/ramdisk/packages + key: dotnet-packages-linux-{{ checksum ".circleci/checksum.txt" }} - run: name: Running unit tests - command: dotnet test + command: dotnet test -r linux-x64 workflows: build-and-test: jobs: diff --git a/.circleci/genchecksum.sh b/.circleci/genchecksum.sh new file mode 100755 index 000000000..1d30da007 --- /dev/null +++ b/.circleci/genchecksum.sh @@ -0,0 +1 @@ +sha1sum ../Projects/*/*.csproj ../Projects/Argon2/*.cs > "checksum.txt" diff --git a/.gitignore b/.gitignore index b814ef215..76af4a723 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,6 @@ /.vs .DS_Store + +/packages/* +!/packages/do_not_delete diff --git a/ModernUO.sln b/ModernUO.sln index a2edfb108..e1d9f033b 100644 --- a/ModernUO.sln +++ b/ModernUO.sln @@ -2,45 +2,53 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.29102.190 MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Argon2", "Projects\Argon2\Argon2.csproj", "{97150E9D-5561-4D3C-9B78-E795544507D0}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Server", "Projects\Server\Server.csproj", "{5E93BB35-3661-4822-9A8A-859726BAD87F}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Scripts", "Projects\Scripts\Scripts.csproj", "{83CF2484-BCCB-4B7C-9C5F-7AB43AEA5E8F}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Server.Tests", "Projects\Server.Tests\Server.Tests.csproj", "{D7A5D3AF-D607-46EF-BAAD-0D424190311F}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Server.Tests", "Projects\Server.Tests\Server.Tests.csproj", "{D7A5D3AF-D607-46EF-BAAD-0D424190311F}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Scripts.Tests", "Projects\Scripts.Tests\Scripts.Tests.csproj", "{3C4797F9-603E-44EF-8E8C-9275CC9EA74B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Scripts.Tests", "Projects\Scripts.Tests\Scripts.Tests.csproj", "{3C4797F9-603E-44EF-8E8C-9275CC9EA74B}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution - Analyze|Any CPU = Analyze|Any CPU - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU + Analyze|x64 = Analyze|x64 + Debug|x64 = Debug|x64 + Release|x64 = Release|x64 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {5E93BB35-3661-4822-9A8A-859726BAD87F}.Analyze|Any CPU.ActiveCfg = Analyze|Any CPU - {5E93BB35-3661-4822-9A8A-859726BAD87F}.Analyze|Any CPU.Build.0 = Analyze|Any CPU - {5E93BB35-3661-4822-9A8A-859726BAD87F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5E93BB35-3661-4822-9A8A-859726BAD87F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5E93BB35-3661-4822-9A8A-859726BAD87F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5E93BB35-3661-4822-9A8A-859726BAD87F}.Release|Any CPU.Build.0 = Release|Any CPU - {83CF2484-BCCB-4B7C-9C5F-7AB43AEA5E8F}.Analyze|Any CPU.ActiveCfg = Analyze|Any CPU - {83CF2484-BCCB-4B7C-9C5F-7AB43AEA5E8F}.Analyze|Any CPU.Build.0 = Analyze|Any CPU - {83CF2484-BCCB-4B7C-9C5F-7AB43AEA5E8F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {83CF2484-BCCB-4B7C-9C5F-7AB43AEA5E8F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {83CF2484-BCCB-4B7C-9C5F-7AB43AEA5E8F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {83CF2484-BCCB-4B7C-9C5F-7AB43AEA5E8F}.Release|Any CPU.Build.0 = Release|Any CPU - {D7A5D3AF-D607-46EF-BAAD-0D424190311F}.Analyze|Any CPU.ActiveCfg = Debug|Any CPU - {D7A5D3AF-D607-46EF-BAAD-0D424190311F}.Analyze|Any CPU.Build.0 = Debug|Any CPU - {D7A5D3AF-D607-46EF-BAAD-0D424190311F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D7A5D3AF-D607-46EF-BAAD-0D424190311F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D7A5D3AF-D607-46EF-BAAD-0D424190311F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D7A5D3AF-D607-46EF-BAAD-0D424190311F}.Release|Any CPU.Build.0 = Release|Any CPU - {3C4797F9-603E-44EF-8E8C-9275CC9EA74B}.Analyze|Any CPU.ActiveCfg = Debug|Any CPU - {3C4797F9-603E-44EF-8E8C-9275CC9EA74B}.Analyze|Any CPU.Build.0 = Debug|Any CPU - {3C4797F9-603E-44EF-8E8C-9275CC9EA74B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3C4797F9-603E-44EF-8E8C-9275CC9EA74B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3C4797F9-603E-44EF-8E8C-9275CC9EA74B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3C4797F9-603E-44EF-8E8C-9275CC9EA74B}.Release|Any CPU.Build.0 = Release|Any CPU + {5E93BB35-3661-4822-9A8A-859726BAD87F}.Analyze|x64.ActiveCfg = Release|x64 + {5E93BB35-3661-4822-9A8A-859726BAD87F}.Analyze|x64.Build.0 = Release|x64 + {5E93BB35-3661-4822-9A8A-859726BAD87F}.Debug|x64.ActiveCfg = Debug|x64 + {5E93BB35-3661-4822-9A8A-859726BAD87F}.Debug|x64.Build.0 = Debug|x64 + {5E93BB35-3661-4822-9A8A-859726BAD87F}.Release|x64.ActiveCfg = Release|x64 + {5E93BB35-3661-4822-9A8A-859726BAD87F}.Release|x64.Build.0 = Release|x64 + {83CF2484-BCCB-4B7C-9C5F-7AB43AEA5E8F}.Analyze|x64.ActiveCfg = Release|x64 + {83CF2484-BCCB-4B7C-9C5F-7AB43AEA5E8F}.Analyze|x64.Build.0 = Release|x64 + {83CF2484-BCCB-4B7C-9C5F-7AB43AEA5E8F}.Debug|x64.ActiveCfg = Debug|x64 + {83CF2484-BCCB-4B7C-9C5F-7AB43AEA5E8F}.Debug|x64.Build.0 = Debug|x64 + {83CF2484-BCCB-4B7C-9C5F-7AB43AEA5E8F}.Release|x64.ActiveCfg = Release|x64 + {83CF2484-BCCB-4B7C-9C5F-7AB43AEA5E8F}.Release|x64.Build.0 = Release|x64 + {D7A5D3AF-D607-46EF-BAAD-0D424190311F}.Analyze|x64.ActiveCfg = Debug|x64 + {D7A5D3AF-D607-46EF-BAAD-0D424190311F}.Analyze|x64.Build.0 = Debug|x64 + {D7A5D3AF-D607-46EF-BAAD-0D424190311F}.Debug|x64.ActiveCfg = Debug|x64 + {D7A5D3AF-D607-46EF-BAAD-0D424190311F}.Debug|x64.Build.0 = Debug|x64 + {D7A5D3AF-D607-46EF-BAAD-0D424190311F}.Release|x64.ActiveCfg = Release|x64 + {D7A5D3AF-D607-46EF-BAAD-0D424190311F}.Release|x64.Build.0 = Release|x64 + {3C4797F9-603E-44EF-8E8C-9275CC9EA74B}.Analyze|x64.ActiveCfg = Debug|x64 + {3C4797F9-603E-44EF-8E8C-9275CC9EA74B}.Analyze|x64.Build.0 = Debug|x64 + {3C4797F9-603E-44EF-8E8C-9275CC9EA74B}.Debug|x64.ActiveCfg = Debug|x64 + {3C4797F9-603E-44EF-8E8C-9275CC9EA74B}.Debug|x64.Build.0 = Debug|x64 + {3C4797F9-603E-44EF-8E8C-9275CC9EA74B}.Release|x64.ActiveCfg = Release|x64 + {3C4797F9-603E-44EF-8E8C-9275CC9EA74B}.Release|x64.Build.0 = Release|x64 + {97150E9D-5561-4D3C-9B78-E795544507D0}.Analyze|x64.ActiveCfg = Release|x64 + {97150E9D-5561-4D3C-9B78-E795544507D0}.Analyze|x64.Build.0 = Release|x64 + {97150E9D-5561-4D3C-9B78-E795544507D0}.Debug|x64.ActiveCfg = Debug|x64 + {97150E9D-5561-4D3C-9B78-E795544507D0}.Debug|x64.Build.0 = Debug|x64 + {97150E9D-5561-4D3C-9B78-E795544507D0}.Release|x64.ActiveCfg = Release|x64 + {97150E9D-5561-4D3C-9B78-E795544507D0}.Release|x64.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/NuGet.Config b/NuGet.Config new file mode 100644 index 000000000..43de52fb5 --- /dev/null +++ b/NuGet.Config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Projects/Argon2/Argon2.cs b/Projects/Argon2/Argon2.cs new file mode 100644 index 000000000..9be9b86be --- /dev/null +++ b/Projects/Argon2/Argon2.cs @@ -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 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); + } + } +} diff --git a/Projects/Argon2/Argon2.csproj b/Projects/Argon2/Argon2.csproj new file mode 100644 index 000000000..b76f1e543 --- /dev/null +++ b/Projects/Argon2/Argon2.csproj @@ -0,0 +1,42 @@ + + + win-x64;linux-x64;osx-x64 + 1.0.0 + Server + Argon2.Bindings + x64 + 8.0 + netcoreapp3.1 + true + true + true + false + Debug;Release;Analyze + WINDOWS + 1.0.0 + x64 + + + + true + runtimes/win-x64/native + PreserveNewest + + + true + runtimes/osx-x64/native + PreserveNewest + + + true + runtimes/linux-x64/native + PreserveNewest + + + + + + + diff --git a/Projects/Argon2/Argon2.nuspec b/Projects/Argon2/Argon2.nuspec new file mode 100644 index 000000000..d4f061905 --- /dev/null +++ b/Projects/Argon2/Argon2.nuspec @@ -0,0 +1,15 @@ + + + + Argon2.Bindings + 1.0.0 + Kamron Batman + Kamron Batman + MIT + https://github.com/modernUO/ModernUO + false + Argon2 C# Bindings for ModernUO + Initial release + Copyright 2020 + + diff --git a/Projects/Argon2/Argon2Context.cs b/Projects/Argon2/Argon2Context.cs new file mode 100644 index 000000000..302e1cca2 --- /dev/null +++ b/Projects/Argon2/Argon2Context.cs @@ -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; + } +} diff --git a/Projects/Argon2/Argon2Error.cs b/Projects/Argon2/Argon2Error.cs new file mode 100644 index 000000000..4509170ff --- /dev/null +++ b/Projects/Argon2/Argon2Error.cs @@ -0,0 +1,77 @@ +namespace Server.Accounting.Security +{ + /// + /// 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 + /// + public enum Argon2Error + { + /// + /// The operation was successful + /// + OK = 0, + + /// + /// The output hash length is less than 4 bytes + /// + OUTPUT_TOO_SHORT = -2, + + /// + /// The salt is less than 8 bytes + /// + SALT_TOO_SHORT = -6, + + /// + /// The time cost is less than 1 + /// + TIME_TOO_SMALL = -12, + + /// + /// The memory cost is less than 8 (KiB) + /// + MEMORY_TOO_LITTLE = -14, + /// + /// The memory cost is greater than 2^21 (KiB) (2 GiB) + /// + MEMORY_TOO_MUCH = -15, + + /// + /// The parallelism is less than 1 + /// + LANES_TOO_FEW = -16, + /// + /// The parallelism is greater than 16,777,215 + /// + LANES_TOO_MANY = -17, + + /// + /// Memory allocation failed + /// + MEMORY_ALLOCATION_ERROR = -22, + + /// + /// The parallelism is less than 1 + /// + THREADS_TOO_FEW = -28, + /// + /// The parallelism is greater than 16,777,215 + /// + THREADS_TOO_MANY = -29, + + /// + /// This will not be returned from the C# PasswordHasher wrapper + /// + DECODING_FAIL = -32, + /// + /// Unable to create the number of threads requested + /// + THREAD_FAIL = -33, + + /// + /// This will not be returned from the C# PasswordHasher wrapper + /// + VERIFY_MISMATCH = -35 + } +} diff --git a/Projects/Argon2/Argon2Exception.cs b/Projects/Argon2/Argon2Exception.cs new file mode 100644 index 000000000..f511ead78 --- /dev/null +++ b/Projects/Argon2/Argon2Exception.cs @@ -0,0 +1,20 @@ +using System; + +namespace Server.Accounting.Security +{ + /// + /// 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) + /// + public class Argon2Exception : Exception + { + /// + /// Construct an Argon2Exception with the specified Argon2 error code + /// Which method the Argon2Exception originated from + /// The error returned from the Argon2 library + /// + public Argon2Exception(string action, Argon2Error error) : base(string.Format("Error during Argon2 {0}: ({1}) {2}", action, (int)error, error)) {} + } +} diff --git a/Projects/Argon2/Argon2PasswordHasher.cs b/Projects/Argon2/Argon2PasswordHasher.cs new file mode 100644 index 000000000..75f1f2d42 --- /dev/null +++ b/Projects/Argon2/Argon2PasswordHasher.cs @@ -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 +{ + /// + /// 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 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); + + + /// + /// 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; } + + + /// + /// 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() + { + TimeCost = 3; + MemoryCost = 8192; + Parallelism = 1; + ArgonType = Argon2Type.Argon2i; + HashLength = 32; + StringEncoding = Encoding.UTF8; + } + + + /// + /// Initialize the Argon2 PasswordHasher with the performance and algorithm settings to use while hashing + /// How many iterations of the Argon2 hash to perform (default: 3, must be at least 1) + /// How much memory to use while hashing in kibibytes (KiB) (default: 8192 KiB [8 MiB], must be at least 8 KiB) + /// How many threads to use while hashing (default: 1, must be at least 1) + /// The type of Argon2 hashing algorithm to use (Independent [default] or Dependent) + /// The length of the resulting hash in bytes (default: 32) + /// + 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; + } + + + /// + /// 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(string password) + { + CheckNull("Hash", "password", password); + + return Hash(StringEncoding.GetBytes(password)); + } + + /// + /// 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. + /// The raw bytes of the password to be hashed + /// A formatted string representing the hashed password, encoded with the parameters used to perform the hash + /// + public string Hash(byte[] password) + { + CheckNull("Hash", "password", password); + + return Hash(password, GenerateSalt()); + } + + /// + /// 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. + /// A string representing the password to be hashed. The password is first decoded into bytes using StringEncoding (default: Encoding.UTF8) + /// 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) + /// A formatted string representing the hashed password, encoded with the parameters used to perform the hash + /// + public string Hash(string password, string salt) + { + CheckNull("Hash", "password", password, "salt", salt); + + return Hash(StringEncoding.GetBytes(password), StringEncoding.GetBytes(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(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); + } + + + /// + /// 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. + /// A string representing the password to be hashed. The password is first decoded into bytes using StringEncoding (default: Encoding.UTF8) + /// 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) + /// A byte array containing only the resulting hash + /// + public byte[] HashRaw(string password, string salt) + { + CheckNull("HashRaw", "password", password, "salt", salt); + + return HashRaw(StringEncoding.GetBytes(password), StringEncoding.GetBytes(salt)); + } + + /// + /// 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 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; + } + + + /// + /// 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(string expectedHash, string password) + { + CheckNull("Verify", "expectedHash", expectedHash, "password", password); + + return Verify(expectedHash, StringEncoding.GetBytes(password)); + } + + /// + /// 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(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); + } + + + + /// + /// 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 password to hash and compare its result to expectedHash. The password is first decoded into bytes using StringEncoding (default: Encoding.UTF8) + /// 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(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); + } + + /// + /// 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(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; + } + + + /// + /// Generate salt using a Cryptographically-Secure Pseudo-Random Number Generator + /// The number of bytes of salt to generate (default: 16) + /// A array of randomly-generated bytes + /// + public static byte[] GenerateSalt(uint byteLength = 16) + { + var salt = new byte[byteLength]; + Rng.GetBytes(salt); + return salt; + } + + + /// + /// 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(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)); + } + } +} diff --git a/Projects/Argon2/Argon2Type.cs b/Projects/Argon2/Argon2Type.cs new file mode 100644 index 000000000..dd24c27a5 --- /dev/null +++ b/Projects/Argon2/Argon2Type.cs @@ -0,0 +1,18 @@ +namespace Server.Accounting.Security +{ + /// + /// The type of Argon2 hashing algorithm to use. + /// + public enum Argon2Type + { + /// + /// The memory access is dependent upon the hash value (vulnerable to side-channel attacks) + /// + Argon2d = 0, + + /// + /// The memory access is independent upon the hash value (safe from side-channel atacks) + /// + Argon2i = 1 + } +} diff --git a/Projects/Argon2/HashMetadata.cs b/Projects/Argon2/HashMetadata.cs new file mode 100644 index 000000000..ba1758515 --- /dev/null +++ b/Projects/Argon2/HashMetadata.cs @@ -0,0 +1,60 @@ +using System; + +namespace Server.Accounting.Security +{ + /// + /// HashMetadata represents the information stored in the encoded Argon2 format + /// + public class HashMetadata + { + /// + /// 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; } + + /// + /// How much memory to use while hashing in kibibytes (KiB) + /// + public uint MemoryCost { get; set; } + + /// + /// How many iterations of the Argon2 hash to perform + /// + public uint TimeCost { get; set; } + + /// + /// How many threads to use while hashing + /// + public uint Parallelism { get; set; } + + /// + /// The raw bytes of the salt + /// + public byte[] Salt { get; set; } + + /// + /// The raw bytes of the hash + /// + public byte[] Hash { get; set; } + + + /// + /// A base-64 encoded string of the salt, minus the padding (=) characters + /// + public string GetBase64Salt() => Convert.ToBase64String(Salt).Replace("=", ""); + + /// + /// A base-64 encoded string of the hash, minus the padding (=) characters + /// + public string GetBase64Hash() => Convert.ToBase64String(Hash).Replace("=", ""); + + + /// + /// Converts HashMetadata back into the original Argon2 formatted string. + /// + public override string ToString() => + $"$argon2{(ArgonType == Argon2Type.Argon2i ? "i" : "d")}$v=19$m={MemoryCost},t={TimeCost},p={Parallelism}${GetBase64Salt()}${GetBase64Hash()}"; + } +} diff --git a/Projects/Argon2/Utility.cs b/Projects/Argon2/Utility.cs new file mode 100644 index 000000000..e000e30ed --- /dev/null +++ b/Projects/Argon2/Utility.cs @@ -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; + } +} diff --git a/Projects/Argon2/runtimes/linux-x64/native/libargon2.so b/Projects/Argon2/runtimes/linux-x64/native/libargon2.so new file mode 100755 index 000000000..5303d8d75 Binary files /dev/null and b/Projects/Argon2/runtimes/linux-x64/native/libargon2.so differ diff --git a/Projects/Argon2/runtimes/osx-x64/native/libargon2.dylib b/Projects/Argon2/runtimes/osx-x64/native/libargon2.dylib new file mode 100755 index 000000000..1c0cc3953 Binary files /dev/null and b/Projects/Argon2/runtimes/osx-x64/native/libargon2.dylib differ diff --git a/Projects/Argon2/runtimes/win-x64/native/argon2.dll b/Projects/Argon2/runtimes/win-x64/native/argon2.dll new file mode 100644 index 000000000..6a46dd60d Binary files /dev/null and b/Projects/Argon2/runtimes/win-x64/native/argon2.dll differ diff --git a/Projects/Scripts.Tests/Accounting/Security/Argon2PasswordProtectionTest.cs b/Projects/Scripts.Tests/Accounting/Security/Argon2PasswordProtectionTest.cs new file mode 100644 index 000000000..082ddb0cb --- /dev/null +++ b/Projects/Scripts.Tests/Accounting/Security/Argon2PasswordProtectionTest.cs @@ -0,0 +1,30 @@ +using Server.Accounting.Security; +using Xunit; + +namespace Server.Tests.Accounting.Security +{ + public class Argon2PasswordProtectionTest + { + private const string plainPassword = "hello-good-sir"; + + [Fact] + public void TestValidates() + { + var passwordProtection = new Argon2PasswordProtection(); + + string encryptedPassword = passwordProtection.EncryptPassword(plainPassword); + + Assert.True(passwordProtection.ValidatePassword(encryptedPassword, plainPassword)); + } + + [Fact] + public void TestPasswordDoesNotValidate() + { + var passwordProtection = new Argon2PasswordProtection(); + + string encryptedPassword = passwordProtection.EncryptPassword(plainPassword); + + Assert.False(passwordProtection.ValidatePassword(encryptedPassword, "Not the same password")); + } + } +} diff --git a/Projects/Scripts.Tests/Accounting/PBKDF2PasswordProtectionTest.cs b/Projects/Scripts.Tests/Accounting/Security/PBKDF2PasswordProtectionTest.cs similarity index 87% rename from Projects/Scripts.Tests/Accounting/PBKDF2PasswordProtectionTest.cs rename to Projects/Scripts.Tests/Accounting/Security/PBKDF2PasswordProtectionTest.cs index 1b1ec46bf..58eba9ca0 100644 --- a/Projects/Scripts.Tests/Accounting/PBKDF2PasswordProtectionTest.cs +++ b/Projects/Scripts.Tests/Accounting/Security/PBKDF2PasswordProtectionTest.cs @@ -1,7 +1,7 @@ +using Server.Accounting.Security; using Xunit; -using Server.Accounting; -namespace Server.Tests.Accounting +namespace Server.Tests.Accounting.Security { public class PBKDF2PasswordProtectionTest { diff --git a/Projects/Scripts.Tests/Scripts.Tests.csproj b/Projects/Scripts.Tests/Scripts.Tests.csproj index d67207c07..69728f1bd 100644 --- a/Projects/Scripts.Tests/Scripts.Tests.csproj +++ b/Projects/Scripts.Tests/Scripts.Tests.csproj @@ -1,7 +1,8 @@ netcoreapp3.1 - false + x64 + WINDOWS diff --git a/Projects/Scripts/Accounting/Account.cs b/Projects/Scripts/Accounting/Account.cs index 1252c8855..6fedb2ccb 100644 --- a/Projects/Scripts/Accounting/Account.cs +++ b/Projects/Scripts/Accounting/Account.cs @@ -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); diff --git a/Projects/Scripts/Accounting/AccountSecurity.cs b/Projects/Scripts/Accounting/AccountSecurity.cs deleted file mode 100644 index 0e14425ea..000000000 --- a/Projects/Scripts/Accounting/AccountSecurity.cs +++ /dev/null @@ -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; - } - } -} diff --git a/Projects/Scripts/Accounting/Security/AccountSecurity.cs b/Projects/Scripts/Accounting/Security/AccountSecurity.cs new file mode 100644 index 000000000..57f5dc37b --- /dev/null +++ b/Projects/Scripts/Accounting/Security/AccountSecurity.cs @@ -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; + } + } +} diff --git a/Projects/Scripts/Accounting/Security/Argon2PasswordProtection.cs b/Projects/Scripts/Accounting/Security/Argon2PasswordProtection.cs new file mode 100644 index 000000000..3d7cc0611 --- /dev/null +++ b/Projects/Scripts/Accounting/Security/Argon2PasswordProtection.cs @@ -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); + } +} diff --git a/Projects/Scripts/Accounting/Security/MD5PasswordProtection.cs b/Projects/Scripts/Accounting/Security/MD5PasswordProtection.cs new file mode 100644 index 000000000..23fc59a75 --- /dev/null +++ b/Projects/Scripts/Accounting/Security/MD5PasswordProtection.cs @@ -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 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; + } +} diff --git a/Projects/Scripts/Accounting/PBKDF2PasswordProtection.cs b/Projects/Scripts/Accounting/Security/PBKDF2PasswordProtection.cs similarity index 91% rename from Projects/Scripts/Accounting/PBKDF2PasswordProtection.cs rename to Projects/Scripts/Accounting/Security/PBKDF2PasswordProtection.cs index da0944e15..672efc6d5 100644 --- a/Projects/Scripts/Accounting/PBKDF2PasswordProtection.cs +++ b/Projects/Scripts/Accounting/Security/PBKDF2PasswordProtection.cs @@ -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; diff --git a/Projects/Scripts/Accounting/Security/SHA1PasswordProtection.cs b/Projects/Scripts/Accounting/Security/SHA1PasswordProtection.cs new file mode 100644 index 000000000..2f45c5e85 --- /dev/null +++ b/Projects/Scripts/Accounting/Security/SHA1PasswordProtection.cs @@ -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 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; + } +} diff --git a/Projects/Scripts/Scripts.csproj b/Projects/Scripts/Scripts.csproj index 114419f4f..a30ebe1c3 100644 --- a/Projects/Scripts/Scripts.csproj +++ b/Projects/Scripts/Scripts.csproj @@ -19,6 +19,8 @@ $(SolutionDir)\Distribution\Assemblies true Debug;Release;Analyze + WINDOWS + x64 @@ -38,14 +40,14 @@ - - TRACE;DEBUG + + TRACE;DEBUG;$(DefineConstants) false - + true - + true true true @@ -64,6 +66,7 @@ + @@ -74,4 +77,21 @@ + + + 1.1.118 + all + + + stylecop.json + + + 2.9.8 + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + Rules.rulest + + diff --git a/Projects/Server.Tests/Server.Tests.csproj b/Projects/Server.Tests/Server.Tests.csproj index 09142eb9b..562d89327 100644 --- a/Projects/Server.Tests/Server.Tests.csproj +++ b/Projects/Server.Tests/Server.Tests.csproj @@ -2,6 +2,7 @@ netcoreapp3.1 false + AnyCPU;x64 diff --git a/Projects/Server/Buffers/BufferReader.cs b/Projects/Server/Buffers/BufferReader.cs index f35d4629d..b534ef704 100644 --- a/Projects/Server/Buffers/BufferReader.cs +++ b/Projects/Server/Buffers/BufferReader.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. // See LICENSE file in the project root for full license information. -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Threading; diff --git a/Projects/Server/Properties/PublishProfiles/Linux-SelfContained.pubxml b/Projects/Server/Properties/PublishProfiles/Linux-SelfContained.pubxml deleted file mode 100644 index c43c47567..000000000 --- a/Projects/Server/Properties/PublishProfiles/Linux-SelfContained.pubxml +++ /dev/null @@ -1,10 +0,0 @@ - - - - FileSystem - Release - linux-x64 - true - true - - diff --git a/Projects/Server/Properties/PublishProfiles/Linux.pubxml b/Projects/Server/Properties/PublishProfiles/Linux.pubxml deleted file mode 100644 index fb76af4c2..000000000 --- a/Projects/Server/Properties/PublishProfiles/Linux.pubxml +++ /dev/null @@ -1,9 +0,0 @@ - - - - FileSystem - Release - linux-x64 - false - - diff --git a/Projects/Server/Properties/PublishProfiles/OSX-SelfContained.pubxml b/Projects/Server/Properties/PublishProfiles/OSX-SelfContained.pubxml deleted file mode 100644 index 2f9813651..000000000 --- a/Projects/Server/Properties/PublishProfiles/OSX-SelfContained.pubxml +++ /dev/null @@ -1,10 +0,0 @@ - - - - FileSystem - Release - osx-x64 - true - true - - diff --git a/Projects/Server/Properties/PublishProfiles/OSX.pubxml b/Projects/Server/Properties/PublishProfiles/OSX.pubxml deleted file mode 100644 index 7c45904d8..000000000 --- a/Projects/Server/Properties/PublishProfiles/OSX.pubxml +++ /dev/null @@ -1,9 +0,0 @@ - - - - FileSystem - Release - osx-x64 - false - - diff --git a/Projects/Server/Properties/PublishProfiles/Windows-SelfContained.pubxml b/Projects/Server/Properties/PublishProfiles/Windows-SelfContained.pubxml deleted file mode 100644 index a43797ef8..000000000 --- a/Projects/Server/Properties/PublishProfiles/Windows-SelfContained.pubxml +++ /dev/null @@ -1,10 +0,0 @@ - - - - FileSystem - Release - win-x64 - true - true - - diff --git a/Projects/Server/Properties/PublishProfiles/Windows.pubxml b/Projects/Server/Properties/PublishProfiles/Windows.pubxml deleted file mode 100644 index c6a68c538..000000000 --- a/Projects/Server/Properties/PublishProfiles/Windows.pubxml +++ /dev/null @@ -1,9 +0,0 @@ - - - - FileSystem - Release - win-x64 - false - - diff --git a/Projects/Server/Server.csproj b/Projects/Server/Server.csproj index b07198e22..a629ba630 100644 --- a/Projects/Server/Server.csproj +++ b/Projects/Server/Server.csproj @@ -6,7 +6,7 @@ Server.Core ModernUO - 0.3.0 + 0.3.1 Kamron Batman ModernUO ModernUO Server @@ -17,13 +17,14 @@ x64 8.0 $(SolutionDir)\Distribution - $(SolutionDir)\Distribution - $(SolutionDir)\Distribution + true true false Debug;Release;Analyze + x64 @@ -48,14 +49,14 @@ - + TRACE;DEBUG false - + true - + true true true @@ -86,13 +87,21 @@ - - - - + + + 1.1.118 + all + + + stylecop.json + + + 2.9.8 all runtime; build; native; contentfiles; analyzers; buildtransitive - + + Rules.rulest + diff --git a/THIRD-PARTY-NOTICES b/THIRD-PARTY-NOTICES index 0e65567e8..b8636df40 100644 --- a/THIRD-PARTY-NOTICES +++ b/THIRD-PARTY-NOTICES @@ -580,6 +580,8 @@ consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. +License notice for BufferReader/BufferWrite +--------------------------- The MIT License (MIT) Copyright (c) Harry Pierson @@ -601,3 +603,27 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +License notice for csharp-argon-2 +--------------------------- +The MIT License (MIT) + +Copyright (c) 2016 Kevin Spinar (Alipha) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Tools/clean.sh b/Tools/clean.sh index d289d4a1b..f8054d000 100755 --- a/Tools/clean.sh +++ b/Tools/clean.sh @@ -1,4 +1,5 @@ rm -rf Projects/*/obj +rm -rf Projects/*/bin rm -f Distribution/Assemblies/BouncyCastle.Crypto.dll rm -f Distribution/Assemblies/MailKit.dll @@ -12,10 +13,15 @@ rm -f Distribution/Assemblies/Microsoft.Extensions.Logging.Abstractions.dll rm -f Distribution/Assemblies/Microsoft.Extensions.Primitives.dll rm -f Distribution/Assemblies/MimeKit.dll rm -f Distribution/Assemblies/System.IO.Pipelines.dll +rm -f Distribution/Assemblies/Argon2.Bindings.dll +rm -f Distribution/Assemblies/libargon2.so +rm -f Distribution/Assemblies/libargon2.dylib +rm -f Distribution/Assemblies/argon2.dll rm -f Distribution/libdrng.dll rm -f Distribution/libuv.dll rm -f Distribution/libuv.so +rm -f Distribution/libuv.dylib rm -f Distribution/Microsoft.AspNetCore.Connections.Abstractions.dll rm -f Distribution/Microsoft.AspNetCore.Http.Features.dll rm -f Distribution/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.dll diff --git a/Tools/publish-linux-debug.cmd b/Tools/publish-linux-debug.cmd index a33b7d284..7d833938c 100755 --- a/Tools/publish-linux-debug.cmd +++ b/Tools/publish-linux-debug.cmd @@ -1 +1,2 @@ -dotnet publish -c Debug /p:PublishProfile=Linux +dotnet build -c Debug -r linux-x64 Projects/Argon2/Argon2.csproj +dotnet publish -c Debug -r linux-x64 --self-contained=false diff --git a/Tools/publish-linux-debug.sh b/Tools/publish-linux-debug.sh index 69c0e045f..e121aceb1 100755 --- a/Tools/publish-linux-debug.sh +++ b/Tools/publish-linux-debug.sh @@ -1 +1,2 @@ -dotnet publish -c Debug /p:PublishProfile=Linux +dotnet build -c Debug -r linux-x64 Projects/Argon2/Argon2.csproj +dotnet publish -c Debug -r linux-x64 --self-contained=false diff --git a/Tools/publish-linux.cmd b/Tools/publish-linux.cmd index 66e59e289..611f1193a 100755 --- a/Tools/publish-linux.cmd +++ b/Tools/publish-linux.cmd @@ -1 +1,2 @@ -dotnet publish -c Release /p:PublishProfile=Linux +dotnet build -c Release -r linux-x64 Projects/Argon2/Argon2.csproj +dotnet publish -c Release -r linux-x64 --self-contained=false diff --git a/Tools/publish-linux.sh b/Tools/publish-linux.sh index 281501fea..f87640036 100755 --- a/Tools/publish-linux.sh +++ b/Tools/publish-linux.sh @@ -1 +1,2 @@ -dotnet publish -c Release /p:PublishProfile=Linux +dotnet build -c Release -r linux-x64 Projects/Argon2/Argon2.csproj +dotnet publish -c Release -r linux-x64 --self-contained=false diff --git a/Tools/publish-osx-debug.cmd b/Tools/publish-osx-debug.cmd index a8b55ff26..4566baa3e 100755 --- a/Tools/publish-osx-debug.cmd +++ b/Tools/publish-osx-debug.cmd @@ -1 +1,2 @@ -dotnet publish -c Debug /p:PublishProfile=OSX +dotnet build -c Debug -r osx-x64 Projects/Argon2/Argon2.csproj +dotnet publish -c Debug -r osx-x64 --self-contained=false diff --git a/Tools/publish-osx-debug.sh b/Tools/publish-osx-debug.sh index 7bf73bc47..e66f2db29 100755 --- a/Tools/publish-osx-debug.sh +++ b/Tools/publish-osx-debug.sh @@ -1 +1,2 @@ -dotnet publish -c Debug /p:PublishProfile=OSX +dotnet build -c Debug -r osx-x64 Projects/Argon2/Argon2.csproj +dotnet publish -c Debug -r osx-x64 --self-contained=false diff --git a/Tools/publish-osx.cmd b/Tools/publish-osx.cmd index 6952d1c4a..9565404f6 100755 --- a/Tools/publish-osx.cmd +++ b/Tools/publish-osx.cmd @@ -1 +1,2 @@ -dotnet publish -c Release /p:PublishProfile=OSX +dotnet build -c Release -r osx-x64 Projects/Argon2/Argon2.csproj +dotnet publish -c Release -r osx-x64 --self-contained=false diff --git a/Tools/publish-osx.sh b/Tools/publish-osx.sh index 73036b71d..249fe207a 100755 --- a/Tools/publish-osx.sh +++ b/Tools/publish-osx.sh @@ -1 +1,2 @@ -dotnet publish -c Release /p:PublishProfile=OSX +dotnet build -c Release -r osx-x64 Projects/Argon2/Argon2.csproj +dotnet publish -c Release -r osx-x64 --self-contained=false diff --git a/Tools/publish-windows-debug.cmd b/Tools/publish-windows-debug.cmd index d52f8b2e7..1398c2e10 100755 --- a/Tools/publish-windows-debug.cmd +++ b/Tools/publish-windows-debug.cmd @@ -1 +1,2 @@ -dotnet publish -c Debug /p:PublishProfile=Windows +dotnet build -c Debug -r win-x64 Projects/Argon2/Argon2.csproj +dotnet publish -c Debug -r win-x64 --self-contained=false diff --git a/Tools/publish-windows-debug.sh b/Tools/publish-windows-debug.sh index 1a0b31cc8..3b6a9f8b7 100755 --- a/Tools/publish-windows-debug.sh +++ b/Tools/publish-windows-debug.sh @@ -1 +1,2 @@ -dotnet publish -c Debug /p:PublishProfile=Windows +dotnet build -c Debug -r win-x64 Projects/Argon2/Argon2.csproj +dotnet publish -c Debug -r win-x64 --self-contained=false diff --git a/Tools/publish-windows.cmd b/Tools/publish-windows.cmd index fe2ac4372..6bb720871 100755 --- a/Tools/publish-windows.cmd +++ b/Tools/publish-windows.cmd @@ -1 +1,2 @@ -dotnet publish -c Release /p:PublishProfile=Windows +dotnet build -c Release -r win-x64 Projects/Argon2/Argon2.csproj +dotnet publish -c Release -r win-x64 --self-contained=false diff --git a/Tools/publish-windows.sh b/Tools/publish-windows.sh index 5b51c26c6..1e359fac6 100755 --- a/Tools/publish-windows.sh +++ b/Tools/publish-windows.sh @@ -1 +1,2 @@ -dotnet publish -c Release /p:PublishProfile=Windows +dotnet build -c Release -r win-x64 Projects/Argon2/Argon2.csproj +dotnet publish -c Release -r win-x64 --self-contained=false diff --git a/packages/do_not_delete b/packages/do_not_delete new file mode 100644 index 000000000..e69de29bb