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
This commit is contained in:
Adam Hathcock
2025-05-30 13:15:44 +01:00
committed by GitHub
parent b6be7a351f
commit 68ace02e2d
7 changed files with 255 additions and 62 deletions
+165
View File
@@ -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("-", "");
}
}
@@ -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
/// <param name="input">the value to hash</param>
/// <param name="format"><c>"x2"</c> for lower case, <c>"X2"</c> for uppercase.</param>
/// <param name="length">Desired length of the returned string. Must be 2 &#x2264; Length &#x2264; 64, and must be a multiple of 2</param>
/// <returns><inheritdoc cref="Sha256(string, string?, int)"/></returns>
/// <returns><inheritdoc cref="GetString(string, string?, int)"/></returns>
[Pure]
public static string Sha256(
public static string GetString(
ReadOnlySpan<char> input,
[StringSyntax(StringSyntaxAttribute.NumericFormat)] string? format = "x2",
int length = SHA256.HashSizeInBytes * sizeof(char)
@@ -45,7 +45,7 @@ public static class Crypt
/// <exception cref="FormatException"><paramref name="format"/> is not a recognised numeric format</exception>
/// <exception cref="ArgumentOutOfRangeException"><inheritdoc cref="StringBuilder.ToString(int, int)"/></exception>
[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);
}
/// <inheritdoc cref="Sha256(string, string?, int)"/>
/// <remarks>MD5 is a broken cryptographic algorithm and should be used subject to review see CA5351</remarks>
[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);
}
}
+4 -4
View File
@@ -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<Account>
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<Account>
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()
+3 -3
View File
@@ -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);
}
@@ -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());
}
}
@@ -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);
}
}
@@ -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<object[]> SmallTestCasesMd5() =>
SmallTestCases(SmallTestCases(), EnumerableExtensions.RangeFrom(0, 32));
public static IEnumerable<object[]> 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);
}
}