From 9ec4f1d310f188e133fe8a9950f87635b2a8e088 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Wed, 16 Oct 2024 10:12:34 -0600 Subject: [PATCH 1/2] Fix conflict --- .../DynamicCodeDumper.csproj | 3 + .../MessagePack/MessagePackSecurity.cs | 253 ++++++++------- .../Assets/Scripts/MessagePack/SipHash.cs | 289 ++++++++++++++++++ .../Scripts/MessagePack/SipHash.cs.meta | 11 + .../ShareTests/MessagePackSecurityTests.cs | 7 +- .../Scripts/Tests/ShareTests/SipHashTests.cs | 117 +++++++ .../Tests/ShareTests/SipHashTests.cs.meta | 11 + .../MessagePack.Tests.csproj | 1 + 8 files changed, 553 insertions(+), 139 deletions(-) create mode 100644 src/MessagePack.UnityClient/Assets/Scripts/MessagePack/SipHash.cs create mode 100644 src/MessagePack.UnityClient/Assets/Scripts/MessagePack/SipHash.cs.meta create mode 100644 src/MessagePack.UnityClient/Assets/Scripts/Tests/ShareTests/SipHashTests.cs create mode 100644 src/MessagePack.UnityClient/Assets/Scripts/Tests/ShareTests/SipHashTests.cs.meta diff --git a/sandbox/DynamicCodeDumper/DynamicCodeDumper.csproj b/sandbox/DynamicCodeDumper/DynamicCodeDumper.csproj index 0daf9da44..9a41bc14d 100644 --- a/sandbox/DynamicCodeDumper/DynamicCodeDumper.csproj +++ b/sandbox/DynamicCodeDumper/DynamicCodeDumper.csproj @@ -75,6 +75,9 @@ Code\MessagePackSecurity.cs + + Code\SipHash.cs + Code\MonoProtection.cs diff --git a/src/MessagePack.UnityClient/Assets/Scripts/MessagePack/MessagePackSecurity.cs b/src/MessagePack.UnityClient/Assets/Scripts/MessagePack/MessagePackSecurity.cs index 2b743afa4..aae2b24f6 100644 --- a/src/MessagePack.UnityClient/Assets/Scripts/MessagePack/MessagePackSecurity.cs +++ b/src/MessagePack.UnityClient/Assets/Scripts/MessagePack/MessagePackSecurity.cs @@ -6,7 +6,9 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using System.Runtime.CompilerServices; using System.Runtime.ExceptionServices; +using System.Runtime.InteropServices; using MessagePack.Formatters; using MessagePack.Internal; @@ -31,6 +33,8 @@ public class MessagePackSecurity MaximumObjectGraphDepth = 500, }; + private static readonly SipHash Hash = new(); + private readonly ObjectFallbackEqualityComparer objectFallbackEqualityComparer; private MessagePackSecurity() @@ -61,7 +65,7 @@ protected MessagePackSecurity(MessagePackSecurity copyFrom) /// This can mitigate some denial of service attacks when deserializing untrusted code. /// /// - /// The value is false for and true for . + /// The value is for and for . /// public bool HashCollisionResistant { get; private set; } @@ -138,6 +142,51 @@ public IEqualityComparer GetEqualityComparer() return this.HashCollisionResistant ? GetHashCollisionResistantEqualityComparer() : EqualityComparer.Default; } + private class HashResistantCache + { + internal static readonly IEqualityComparer? EqualityComparer; + + static HashResistantCache() + { + // We have to specially handle some 32-bit types (e.g. float) where multiple in-memory representations should hash to the same value. + // Any type supported by the PrimitiveObjectFormatter should be added here if supporting it as a key in a collection makes sense. + EqualityComparer = + typeof(T) == typeof(bool) ? (IEqualityComparer)CollisionResistantHasherUnmanaged.Instance : + typeof(T) == typeof(char) ? (IEqualityComparer)CollisionResistantHasherUnmanaged.Instance : + typeof(T) == typeof(sbyte) ? (IEqualityComparer)CollisionResistantHasherUnmanaged.Instance : + typeof(T) == typeof(byte) ? (IEqualityComparer)CollisionResistantHasherUnmanaged.Instance : + typeof(T) == typeof(short) ? (IEqualityComparer)CollisionResistantHasherUnmanaged.Instance : + typeof(T) == typeof(ushort) ? (IEqualityComparer)CollisionResistantHasherUnmanaged.Instance : + typeof(T) == typeof(int) ? (IEqualityComparer)CollisionResistantHasherUnmanaged.Instance : + typeof(T) == typeof(uint) ? (IEqualityComparer)CollisionResistantHasherUnmanaged.Instance : + typeof(T) == typeof(long) ? (IEqualityComparer)CollisionResistantHasherUnmanaged.Instance : + typeof(T) == typeof(ulong) ? (IEqualityComparer)CollisionResistantHasherUnmanaged.Instance : + typeof(T) == typeof(Guid) ? (IEqualityComparer)CollisionResistantHasherUnmanaged.Instance : + + // Data types that are managed or have multiple in-memory representations for equivalent values: + typeof(T) == typeof(float) ? (IEqualityComparer)SingleEqualityComparer.Instance : + typeof(T) == typeof(double) ? (IEqualityComparer)DoubleEqualityComparer.Instance : + typeof(T) == typeof(string) ? (IEqualityComparer)StringEqualityComparer.Instance : + typeof(T) == typeof(DateTime) ? (IEqualityComparer)DateTimeEqualityComparer.Instance : + typeof(T) == typeof(DateTimeOffset) ? (IEqualityComparer)DateTimeOffsetEqualityComparer.Instance : + + // Call out each primitive behind an enum explicitly to avoid dynamically generating code. + typeof(T).GetTypeInfo().IsEnum && typeof(T).GetTypeInfo().GetEnumUnderlyingType() is Type underlying ? ( + underlying == typeof(byte) ? CollisionResistantEnumHasher.Instance : + underlying == typeof(sbyte) ? CollisionResistantEnumHasher.Instance : + underlying == typeof(ushort) ? CollisionResistantEnumHasher.Instance : + underlying == typeof(short) ? CollisionResistantEnumHasher.Instance : + underlying == typeof(uint) ? CollisionResistantEnumHasher.Instance : + underlying == typeof(int) ? CollisionResistantEnumHasher.Instance : + underlying == typeof(ulong) ? CollisionResistantEnumHasher.Instance : + underlying == typeof(long) ? CollisionResistantEnumHasher.Instance : + null) : + + // Failsafe. If we don't recognize the type, don't assume we have a good, secure hash function for it. + null; + } + } + /// /// Returns a hash collision resistant equality comparer. /// @@ -145,54 +194,20 @@ public IEqualityComparer GetEqualityComparer() /// A hash collision resistant equality comparer. protected virtual IEqualityComparer GetHashCollisionResistantEqualityComparer() { - IEqualityComparer result = null; - if (typeof(T).GetTypeInfo().IsEnum) + if (HashResistantCache.EqualityComparer is { } result) { - Type underlyingType = typeof(T).GetTypeInfo().GetEnumUnderlyingType(); - result = - underlyingType == typeof(sbyte) ? CollisionResistantHasher.Instance : - underlyingType == typeof(byte) ? CollisionResistantHasher.Instance : - underlyingType == typeof(short) ? CollisionResistantHasher.Instance : - underlyingType == typeof(ushort) ? CollisionResistantHasher.Instance : - underlyingType == typeof(int) ? CollisionResistantHasher.Instance : - underlyingType == typeof(uint) ? CollisionResistantHasher.Instance : - null; + return result; } - else + + if (typeof(T) == typeof(object)) { - // For anything 32-bits and under, our fallback base secure hasher is usually adequate since it makes the hash unpredictable. - // We should have special implementations for any value that is larger than 32-bits in order to make sure - // that all the data gets hashed securely rather than trivially and predictably compressed into 32-bits before being hashed. - // We also have to specially handle some 32-bit types (e.g. float) where multiple in-memory representations should hash to the same value. - // Any type supported by the PrimitiveObjectFormatter should be added here if supporting it as a key in a collection makes sense. - result = - // 32-bits or smaller: - typeof(T) == typeof(bool) ? CollisionResistantHasher.Instance : - typeof(T) == typeof(char) ? CollisionResistantHasher.Instance : - typeof(T) == typeof(sbyte) ? CollisionResistantHasher.Instance : - typeof(T) == typeof(byte) ? CollisionResistantHasher.Instance : - typeof(T) == typeof(short) ? CollisionResistantHasher.Instance : - typeof(T) == typeof(ushort) ? CollisionResistantHasher.Instance : - typeof(T) == typeof(int) ? CollisionResistantHasher.Instance : - typeof(T) == typeof(uint) ? CollisionResistantHasher.Instance : - - // Larger than 32-bits (or otherwise require special handling): - typeof(T) == typeof(long) ? (IEqualityComparer)Int64EqualityComparer.Instance : - typeof(T) == typeof(ulong) ? (IEqualityComparer)UInt64EqualityComparer.Instance : - typeof(T) == typeof(float) ? (IEqualityComparer)SingleEqualityComparer.Instance : - typeof(T) == typeof(double) ? (IEqualityComparer)DoubleEqualityComparer.Instance : - typeof(T) == typeof(string) ? (IEqualityComparer)StringEqualityComparer.Instance : - typeof(T) == typeof(Guid) ? (IEqualityComparer)GuidEqualityComparer.Instance : - typeof(T) == typeof(DateTime) ? (IEqualityComparer)DateTimeEqualityComparer.Instance : - typeof(T) == typeof(DateTimeOffset) ? (IEqualityComparer)DateTimeOffsetEqualityComparer.Instance : - typeof(T) == typeof(object) ? (IEqualityComparer)this.objectFallbackEqualityComparer : - null; + return (IEqualityComparer)this.objectFallbackEqualityComparer; } // Any type we don't explicitly whitelist here shouldn't be allowed to use as the key in a hash-based collection since it isn't known to be hash resistant. // This method can of course be overridden to add more hash collision resistant type support, or the deserializing party can indicate that the data is Trusted // so that this method doesn't even get called. - return result ?? throw new TypeAccessException($"No hash-resistant equality comparer available for type: {typeof(T)}"); + throw new TypeAccessException($"No hash-resistant equality comparer available for type: {typeof(T)}"); } /// @@ -232,25 +247,41 @@ public void DepthStep(ref MessagePackReader reader) /// protected virtual MessagePackSecurity Clone() => new MessagePackSecurity(this); + private static int SecureHash(T value) + where T : unmanaged + { + Span span = stackalloc T[1]; + span[0] = value; + return unchecked((int)Hash.Compute(MemoryMarshal.Cast(span))); + } + + private static int SecureHash(ReadOnlySpan data) => unchecked((int)Hash.Compute(data)); + /// /// A hash collision resistant implementation of . /// /// The type of key that will be hashed. - private class CollisionResistantHasher : IEqualityComparer, IEqualityComparer + private abstract class CollisionResistantHasher : IEqualityComparer, IEqualityComparer { - internal static readonly CollisionResistantHasher Instance = new CollisionResistantHasher(); - - public bool Equals(T x, T y) => EqualityComparer.Default.Equals(x, y); + public bool Equals(T? x, T? y) => EqualityComparer.Default.Equals(x, y); - bool IEqualityComparer.Equals(object x, object y) => ((IEqualityComparer)EqualityComparer.Default).Equals(x, y); + bool IEqualityComparer.Equals(object? x, object? y) => ((IEqualityComparer)EqualityComparer.Default).Equals(x, y); public int GetHashCode(object obj) => this.GetHashCode((T)obj); - public virtual int GetHashCode(T value) => HashCode.Combine(value); + public abstract int GetHashCode(T value); + } + + private class CollisionResistantHasherUnmanaged : CollisionResistantHasher + where T : unmanaged + { + internal static readonly CollisionResistantHasherUnmanaged Instance = new(); + + public override int GetHashCode(T value) => SecureHash(value); } /// - /// A special hash-resistent equality comparer that defers picking the actual implementation + /// A special hash-resistant equality comparer that defers picking the actual implementation /// till it can check the runtime type of each value to be hashed. /// private class ObjectFallbackEqualityComparer : IEqualityComparer, IEqualityComparer @@ -264,9 +295,9 @@ internal ObjectFallbackEqualityComparer(MessagePackSecurity security) this.security = security ?? throw new ArgumentNullException(nameof(security)); } - bool IEqualityComparer.Equals(object x, object y) => EqualityComparer.Default.Equals(x, y); + bool IEqualityComparer.Equals(object? x, object? y) => EqualityComparer.Default.Equals(x, y); - bool IEqualityComparer.Equals(object x, object y) => ((IEqualityComparer)EqualityComparer.Default).Equals(x, y); + bool IEqualityComparer.Equals(object? x, object? y) => ((IEqualityComparer)EqualityComparer.Default).Equals(x, y); public int GetHashCode(object value) { @@ -284,15 +315,16 @@ public int GetHashCode(object value) return value.GetHashCode(); } - if (!equalityComparerCache.TryGetValue(valueType, out IEqualityComparer equalityComparer)) + if (!equalityComparerCache.TryGetValue(valueType, out IEqualityComparer? equalityComparer)) { try { - equalityComparer = (IEqualityComparer)GetHashCollisionResistantEqualityComparerOpenGenericMethod.Value.MakeGenericMethod(valueType).Invoke(this.security, Array.Empty()); + equalityComparer = (IEqualityComparer)GetHashCollisionResistantEqualityComparerOpenGenericMethod.Value.MakeGenericMethod(valueType).Invoke(this.security, Array.Empty())!; } - catch (TargetInvocationException ex) + catch (TargetInvocationException ex) when (ex.InnerException is not null) { ExceptionDispatchInfo.Capture(ex.InnerException).Throw(); + throw null!; // not reachable } equalityComparerCache.TryAdd(valueType, equalityComparer); @@ -302,116 +334,69 @@ public int GetHashCode(object value) } } - private class UInt64EqualityComparer : CollisionResistantHasher + private class SingleEqualityComparer : CollisionResistantHasherUnmanaged { - internal static new readonly UInt64EqualityComparer Instance = new UInt64EqualityComparer(); - - public override int GetHashCode(ulong value) => HashCode.Combine((uint)(value >> 32), unchecked((uint)value)); - } - - private class Int64EqualityComparer : CollisionResistantHasher - { - internal static new readonly Int64EqualityComparer Instance = new Int64EqualityComparer(); - - public override int GetHashCode(long value) => HashCode.Combine((int)(value >> 32), unchecked((int)value)); - } - - private class SingleEqualityComparer : CollisionResistantHasher - { - internal static new readonly SingleEqualityComparer Instance = new SingleEqualityComparer(); + internal static new readonly SingleEqualityComparer Instance = new(); public override unsafe int GetHashCode(float value) - { - // Special check for 0.0 so that the hash of 0.0 and -0.0 will equal. - if (value == 0.0f) + => base.GetHashCode(value switch { - return HashCode.Combine(0); - } - - // Standardize on the binary representation of NaN prior to hashing. - if (float.IsNaN(value)) - { - value = float.NaN; - } - - int l = *(int*)&value; - return l; - } + 0.0f => 0, // Special check for 0.0 so that the hash of 0.0 and -0.0 will equal. + float.NaN => float.NaN, // Standardize on the binary representation of NaN prior to hashing. + _ => value, + }); } - private class DoubleEqualityComparer : CollisionResistantHasher + private class DoubleEqualityComparer : CollisionResistantHasherUnmanaged { - internal static new readonly DoubleEqualityComparer Instance = new DoubleEqualityComparer(); + internal static new readonly DoubleEqualityComparer Instance = new(); public override unsafe int GetHashCode(double value) - { - // Special check for 0.0 so that the hash of 0.0 and -0.0 will equal. - if (value == 0.0) + => base.GetHashCode(value switch { - return HashCode.Combine(0); - } + 0.0 => 0, // Special check for 0.0 so that the hash of 0.0 and -0.0 will equal. + double.NaN => double.NaN, // Standardize on the binary representation of NaN prior to hashing. + _ => value, + }); + } - // Standardize on the binary representation of NaN prior to hashing. - if (double.IsNaN(value)) - { - value = double.NaN; - } + private class DateTimeEqualityComparer : CollisionResistantHasherUnmanaged + { + internal static new readonly DateTimeEqualityComparer Instance = new(); - long l = *(long*)&value; - return HashCode.Combine((int)(l >> 32), unchecked((int)l)); - } + public override unsafe int GetHashCode(DateTime value) => SecureHash(value.Ticks); } - private class GuidEqualityComparer : CollisionResistantHasher + private class DateTimeOffsetEqualityComparer : CollisionResistantHasherUnmanaged { - internal static new readonly GuidEqualityComparer Instance = new GuidEqualityComparer(); - - public override unsafe int GetHashCode(Guid value) - { - var hash = default(HashCode); - int* pGuid = (int*)&value; - for (int i = 0; i < sizeof(Guid) / sizeof(int); i++) - { - hash.Add(pGuid[i]); - } + internal static new readonly DateTimeOffsetEqualityComparer Instance = new(); - return hash.ToHashCode(); - } + public override unsafe int GetHashCode(DateTimeOffset value) => SecureHash(value.UtcDateTime.Ticks); } private class StringEqualityComparer : CollisionResistantHasher { - internal static new readonly StringEqualityComparer Instance = new StringEqualityComparer(); + internal static readonly StringEqualityComparer Instance = new(); public override int GetHashCode(string value) { -#if NETCOREAPP - // .NET Core already has a secure string hashing function. Just use it. - return value?.GetHashCode() ?? 0; -#else - var hash = default(HashCode); - for (int i = 0; i < value.Length; i++) - { - hash.Add(value[i]); - } - - return hash.ToHashCode(); -#endif + // The Cast call could result in OverflowException at runtime if value is greater than 1bn chars in length. + return SecureHash(MemoryMarshal.Cast(value.AsSpan())); } } - private class DateTimeEqualityComparer : CollisionResistantHasher + private class CollisionResistantEnumHasher : IEqualityComparer, IEqualityComparer + where TUnderlying : unmanaged { - internal static new readonly DateTimeEqualityComparer Instance = new DateTimeEqualityComparer(); + internal static readonly CollisionResistantEnumHasher Instance = new(); - public override unsafe int GetHashCode(DateTime value) => HashCode.Combine((int)(value.Ticks >> 32), unchecked((int)value.Ticks), value.Kind); - } + public bool Equals(TEnum? x, TEnum? y) => EqualityComparer.Default.Equals(x, y); - private class DateTimeOffsetEqualityComparer : CollisionResistantHasher - { - internal static new readonly DateTimeOffsetEqualityComparer Instance = new DateTimeOffsetEqualityComparer(); + public int GetHashCode(TEnum obj) => SecureHash(Unsafe.As(ref obj)); + + bool IEqualityComparer.Equals(object? x, object? y) => x is TEnum e1 && y is TEnum e2 && Equals(e1, e2); - public override unsafe int GetHashCode(DateTimeOffset value) => HashCode.Combine((int)(value.UtcTicks >> 32), unchecked((int)value.UtcTicks)); + int IEqualityComparer.GetHashCode(object obj) => GetHashCode((TEnum)obj); } } -} +} \ No newline at end of file diff --git a/src/MessagePack.UnityClient/Assets/Scripts/MessagePack/SipHash.cs b/src/MessagePack.UnityClient/Assets/Scripts/MessagePack/SipHash.cs new file mode 100644 index 000000000..beb3fa5b1 --- /dev/null +++ b/src/MessagePack.UnityClient/Assets/Scripts/MessagePack/SipHash.cs @@ -0,0 +1,289 @@ +// Copyright (c) All contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +//// Originally from: https://github.com/paya-cz/siphash +//// Author: Pavel Werl +//// License: Public Domain +//// SipHash website: https://131002.net/siphash/ + +using System; +using System.Buffers; +using System.Buffers.Binary; +using System.Runtime.CompilerServices; +using System.Security.Cryptography; + +namespace MessagePack +{ + /// + /// Implements the SipHash pseudo-random function. + /// + /// + /// This class is immutable and thread-safe. + /// + internal class SipHash + { + /// + /// Part of the initial 256-bit internal state. + /// + private readonly ulong initialState0; + + /// + /// Part of the initial 256-bit internal state. + /// + private readonly ulong initialState1; + + /// Initializes a new instance of the class using a random key. + public SipHash() + { + using RandomNumberGenerator rng = RandomNumberGenerator.Create(); +#if SPAN_BUILTIN + Span key = stackalloc byte[16]; + rng.GetBytes(key); +#else + byte[] buffer = ArrayPool.Shared.Rent(16); + rng.GetBytes(buffer, 0, 16); + Span key = buffer; +#endif + + this.initialState0 = 0x736f6d6570736575UL ^ BinaryPrimitives.ReadUInt64LittleEndian(key); + this.initialState1 = 0x646f72616e646f6dUL ^ BinaryPrimitives.ReadUInt64LittleEndian(key.Slice(sizeof(ulong))); + +#if !SPAN_BUILTIN + ArrayPool.Shared.Return(buffer); +#endif + } + + /// Initializes a new instance of the class using the specified 128-bit key. + /// Key for the SipHash pseudo-random function. Must be exactly 16 bytes long. + /// Thrown when is not exactly 16 bytes long (128 bits). + public SipHash(ReadOnlySpan key) + { + if (key.Length != 16) + { + throw new ArgumentException("SipHash key must be exactly 128-bit long (16 bytes).", nameof(key)); + } + + this.initialState0 = 0x736f6d6570736575UL ^ BinaryPrimitives.ReadUInt64LittleEndian(key); + this.initialState1 = 0x646f72616e646f6dUL ^ BinaryPrimitives.ReadUInt64LittleEndian(key.Slice(sizeof(ulong))); + } + + /// + /// Gets a 128-bit SipHash key. + /// + /// The 16-byte buffer that receives the key originally provided to the constructor. + public void GetKey(Span key) + { + if (key.Length != 16) + { + throw new ArgumentException("SipHash key must be exactly 128-bit long (16 bytes).", nameof(key)); + } + + BinaryPrimitives.WriteUInt64LittleEndian(key, this.initialState0 ^ 0x736f6d6570736575UL); + BinaryPrimitives.WriteUInt64LittleEndian(key.Slice(sizeof(ulong)), this.initialState1 ^ 0x646f72616e646f6dUL); + } + + /// Computes 64-bit SipHash tag for the specified message. + /// The byte array for which to computer SipHash tag. + /// Returns 64-bit (8 bytes) SipHash tag. + public long Compute(ReadOnlySpan data) + { + static T Read(in byte start) + where T : unmanaged => Unsafe.ReadUnaligned(ref Unsafe.AsRef(in start)); + + unchecked + { + // SipHash internal state + ulong v0 = this.initialState0; + ulong v1 = this.initialState1; + + // It is faster to load the initialStateX fields from memory again than to reference v0 and v1: + ulong v2 = 0x1F160A001E161714UL ^ this.initialState0; + ulong v3 = 0x100A160317100A1EUL ^ this.initialState1; + + // We process data in 64-bit blocks + ulong block; + + // The last 64-bit block of data + int finalBlockPosition = data.Length & ~7; + + // Process the input data in blocks of 64 bits + for (int blockPosition = 0; blockPosition < finalBlockPosition; blockPosition += sizeof(ulong)) + { + block = Read(data[blockPosition]); + + v3 ^= block; + + // Round 1 + v0 += v1; + v2 += v3; + v1 = v1 << 13 | v1 >> 51; + v3 = v3 << 16 | v3 >> 48; + v1 ^= v0; + v3 ^= v2; + v0 = v0 << 32 | v0 >> 32; + v2 += v1; + v0 += v3; + v1 = v1 << 17 | v1 >> 47; + v3 = v3 << 21 | v3 >> 43; + v1 ^= v2; + v3 ^= v0; + v2 = v2 << 32 | v2 >> 32; + + // Round 2 + v0 += v1; + v2 += v3; + v1 = v1 << 13 | v1 >> 51; + v3 = v3 << 16 | v3 >> 48; + v1 ^= v0; + v3 ^= v2; + v0 = v0 << 32 | v0 >> 32; + v2 += v1; + v0 += v3; + v1 = v1 << 17 | v1 >> 47; + v3 = v3 << 21 | v3 >> 43; + v1 ^= v2; + v3 ^= v0; + v2 = v2 << 32 | v2 >> 32; + + v0 ^= block; + } + + // Load the remaining bytes + block = (ulong)data.Length << 56; + switch (data.Length & 7) + { + case 7: + block |= Read(data[finalBlockPosition]) | (ulong)Read(data[finalBlockPosition + 4]) << 32 | (ulong)data[finalBlockPosition + 6] << 48; + break; + case 6: + block |= Read(data[finalBlockPosition]) | (ulong)Read(data[finalBlockPosition + 4]) << 32; + break; + case 5: + block |= Read(data[finalBlockPosition]) | (ulong)data[finalBlockPosition + 4] << 32; + break; + case 4: + block |= Read(data[finalBlockPosition]); + break; + case 3: + block |= Read(data[finalBlockPosition]) | (ulong)data[finalBlockPosition + 2] << 16; + break; + case 2: + block |= Read(data[finalBlockPosition]); + break; + case 1: + block |= data[finalBlockPosition]; + break; + } + + // Process the final block + { + v3 ^= block; + + // Round 1 + v0 += v1; + v2 += v3; + v1 = v1 << 13 | v1 >> 51; + v3 = v3 << 16 | v3 >> 48; + v1 ^= v0; + v3 ^= v2; + v0 = v0 << 32 | v0 >> 32; + v2 += v1; + v0 += v3; + v1 = v1 << 17 | v1 >> 47; + v3 = v3 << 21 | v3 >> 43; + v1 ^= v2; + v3 ^= v0; + v2 = v2 << 32 | v2 >> 32; + + // Round 2 + v0 += v1; + v2 += v3; + v1 = v1 << 13 | v1 >> 51; + v3 = v3 << 16 | v3 >> 48; + v1 ^= v0; + v3 ^= v2; + v0 = v0 << 32 | v0 >> 32; + v2 += v1; + v0 += v3; + v1 = v1 << 17 | v1 >> 47; + v3 = v3 << 21 | v3 >> 43; + v1 ^= v2; + v3 ^= v0; + v2 = v2 << 32 | v2 >> 32; + + v0 ^= block; + v2 ^= 0xff; + } + + // 4 finalization rounds + { + // Round 1 + v0 += v1; + v2 += v3; + v1 = v1 << 13 | v1 >> 51; + v3 = v3 << 16 | v3 >> 48; + v1 ^= v0; + v3 ^= v2; + v0 = v0 << 32 | v0 >> 32; + v2 += v1; + v0 += v3; + v1 = v1 << 17 | v1 >> 47; + v3 = v3 << 21 | v3 >> 43; + v1 ^= v2; + v3 ^= v0; + v2 = v2 << 32 | v2 >> 32; + + // Round 2 + v0 += v1; + v2 += v3; + v1 = v1 << 13 | v1 >> 51; + v3 = v3 << 16 | v3 >> 48; + v1 ^= v0; + v3 ^= v2; + v0 = v0 << 32 | v0 >> 32; + v2 += v1; + v0 += v3; + v1 = v1 << 17 | v1 >> 47; + v3 = v3 << 21 | v3 >> 43; + v1 ^= v2; + v3 ^= v0; + v2 = v2 << 32 | v2 >> 32; + + // Round 3 + v0 += v1; + v2 += v3; + v1 = v1 << 13 | v1 >> 51; + v3 = v3 << 16 | v3 >> 48; + v1 ^= v0; + v3 ^= v2; + v0 = v0 << 32 | v0 >> 32; + v2 += v1; + v0 += v3; + v1 = v1 << 17 | v1 >> 47; + v3 = v3 << 21 | v3 >> 43; + v1 ^= v2; + v3 ^= v0; + v2 = v2 << 32 | v2 >> 32; + + // Round 4 + v0 += v1; + v2 += v3; + v1 = v1 << 13 | v1 >> 51; + v3 = v3 << 16 | v3 >> 48; + v1 ^= v0; + v3 ^= v2; + v0 = v0 << 32 | v0 >> 32; + v2 += v1; + v0 += v3; + v1 = v1 << 17 | v1 >> 47; + v3 = v3 << 21 | v3 >> 43; + v1 ^= v2; + v3 ^= v0; + v2 = v2 << 32 | v2 >> 32; + } + + return (long)((v0 ^ v1) ^ (v2 ^ v3)); + } + } + } +} diff --git a/src/MessagePack.UnityClient/Assets/Scripts/MessagePack/SipHash.cs.meta b/src/MessagePack.UnityClient/Assets/Scripts/MessagePack/SipHash.cs.meta new file mode 100644 index 000000000..b8ac22068 --- /dev/null +++ b/src/MessagePack.UnityClient/Assets/Scripts/MessagePack/SipHash.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c78b7a26485c4131ae8bcfae61e01435 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/MessagePack.UnityClient/Assets/Scripts/Tests/ShareTests/MessagePackSecurityTests.cs b/src/MessagePack.UnityClient/Assets/Scripts/Tests/ShareTests/MessagePackSecurityTests.cs index d89411e74..74121e5d7 100644 --- a/src/MessagePack.UnityClient/Assets/Scripts/Tests/ShareTests/MessagePackSecurityTests.cs +++ b/src/MessagePack.UnityClient/Assets/Scripts/Tests/ShareTests/MessagePackSecurityTests.cs @@ -125,11 +125,8 @@ public void EqualityComparer_Enums() Assert.NotNull(MessagePackSecurity.UntrustedData.GetEqualityComparer()); Assert.NotNull(MessagePackSecurity.UntrustedData.GetEqualityComparer()); Assert.NotNull(MessagePackSecurity.UntrustedData.GetEqualityComparer()); - - // Supporting enums with backing integers that exceed 32-bits would likely require Ref.Emit of new types - // since C# doesn't let us cast T to the underlying int type. - Assert.Throws(() => MessagePackSecurity.UntrustedData.GetEqualityComparer()); - Assert.Throws(() => MessagePackSecurity.UntrustedData.GetEqualityComparer()); + Assert.NotNull(MessagePackSecurity.UntrustedData.GetEqualityComparer()); + Assert.NotNull(MessagePackSecurity.UntrustedData.GetEqualityComparer()); } [Fact] diff --git a/src/MessagePack.UnityClient/Assets/Scripts/Tests/ShareTests/SipHashTests.cs b/src/MessagePack.UnityClient/Assets/Scripts/Tests/ShareTests/SipHashTests.cs new file mode 100644 index 000000000..1e439cdab --- /dev/null +++ b/src/MessagePack.UnityClient/Assets/Scripts/Tests/ShareTests/SipHashTests.cs @@ -0,0 +1,117 @@ +// Copyright (c) All contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Linq; +using MessagePack; +using Xunit; + +public class SipHashTests +{ + // Test battery taken from: https://github.com/veorq/SipHash/blob/master/main.c + private static readonly uint[][] Vectors = new[] + { + new uint[] { 0x31, 0x0e, 0x0e, 0xdd, 0x47, 0xdb, 0x6f, 0x72 }, + new uint[] { 0xfd, 0x67, 0xdc, 0x93, 0xc5, 0x39, 0xf8, 0x74 }, + new uint[] { 0x5a, 0x4f, 0xa9, 0xd9, 0x09, 0x80, 0x6c, 0x0d }, + new uint[] { 0x2d, 0x7e, 0xfb, 0xd7, 0x96, 0x66, 0x67, 0x85 }, + new uint[] { 0xb7, 0x87, 0x71, 0x27, 0xe0, 0x94, 0x27, 0xcf }, + new uint[] { 0x8d, 0xa6, 0x99, 0xcd, 0x64, 0x55, 0x76, 0x18 }, + new uint[] { 0xce, 0xe3, 0xfe, 0x58, 0x6e, 0x46, 0xc9, 0xcb }, + new uint[] { 0x37, 0xd1, 0x01, 0x8b, 0xf5, 0x00, 0x02, 0xab }, + new uint[] { 0x62, 0x24, 0x93, 0x9a, 0x79, 0xf5, 0xf5, 0x93 }, + new uint[] { 0xb0, 0xe4, 0xa9, 0x0b, 0xdf, 0x82, 0x00, 0x9e }, + new uint[] { 0xf3, 0xb9, 0xdd, 0x94, 0xc5, 0xbb, 0x5d, 0x7a }, + new uint[] { 0xa7, 0xad, 0x6b, 0x22, 0x46, 0x2f, 0xb3, 0xf4 }, + new uint[] { 0xfb, 0xe5, 0x0e, 0x86, 0xbc, 0x8f, 0x1e, 0x75 }, + new uint[] { 0x90, 0x3d, 0x84, 0xc0, 0x27, 0x56, 0xea, 0x14 }, + new uint[] { 0xee, 0xf2, 0x7a, 0x8e, 0x90, 0xca, 0x23, 0xf7 }, + new uint[] { 0xe5, 0x45, 0xbe, 0x49, 0x61, 0xca, 0x29, 0xa1 }, + new uint[] { 0xdb, 0x9b, 0xc2, 0x57, 0x7f, 0xcc, 0x2a, 0x3f }, + new uint[] { 0x94, 0x47, 0xbe, 0x2c, 0xf5, 0xe9, 0x9a, 0x69 }, + new uint[] { 0x9c, 0xd3, 0x8d, 0x96, 0xf0, 0xb3, 0xc1, 0x4b }, + new uint[] { 0xbd, 0x61, 0x79, 0xa7, 0x1d, 0xc9, 0x6d, 0xbb }, + new uint[] { 0x98, 0xee, 0xa2, 0x1a, 0xf2, 0x5c, 0xd6, 0xbe }, + new uint[] { 0xc7, 0x67, 0x3b, 0x2e, 0xb0, 0xcb, 0xf2, 0xd0 }, + new uint[] { 0x88, 0x3e, 0xa3, 0xe3, 0x95, 0x67, 0x53, 0x93 }, + new uint[] { 0xc8, 0xce, 0x5c, 0xcd, 0x8c, 0x03, 0x0c, 0xa8 }, + new uint[] { 0x94, 0xaf, 0x49, 0xf6, 0xc6, 0x50, 0xad, 0xb8 }, + new uint[] { 0xea, 0xb8, 0x85, 0x8a, 0xde, 0x92, 0xe1, 0xbc }, + new uint[] { 0xf3, 0x15, 0xbb, 0x5b, 0xb8, 0x35, 0xd8, 0x17 }, + new uint[] { 0xad, 0xcf, 0x6b, 0x07, 0x63, 0x61, 0x2e, 0x2f }, + new uint[] { 0xa5, 0xc9, 0x1d, 0xa7, 0xac, 0xaa, 0x4d, 0xde }, + new uint[] { 0x71, 0x65, 0x95, 0x87, 0x66, 0x50, 0xa2, 0xa6 }, + new uint[] { 0x28, 0xef, 0x49, 0x5c, 0x53, 0xa3, 0x87, 0xad }, + new uint[] { 0x42, 0xc3, 0x41, 0xd8, 0xfa, 0x92, 0xd8, 0x32 }, + new uint[] { 0xce, 0x7c, 0xf2, 0x72, 0x2f, 0x51, 0x27, 0x71 }, + new uint[] { 0xe3, 0x78, 0x59, 0xf9, 0x46, 0x23, 0xf3, 0xa7 }, + new uint[] { 0x38, 0x12, 0x05, 0xbb, 0x1a, 0xb0, 0xe0, 0x12 }, + new uint[] { 0xae, 0x97, 0xa1, 0x0f, 0xd4, 0x34, 0xe0, 0x15 }, + new uint[] { 0xb4, 0xa3, 0x15, 0x08, 0xbe, 0xff, 0x4d, 0x31 }, + new uint[] { 0x81, 0x39, 0x62, 0x29, 0xf0, 0x90, 0x79, 0x02 }, + new uint[] { 0x4d, 0x0c, 0xf4, 0x9e, 0xe5, 0xd4, 0xdc, 0xca }, + new uint[] { 0x5c, 0x73, 0x33, 0x6a, 0x76, 0xd8, 0xbf, 0x9a }, + new uint[] { 0xd0, 0xa7, 0x04, 0x53, 0x6b, 0xa9, 0x3e, 0x0e }, + new uint[] { 0x92, 0x59, 0x58, 0xfc, 0xd6, 0x42, 0x0c, 0xad }, + new uint[] { 0xa9, 0x15, 0xc2, 0x9b, 0xc8, 0x06, 0x73, 0x18 }, + new uint[] { 0x95, 0x2b, 0x79, 0xf3, 0xbc, 0x0a, 0xa6, 0xd4 }, + new uint[] { 0xf2, 0x1d, 0xf2, 0xe4, 0x1d, 0x45, 0x35, 0xf9 }, + new uint[] { 0x87, 0x57, 0x75, 0x19, 0x04, 0x8f, 0x53, 0xa9 }, + new uint[] { 0x10, 0xa5, 0x6c, 0xf5, 0xdf, 0xcd, 0x9a, 0xdb }, + new uint[] { 0xeb, 0x75, 0x09, 0x5c, 0xcd, 0x98, 0x6c, 0xd0 }, + new uint[] { 0x51, 0xa9, 0xcb, 0x9e, 0xcb, 0xa3, 0x12, 0xe6 }, + new uint[] { 0x96, 0xaf, 0xad, 0xfc, 0x2c, 0xe6, 0x66, 0xc7 }, + new uint[] { 0x72, 0xfe, 0x52, 0x97, 0x5a, 0x43, 0x64, 0xee }, + new uint[] { 0x5a, 0x16, 0x45, 0xb2, 0x76, 0xd5, 0x92, 0xa1 }, + new uint[] { 0xb2, 0x74, 0xcb, 0x8e, 0xbf, 0x87, 0x87, 0x0a }, + new uint[] { 0x6f, 0x9b, 0xb4, 0x20, 0x3d, 0xe7, 0xb3, 0x81 }, + new uint[] { 0xea, 0xec, 0xb2, 0xa3, 0x0b, 0x22, 0xa8, 0x7f }, + new uint[] { 0x99, 0x24, 0xa4, 0x3c, 0xc1, 0x31, 0x57, 0x24 }, + new uint[] { 0xbd, 0x83, 0x8d, 0x3a, 0xaf, 0xbf, 0x8d, 0xb7 }, + new uint[] { 0x0b, 0x1a, 0x2a, 0x32, 0x65, 0xd5, 0x1a, 0xea }, + new uint[] { 0x13, 0x50, 0x79, 0xa3, 0x23, 0x1c, 0xe6, 0x60 }, + new uint[] { 0x93, 0x2b, 0x28, 0x46, 0xe4, 0xd7, 0x06, 0x66 }, + new uint[] { 0xe1, 0x91, 0x5f, 0x5c, 0xb1, 0xec, 0xa4, 0x6c }, + new uint[] { 0xf3, 0x25, 0x96, 0x5c, 0xa1, 0x6d, 0x62, 0x9f }, + new uint[] { 0x57, 0x5f, 0xf2, 0x8e, 0x60, 0x38, 0x1b, 0xe5 }, + new uint[] { 0x72, 0x45, 0x06, 0xeb, 0x4c, 0x32, 0x8a, 0x95 }, + }; + + [Fact] + public void TestBattery_Aligned() => TestHelper(false); + + [Fact] + public void TestBattery_Unaligned() => TestHelper(true); + + private void TestHelper(bool unaligned) + { + // 128-bit key + byte[] key = Enumerable.Range(0, 16).Select(x => (byte)x).ToArray(); + + // SipHash initialized with the key + SipHash prf = new SipHash(key); + + // Perform the test battery + Span message = new byte[Vectors.Length + 1]; + if (unaligned) + { + message = message.Slice(1); + } + + for (int i = 0; i < Vectors.Length; i++) + { + message[i] = (byte)i; + + // Compute the tag + long actual = prf.Compute(message.Slice(0, i)); + + // Get the target tag + long expected = BitConverter.ToInt64(Vectors[i].Select(x => (byte)x).ToArray(), 0); + + if (expected != actual) + { + throw new Exception($"Test vector failed for {i:N}-byte message!"); + } + } + } +} diff --git a/src/MessagePack.UnityClient/Assets/Scripts/Tests/ShareTests/SipHashTests.cs.meta b/src/MessagePack.UnityClient/Assets/Scripts/Tests/ShareTests/SipHashTests.cs.meta new file mode 100644 index 000000000..996b7da44 --- /dev/null +++ b/src/MessagePack.UnityClient/Assets/Scripts/Tests/ShareTests/SipHashTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c71df5af28304fe2991b6aaba359db90 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/tests/MessagePack.Tests/MessagePack.Tests.csproj b/tests/MessagePack.Tests/MessagePack.Tests.csproj index 5fb26383b..6e4b565f4 100644 --- a/tests/MessagePack.Tests/MessagePack.Tests.csproj +++ b/tests/MessagePack.Tests/MessagePack.Tests.csproj @@ -11,6 +11,7 @@ + From a71d0a10a5ff8f1f97c81efd8949ee80fa28128f Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Thu, 17 Oct 2024 11:26:46 -0600 Subject: [PATCH 2/2] Use safer unaligned-read methods --- .../Assets/Scripts/MessagePack/SipHash.cs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/MessagePack.UnityClient/Assets/Scripts/MessagePack/SipHash.cs b/src/MessagePack.UnityClient/Assets/Scripts/MessagePack/SipHash.cs index beb3fa5b1..4cc256c0c 100644 --- a/src/MessagePack.UnityClient/Assets/Scripts/MessagePack/SipHash.cs +++ b/src/MessagePack.UnityClient/Assets/Scripts/MessagePack/SipHash.cs @@ -10,6 +10,7 @@ using System.Buffers; using System.Buffers.Binary; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Security.Cryptography; namespace MessagePack @@ -87,9 +88,6 @@ public void GetKey(Span key) /// Returns 64-bit (8 bytes) SipHash tag. public long Compute(ReadOnlySpan data) { - static T Read(in byte start) - where T : unmanaged => Unsafe.ReadUnaligned(ref Unsafe.AsRef(in start)); - unchecked { // SipHash internal state @@ -109,7 +107,7 @@ static T Read(in byte start) // Process the input data in blocks of 64 bits for (int blockPosition = 0; blockPosition < finalBlockPosition; blockPosition += sizeof(ulong)) { - block = Read(data[blockPosition]); + block = MemoryMarshal.Read(data.Slice(blockPosition)); v3 ^= block; @@ -153,22 +151,22 @@ static T Read(in byte start) switch (data.Length & 7) { case 7: - block |= Read(data[finalBlockPosition]) | (ulong)Read(data[finalBlockPosition + 4]) << 32 | (ulong)data[finalBlockPosition + 6] << 48; + block |= MemoryMarshal.Read(data.Slice(finalBlockPosition)) | (ulong)MemoryMarshal.Read(data.Slice(finalBlockPosition + 4)) << 32 | (ulong)data[finalBlockPosition + 6] << 48; break; case 6: - block |= Read(data[finalBlockPosition]) | (ulong)Read(data[finalBlockPosition + 4]) << 32; + block |= MemoryMarshal.Read(data.Slice(finalBlockPosition)) | (ulong)MemoryMarshal.Read(data.Slice(finalBlockPosition + 4)) << 32; break; case 5: - block |= Read(data[finalBlockPosition]) | (ulong)data[finalBlockPosition + 4] << 32; + block |= MemoryMarshal.Read(data.Slice(finalBlockPosition)) | (ulong)data[finalBlockPosition + 4] << 32; break; case 4: - block |= Read(data[finalBlockPosition]); + block |= MemoryMarshal.Read(data.Slice(finalBlockPosition)); break; case 3: - block |= Read(data[finalBlockPosition]) | (ulong)data[finalBlockPosition + 2] << 16; + block |= MemoryMarshal.Read(data.Slice(finalBlockPosition)) | (ulong)data[finalBlockPosition + 2] << 16; break; case 2: - block |= Read(data[finalBlockPosition]); + block |= MemoryMarshal.Read(data.Slice(finalBlockPosition)); break; case 1: block |= data[finalBlockPosition];