diff --git a/src/libraries/Common/src/System/HashCodeRandomization.cs b/src/libraries/Common/src/System/HashCodeRandomization.cs new file mode 100644 index 00000000000000..16cd6cb577c052 --- /dev/null +++ b/src/libraries/Common/src/System/HashCodeRandomization.cs @@ -0,0 +1,58 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System +{ + // Contains helpers for calculating randomized hash codes of common types. + // Since these hash codes are randomized, callers must not persist them between + // AppDomain restarts. There's still the potential for limited collisions + // if two distinct types have the same bit pattern (e.g., string.Empty and (int)0). + // This should be acceptable because the number of practical collisions is + // limited by the number of distinct types used here, and we expect callers to + // have a small, fixed set of accepted types for any hash-based collection. + // If we really do need to address this in the future, we can use a seed per type + // rather than a global seed for the entire AppDomain. + internal static class HashCodeRandomization + { + public static int GetRandomizedOrdinalHashCode(this string value) + { +#if NETCOREAPP + // In .NET Core, string hash codes are already randomized. + + return value.GetHashCode(); +#else + // Downlevel, we need to perform randomization ourselves. There's still + // the potential for limited collisions ("Hello!" and "Hello!\0"), but + // this shouldn't be a problem in practice. If we need to address it, + // we can mix the string length into the accumulator before running the + // string contents through. + // + // We'll pull out pairs of chars and write 32 bits at a time. + + HashCode hashCode = default; + int pair = 0; + for (int i = 0; i < value.Length; i++) + { + int ch = value[i]; + if ((i & 1) == 0) + { + pair = ch << 16; // first member of pair + } + else + { + pair |= ch; // second member of pair + hashCode.Add(pair); // write pair as single unit + pair = 0; + } + } + hashCode.Add(pair); // flush any leftover data (could be 0 or 1 chars) + return hashCode.ToHashCode(); +#endif + } + + public static int GetRandomizedHashCode(this int value) + { + return HashCode.Combine(value); + } + } +} diff --git a/src/libraries/System.Security.Cryptography.Cose/src/System.Security.Cryptography.Cose.csproj b/src/libraries/System.Security.Cryptography.Cose/src/System.Security.Cryptography.Cose.csproj index 2c66e4920a1e71..e2ef885d16954b 100644 --- a/src/libraries/System.Security.Cryptography.Cose/src/System.Security.Cryptography.Cose.csproj +++ b/src/libraries/System.Security.Cryptography.Cose/src/System.Security.Cryptography.Cose.csproj @@ -13,6 +13,7 @@ + @@ -32,6 +33,11 @@ + + + + + diff --git a/src/libraries/System.Security.Cryptography.Cose/src/System/Security/Cryptography/Cose/CoseHeaderLabel.cs b/src/libraries/System.Security.Cryptography.Cose/src/System/Security/Cryptography/Cose/CoseHeaderLabel.cs index e3fd261674f31d..1c800f871fcda7 100644 --- a/src/libraries/System.Security.Cryptography.Cose/src/System/Security/Cryptography/Cose/CoseHeaderLabel.cs +++ b/src/libraries/System.Security.Cryptography.Cose/src/System/Security/Cryptography/Cose/CoseHeaderLabel.cs @@ -54,12 +54,16 @@ public bool Equals(CoseHeaderLabel other) public override int GetHashCode() { + // Since this type is used as a key in a dictionary (see CoseHeaderMap) + // and since the label is potentially adversary-provided, we'll need + // to randomize the hash code. + if (LabelAsString != null) { - return LabelAsString.GetHashCode(); + return LabelAsString.GetRandomizedOrdinalHashCode(); } - return LabelAsInt32.GetHashCode(); + return LabelAsInt32.GetRandomizedHashCode(); } public static bool operator ==(CoseHeaderLabel left, CoseHeaderLabel right) => left.Equals(right); diff --git a/src/libraries/System.Security.Cryptography.Cose/tests/CoseHeaderLabelTests.cs b/src/libraries/System.Security.Cryptography.Cose/tests/CoseHeaderLabelTests.cs index a7d65a1d9a5216..d4c4b44b296879 100644 --- a/src/libraries/System.Security.Cryptography.Cose/tests/CoseHeaderLabelTests.cs +++ b/src/libraries/System.Security.Cryptography.Cose/tests/CoseHeaderLabelTests.cs @@ -10,23 +10,35 @@ public class CoseHeaderLabelTests [Fact] public void CoseHeaderLabel_GetHashCode() { - CoseHeaderLabel label = default; - Assert.Equal(0, label.GetHashCode()); // There's no need to call GetHashCode on integers as they are their own hashcode. + // First, construct a COSE header of (0) using several different methods. + // They should all have the same hash code, though we don't know what + // the hash code is. - label = new CoseHeaderLabel(); - Assert.Equal(0, label.GetHashCode()); + CoseHeaderLabel label1 = default; + CoseHeaderLabel label2 = new CoseHeaderLabel(); + CoseHeaderLabel label3 = new CoseHeaderLabel(0); - label = new CoseHeaderLabel(0); - Assert.Equal(0, label.GetHashCode()); + int label1HashCode = label1.GetHashCode(); + Assert.Equal(label1HashCode, label2.GetHashCode()); + Assert.Equal(label1HashCode, label3.GetHashCode()); - label = new CoseHeaderLabel(""); - Assert.Equal("".GetHashCode(), label.GetHashCode()); + // Make sure the integer hash code calculation really is randomized. + // Checking 1 & 2 together rather than independently cuts the false + // positive rate down to nearly 1 in 2^64. - label = new CoseHeaderLabel(1); - Assert.Equal(1, label.GetHashCode()); + bool isReturningNormalInt32HashCode = + (new CoseHeaderLabel(1).GetHashCode() == 1) + && (new CoseHeaderLabel(2).GetHashCode() == 2); + Assert.False(isReturningNormalInt32HashCode); - label = new CoseHeaderLabel("content-type"); - Assert.Equal("content-type".GetHashCode(), label.GetHashCode()); + // Make sure the string hash code calculation really is randomized. + // Checking 1 & 2 together rather than independently cuts the false + // positive rate down to nearly 1 in 2^64. + + bool isReturningNormalStringHashCode = + (new CoseHeaderLabel("Hello").GetHashCode() == "Hello".GetHashCode()) + && (new CoseHeaderLabel("World").GetHashCode() == "World".GetHashCode()); + Assert.Equal(PlatformDetection.IsNetCore, isReturningNormalStringHashCode); } [Fact]