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