diff --git a/.editorconfig b/.editorconfig index 06fbf7f64..bbd3bbfde 100644 --- a/.editorconfig +++ b/.editorconfig @@ -275,3 +275,7 @@ dotnet_diagnostic.CA2242.severity = warning # MEF006: No importing constructor dotnet_diagnostic.MEF006.severity = silent + +# Files vendored from dotnet/runtime keep their upstream formatting so future syncs diff cleanly. +[ICSharpCode.Decompiler/Util/Sha1ForNonSecretPurposes.cs] +generated_code = true diff --git a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj index 210d34749..b984447c2 100644 --- a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj +++ b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj @@ -395,6 +395,7 @@ + diff --git a/ICSharpCode.Decompiler.Tests/Util/Sha1ForNonSecretPurposesTests.cs b/ICSharpCode.Decompiler.Tests/Util/Sha1ForNonSecretPurposesTests.cs new file mode 100644 index 000000000..9f3f2df7e --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/Util/Sha1ForNonSecretPurposesTests.cs @@ -0,0 +1,111 @@ +// Copyright (c) 2026 Siegfried Pammer +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Linq; +using System.Text; + +using NUnit.Framework; + +namespace ICSharpCode.Decompiler.Tests.Util +{ + [TestFixture] + public class Sha1ForNonSecretPurposesTests + { + static string OneShotHash(byte[] input) + { + byte[] output = new byte[20]; + Sha1ForNonSecretPurposes.HashData(input, output); + return ToHex(output); + } + + static string IncrementalHash(byte[] input) + { + var sha1 = default(Sha1ForNonSecretPurposes); + sha1.Start(); + sha1.Append(input); + byte[] output = new byte[20]; + sha1.Finish(output); + return ToHex(output); + } + + static string ToHex(byte[] bytes) + { + return string.Concat(bytes.Select(b => b.ToString("x2"))); + } + + // FIPS 180 test vectors + [TestCase("", "da39a3ee5e6b4b0d3255bfef95601890afd80709")] + [TestCase("abc", "a9993e364706816aba3e25717850c26c9cd0d89d")] + [TestCase("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", "84983e441c3bd26ebaae4aa1f95129e5e54670f1")] + public void KnownVectors(string input, string expectedHex) + { + byte[] bytes = Encoding.ASCII.GetBytes(input); + Assert.That(OneShotHash(bytes), Is.EqualTo(expectedHex)); + Assert.That(IncrementalHash(bytes), Is.EqualTo(expectedHex)); + } + + [Test] + public void MatchesPlatformSha1AroundBlockBoundaries() + { + // Cover all interesting positions relative to the 64-byte block size and the + // 8-byte length suffix: 0, 1, 55, 56, 57, 63, 64, 65, 119, 120, 127, 128, ... + using var platformSha1 = System.Security.Cryptography.SHA1.Create(); + for (int length = 0; length <= 130; length++) + { + byte[] input = new byte[length]; + for (int i = 0; i < length; i++) + { + input[i] = unchecked((byte)(i * 131 + 7)); + } + string expected = ToHex(platformSha1.ComputeHash(input)); + Assert.That(OneShotHash(input), Is.EqualTo(expected), $"one-shot, length {length}"); + Assert.That(IncrementalHash(input), Is.EqualTo(expected), $"incremental, length {length}"); + } + } + + [Test] + public void StartResetsTheIncrementalState() + { + var sha1 = default(Sha1ForNonSecretPurposes); + sha1.Start(); + sha1.Append(Encoding.ASCII.GetBytes("garbage that must not leak into the second hash")); + byte[] output = new byte[20]; + sha1.Finish(output); + + sha1.Start(); + sha1.Append(Encoding.ASCII.GetBytes("abc")); + sha1.Finish(output); + Assert.That(ToHex(output), Is.EqualTo("a9993e364706816aba3e25717850c26c9cd0d89d")); + } + + [Test] + public void EcmaStandardPublicKeyYieldsKnownToken() + { + // The strong-name public key token is defined as the last 8 bytes of the + // SHA-1 of the public key, in reversed order. The ECMA standard public key + // must produce the well-known token b77a5c561934e089 (mscorlib et al.). + byte[] ecmaKey = new byte[16]; + ecmaKey[8] = 0x04; + byte[] hash = new byte[20]; + Sha1ForNonSecretPurposes.HashData(ecmaKey, hash); + string token = ToHex(hash.Skip(12).Reverse().ToArray()); + Assert.That(token, Is.EqualTo("b77a5c561934e089")); + } + } +} diff --git a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj index 800dcc15b..146f9b5a5 100644 --- a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj +++ b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj @@ -499,6 +499,7 @@ + diff --git a/ICSharpCode.Decompiler/Metadata/AssemblyReferences.cs b/ICSharpCode.Decompiler/Metadata/AssemblyReferences.cs index c7ca9ba7f..d59ff406e 100644 --- a/ICSharpCode.Decompiler/Metadata/AssemblyReferences.cs +++ b/ICSharpCode.Decompiler/Metadata/AssemblyReferences.cs @@ -24,7 +24,6 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using System.Reflection.Metadata; -using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; @@ -273,10 +272,9 @@ namespace ICSharpCode.Decompiler.Metadata var bytes = Metadata.GetBlobBytes(entry.PublicKeyOrToken); if ((entry.Flags & AssemblyFlags.PublicKey) != 0) { - using (var hasher = SHA1.Create()) - { - bytes = hasher.ComputeHash(bytes).Skip(12).ToArray(); - } + byte[] hash = new byte[20]; + Sha1ForNonSecretPurposes.HashData(bytes, hash); + bytes = hash.Skip(12).ToArray(); } publicKeyToken = bytes; diff --git a/ICSharpCode.Decompiler/Metadata/MetadataExtensions.cs b/ICSharpCode.Decompiler/Metadata/MetadataExtensions.cs index 5155f428e..431f089d0 100644 --- a/ICSharpCode.Decompiler/Metadata/MetadataExtensions.cs +++ b/ICSharpCode.Decompiler/Metadata/MetadataExtensions.cs @@ -24,7 +24,6 @@ using System.Linq; using System.Reflection; using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; -using System.Security.Cryptography; using System.Text; using ICSharpCode.Decompiler.TypeSystem; @@ -40,12 +39,10 @@ namespace ICSharpCode.Decompiler.Metadata static string CalculatePublicKeyToken(BlobHandle blob, MetadataReader reader) { // Calculate public key token: - // 1. hash the public key (always use SHA1). - byte[] publicKeyTokenBytes; - using (var hasher = SHA1.Create()) - { - publicKeyTokenBytes = hasher.ComputeHash(reader.GetBlobBytes(blob)); - } + // 1. hash the public key (the strong-name format mandates SHA-1; a managed + // implementation is used so this works under restrictive crypto policies). + byte[] publicKeyTokenBytes = new byte[20]; + Sha1ForNonSecretPurposes.HashData(reader.GetBlobBytes(blob), publicKeyTokenBytes); // 2. take the last 8 bytes // 3. according to Cecil we need to reverse them, other sources did not mention this. diff --git a/ICSharpCode.Decompiler/Util/BitOperations.cs b/ICSharpCode.Decompiler/Util/BitOperations.cs index 7edeab21a..53c6756f8 100644 --- a/ICSharpCode.Decompiler/Util/BitOperations.cs +++ b/ICSharpCode.Decompiler/Util/BitOperations.cs @@ -7,6 +7,10 @@ namespace System.Numerics { internal static class BitOperations { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint RotateLeft(uint value, int offset) + => (value << offset) | (value >> (32 - offset)); + private static ReadOnlySpan TrailingZeroCountDeBruijn => new byte[32] { 00, 01, 28, 02, 29, 14, 24, 03, diff --git a/ICSharpCode.Decompiler/Util/Sha1ForNonSecretPurposes.cs b/ICSharpCode.Decompiler/Util/Sha1ForNonSecretPurposes.cs new file mode 100644 index 000000000..fed5d6d01 --- /dev/null +++ b/ICSharpCode.Decompiler/Util/Sha1ForNonSecretPurposes.cs @@ -0,0 +1,240 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Vendored from dotnet/runtime, src/libraries/Common/src/System/Sha1ForNonSecretPurposes.cs +// (commit 6e51f762bc4c98ea90ae6ca21c4e220b4b2e7a5c). The only changes are the "unchecked" blocks +// in Finish and Drain: upstream compiles without overflow checks, while this project sets +// CheckForOverflowUnderflow, and SHA-1 relies on wrapping 32-bit arithmetic. +// +// Strong-name public-key tokens are defined by ECMA-335 as a SHA-1 digest, so this managed +// implementation is used instead of System.Security.Cryptography.SHA1 to keep reading assembly +// metadata independent of the host crypto policy (FIPS-mode systems refuse to create platform +// SHA-1 instances). + +using System.Buffers.Binary; +using System.Diagnostics; +using System.Numerics; + +namespace System +{ + /// + /// Implements the SHA1 hashing algorithm. Note that + /// implementation is for hashing public information. Do not + /// use code to hash private data, as implementation does + /// not take any steps to avoid information disclosure. + /// + internal struct Sha1ForNonSecretPurposes + { + private long _length; // Total message length in bits + private uint[] _w; // Workspace + private int _pos; // Length of current chunk in bytes + + /// + /// Computes the SHA1 hash of the provided data. + /// + /// The data to hash. + /// The buffer to receive the hash value. + public static unsafe void HashData(ReadOnlySpan source, Span destination) + { + Debug.Assert(destination.Length == 20); + + Span w = stackalloc uint[85]; + + Start(w); + + int originalLength = source.Length; + + while (source.Length >= 64) + { + for (int i = 0; i < 16; i++) + { + w[i] = BinaryPrimitives.ReadUInt32BigEndian(source); + source = source.Slice(4); + } + Drain(w); + } + + Span tail = stackalloc byte[2 * 64]; + source.CopyTo(tail); + int pos = source.Length; + tail[pos++] = 0x80; + while ((pos & 63) != 56) + { + tail[pos++] = 0x00; + } + BinaryPrimitives.WriteUInt64BigEndian(tail.Slice(pos), (ulong)originalLength * 8); + tail = tail.Slice(0, pos + 8); + + while (tail.Length > 0) + { + for (int i = 0; i < 16; i++) + { + w[i] = BinaryPrimitives.ReadUInt32BigEndian(tail); + tail = tail.Slice(4); + } + Drain(w); + } + + for (int i = 80; i < w.Length; i++) + { + BinaryPrimitives.WriteUInt32BigEndian(destination, w[i]); + destination = destination.Slice(4); + } + } + + /// + /// Call Start() to initialize the hash object. + /// + public void Start() + { + Start(_w ??= new uint[85]); + + _length = 0; + _pos = 0; + } + + private static void Start(Span w) + { + w[80] = 0x67452301; + w[81] = 0xEFCDAB89; + w[82] = 0x98BADCFE; + w[83] = 0x10325476; + w[84] = 0xC3D2E1F0; + } + + /// + /// Adds an input byte to the hash. + /// + /// Data to include in the hash. + public void Append(byte input) + { + int idx = _pos >> 2; + _w[idx] = (_w[idx] << 8) | input; + if (64 == ++_pos) + { + Drain(); + } + } + + /// + /// Adds input bytes to the hash. + /// + /// + /// Data to include in the hash. Must not be null. + /// + public void Append(ReadOnlySpan input) + { + foreach (byte b in input) + { + Append(b); + } + } + + /// + /// Retrieves the hash value. + /// Note that after calling function, the hash object should + /// be considered uninitialized. Subsequent calls to Append or + /// Finish will produce useless results. Call Start() to + /// reinitialize. + /// + /// + /// Buffer to receive the hash value. Must not be null. + /// Up to 20 bytes of hash will be written to the output buffer. + /// If the buffer is smaller than 20 bytes, the remaining hash + /// bytes will be lost. If the buffer is larger than 20 bytes, the + /// rest of the buffer is left unmodified. + /// + public void Finish(Span output) + { + Debug.Assert(output.Length == 20); + + long l = _length + 8 * _pos; + Append(0x80); + while (_pos != 56) + { + Append(0x00); + } + + unchecked + { + Append((byte)(l >> 56)); + Append((byte)(l >> 48)); + Append((byte)(l >> 40)); + Append((byte)(l >> 32)); + Append((byte)(l >> 24)); + Append((byte)(l >> 16)); + Append((byte)(l >> 8)); + Append((byte)l); + } + + for (int i = 80; i < _w.Length; i++) + { + BinaryPrimitives.WriteUInt32BigEndian(output, _w[i]); + output = output.Slice(4); + } + } + + /// + /// Called when pos reaches 64. + /// + private void Drain() + { + Drain(_w); + _length += 512; // 64 bytes == 512 bits + _pos = 0; + } + + private static void Drain(Span w) + { + unchecked + { + var _ = w[84]; // Hint to eliminate bounds checks + + for (int i = 16; i < 80; i++) + { + w[i] = BitOperations.RotateLeft(w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16], 1); + } + + uint a = w[80]; + uint b = w[81]; + uint c = w[82]; + uint d = w[83]; + uint e = w[84]; + + for (int i = 0; i < 20; i++) + { + const uint k = 0x5A827999; + uint f = (b & c) | ((~b) & d); + uint temp = BitOperations.RotateLeft(a, 5) + f + e + k + w[i]; e = d; d = c; c = BitOperations.RotateLeft(b, 30); b = a; a = temp; + } + + for (int i = 20; i < 40; i++) + { + uint f = b ^ c ^ d; + const uint k = 0x6ED9EBA1; + uint temp = BitOperations.RotateLeft(a, 5) + f + e + k + w[i]; e = d; d = c; c = BitOperations.RotateLeft(b, 30); b = a; a = temp; + } + + for (int i = 40; i < 60; i++) + { + uint f = (b & c) | (b & d) | (c & d); + const uint k = 0x8F1BBCDC; + uint temp = BitOperations.RotateLeft(a, 5) + f + e + k + w[i]; e = d; d = c; c = BitOperations.RotateLeft(b, 30); b = a; a = temp; + } + + for (int i = 60; i < 80; i++) + { + uint f = b ^ c ^ d; + const uint k = 0xCA62C1D6; + uint temp = BitOperations.RotateLeft(a, 5) + f + e + k + w[i]; e = d; d = c; c = BitOperations.RotateLeft(b, 30); b = a; a = temp; + } + + w[80] += a; + w[81] += b; + w[82] += c; + w[83] += d; + w[84] += e; + } + } + } +}