Skip to content

Consider ifdefing away Sha1ForNonSecretPurposes on Windows #45237

@GrabYourPitchforks

Description

@GrabYourPitchforks

We have a managed implementation of SHA-1 hanging around for use in EventSource and related APIs. We should consider ifdefing it away and relying on the OS's underlying implementation on runtimes where we know an OS implementation exists. This can help compliance, as getting an exemption for SHA-1 for compat purposes is far easier than getting an exemption for carrying our own implementation. There are also some small perf wins.

Benchmark code below the fold

public class Sha1Runner
{
    private byte[] _input;
    private byte[] _digest = new byte[20];

    [Params(0, 8, 12, 24, 32, 64, 128, 256)]
    public int InputSizeInBytes { get; set; }

    [GlobalSetup]
    public void Setup()
    {
        _input = new byte[InputSizeInBytes];
        RandomNumberGenerator.Fill(_input);
    }

    [Benchmark(Baseline = true)]
    public byte[] UseManaged()
    {
        Sha1ForNonSecretPurposes sha1 = default;
        sha1.Start();
        sha1.Append(_input);
        sha1.Finish(_digest);
        return _digest;
    }

    [Benchmark(Baseline = false)]
    public byte[] UseBCrypt()
    {
        SHA1.HashData(_input, _digest);
        return _digest;
    }

    private struct Sha1ForNonSecretPurposes
    {
        private long length; // Total message length in bits
        private uint[] w; // Workspace
        private int pos; // Length of current chunk in bytes

        /// <summary>
        /// Call Start() to initialize the hash object.
        /// </summary>
        [SkipLocalsInit]
        public void Start()
        {
            this.w ??= new uint[85];

            this.length = 0;
            this.pos = 0;
            this.w[80] = 0x67452301;
            this.w[81] = 0xEFCDAB89;
            this.w[82] = 0x98BADCFE;
            this.w[83] = 0x10325476;
            this.w[84] = 0xC3D2E1F0;
        }

        /// <summary>
        /// Adds an input byte to the hash.
        /// </summary>
        /// <param name="input">Data to include in the hash.</param>
        [SkipLocalsInit]
        public void Append(byte input)
        {
            this.w[this.pos / 4] = (this.w[this.pos / 4] << 8) | input;
            if (64 == ++this.pos)
            {
                this.Drain();
            }
        }

        /// <summary>
        /// Adds input bytes to the hash.
        /// </summary>
        /// <param name="input">
        /// Data to include in the hash. Must not be null.
        /// </param>
        [SkipLocalsInit]
#if ES_BUILD_STANDALONE
        public void Append(byte[] input)
#else
        public void Append(ReadOnlySpan<byte> input)
#endif
        {
            foreach (byte b in input)
            {
                this.Append(b);
            }
        }

        /// <summary>
        /// Retrieves the hash value.
        /// Note that after calling this function, the hash object should
        /// be considered uninitialized. Subsequent calls to Append or
        /// Finish will produce useless results. Call Start() to
        /// reinitialize.
        /// </summary>
        /// <param name="output">
        /// 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.
        /// </param>
        [SkipLocalsInit]
        public void Finish(byte[] output)
        {
            long l = this.length + 8 * this.pos;
            this.Append(0x80);
            while (this.pos != 56)
            {
                this.Append(0x00);
            }

            unchecked
            {
                this.Append((byte)(l >> 56));
                this.Append((byte)(l >> 48));
                this.Append((byte)(l >> 40));
                this.Append((byte)(l >> 32));
                this.Append((byte)(l >> 24));
                this.Append((byte)(l >> 16));
                this.Append((byte)(l >> 8));
                this.Append((byte)l);

                int end = output.Length < 20 ? output.Length : 20;
                for (int i = 0; i != end; i++)
                {
                    uint temp = this.w[80 + i / 4];
                    output[i] = (byte)(temp >> 24);
                    this.w[80 + i / 4] = temp << 8;
                }
            }
        }

        /// <summary>
        /// Called when this.pos reaches 64.
        /// </summary>
        [SkipLocalsInit]
        private void Drain()
        {
            for (int i = 16; i != 80; i++)
            {
                this.w[i] = BitOperations.RotateLeft(this.w[i - 3] ^ this.w[i - 8] ^ this.w[i - 14] ^ this.w[i - 16], 1);
            }

            unchecked
            {
                uint a = this.w[80];
                uint b = this.w[81];
                uint c = this.w[82];
                uint d = this.w[83];
                uint e = this.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 + this.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 + this.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 + this.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 + this.w[i]; e = d; d = c; c = BitOperations.RotateLeft(b, 30); b = a; a = temp;
                }

                this.w[80] += a;
                this.w[81] += b;
                this.w[82] += c;
                this.w[83] += d;
                this.w[84] += e;
            }

            this.length += 512; // 64 bytes == 512 bits
            this.pos = 0;
        }
    }
}

Method InputSizeInBytes Mean Error StdDev Ratio
UseManaged 0 354.8 ns 2.63 ns 2.34 ns 1.00
UseBCrypt 0 283.2 ns 1.68 ns 1.49 ns 0.80
UseManaged 8 367.5 ns 5.09 ns 4.76 ns 1.00
UseBCrypt 8 287.1 ns 1.42 ns 1.26 ns 0.78
UseManaged 12 329.7 ns 2.29 ns 2.03 ns 1.00
UseBCrypt 12 285.9 ns 1.22 ns 1.02 ns 0.87
UseManaged 24 352.7 ns 2.24 ns 2.09 ns 1.00
UseBCrypt 24 292.6 ns 1.75 ns 1.63 ns 0.83
UseManaged 32 362.8 ns 3.31 ns 3.09 ns 1.00
UseBCrypt 32 288.2 ns 1.95 ns 1.53 ns 0.80
UseManaged 64 597.5 ns 3.44 ns 3.22 ns 1.00
UseBCrypt 64 373.3 ns 2.31 ns 2.16 ns 0.62
UseManaged 128 885.1 ns 7.33 ns 6.50 ns 1.00
UseBCrypt 128 453.9 ns 3.26 ns 3.05 ns 0.51
UseManaged 256 1,450.3 ns 11.29 ns 9.42 ns 1.00
UseBCrypt 256 603.9 ns 3.42 ns 2.85 ns 0.42

Metadata

Metadata

Assignees

Labels

area-MetaenhancementProduct code improvement that does NOT require public API changes/additionshelp wanted[up-for-grabs] Good issue for external contributors

Type

No type

Projects

Relationships

None yet

Development

No branches or pull requests

Issue actions