From 2b4a6e1de141d1d753f2772f5258bfb0b40f9052 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Sat, 2 May 2020 00:32:00 -0700 Subject: [PATCH] Adds SHA512 support for ServUO (#124) --- .../Security/Argon2PasswordProtectionTest.cs | 30 ------------ .../Security/PBKDF2PasswordProtectionTest.cs | 30 ------------ .../Security/PasswordProtectionTest.cs | 46 +++++++++++++++++++ Projects/Scripts/Accounting/Account.cs | 34 ++++++++------ .../Accounting/Security/AccountSecurity.cs | 8 ++-- .../Security/MD5PasswordProtection.cs | 4 +- .../Security/SHA1PasswordProtection.cs | 4 +- .../Security/SHA2PasswordProtection.cs | 25 ++++++++++ 8 files changed, 99 insertions(+), 82 deletions(-) delete mode 100644 Projects/Scripts.Tests/Accounting/Security/Argon2PasswordProtectionTest.cs delete mode 100644 Projects/Scripts.Tests/Accounting/Security/PBKDF2PasswordProtectionTest.cs create mode 100644 Projects/Scripts.Tests/Accounting/Security/PasswordProtectionTest.cs create mode 100644 Projects/Scripts/Accounting/Security/SHA2PasswordProtection.cs diff --git a/Projects/Scripts.Tests/Accounting/Security/Argon2PasswordProtectionTest.cs b/Projects/Scripts.Tests/Accounting/Security/Argon2PasswordProtectionTest.cs deleted file mode 100644 index 082ddb0cb..000000000 --- a/Projects/Scripts.Tests/Accounting/Security/Argon2PasswordProtectionTest.cs +++ /dev/null @@ -1,30 +0,0 @@ -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/Security/PBKDF2PasswordProtectionTest.cs b/Projects/Scripts.Tests/Accounting/Security/PBKDF2PasswordProtectionTest.cs deleted file mode 100644 index 58eba9ca0..000000000 --- a/Projects/Scripts.Tests/Accounting/Security/PBKDF2PasswordProtectionTest.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Server.Accounting.Security; -using Xunit; - -namespace Server.Tests.Accounting.Security -{ - public class PBKDF2PasswordProtectionTest - { - private const string plainPassword = "hello-good-sir"; - - [Fact] - public void TestValidates() - { - var passwordProtection = new PBKDF2PasswordProtection(); - - string encryptedPassword = passwordProtection.EncryptPassword(plainPassword); - - Assert.True(passwordProtection.ValidatePassword(encryptedPassword, plainPassword)); - } - - [Fact] - public void TestPasswordDoesNotValidate() - { - var passwordProtection = new PBKDF2PasswordProtection(); - - string encryptedPassword = passwordProtection.EncryptPassword(plainPassword); - - Assert.False(passwordProtection.ValidatePassword(encryptedPassword, "Not the same password")); - } - } -} diff --git a/Projects/Scripts.Tests/Accounting/Security/PasswordProtectionTest.cs b/Projects/Scripts.Tests/Accounting/Security/PasswordProtectionTest.cs new file mode 100644 index 000000000..3ca0707e7 --- /dev/null +++ b/Projects/Scripts.Tests/Accounting/Security/PasswordProtectionTest.cs @@ -0,0 +1,46 @@ +using System; +using Server.Accounting; +using Server.Accounting.Security; +using Xunit; + +namespace Server.Tests.Accounting.Security +{ + public class PasswordProtectionTest + { + private const string plainPassword = "hello-good-sir"; + + [Theory] + [InlineData(typeof(Argon2PasswordProtection))] + [InlineData(typeof(PBKDF2PasswordProtection))] + [InlineData(typeof(SHA2PasswordProtection))] + [InlineData(typeof(SHA1PasswordProtection))] + [InlineData(typeof(MD5PasswordProtection))] + public void TestValidates(Type protectionType) + { + IPasswordProtection passwordProtection = Activator.CreateInstance(protectionType) as IPasswordProtection; + if (passwordProtection == null) + Assert.False(true, $"{protectionType.Name} is not an IPasswordProtection."); + + string encryptedPassword = passwordProtection.EncryptPassword(plainPassword); + + Assert.True(passwordProtection.ValidatePassword(encryptedPassword, plainPassword)); + } + + [Theory] + [InlineData(typeof(Argon2PasswordProtection))] + [InlineData(typeof(PBKDF2PasswordProtection))] + [InlineData(typeof(SHA2PasswordProtection))] + [InlineData(typeof(SHA1PasswordProtection))] + [InlineData(typeof(MD5PasswordProtection))] + public void TestPasswordDoesNotValidate(Type protectionType) + { + IPasswordProtection passwordProtection = Activator.CreateInstance(protectionType) as IPasswordProtection; + if (passwordProtection == null) + Assert.False(true, $"{protectionType.Name} is not an IPasswordProtection."); + + string encryptedPassword = passwordProtection.EncryptPassword(plainPassword); + + Assert.False(passwordProtection.ValidatePassword(encryptedPassword, "Not the same password")); + } + } +} diff --git a/Projects/Scripts/Accounting/Account.cs b/Projects/Scripts/Accounting/Account.cs index 6fedb2ccb..39473a93d 100644 --- a/Projects/Scripts/Accounting/Account.cs +++ b/Projects/Scripts/Accounting/Account.cs @@ -450,31 +450,35 @@ namespace Server.Accounting Accounts.Add(this); } + private bool UpgradePassword(string password, PasswordProtectionAlgorithm algorithm) + { + if (password == null || algorithm < m_PasswordAlgorithm) return false; + + m_PasswordAlgorithm = algorithm; + Password = password?.Replace("-", string.Empty); + return true; + } + public Account(XmlElement node) { Username = Utility.GetText(node["username"], "empty"); - // Note: ModernUO doesn't support plain passwords, MD5, or SHA1. - // 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); + // Backward compatibility with RunUO/ServUO if (m_PasswordAlgorithm == PasswordProtectionAlgorithm.None) { - string md5Password = Utility.GetText(node["cryptPassword"], null); - string sha1Password = Utility.GetText(node["newCryptPassword"], null); + bool upgraded = + UpgradePassword(Utility.GetText(node["newSecureCryptPassword"], null), PasswordProtectionAlgorithm.SHA2) || + UpgradePassword(Utility.GetText(node["newCryptPassword"], null), PasswordProtectionAlgorithm.SHA1) || + UpgradePassword(Utility.GetText(node["cryptPassword"], null), PasswordProtectionAlgorithm.MD5); - if (sha1Password != null) - { - Password = sha1Password; - m_PasswordAlgorithm = PasswordProtectionAlgorithm.SHA1; - } - else if (md5Password != null) - { - Password = md5Password; - m_PasswordAlgorithm = PasswordProtectionAlgorithm.MD5; - } + // Automatically upgrade plain passwords to current algorithm. + if (!upgraded) + SetPassword(Utility.GetText(node["password"], null)); } + else + Password = Utility.GetText(node["password"], null); Enum.TryParse(Utility.GetText(node["accessLevel"], "Player"), true, out m_AccessLevel); Flags = Utility.GetXMLInt32(Utility.GetText(node["flags"], "0"), 0); diff --git a/Projects/Scripts/Accounting/Security/AccountSecurity.cs b/Projects/Scripts/Accounting/Security/AccountSecurity.cs index 57f5dc37b..34375ee8d 100644 --- a/Projects/Scripts/Accounting/Security/AccountSecurity.cs +++ b/Projects/Scripts/Accounting/Security/AccountSecurity.cs @@ -4,13 +4,14 @@ namespace Server.Accounting.Security { public enum PasswordProtectionAlgorithm { - // Obsolete algorithms. These are not secure! + // Obsolete algorithms from RunUO. These are not secure! // They are included for password upgrades only. None, MD5, SHA1, - // Support algorithms + // Supported algorithms + SHA2, // ServUO compatibility PBKDF2, Argon2 // Recommended algorithm for real security. } @@ -24,7 +25,7 @@ namespace Server.Accounting.Security public static void Configure() { - if (AlgorithmName < PasswordProtectionAlgorithm.PBKDF2) + if (AlgorithmName < PasswordProtectionAlgorithm.SHA2) throw new Exception($"Security: {AlgorithmName} is obselete and not secure. Do not use it."); } @@ -34,6 +35,7 @@ namespace Server.Accounting.Security { PasswordProtectionAlgorithm.MD5 => MD5PasswordProtection.Instance, PasswordProtectionAlgorithm.SHA1 => SHA1PasswordProtection.Instance, + PasswordProtectionAlgorithm.SHA2 => SHA2PasswordProtection.Instance, PasswordProtectionAlgorithm.PBKDF2 => PBKDF2PasswordProtection.Instance, PasswordProtectionAlgorithm.Argon2 => Argon2PasswordProtection.Instance, _ => null diff --git a/Projects/Scripts/Accounting/Security/MD5PasswordProtection.cs b/Projects/Scripts/Accounting/Security/MD5PasswordProtection.cs index 23fc59a75..bdb3902cc 100644 --- a/Projects/Scripts/Accounting/Security/MD5PasswordProtection.cs +++ b/Projects/Scripts/Accounting/Security/MD5PasswordProtection.cs @@ -1,7 +1,7 @@ using System; -using System.Runtime.InteropServices; using System.Security.Cryptography; using System.Text; +using Server.Misc; namespace Server.Accounting.Security { @@ -16,7 +16,7 @@ namespace Server.Accounting.Security byte[] bytes = new byte[Encoding.ASCII.GetByteCount(password)]; Encoding.ASCII.GetBytes(password, bytes); - return BitConverter.ToString(m_MD5HashProvider.ComputeHash(bytes)); + return HexStringConverter.GetString(m_MD5HashProvider.ComputeHash(bytes)); } public bool ValidatePassword(string encryptedPassword, string plainPassword) => diff --git a/Projects/Scripts/Accounting/Security/SHA1PasswordProtection.cs b/Projects/Scripts/Accounting/Security/SHA1PasswordProtection.cs index 2f45c5e85..97cf0dcf9 100644 --- a/Projects/Scripts/Accounting/Security/SHA1PasswordProtection.cs +++ b/Projects/Scripts/Accounting/Security/SHA1PasswordProtection.cs @@ -1,7 +1,7 @@ using System; -using System.Runtime.InteropServices; using System.Security.Cryptography; using System.Text; +using Server.Misc; namespace Server.Accounting.Security { @@ -16,7 +16,7 @@ namespace Server.Accounting.Security byte[] bytes = new byte[Encoding.ASCII.GetByteCount(password)]; Encoding.ASCII.GetBytes(password, bytes); - return BitConverter.ToString(m_SHA1HashProvider.ComputeHash(bytes)); + return HexStringConverter.GetString(m_SHA1HashProvider.ComputeHash(bytes)); } public bool ValidatePassword(string encryptedPassword, string plainPassword) => diff --git a/Projects/Scripts/Accounting/Security/SHA2PasswordProtection.cs b/Projects/Scripts/Accounting/Security/SHA2PasswordProtection.cs new file mode 100644 index 000000000..a58aaa18f --- /dev/null +++ b/Projects/Scripts/Accounting/Security/SHA2PasswordProtection.cs @@ -0,0 +1,25 @@ +using System; +using System.Security.Cryptography; +using System.Text; +using Server.Misc; + +namespace Server.Accounting.Security +{ + public class SHA2PasswordProtection : IPasswordProtection + { + public static IPasswordProtection Instance = new SHA2PasswordProtection(); + private SHA512CryptoServiceProvider m_SHA2HashProvider = new SHA512CryptoServiceProvider(); + + 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 HexStringConverter.GetString(m_SHA2HashProvider.ComputeHash(bytes)); + } + + public bool ValidatePassword(string encryptedPassword, string plainPassword) => + EncryptPassword(plainPassword) == encryptedPassword; + } +}