From 68ace02e2d1b4b0e0c5a8ecf6845acc72fcf65e2 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Fri, 30 May 2025 13:15:44 +0100 Subject: [PATCH] Use custom md5 just for account/user IDs, not anything real (#314) * Use custom md5 just for account/user IDs, not anything real * test fixes * To lower and upper as needed --- src/Speckle.Sdk/Common/Md5.cs | 165 ++++++++++++++++++ .../{Helpers/Crypt.cs => Common/Sha256.cs} | 36 +--- src/Speckle.Sdk/Credentials/Account.cs | 8 +- src/Speckle.Sdk/Serialisation/IdGenerator.cs | 6 +- .../Benchmarks/CryptSha256Hash.cs | 5 +- .../Speckle.Sdk.Tests.Unit/Common/Md5Tests.cs | 71 ++++++++ .../Models/UtilitiesTests.cs | 26 +-- 7 files changed, 255 insertions(+), 62 deletions(-) create mode 100644 src/Speckle.Sdk/Common/Md5.cs rename src/Speckle.Sdk/{Helpers/Crypt.cs => Common/Sha256.cs} (66%) create mode 100644 tests/Speckle.Sdk.Tests.Unit/Common/Md5Tests.cs diff --git a/src/Speckle.Sdk/Common/Md5.cs b/src/Speckle.Sdk/Common/Md5.cs new file mode 100644 index 00000000..5d88303e --- /dev/null +++ b/src/Speckle.Sdk/Common/Md5.cs @@ -0,0 +1,165 @@ +namespace Speckle.Sdk.Common; + +// MD5 implementation in pure C# (public domain / no dependencies) +// Not for cryptographic purposes +// Using this instead of changing ID generation but avoiding built in MD5 for FIPS compliance + +public static class Md5 +{ + // Standard initial values + private static readonly uint[] T = + [ + 0xd76aa478, + 0xe8c7b756, + 0x242070db, + 0xc1bdceee, + 0xf57c0faf, + 0x4787c62a, + 0xa8304613, + 0xfd469501, + 0x698098d8, + 0x8b44f7af, + 0xffff5bb1, + 0x895cd7be, + 0x6b901122, + 0xfd987193, + 0xa679438e, + 0x49b40821, + 0xf61e2562, + 0xc040b340, + 0x265e5a51, + 0xe9b6c7aa, + 0xd62f105d, + 0x02441453, + 0xd8a1e681, + 0xe7d3fbc8, + 0x21e1cde6, + 0xc33707d6, + 0xf4d50d87, + 0x455a14ed, + 0xa9e3e905, + 0xfcefa3f8, + 0x676f02d9, + 0x8d2a4c8a, + 0xfffa3942, + 0x8771f681, + 0x6d9d6122, + 0xfde5380c, + 0xa4beea44, + 0x4bdecfa9, + 0xf6bb4b60, + 0xbebfbc70, + 0x289b7ec6, + 0xeaa127fa, + 0xd4ef3085, + 0x04881d05, + 0xd9d4d039, + 0xe6db99e5, + 0x1fa27cf8, + 0xc4ac5665, + 0xf4292244, + 0x432aff97, + 0xab9423a7, + 0xfc93a039, + 0x655b59c3, + 0x8f0ccc92, + 0xffeff47d, + 0x85845dd1, + 0x6fa87e4f, + 0xfe2ce6e0, + 0xa3014314, + 0x4e0811a1, + 0xf7537e82, + 0xbd3af235, + 0x2ad7d2bb, + 0xeb86d391, + ]; + + public static byte[] ComputeHash(byte[] input) + { + // Pad input + int origLenBits = input.Length * 8; + int padLen = (56 - (input.Length + 1) % 64 + 64) % 64; + byte[] padded = new byte[input.Length + 1 + padLen + 8]; + Array.Copy(input, padded, input.Length); + padded[input.Length] = 0x80; + BitConverter.GetBytes((long)origLenBits).CopyTo(padded, padded.Length - 8); + + // Initialize MD5 buffer + uint a = 0x67452301; + uint b = 0xefcdab89; + uint c = 0x98badcfe; + uint d = 0x10325476; + + for (int i = 0; i < padded.Length / 64; i++) + { + uint[] M = new uint[16]; + for (int j = 0; j < 16; j++) + { + M[j] = BitConverter.ToUInt32(padded, (i * 64) + j * 4); + } + + uint AA = a, + BB = b, + CC = c, + DD = d; + for (int j = 0; j < 64; j++) + { + uint f, + g; + if (j < 16) + { + f = (b & c) | (~b & d); + g = (uint)j; + } + else if (j < 32) + { + f = (d & b) | (~d & c); + g = (uint)((5 * j + 1) % 16); + } + else if (j < 48) + { + f = b ^ c ^ d; + g = (uint)((3 * j + 5) % 16); + } + else + { + f = c ^ (b | ~d); + g = (uint)((7 * j) % 16); + } + + uint temp = d; + d = c; + c = b; + b += LeftRotate(a + f + T[j] + M[g], S(j)); + a = temp; + } + a += AA; + b += BB; + c += CC; + d += DD; + } + + byte[] output = new byte[16]; + Array.Copy(BitConverter.GetBytes(a), 0, output, 0, 4); + Array.Copy(BitConverter.GetBytes(b), 0, output, 4, 4); + Array.Copy(BitConverter.GetBytes(c), 0, output, 8, 4); + Array.Copy(BitConverter.GetBytes(d), 0, output, 12, 4); + return output; + } + + private static int S(int i) + { + int[] s = { 7, 12, 17, 22, 5, 9, 14, 20, 4, 11, 16, 23, 6, 10, 15, 21 }; + return s[(i / 16) * 4 + (i % 4)]; + } + + private static uint LeftRotate(uint x, int c) => (x << c) | (x >> (32 - c)); + + // Convenience method to get hex string + public static string GetString(string input) + { + var hash = ComputeHash(System.Text.Encoding.UTF8.GetBytes(input)); + return BitConverter.ToString(hash).Replace("-", ""); + } +} diff --git a/src/Speckle.Sdk/Helpers/Crypt.cs b/src/Speckle.Sdk/Common/Sha256.cs similarity index 66% rename from src/Speckle.Sdk/Helpers/Crypt.cs rename to src/Speckle.Sdk/Common/Sha256.cs index 66aaa7c8..953880af 100644 --- a/src/Speckle.Sdk/Helpers/Crypt.cs +++ b/src/Speckle.Sdk/Common/Sha256.cs @@ -6,17 +6,17 @@ using System.Text; using System.Runtime.InteropServices; #endif -namespace Speckle.Sdk.Helpers; +namespace Speckle.Sdk.Common; -public static class Crypt +public static class Sha256 { #if NET6_0_OR_GREATER /// the value to hash /// "x2" for lower case, "X2" for uppercase. /// Desired length of the returned string. Must be 2 ≤ Length ≤ 64, and must be a multiple of 2 - /// + /// [Pure] - public static string Sha256( + public static string GetString( ReadOnlySpan input, [StringSyntax(StringSyntaxAttribute.NumericFormat)] string? format = "x2", int length = SHA256.HashSizeInBytes * sizeof(char) @@ -45,7 +45,7 @@ public static class Crypt /// is not a recognised numeric format /// [Pure] - public static string Sha256( + public static string GetString( string input, [StringSyntax(StringSyntaxAttribute.NumericFormat)] string? format = "x2", int length = 64 @@ -67,30 +67,4 @@ public static class Crypt return sb.ToString(0, length); } - - /// - /// MD5 is a broken cryptographic algorithm and should be used subject to review see CA5351 - [Pure] - [SuppressMessage("Security", "CA5351:Do Not Use Broken Cryptographic Algorithms")] - public static string Md5( - string input, - [StringSyntax(StringSyntaxAttribute.NumericFormat)] string? format = "x2", - int length = 32 - ) - { - byte[] inputBytes = Encoding.ASCII.GetBytes(input.ToLowerInvariant()); -#if NETSTANDARD2_0 - using MD5 md5 = MD5.Create(); - byte[] hashBytes = md5.ComputeHash(inputBytes); -#else - byte[] hashBytes = MD5.HashData(inputBytes); -#endif - StringBuilder sb = new(32); - for (int i = 0; i < hashBytes.Length; i++) - { - sb.Append(hashBytes[i].ToString(format)); - } - - return sb.ToString(0, length); - } } diff --git a/src/Speckle.Sdk/Credentials/Account.cs b/src/Speckle.Sdk/Credentials/Account.cs index b58b9c21..c462e26b 100644 --- a/src/Speckle.Sdk/Credentials/Account.cs +++ b/src/Speckle.Sdk/Credentials/Account.cs @@ -1,6 +1,6 @@ using System.Runtime.InteropServices; using Speckle.Sdk.Api.GraphQL.Models; -using Speckle.Sdk.Helpers; +using Speckle.Sdk.Common; namespace Speckle.Sdk.Credentials; @@ -25,7 +25,7 @@ public class Account : IEquatable throw new InvalidOperationException("Incomplete account info: cannot generate id."); } - _id = Crypt.Md5(userInfo.email + serverInfo.url, "X2"); + _id = Md5.GetString(userInfo.email + serverInfo.url).ToUpperInvariant(); } return _id; } @@ -62,13 +62,13 @@ public class Account : IEquatable public string GetHashedEmail() { string email = userInfo?.email ?? "unknown"; - return "@" + Crypt.Md5(email, "X2"); + return "@" + Md5.GetString(email).ToUpperInvariant(); } public string GetHashedServer() { string url = serverInfo?.url ?? AccountManager.DEFAULT_SERVER_URL; - return Crypt.Md5(CleanURL(url), "X2"); + return Md5.GetString(CleanURL(url)).ToUpperInvariant(); } public override string ToString() diff --git a/src/Speckle.Sdk/Serialisation/IdGenerator.cs b/src/Speckle.Sdk/Serialisation/IdGenerator.cs index b732c7e7..0d33482e 100644 --- a/src/Speckle.Sdk/Serialisation/IdGenerator.cs +++ b/src/Speckle.Sdk/Serialisation/IdGenerator.cs @@ -1,5 +1,5 @@ using System.Diagnostics.Contracts; -using Speckle.Sdk.Helpers; +using Speckle.Sdk.Common; using Speckle.Sdk.Models; namespace Speckle.Sdk.Serialisation; @@ -10,9 +10,9 @@ public static class IdGenerator public static Id ComputeId(Json serialized) { #if NET6_0_OR_GREATER - string hash = Crypt.Sha256(serialized.Value.AsSpan(), length: HashUtility.HASH_LENGTH); + string hash = Sha256.GetString(serialized.Value.AsSpan(), length: HashUtility.HASH_LENGTH); #else - string hash = Crypt.Sha256(serialized.Value, length: HashUtility.HASH_LENGTH); + string hash = Sha256.GetString(serialized.Value, length: HashUtility.HASH_LENGTH); #endif return new Id(hash); } diff --git a/tests/Speckle.Sdk.Tests.Performance/Benchmarks/CryptSha256Hash.cs b/tests/Speckle.Sdk.Tests.Performance/Benchmarks/CryptSha256Hash.cs index 39889f50..645ce4c9 100644 --- a/tests/Speckle.Sdk.Tests.Performance/Benchmarks/CryptSha256Hash.cs +++ b/tests/Speckle.Sdk.Tests.Performance/Benchmarks/CryptSha256Hash.cs @@ -1,6 +1,5 @@ using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Engines; -using Speckle.Sdk.Helpers; namespace Speckle.Sdk.Tests.Performance.Benchmarks; @@ -20,12 +19,12 @@ public class CryptSha256Hash [Benchmark] public string Sha256() { - return Crypt.Sha256(testData); + return Speckle.Sdk.Common.Sha256.GetString(testData); } [Benchmark] public string Sha256_Span() { - return Crypt.Sha256(testData.AsSpan()); + return Speckle.Sdk.Common.Sha256.GetString(testData.AsSpan()); } } diff --git a/tests/Speckle.Sdk.Tests.Unit/Common/Md5Tests.cs b/tests/Speckle.Sdk.Tests.Unit/Common/Md5Tests.cs new file mode 100644 index 00000000..db46c9fd --- /dev/null +++ b/tests/Speckle.Sdk.Tests.Unit/Common/Md5Tests.cs @@ -0,0 +1,71 @@ +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; +using System.Security.Cryptography; +using System.Text; +using FluentAssertions; + +namespace Speckle.Sdk.Tests.Unit.Common; + +public class Md5Tests +{ + [Theory] + [InlineData("", "d41d8cd98f00b204e9800998ecf8427e")] + [InlineData("a", "0cc175b9c0f1b6a831c399e269772661")] + [InlineData("abc", "900150983cd24fb0d6963f7d28e17f72")] + [InlineData("message digest", "f96b697d7cb7938d525a2f31aaf161d0")] + [InlineData("abcdefghijklmnopqrstuvwxyz", "c3fcd3d76192e4007dfb496cca67e13b")] + [InlineData("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", "d174ab98d277d9f5a5611c2c9f419d9f")] + [InlineData( + "12345678901234567890123456789012345678901234567890123456789012345678901234567890", + "57edf4a22be3c955ac49da2e2107b67a" + )] + public void Md5_Hash_Is_Correct(string input, string expectedHex) + { + string actual = Speckle.Sdk.Common.Md5.GetString(input).ToLowerInvariant(); + expectedHex.Should().Be(actual); + } + + [Theory] + [InlineData("", "d41d8cd98f00b204e9800998ecf8427e")] + [InlineData("a", "0cc175b9c0f1b6a831c399e269772661")] + [InlineData("abc", "900150983cd24fb0d6963f7d28e17f72")] + [InlineData("message digest", "f96b697d7cb7938d525a2f31aaf161d0")] + [InlineData("abcdefghijklmnopqrstuvwxyz", "c3fcd3d76192e4007dfb496cca67e13b")] + [InlineData("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", "d174ab98d277d9f5a5611c2c9f419d9f")] + [InlineData( + "12345678901234567890123456789012345678901234567890123456789012345678901234567890", + "57edf4a22be3c955ac49da2e2107b67a" + )] + public void Md5_Compare(string input, string expectedHex) + { + //old always did to lower for some reason. + string actual = Speckle.Sdk.Common.Md5.GetString(input).ToLowerInvariant(); + string old = OldMd5(input); + expectedHex.Should().Be(actual); + expectedHex.Should().Be(old); + } + + [Pure] + [SuppressMessage("Security", "CA5351:Do Not Use Broken Cryptographic Algorithms")] + public static string OldMd5( + string input, + [StringSyntax(StringSyntaxAttribute.NumericFormat)] string? format = "x2", + int length = 32 + ) + { + byte[] inputBytes = Encoding.ASCII.GetBytes(input); +#if NETSTANDARD2_0 + using MD5 md5 = MD5.Create(); + byte[] hashBytes = md5.ComputeHash(inputBytes); +#else + byte[] hashBytes = MD5.HashData(inputBytes); +#endif + StringBuilder sb = new(32); + for (int i = 0; i < hashBytes.Length; i++) + { + sb.Append(hashBytes[i].ToString(format)); + } + + return sb.ToString(0, length); + } +} diff --git a/tests/Speckle.Sdk.Tests.Unit/Models/UtilitiesTests.cs b/tests/Speckle.Sdk.Tests.Unit/Models/UtilitiesTests.cs index 32bf1d17..6bb6d01f 100644 --- a/tests/Speckle.Sdk.Tests.Unit/Models/UtilitiesTests.cs +++ b/tests/Speckle.Sdk.Tests.Unit/Models/UtilitiesTests.cs @@ -1,6 +1,5 @@ using FluentAssertions; using Speckle.Sdk.Dependencies; -using Speckle.Sdk.Helpers; namespace Speckle.Sdk.Tests.Unit.Models; @@ -45,9 +44,6 @@ public sealed class HashUtilityTests } } - public static IEnumerable SmallTestCasesMd5() => - SmallTestCases(SmallTestCases(), EnumerableExtensions.RangeFrom(0, 32)); - public static IEnumerable SmallTestCasesSha256() => SmallTestCases(SmallTestCases(), EnumerableExtensions.RangeFrom(2, 64)); @@ -69,24 +65,12 @@ public sealed class HashUtilityTests ]; } - [Theory] - [MemberData(nameof(SmallTestCasesMd5))] - public void Md5(string input, string _, string expected, int length) - { - var resultLower = Crypt.Md5(input, "x2", length); - var resultUpper = Crypt.Md5(input, "X2", length); - - resultLower.Should().Be(new string(expected.ToLower()[..length])); - - resultUpper.Should().Be(new string(expected.ToUpper()[..length])); - } - [Theory] [MemberData(nameof(SmallTestCasesSha256))] public void Sha256(string input, string expected, string _, int length) { - var resultLower = Crypt.Sha256(input, "x2", length); - var resultUpper = Crypt.Sha256(input, "X2", length); + var resultLower = Speckle.Sdk.Common.Sha256.GetString(input, "x2", length); + var resultUpper = Speckle.Sdk.Common.Sha256.GetString(input, "X2", length); resultLower.Should().Be(new string(expected.ToLower()[..length])); @@ -102,8 +86,8 @@ public sealed class HashUtilityTests int length //Span version of the function must have multiple of 2 ) { - var resultLowerSpan = Crypt.Sha256(input.AsSpan(), "x2", length); - var resultUpperSpan = Crypt.Sha256(input.AsSpan(), "X2", length); + var resultLowerSpan = Speckle.Sdk.Common.Sha256.GetString(input.AsSpan(), "x2", length); + var resultUpperSpan = Speckle.Sdk.Common.Sha256.GetString(input.AsSpan(), "X2", length); resultLowerSpan.Should().Be(new string(expected.ToLower()[..length])); @@ -114,7 +98,7 @@ public sealed class HashUtilityTests [MemberData(nameof(LargeTestCases))] public void Sha256_LargeDataTests(string input, string expected) { - var computedHash = Crypt.Sha256(input.AsSpan()); + var computedHash = Speckle.Sdk.Common.Sha256.GetString(input.AsSpan()); computedHash.Should().Be(expected); } }