加密哈希:它们是什么,以及为什么你应该和它们做朋友






4.92/5 (58投票s)
对加密哈希的描述以及如何计算它们的实际示例。
(如果没有 VS 2015 可再发行组件,请在此处查找。)
源代码:可在 GitHub 上获取。
引言 - 什么是哈希?
哈希函数 被定义为一个将任意大小的数据映射到固定大小数据的函数。以下面的图片为例
这是一个将任意长度的姓名映射到一个整数的哈希函数。这个哈希函数将简单地计算姓名中有多少个字母来找到对应的整数。请注意,对于这个函数来说,要找到将被映射到同一个整数的不同键的例子并不难。
一些现代编程语言,如 Ruby,拥有称为 哈希表的数据结构,用于实现字典,它将键映射到值。这些表使用哈希函数来计算数组槽中的索引。当与一个好的哈希函数一起使用时,哈希表可以非常高效。
加密哈希函数是不可逆的,或者换句话说,是单向函数。这意味着通过仅查看函数的输出(称为消息摘要),几乎不可能重构函数的输入(通常称为消息)。
与常规哈希函数不同,后者在反向时也存在一些困难,加密哈希函数即使在攻击者知道理论和所用算法的情况下也极难反转。仅凭哈希值,攻击者应该对原始消息一无所知,甚至不知道消息的大小(显然,第一个例子的情况并非如此)。
除此之外,加密哈希函数还具有以下特性
- 对于任何给定的消息,计算哈希值在计算上很容易。
- 消息完整性:在不修改消息摘要的情况下,无法修改消息。
- 抗碰撞性:找到两个生成相同摘要的不同消息是不可行的。
下面列出了最常见的哈希函数
- MD5:于 1992 年发布,具有 128 位摘要大小。如今,它已被认为在密码学上存在漏洞。
- SHA-1:由 NSA 开发,于 1995 年标准化。摘要大小为 160 位。它已不再被认为是安全的,新开发应实现 SHA-2 或 SHA-3。Internet Explorer、Chrome 和 Firefox 浏览器都已宣布将在 2017 年停止接受 SHA-1 SSL 证书。2017 年 2 月,CWI Amsterdam 和 Google 宣布他们发现了第一个 SHA1 碰撞,据他们说,“这强调了停止使用 SHA-1 的必要性”。我们将在实际示例部分自己找到碰撞的证据。
- SHA-2:是一系列六种加密哈希函数,具有四种不同的摘要大小:224 位、256 位、384 位和 512 位。也由 NSA 设计,并于 2001 年作为美国联邦标准 (FIPS) 发布。
- SHA-3 标准于 2015 年 8 月由 NIST 发布。
加密哈希函数在公钥密码学中扮演着重要角色,但在本文中,我们将研究它们在计算/验证校验和中的应用。
将加密哈希用作校验和
从互联网下载文件
许多提供下载的网站会随可下载文件一起提供加密哈希。例如,在Notepad++ 下载页面上,用户会找到
作者还开玩笑地称任何想检查摘要的人是偏执狂。让我们稍作解释,然后让您决定验证下载是否是偏执狂。
列出文件的摘要有两个目的
- 安全性。如果您通过互联网下载了一个文件,对该文件执行哈希操作,并验证计算出的摘要是否与互联网上提供的摘要匹配,那么您就可以确信您刚刚下载的文件是真实的,即未被篡改。有人可能会争辩说,从同一网站获取文件和摘要列表并不是特别安全,因为篡改文件的攻击者很可能也能修改摘要列表。具有安全连接(HTTPS)的网站和来自邮件列表公告的 PGP 签名电子邮件是获取摘要列表的好地方。
- 完整性。哈希值有助于检测错误,因为对原始文件的任何微小修改都会产生完全不同的摘要。但是,仅凭摘要无法确切检测到发生了什么变化,因此正确的做法是丢弃下载的文件并重新开始下载。
这就是一个校验和列表的样子
另一个例子 - 下载操作系统
也许您已经决定验证您进行的每一次下载都有点过度。如果确实如此,没问题,这由您决定。但对于非常大的文件,或者非常重要的文件呢?
这是Ubuntu Vivid Vervet的下载页面的截图。除了各种文件格式的下载选项外,总会有一个文件摘要列表。
请注意,他们还提供SHA-256校验和,这符合建议新开发应使用现代哈希算法的规定。
通过本地网络或外部媒体传输文件
如果您通过本地网络(可能是公司的内网)传输过文件,您可能已经熟悉以下消息
这里的第一个建议是:不要在网络上复制粘贴文件。拜托,别这样。我见过几种情况,由于传输错误,文件从一台机器上被复制走了,而在另一端却根本没有出现。
我建议您将文件复制到远程计算机,传输完成后,在本地和远程计算机上都对文件进行哈希计算。如果摘要匹配,您就可以安全地删除原始文件了。在我们公司,我们不能冒丢失重要文件的风险。
同样的逻辑也适用于您的个人文件。外部硬盘对于备份您的音乐、照片、视频等非常有用,但可能会发生故障,尤其是当我们谈论 USB 供电设备时。因此,当您不确定传输是否成功完成时,请谨慎行事,并验证摘要。
实际示例
动手实践一下:例如,让我们下载HxD,一个非常好的十六进制编辑器,并验证它的摘要。我们将使用免费的#ashing,它在 Windows 上有一个简单的图形用户界面。
这是我们在 HxD 的下载页面上找到的内容
下载文件后,打开您的Downloads文件夹并找到HxDSetupEN.zip。
然后,打开#ashing并将文件拖放到程序窗口中。或者,通过图形界面浏览文件。
单击 SHA-1 按钮执行哈希操作
点击“Verify”,然后复制 HxD 网站上找到的摘要,并粘贴到新出现的对话框中。请注意不要复制摘要末尾的额外空格。
如果一切顺利,您将看到一个确认摘要匹配的消息
证明 SHA-1 碰撞
SHAttered 攻击在此处进行了说明。正如您所见,有两个 PDF 文件可用,在外观和内容上非常相似,唯一的区别是背景颜色
现在,让我们看看当我们使用不同的加密哈希函数计算这两个文件的摘要时会发生什么。我打开了两个#ashing应用程序实例并计算了摘要
您可以看到,两个文件的 SHA-1 摘要完全相同。因此,如果两个不同的(无关紧要它们是否相似)文件具有相同的摘要,这就在实践中证明了该算法已损坏。
然而,有趣的是,这两个文件代表 SHA-1 的碰撞并不意味着它们也会代表较不安全(MD5)或更安全(SHA-256、SHA-512)算法的碰撞。
哈希在区块链中的应用
自区块链技术变得流行以来,“哈希”一词已成为一个热门词汇。加密哈希确实在区块链实现中有许多用途。在本节中,我们将分析一些哈希在比特币网络中的使用示例。
区块哈希
如果您不熟悉区块链的概念,请查看路透社的这个可视化指南。我想让您理解的主要观点是
- 比特币交易,它将任意金额从一个加密密钥的持有者转移到另一个持有者,可以被视为一条记录;
- 一个区块由一系列记录组成,平均每十分钟创建一个;
- 当一个区块满了时,它的内容将与前一个区块的哈希一起被哈希,从而创建一个链。
区块的哈希值可以用来引用区块本身,正如我们在下面的图片中可以看到的,这张图片来自一个区块浏览器:
为了更详细的解释,我强烈推荐《精通比特币》这本书,特别是第 6 章。
比特币地址
抽象
比特币地址实际上是一种抽象。简单来说,将币发送到比特币地址的行为,在底层意味着,如果您拥有一个特定的私钥,您就允许某人花费特定数量的币。
有不同类型的比特币地址,但最常见的一种是P2PKH。顺着这个思路,比特币交易包括使用您的私钥将一定数量的币绑定到公钥。谁拥有相应的私钥,谁就能花费这些币。
为什么创建地址而不是直接使用公钥?除了有利于抽象,因为并非所有比特币用户都期望了解密码学,我们还可以提到几点
- 地址更短,而比特币费用基于交易大小。
- 通过使用Base58Check 编码,地址包含一个校验和,有助于防止将资金转移给错误的人,并且它们不包含看起来相似的字符(如大写字母
o
和数字零,大写字母i
和数字一)。请记住,有些用户会将他们的密钥写在纸上。
创建地址
比特币地址创建的描述可以在此处详细查看,但简化来说,它是
address = BASE58Check( RIPEMD160( SHA256(PUBLIC_KEY)) )
其中RIPEMD160
和SHA-256
是加密函数。RIPEMD160
的输出是 20 字节。最终的比特币地址通常为 34 或 35 字节长,这比 65 字节长的常规公钥要短得多。
工作量证明
工作量证明 是一种机制,通过要求服务请求者付出一定的劳动来阻止拒绝服务攻击和其他滥用行为,如 SPAM。根据设计,比特币区块链中的区块平均每几分钟创建一个。为了让区块被网络参与者接受,矿工必须完成一个工作量证明。
猜猜怎么着?比特币的工作量证明包括找到一个以任意数量零开头的哈希值。操作方法如下
- 获取区块的哈希值(区块中所有交易的哈希)。
- 在末尾附加一个整数值,称为nonce,从 0 开始。
- 对这个组合执行 SHA-256。
- 如果结果不符合规则,则增加 nonce 并重新计算。
难度决定了哈希值必须有多小;换句话说,它必须以多少个零开头。
可用工具
在Linux(以及许多其他类 Unix 操作系统)上,默认安装了一组执行加密哈希的程序:md5sum
、sha1sum
、sha224sum
、sha256sum
、sha384sum
、sha512sum
。
在Windows上,没有内置的校验和工具。您可以安装 OpenSSL进行终端操作,或者选择一个图形化工具。除了#ashing,还有许多其他计算哈希的选项。Gizmo's freeware有一个详细列表,列出了每种工具的优缺点。
在OS X上,终端中内置了shasum命令。例如,要生成文件的 SHA-512 哈希
shasum -a 512 /path/to/file
请在评论中告诉我您最喜欢的工具,以及为什么。
代码片段(C++)
使用 OpenSSL
优点:开源、跨平台、功能齐全的工具包。
缺点:必须确保 OpenSSL 的加密库在目标计算机上可用,否则程序将无法运行。
如上所述,您需要将程序链接到加密库。
许多Linux发行版通过包管理器(RPM、aptitude、YaST)提供了 OpenSSL 的便捷安装。安装后,只需在程序的 makefile 中添加“-lcrypto
”即可。
可以在此处找到Windows的安装程序。Windows 的加密库以前名为libeay32.dll,自 1.1.0 版本以来,OpenSSL 已将该库名称更改为libcrypto.dll。
首先,包含必要的头文件(这里有一些关于SHA 函数和MD5的文档),并声明一个函数来在屏幕上打印摘要
#include <stdio.h>
#include <string.h>
#include <openssl/sha.h>
#include <openssl/md5.h>
void printDigest(unsigned char* auchDigest, int iDigestSize)
{
for (int i = 0; i < iDigestSize; i++ )
{
printf("%02X", auchDigest[i]);
}
printf("\n");
}
当您已知工作数据的尺寸时,这非常简单
void HashFromMemory( unsigned char* auchBufferToHash, int iBufferLen)
{
unsigned char auchDigest[64] = {0};
MD5(auchBufferToHash, iBufferLen, auchDigest);
printDigest(auchDigest, 16);
SHA1(auchBufferToHash, iBufferLen, auchDigest);
printDigest(auchDigest, 20);
SHA224(auchBufferToHash, iBufferLen, auchDigest);
printDigest(auchDigest, 28);
SHA256(auchBufferToHash, iBufferLen, auchDigest);
printDigest(auchDigest, 32);
SHA384(auchBufferToHash, iBufferLen, auchDigest);
printDigest(auchDigest, 48);
SHA512(auchBufferToHash, iBufferLen, auchDigest);
printDigest(auchDigest, 64);
}
当您不知道尺寸,或者输入可能非常大时,一个好的方法是分块读取数据
void HashFromFile(const char* szFileToHash)
{
FILE* fp = NULL;
fp = fopen( szFileToHash, "rb");
if ( NULL == fp )
{
printf("Error opening file\n");
return;
}
// Retrieve the size of the file
fseek(fp, 0, SEEK_END);
long lSize = ftell(fp);
rewind(fp);
unsigned char btWorkBuffer[1024] = {0}; // read file in 1024-byte chunks
unsigned char auchDigest[64] = {0};
// Hash contexts
MD5_CTX ctx_md5;
SHA_CTX ctx_sha1;
SHA256_CTX ctx_sha224;
SHA256_CTX ctx_sha256;
SHA512_CTX ctx_sha384;
SHA512_CTX ctx_sha512;
if ( 1 != MD5_Init(&ctx_md5) || 1 != SHA1_Init(&ctx_sha1)
|| 1 != SHA224_Init(&ctx_sha224) ||
1 != SHA256_Init(&ctx_sha256) || 1 != SHA384_Init(&ctx_sha384)
|| 1 != SHA512_Init(&ctx_sha512) )
{
// Unexpected error
printf("Error initializing at least one context\n");
return;
}
// Update the hash
int iRead = 0;
while ( iRead = fread(btWorkBuffer, 1, 1024, fp) )
{
if ( 1 != MD5_Update(&ctx_md5, btWorkBuffer, iRead)
|| 1 != SHA1_Update(&ctx_sha1, btWorkBuffer, iRead) ||
1 != SHA224_Update(&ctx_sha224, btWorkBuffer, iRead)
|| 1 != SHA256_Update(&ctx_sha256, btWorkBuffer, iRead) ||
1 != SHA384_Update(&ctx_sha384, btWorkBuffer, iRead)
|| 1 != SHA512_Update(&ctx_sha512, btWorkBuffer, iRead) )
{
// Unexpected error
printf("Error updating at least one hash\n");
return;
}
}
// Finalize the hash
if ( 1 == MD5_Final(auchDigest, &ctx_md5) )
{
printf("MD5:"); printDigest(auchDigest, 16);
}
if ( 1 == SHA1_Final(auchDigest, &ctx_sha1) )
{
printf("SHA1:"); printDigest(auchDigest, 20);
}
if ( 1 == SHA224_Final(auchDigest, &ctx_sha224) )
{
printf("SHA224:"); printDigest(auchDigest, 28);
}
if ( 1 == SHA256_Final(auchDigest, &ctx_sha256) )
{
printf("SHA256:"); printDigest(auchDigest, 32);
}
if ( 1 == SHA384_Final(auchDigest, &ctx_sha384) )
{
printf("SHA384:"); printDigest(auchDigest, 48);
}
if ( 1 == SHA512_Final(auchDigest, &ctx_sha512) )
{
printf("SHA512:"); printDigest(auchDigest, 64);
}
}
调用方式如下
int main(int argc, char* argv[])
{
// Hash the contents of a buffer in memory
unsigned char myBuffer[10] = {0}; // buffer with 10 NULL bytes (0x00)
HashFromMemory(myBuffer, sizeof(myBuffer));
// Hash the contents of a file
HashFromFile("data.txt");
return 0;
}
还有一个更高级的方法,不仅用于执行哈希操作,也用于密码学,它使用一种称为 BIO 的输入输出抽象。这是通过这种方式计算 SHA-1 的一个示例。