前言
有网友在交流群中询问,文件 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:
这又是为什么呢?我们下回分解!
添加微信号【MyIO666】,邀你加入技术交流群