.NET如何计算文件MD5哈希

前言

有网友在交流群中询问,文件 MD5 是全部读取到内存后计算出来的,还是拿到流就可以计算出来了:

原理上来说,MD5 需要对全部内容做运算,所以应该是获取所有内容后再计算的。.

但是,如果全部读取到内存后再计算,又不太现实,比如读取一个 1T 大小的文件。

Talk Is Cheap. Show Me The Code.

让我们来看看 .NET 中具体是如何实现的。

分析代码

.NET 下计算哈希的方法是ComputeHash:

public byte[] ComputeHash(Stream inputStream)
{
    if (_disposed)
        throw new ObjectDisposedException(null);

    // Use ArrayPool.Shared instead of CryptoPool because the array is passed out.
    byte[] buffer = ArrayPool<byte>.Shared.Rent(4096);

    int bytesRead;
    int clearLimit = 0;

    while ((bytesRead = inputStream.Read(buffer, 0, buffer.Length)) > 0)
    {
        if (bytesRead > clearLimit)
        {
            clearLimit = bytesRead;
        }

        HashCore(buffer, 0, bytesRead);
    }

    CryptographicOperations.ZeroMemory(buffer.AsSpan(0, clearLimit));
    ArrayPool<byte>.Shared.Return(buffer, clearArray: false);
    return CaptureHashCodeAndReinitialize();
}

通过while ((bytesRead = inputStream.Read(buffer, 0, buffer.Length)) > 0)可以判断出,确实是获取了所有内容。

但是是分段获取的,每次最多只读取 4096 字节(byte[] buffer = ArrayPool<byte>.Shared.Rent(4096);)。

关键是,分段读取出的字节,会合并放到内存中去计算哈希吗?

HashCore

分段读取出的字节是交由HashCore方法处理的。

它的具体实现代码在LiteHash结构中:

public void Append(ReadOnlySpan<byte> data)
{
    if (data.IsEmpty)
    {
        return;
    }

    Check(Interop.Crypto.EvpDigestUpdate(_ctx, data, data.Length));
}

而调用的 EvpDigestUpdate 是底层 API,看不到源码:

internal const string CryptoNative = "libSystem.Security.Cryptography.Native.OpenSsl";

[LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_EvpDigestUpdate")]
private static partial int EvpDigestUpdate(SafeEvpMdCtxHandle ctx, ref byte d, int cnt);

在 OpenSSL 官网上,找到了这个 API 的描述:

d处的数据字节哈希到摘要上下文ctx中。可以在同一ctx上多次调用此函数,以对增加的数据进行哈希处理。

也就是说,计算文件哈希实际经过了多次处理。

那如何得到最后的哈希值呢?

CaptureHashCodeAndReinitialize

ComputeHash最后调用 CaptureHashCodeAndReinitialize 方法返回哈希值。

它的具体实现代码也在LiteHash结构中,调用了EvpDigestFinalEx API:

public int Finalize(Span<byte> destination)
{
    Debug.Assert(destination.Length >= _hashSizeInBytes);

    uint length = (uint)destination.Length;
    Check(Interop.Crypto.EvpDigestFinalEx(_ctx, ref MemoryMarshal.GetReference(destination), ref length));

    Debug.Assert(length == _hashSizeInBytes);
    return _hashSizeInBytes;
}

[LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_EvpDigestFinalEx")]
internal static partial int EvpDigestFinalEx(SafeEvpMdCtxHandle ctx, ref byte md, ref uint s);

ctx中检索摘要值并将其放在md中。如果s参数不是 NULL,则写入的数据字节数(即摘要的长度)将写入s处。

结论

通过以上分析,可以得出文件 MD5 哈希计算流程如下:

不过,群里又有网友说,不要用 MD5:

这又是为什么呢?我们下回分解!