图-1
在做了两期加解密系列之后,本章让我们换个话题——“哈希”。
先从“信息技术世界的需求”谈起。
- 当接收方收到一份来自发送方的文件后,如何快速验证文件内容没有丢失、乱序或者被篡改?
- 现实世界里海量的文献如何能被映射成简短而唯一的“索引”,从而方便快速归档和查找?
- 如何做到不把明文密码存储到第三方验证机构,但又可以在第三方安全地验证密码?
- 对于一份电子合同文本来说,如何产生一个消息摘要来确保消息的完整性和真实性?
加解密的方式显然无法满足以上这些需求,这就需要数据安全领域的另一大概念“哈希”出场了。在谈到哈希算法时,书本上给出的定义通常会包括,哈希算法是一种将输入数据转换为固定长度的数字串(哈希值)的数学函数。哈希值是唯一的,即使输入数据微小地变化,哈希值也会截然不同。哈希运算本身不被视为一种加密(Encryption)操作,而是一种散列(Hashing)操作【图-1】。虽然哈希和加密都涉及数据转换,但它们有本质上不同的目标和特性:
图-2
哈希(Hashing):
- 目标:哈希算法的主要目标是将输入数据(原文)转换为一个固定长度的哈希值(散列值)【图-2】。
- 特性:哈希算法是单向的,不可逆的,即无法从哈希值还原出原始数据。它旨在生成唯一的哈希值,以便验证数据完整性、生成数据的唯一标识符以及用于快速数据检索等。
- 典型用途:数据完整性验证、数字签名、密码存储、数据索引等。
加密(Encryption):
- 目标:加密算法的主要目标是将原始数据(明文)转换为经过加密的数据(密文),以便保护数据的机密性,只有授权用户可以解密和访问数据。
- 特性:加密算法是可逆的,只有掌握正确的密钥才能解密密文还原为明文。它的目标是提供保密性和隐私保护。
- 典型用途:数据传输加密、文件加密、通信加密、身份验证等。
通常,这两种技术可以结合使用,以提供更全面的数据安全性,例如,首先对数据进行哈希以验证完整性,然后对数据进行加密以保护隐私和保密性。
四种不同哈希算法实例及讲解:
int main()
{
BYTE* pData = NULL;
DWORD dwDataLength = 0;
DWORD i = 0;
BYTE* pHashData = NULL;
DWORD dwHashDataLength = 0;
// Read target file
GetFileData("..\\readMe.txt", &pData, &dwDataLength);
// MD5
CalculateHash(pData, dwDataLength, CALG_MD5, &pHashData, &dwHashDataLength);
printf("MD5[%d]\n", dwHashDataLength);
for (i = 0; i < dwHashDataLength; i++)
{
printf("%x", pHashData[i]);
}
printf("\n\n", dwHashDataLength);
if (pHashData)
{
delete[]pHashData;
pHashData = NULL;
}
// SHA1 and printout omitted here
CalculateHash(pData, dwDataLength, CALG_SHA1, &pHashData, &dwHashDataLength);
// SHA256 and printout omitted here
CalculateHash(pData, dwDataLength, CALG_SHA_256, &pHashData, &dwHashDataLength);
// SHA512 and printout omitted here
CalculateHash(pData, dwDataLength, CALG_SHA_512, &pHashData, &dwHashDataLength);
system("pause");
return 0;
}
【讲解】以上 main 函数首先对目标文件内容进行读取,然后用封装了的计算哈希函数来进行哈希运算。 封装了的函数有5个参数,分别是目标数据的缓冲区指针和内容长度,输出哈希后的数据指针及其长度,以及将要采用的哈希算法。本例实验了如下四种哈希算法,具体参数为CALG_MD5、CALG_SHA1、CALG_SHA_256 和 CALG_SHA_512:
1. MD5(Message Digest Algorithm 5):
- 优点:计算速度较快,产生128位(16字节)的哈希值。
- 缺点:安全性较差,容易受到碰撞即找到两个不同的输入,它们产生相同的哈希值)。
2. SHA-1(Secure Hash Algorithm 1):
- 优点:较快,广泛使用,产生160位(20字节)的哈希值。
- 缺点:安全性逐渐降低,容易受到碰撞,不再被推荐用于安全应用。
3. SHA-256(Secure Hash Algorithm 256位):
- 优点:较高的安全性,较长的哈希值256位(32字节),广泛用于数字签名和数据完整性验证。
- 缺点:计算速度相对较慢。
4. SHA-512(Secure Hash Algorithm 512位):
- 优点:更长的哈希值512位(64字节),更高的安全性。
- 缺点:计算速度相对较慢,对于一般应用可能过于冗长。
图-3 程序执行后的四种不同哈希算法的输出
BOOL CalculateHash(BYTE* pData, DWORD dwDataLength, ALG_ID algHashType, BYTE** ppHashData, DWORD* pdwHashDataLength)
{
HCRYPTPROV hCryptProv = NULL;
HCRYPTHASH hCryptHash = NULL;
BYTE* pHashData = NULL;
DWORD dwHashDataLength = 0;
DWORD dwTemp = 0;
BOOL bRet = FALSE;
do
{
// Gets the handle to the key container of the specified CSP
bRet = ::CryptAcquireContext(&hCryptProv, NULL, NULL, PROV_RSA_AES, CRYPT_VERIFYCONTEXT);
if (FALSE == bRet)
{
ShowError("CryptAcquireContext");
break;
}
// Create a HASH object and specify the HASH algorithm
bRet = ::CryptCreateHash(hCryptProv, algHashType, NULL, NULL, &hCryptHash);
if (FALSE == bRet)
{
ShowError("CryptCreateHash");
break;
}
// Calculate HASH data
bRet = ::CryptHashData(hCryptHash, pData, dwDataLength, 0);
if (FALSE == bRet)
{
ShowError("CryptHashData");
break;
}
// Get the size of the HASH result
dwTemp = sizeof(dwHashDataLength);
bRet = ::CryptGetHashParam(hCryptHash, HP_HASHSIZE, (BYTE*)(&dwHashDataLength), &dwTemp, 0);
if (FALSE == bRet)
{
ShowError("CryptGetHashParam");
break;
}
// Apply for memory
pHashData = new BYTE[dwHashDataLength];
if (NULL == pHashData)
{
bRet = FALSE;
ShowError("new");
break;
}
::RtlZeroMemory(pHashData, dwHashDataLength);
// Get HASH result data
bRet = ::CryptGetHashParam(hCryptHash, HP_HASHVAL, pHashData, &dwHashDataLength, 0);
if (FALSE == bRet)
{
ShowError("CryptGetHashParam");
break;
}
*ppHashData = pHashData;
*pdwHashDataLength = dwHashDataLength;
} while (FALSE);
if (FALSE == bRet)
{
if (pHashData)
{
delete[]pHashData;
pHashData = NULL;
}
}
if (hCryptHash)
{
::CryptDestroyHash(hCryptHash);
}
if (hCryptProv)
{
::CryptReleaseContext(hCryptProv, 0);
}
return bRet;
}
【讲解】Wincrypt APIs 哈希算法的程序与加解密类似,也是按照 CryptAcquireContext(创建 CSP)、CryptCreateHash(创建哈希对象)和 CryptHashData(计算哈希)的顺序来调用不同的接口。如果顺利的话,再两次调用 CryptGetHashParam,分别通过指定 HP_HASHSIZE 和 HP_HASHVAL,来得到哈希后数据的长度,然后申请对应缓冲区大小,最后塞入哈希后的数据。【图-4】展示的是 MD5 算法下,哈希后数据的长度(16)和内容(内存里的哈希数据与控制台输出【图-3】的完全一致)
图-4
实验环节:
- 测试两台电脑分别对同样的原文运行程序后是否有一样的输出,结果是得到完全一样的输出。如【图-5】 图-5
- 原文中有4KB大小,仅仅篡改了一个标点符号后的哈希结果区别【图-6】。这就是开篇引用的定义中所提及的“哈希值是唯一的,即使输入数据微小地变化,哈希值也会截然不同”。 图-6
- 对于同一句话内容,C++程序与网页版的哈希计算相比较【图-7】。可以看到只要采用相同的哈希算法,不管在任何平台,用任何语言来实现,一样的输入数据都应该能得到一样的哈希后的输出数据。
图-7
写在最后的总结:
在完整调试了一个哈希算法实例之后,此刻是在大脑里复盘哈希算法的好时机,这有助于我们深刻理解哈希的概念和用途,而以下内容刚好与文章开头所提“信息技术世界的需求”相呼应。
- 数据完整性验证、文件完整性检查:哈希值充当数据的唯一摘要,它们是数据内容的固定长度表示。当数据传输或存储时,接收方可以重新计算哈希值,并与发送方提供的哈希值进行比较。如果哈希值匹配,说明数据在传输或存储过程中没有被篡改,从而验证了数据的完整性。
- 数据索引和检索的唯一标识:哈希值可用于加速数据检索过程。数据库和文件系统中,对大段内容常常使用哈希索引,以快速查找和访问数据。
- 密码存储:哈希值常用于存储用户密码。而不是将密码明文存储在数据库中,通常会存储其哈希值。当用户尝试登录时,系统将用户提供的密码进行哈希,然后与存储的哈希值进行比较。
- 数字签名:哈希值常用于数字签名过程中。发送方可以使用私钥对数据的哈希值进行签名,接收方可以使用发送方的公钥来验证签名。这确保了数据的真实性和来源。