【问题标题】:.NET inconsistent hash output.NET 不一致的哈希输出
【发布时间】:2022-12-14 23:07:41
【问题描述】:

我有一个代表一个独特的现实世界对象的类,它的名字是个人身份信息(如果你感兴趣的话,是一个车牌)所以作为基本的第一步,我将散列名称并使用它。 (我知道 - 需要盐等 - 这只是一个基础)

我有一个测试,它使用固定输入(测试名称 - “A”)实例化对象,并断言 Id(散列输出的 Base 64 字符串)符合预期。但是,它偶尔会失败(!!)

我进行了更深入的挖掘,这里是一个条件断点的屏幕截图,该断点仅在哈希输出不正常时才会中断。输入仍然符合预期('bytes' 变量包含 { 65 },但输出不同于正常的 sha384 输出哈希(通常为“rRSq8lAgvvL9Tj617AxQJyzf1mB0sO0DfJoRJUMhqsBymYU3S+6qW4ClBNBIvhhk”)

第 19-25 行被分开了一点以便于调试,但除此之外,这个类应该是这样的。

任何关于这是如何可能的线索都将非常受欢迎。运行 Windows 11,使用 .NET 7 和最新版本的 Visual Studio Enterprise。

这是在哈希输出不正常的情况下命中条件断点的图片:

如果有人想尝试重现它,这里是代码(注意它不一致:它只是偶尔)

using System.Security.Cryptography;
using System.Text;

namespace Domain.Models.Object
{
    /// <summary>
    /// One-way identifier
    /// </summary>
    public record ObjectIdentifier
    {
        private static SHA384 hash = SHA384.Create();

        public ObjectIdentifier(string name)
        {
            if (name == null)
            {
                throw new ArgumentNullException(nameof(name));
            }
            var bytes = Encoding.UTF8.GetBytes(name);


            Id = Convert.ToBase64String(hash.ComputeHash(bytes));

            int x = 0;
            x++;
        }

        public string Id { get; init; }

        public override string ToString()
        {
            return Id;
        }
    }
}

这是测试:

[Fact]
public void ObjectIdentifier_AsExpected()
{
    // arrange
    var obj = new ObjectIdentifier("A");

    // assert
    Assert.Equal("rRSq8lAgvvL9Tj617AxQJyzf1mB0sO0DfJoRJUMhqsBymYU3S+6qW4ClBNBIvhhk", obj.Id);
}

我还注意到新的哈希值并不始终相同:这是另一个失败的结果,输出与之前的屏幕截图不同:

我还注意到将它添加到第 20 行会导致不一致完全停止发生...... 不幸的是,这不是一个合适的修复方法 :P

Debug.Assert(bytes.Length == 1 && bytes[0] == 65)

更新无效输出似乎只是上面提供的两个,我没有观察到任何其他变体。

此外,将其更改为类(而不是记录)也没有任何区别。

我也在测试控制台应用程序中观察到这种效果,该应用程序只提供了两个标识符,但实际上输出了两个以上的哈希值:

【问题讨论】:

  • 可能是线程问题吗? HashAlgorithm.ComputeHash() 不是线程安全的。你可以通过在你的类中添加一个锁定对象字段 static object locker = new object(); 并在你调用 hash.ComputeHash(bytes) 时锁定它来测试它
  • 好点,我会试一试并相应地更新。我的测试设置为并行运行... - 谢谢 :)
  • 是的,就是这样 - 再次感谢 :)

标签: c# .net cryptography


【解决方案1】:

因此,正如 Matt 在 cmets 中友善地指出的那样,此操作不是线程安全的。共享工作者对象(共享是因为它是静态的)是罪魁祸首。一个容易错过的 - 也是为什么必须进行单元测试的一个很好的例子!

我选择使用一个对象池来为每个实例化一个唯一的工作人员,同时允许减少分配开销:

using Domain.Implementations;
using Microsoft.Extensions.ObjectPool;
using System.Text;

namespace Domain.Models.Object
{
    /// <summary>
    /// One-way identifier
    /// </summary>
    public class ObjectIdentifier
    {
        private static ObjectPool<HashWrapper> hashObjects = ObjectPool.Create<HashWrapper>();

        public ObjectIdentifier(string name)
        {
            if (name == null)
            {
                throw new ArgumentNullException(nameof(name));
            }

            var hashworker = hashObjects.Get();
            try
            {
                Id = Convert.ToBase64String(hashworker.Value.ComputeHash(Encoding.UTF8.GetBytes(name)));
            }
            finally
            {
                hashObjects.Return(hashworker);
            }            
        }

        public string Id { get; init; }

        public override string ToString()
        {
            return Id;
        }
    }
}

哈希包装器类(包装工人并为对象池提供公共构造函数)

internal class HashWrapper
{
    public HashWrapper()
    {
        Value = SHA384.Create();
    }

    public HashAlgorithm Value { get; private set; }
}

更新

这种使用共享池的实现在构建更多对象时提供了(非常小的)速度和内存分配的增加。对于较小的数字,会有(非常小的)性能损失。 YMMV - 见下文。

该基准测试运行的版本具有 Shared = true(共享对象上的正常锁定)、false(每个实例化的新 sha worker 实例)和 null [?在列中]:使用对象池发出信号

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-08-24
    • 2014-02-17
    • 1970-01-01
    • 1970-01-01
    • 2011-05-27
    • 2013-02-26
    相关资源
    最近更新 更多