CryptoApi/README.md

929 lines
35 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 背景
Windows提供了一组CryptoAPI函数来对用户的敏感私钥数据提供保护并以灵活的方式对数据进行加密或数字签名。其中实际的加密操作是由加密服务提供程序CSP的独立模块执行。
因为过于复杂的加密算法实现起来非常困难所以在过去许多应用程序只能使用非常简单的加密技术这样做的结果就是加密的数据很容易就可以被人破译。而使用Windows提供的CryptoAPI就可以方便地在应用程序中加入强大的加密功能而不必考虑基本的算法。
本文接下来将介绍使用CryptoAPI计算HASH值、AES加解密以及RSA加解密。
# 9.1.1 HASH值计算
HASH就是把任意长度的输入通过HASH算法变换成固定长度的输出该输出就是HASH值。HASH值的空间通常远小于输入的空间不同的输入可能会得到相同的输出所以不可能从HASH值来确定唯一的输入值。基于这种特性HASH值常用来做数据的完整性校验。
## 函数介绍
### CryptAcquireContext函数
> 用于获取特定加密服务提供者CSP内特定密钥容器的句柄此返回的句柄用于调用使用选定CSP的CryptoAPI函数。
>
> ```c++
> BOOL WINAPI CryptAcquireContext(
> _Out_ HCRYPTPROV *phProv,
> _In_ LPCTSTR pszContainer,
> _In_ LPCTSTR pszProvider,
> _In_ DWORD dwProvType,
> _In_ DWORD dwFlags
> );
> ```
>
> 参数
>
> phProv [out]
> 指向CSP句柄的指针。当完成CSP使用时通过调用CryptReleaseContext函数释放句柄。
> pszContainer [in]
> 密钥容器名称。这是一个以空字符结尾的字符串用于标识CSP的密钥容器。在大多数情况下当dwFlags设置为CRYPT_VERIFYCONTEXT时必须将pszContainer设置为NULL。
> pszProvider [in]
> 包含要使用的CSP名称的空终止字符串。如果此参数为NULL则使用用户默认提供程序。
> dwProvType [in]
> 指定要获取的提供程序的类型。PROV_RSA_AES支持RSA签名算法、AES加密算法以及HASH算法。
> dwFlags [in]
> 标志值。此参数通常设置为0但某些应用程序设置了一个或多个以下标志。
>
> | VALUE | MEANING |
> | -------------------- | ---------------------------------------- |
> | CRYPT_VERIFYCONTEXT | 指出应用程序不需要使用公钥/私钥对。例如,程序只执行哈希和对称加密。 |
> | CRYPT_NEWKEYSET | 用pszContainer指定的名称创建一个新的密钥容器。如果pszContainer为NULL则创建一个具有默认名称的密钥容器。 |
> | CRYPT_MACHINE_KEYSET | 由此标志创建的密钥容器只能由创建者本人或有系统管理员身份的人使用。 |
> | CRYPT_DELETEKEYSET | 删除由pszContainer指定的密钥容器。如果pszContainer 为NULL缺省名称的容器就会被删除。此容器里的所有密钥对也会被删除。 |
> | CRYPT_SILENT | 应用程序要求CSP不显示任何用户界面。 |
>
> 返回值
>
> 如果函数成功函数返回TRUE。
> 如果该功能失败则返回FALSE。有关扩展错误信息请调用GetLastError。
### CryptCreateHash函数
> 创建一个空HASH对象。
>
> ```c++
> BOOL WINAPI CryptCreateHash(
> _In_ HCRYPTPROV hProv,
> _In_ ALG_ID Algid,
> _In_ HCRYPTKEY hKey,
> _In_ DWORD dwFlags,
> _Out_ HCRYPTHASH *phHash
> );
> ```
>
> 参数
>
> hProv [in]
> 通过调用CryptAcquireContext创建的CSP句柄。
> Algid [in]
> 标识要使用的散列算法的ALG_ID值。CALG_MD5表示MD5哈希算法CALG_SHA1表示SHA1哈希算法CALG_SHA256表示SHA256哈希算法。
> hKey [in]
> 对于非键控算法该参数必须设置为0。
> dwFlags [in]
> 通常置为0。
>
> phHash [out]
> 函数将句柄复制到新哈希对象的地址。当完成使用哈希对象时通过调用CryptDestroyHash函数释放句柄。
>
> 返回值
>
> 如果函数成功函数返回TRUE。
> 如果该功能失败则返回FALSE。有关扩展错误信息请调用GetLastError。
### CryptHashData函数
> 将数据添加到HASH对象并进行HASH计算。
>
> ```c++
> BOOL WINAPI CryptHashData(
> _In_ HCRYPTHASH hHash,
> _In_ BYTE *pbData,
> _In_ DWORD dwDataLen,
> _In_ DWORD dwFlags
> );
> ```
>
> 参数
>
> hHash [in]
> 哈希对象的句柄。
> pbData [in]
> 指向包含要添加到散列对象的数据的缓冲区的指针。
> dwDataLen [in]
> 要添加的数据的字节数。如果设置了CRYPT_USERDATA标志则该值必须为零。
> dwFlags [in]
> 通常被置为0。
>
> 返回值
>
> 如果函数成功函数返回TRUE。
> 如果该功能失败则返回FALSE。有关扩展错误信息请调用GetLastError。
### CryptGetHashParam函数
> 从HASH对象中获取指定参数值。
>
> ```c++
> BOOL WINAPI CryptGetHashParam(
> _In_ HCRYPTHASH hHash,
> _In_ DWORD dwParam,
> _Out_ BYTE *pbData,
> _Inout_ DWORD *pdwDataLen,
> _In_ DWORD dwFlags
> );
> ```
>
> 参数
>
> hHash [in]
> 要查询的哈希对象的句柄。
> dwParam [in]
> 查询类型。 该参数可以设置为以下查询之一。
>
> | VALUE | MEANING |
> | ----------- | ---------------------------------------- |
> | HP_ALGID | 参数类型为ALG_ID获取用于指示创建哈希对象时指定的算法。 |
> | HP_HASHSIZE | 参数类型为DWORD获取散列值中的字节数, 该值将根据散列算法而变化。应用程序必须在HP_HASHVAL值之前检索此值以便分配正确的内存量。 |
> | HP_HASHVAL | 获取HASH值。 |
>
> pbData [out]
> 指向接收指定值数据的缓冲区的指针。这些数据的形式根据数值的不同而不同。 此参数可以为NULL以确定所需的内存大小。
> pdwDataLen [in, out]
> 指向指定pbData缓冲区大小以字节为单位的DWORD值的指针。当函数返回时DWORD值包含存储在缓冲区中的字节数。 如果pbData为NULL则将pdwDataLen的值设置为0。
>
> dwFlags [in]
> 保留值必须为0。
>
> 返回值
>
> 如果函数成功函数返回TRUE。
> 如果该功能失败则返回FALSE。有关扩展错误信息请调用GetLastError。
## 实现过程
由上述的函数介绍可以大致了解HASH值的计算流程只要依次调用上述函数即可。那么使用CryptoAPI函数实现HASH值计算的具体实现流程如下所示。
首先任何程序在使用CryptoAPI函数来对数据计算或是加解密之前都需要先调用CryptAcquireContext函数来获取加密服务提供者CSP的句柄。由于本程序实现的是计算数据的HASH值所以将提供者类型设置为PROV_RSA_AES该类型支持常用的HASH算法。并将标志设置为CRYPT_VERIFYCONTEXT不需要使用公私密钥对。该函数首先尝试查找具有dwProvType和pszProvider参数中描述的特征的CSP。如果找到CSP则函数将尝试在CSP中查找与pszContainer参数指定的名称相匹配的密钥容器。通过dwFlags的适当设置此功能还可以创建和销毁密钥容器并且可以在不需要访问私钥时使用临时密钥容器提供对CSP的访问。当不在使用CSP的时候可以通过调用CryptReleaseContext函数释放CSP句柄。
然后就可以调用CryptCreateHash函数在CSP中创建一个空的HASH对象并获取对象句柄可以指定HASH对象使用的HASH算法例如CALG_MD5表示MD5哈希算法CALG_SHA1表示SHA1哈希算法CALG_SHA256表示SHA256哈希算法。当不再使用HASH对象时可以通过调用CryptDestroyHash函数释放HASH对象句柄。
接着就可以继续调用CryptHashData函数来添加数据并按照指定的HASH算法计算数据的HASH值结果存放在HASH对象中。
最后调用CryptGetHashParam函数从HASH对象中获取指定参数。可以获取的参数有3个分别是HP_ALGID、HP_HASHSIZE和HP_HASHVAL。HP_ALGID表示获取HASH算法HP_HASHSIZE表示获取HASH值的数据长度HP_HASHVAL表示获取HASH值。由于不同HASH算法HASH值大小并不是固定。所以在获取HASH值HP_HASHVAL参数之前需要先获取HASH值大小HP_HASHSIZE参数已申请足够的缓冲区存放HASH值。
那么使用CryptoAPI函数计算数据HASH值的具体实现代码如下所示。
```c++
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
{
// 获得指定CSP的密钥容器的句柄
bRet = ::CryptAcquireContext(&hCryptProv, NULL, NULL, PROV_RSA_AES, CRYPT_VERIFYCONTEXT);
if (FALSE == bRet)
{
ShowError("CryptAcquireContext");
break;
}
// 创建一个HASH对象, 指定HASH算法
bRet = ::CryptCreateHash(hCryptProv, algHashType, NULL, NULL, &hCryptHash);
if (FALSE == bRet)
{
ShowError("CryptCreateHash");
break;
}
// 计算HASH数据
bRet = ::CryptHashData(hCryptHash, pData, dwDataLength, 0);
if (FALSE == bRet)
{
ShowError("CryptHashData");
break;
}
// 获取HASH结果的大小
dwTemp = sizeof(dwHashDataLength);
bRet = ::CryptGetHashParam(hCryptHash, HP_HASHSIZE, (BYTE *)(&dwHashDataLength), &dwTemp, 0);
if (FALSE == bRet)
{
ShowError("CryptGetHashParam");
break;
}
// 申请内存
pHashData = new BYTE[dwHashDataLength];
if (NULL == pHashData)
{
bRet = FALSE;
ShowError("new");
break;
}
::RtlZeroMemory(pHashData, dwHashDataLength);
// 获取HASH结果数据
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;
}
```
## 测试
直接运行上述程序获取520.exe文件的所有数据对该文件数据计算HASH值。计算MD5、SHA1、SHA256计算结果如图9-1所示。
![1](demonganpaperimage/1006/1.jpg)
# 9.1.2 AES加解密
AES高级加密标准为最常见的对称加密算法所谓对称加密算法也就是加密和解密使用相同的密钥的加密算法。AES为分组密码分组密码也就是把明文分成一组一组的每组长度相等每次加密一组数据直到加密完整个明文。AES对称加密算法的优势在于算法公开计算量小加密效率高。
### 函数介绍
#### CryptDeriveKey函数
> CryptDeriveKey函数生成从基础数据值派生的加密会话密钥。此功能可确保在使用相同的加密服务提供商CSP和算法时从相同基础数据生成的密钥是相同的。基础数据可以是密码或任何其他用户数据。
>
> ```c++
> BOOL WINAPI CryptDeriveKey(
> _In_ HCRYPTPROV hProv,
> _In_ ALG_ID Algid,
> _In_ HCRYPTHASH hBaseData,
> _In_ DWORD dwFlags,
> _Inout_ HCRYPTKEY *phKey
> );
> ```
>
> 参数
>
> hProv [in]
> 通过调用CryptAcquireContext创建的CSP的HCRYPTPROV句柄。
> Algid [in]
> 标识要为其生成密钥的对称加密算法的ALG_ID结构。CALG_AES_128表示128位AES对称加密算法CALG_DES表示DES对称加密算法。
> hBaseData [in]
> 哈希对象的句柄,它提供了确切的基础数据。
> dwFlags [in]
> 指定生成的密钥的类型。
>
> | VALUE | MEANING |
> | ----------------- | ---------------------------------------- |
> | CRYPT_CREATE_SALT | 由哈希值产生一个会话密钥,有一些需要补位。如果用此标志,密钥将会赋予一个盐值。 |
> | CRYPT_EXPORTABLE | 如果置此标志密钥就可以用CryptExportKey函数导出。 |
> | CRYPT_NO_SALT | 如果置此标志表示40位的密钥不需要分配盐值。 |
> | CRYPT_UPDATE_KEY | 有些CSP从多个哈希值中派生会话密钥。如果这种情况CryptDeriveKey需要多次调用。 |
>
> phKey [in, out]
> 指向HCRYPTKEY变量的指针用于接收新生成的密钥的句柄地址。完成密钥的使用后通过调用CryptDestroyKey函数来释放句柄。
>
> 返回值
>
> 如果函数成功函数返回TRUE。
> 如果该功能失败则返回FALSE。有关扩展错误信息请调用GetLastError。
#### CryptEncrypt函数
> 由CSP模块保存的密钥指定的加密算法来加密数据。
>
> ```c++
> BOOL WINAPI CryptEncrypt(
> _In_ HCRYPTKEY hKey,
> _In_ HCRYPTHASH hHash,
> _In_ BOOL Final,
> _In_ DWORD dwFlags,
> _Inout_ BYTE *pbData,
> _Inout_ DWORD *pdwDataLen,
> _In_ DWORD dwBufLen
> );
> ```
>
> 参数
>
> hKey [in]
> 加密密钥的句柄。密钥指定使用的加密算法。
> hHash [in]
> 哈希对象的句柄。如果不进行散列则此参数必须为NULL。
> Final[in]
> 一个布尔值指定它是否是正在加密的系列中的最后一部分。对于最后一个或唯一的块Final被设置为TRUE如果有更多的块被加密Final被设置为FALSE。
> dwFlags [in]
> 保留置为0。
>
> pbData [in, out]
> 指向包含要加密的明文的缓冲区的指针。该缓冲区中的纯文本被该函数创建的密文覆盖。
> 如果此参数包含NULL则此函数将计算密文所需的大小并将其放在pdwDataLen参数指向的值中。
> pdwDataLen [in, out]
> 指向DWORD值的指针该值在入口处包含pbData缓冲区中明文的长度以字节为单位。退出时此DWORD包含写入到pbData缓冲区的密文的长度以字节为单位
> 使用分组密码时该数据长度必须是块大小的倍数除非这是要加密的最后一部分数据并且Final参数为TRUE。
> dwBufLen [in]
> 指定输入pbData缓冲区的总大小以字节为单位
> 请注意根据使用的算法加密文本可能会大于原始明文。在这种情况下pbData缓冲区需要足够大以包含加密文本和任何填充。通常如果使用流密码密文的大小与明文的大小相同。如果使用分组密码则密文长度大于明文的分组长度。
>
> 返回值
>
> 如果函数成功函数返回TRUE。
> 如果该功能失败则返回FALSE。有关扩展错误信息请调用GetLastError。
####CryptDecrypt函数
> 解密先前使用CryptEncrypt函数进行加密的数据。
>
> ```c++
> BOOL WINAPI CryptDecrypt(
> _In_ HCRYPTKEY hKey,
> _In_ HCRYPTHASH hHash,
> _In_ BOOL Final,
> _In_ DWORD dwFlags,
> _Inout_ BYTE *pbData,
> _Inout_ DWORD *pdwDataLen
> );
> ```
>
> 参数
>
> hKey[in]]
> 密钥的句柄,用于解密。该密钥指定要使用的解密算法。
> hHash [in]
> 哈希对象的句柄。如果不进行散列则该参数必须为NULL。
> Final[in]
> 指定这是否是正在解密的系列中的最后一部分。如果这是最后一个或唯一的块则该值为TRUE。如果这不是最后一个块则该值为FALSE。
> dwFlags [in]
> 置为0。
>
> pbData [in, out]
> 指向包含要解密的数据的缓冲区的指针。解密完成后,明文被放回到同一个缓冲区。
> pdwDataLen [in, out]
> 指向指示pbData缓冲区长度的DWORD值的指针。在调用此函数之前调用应用程序将DWORD值设置为要解密的字节数。 返回时DWORD值包含解密明文的字节数。
> 使用分组密码时该数据长度必须是块大小的倍数除非这是要解密的最后一部分数据并且Final参数为TRUE。
>
> 返回值
>
> 如果函数成功函数返回TRUE。
> 如果该功能失败则返回FALSE。有关扩展错误信息请调用GetLastError。
### 实现过程
AES为分组密码分组密码也就是把明文分成一组一组的每组长度相等每次加密一组数据直到加密完整个明文。在AES标准规范中分组长度只能是128位也就是说每个分组为16个字节每个字节8位。密钥的长度可以使用128位、192位或256位。
那么使用CryptoAPI函数对数据进行128位AES加解密的具体实现流程如下所示。
首先任何程序在使用CryptoAPI函数来对数据计算或是加解密之前都需要先调用CryptAcquireContext函数来获取加密服务提供者CSP的句柄。由于本程序实现的是使用AES对称加密算法加解密数据所以将提供者类型设置为PROV_RSA_AES该类型支持AES算法。并将标志设置为CRYPT_VERIFYCONTEXT不需要使用公私密钥对。当不在使用CSP的时候可以通过调用CryptReleaseContext函数释放CSP句柄。
本文的程序并不直接使用明文密钥作为AES的加密密码而是把明文密钥的MD5值作为基础密钥通过调用CryptDeriveKey函数来派生出AES的加密密钥。所以在调用CryptDeriveKey函数派生密钥之前先对明文密码进行HASH计算。先调用CryptCreateHash函数创建一个空的HASH对象获取HASH对象句柄并设置HASH对象的HASH算法为CALG_MD5。然后调用CryptHashData函数添加数据即明文密钥并使用指定的HASH算法计算数据HASH值并将计算结果存储在HASH对象中。当不在使用HASH对象的时候可以通过调用CryptDestroyHash函数释放对象句柄。
在完成明文密码的MD5值计算后便可以调用CryptDeriveKey函数来派生密钥。指定为CALG_AES_128派生的密钥用于128位AES加密。派生类型为CRYPT_EXPORTABLE表示可以使用CryptExportKey函数获取派生的密钥。在不使用密钥句柄的时候可以调用CryptDestroyKey来释放句柄。
派生密钥完成后便可以调用CryptEncrypt函数来根据派生密钥中指定的加密算法进行加密运算。将参数Final置为TRUE表示该加密是AES加密数据中的最后一组数据这样系统会自动按照分组长度对数据进行填充并计算。其中一定要确保数据缓冲区足够大能够满足加密数据的存放否则程序会出错。
AES加密的具体实现代码如下所示。
```c++
// AES加密
BOOL AesEncrypt(BYTE *pPassword, DWORD dwPasswordLength, BYTE *pData, DWORD &dwDataLength, DWORD dwBufferLength)
{
BOOL bRet = TRUE;
HCRYPTPROV hCryptProv = NULL;
HCRYPTHASH hCryptHash = NULL;
HCRYPTKEY hCryptKey = NULL;
do
{
// 获取CSP句柄
bRet = ::CryptAcquireContext(&hCryptProv, NULL, NULL, PROV_RSA_AES, CRYPT_VERIFYCONTEXT);
if (FALSE == bRet)
{
ShowError("CryptAcquireContext");
break;
}
// 创建HASH对象
bRet = ::CryptCreateHash(hCryptProv, CALG_MD5, NULL, 0, &hCryptHash);
if (FALSE == bRet)
{
ShowError("CryptCreateHash");
break;
}
// 对密钥进行HASH计算
bRet = ::CryptHashData(hCryptHash, pPassword, dwPasswordLength, 0);
if (FALSE == bRet)
{
ShowError("CryptHashData");
break;
}
// 使用HASH来生成密钥
bRet = ::CryptDeriveKey(hCryptProv, CALG_AES_128, hCryptHash, CRYPT_EXPORTABLE, &hCryptKey);
if (FALSE == bRet)
{
ShowError("CryptDeriveKey");
break;
}
// 加密数据
bRet = ::CryptEncrypt(hCryptKey, NULL, TRUE, 0, pData, &dwDataLength, dwBufferLength);
if (FALSE == bRet)
{
ShowError("CryptEncrypt");
break;
}
} while (FALSE);
// 关闭释放
if (hCryptKey)
{
::CryptDestroyKey(hCryptKey);
}
if (hCryptHash)
{
::CryptDestroyHash(hCryptHash);
}
if (hCryptProv)
{
::CryptReleaseContext(hCryptProv, 0);
}
return bRet;
}
```
由于AES是对称加密所以加密解密都是使用同一个密码。为了获取相同的解密密钥需要根据明文密码来计算出派生密钥派生密钥的具体的产生过程与上述加密中的操作相同。
在获取解密密钥后就可以直接调用CryptDecrypt函数来对密文进行解密操作。其中由于加密的时候将参数Final置为TRUE来加密数据的所以在解密的时候也要对应把Final置为TRUE来解密密文。Final置为TRUE表示该数据是AES解密数据中的最后一组数据这样系统会自动按照分组长度对数据进行填充并计算。
AES解密的具体实现代码如下所示。
```c++
// AES解密
BOOL AesDecrypt(BYTE *pPassword, DWORD dwPasswordLength, BYTE *pData, DWORD &dwDataLength, DWORD dwBufferLength)
{
BOOL bRet = TRUE;
HCRYPTPROV hCryptProv = NULL;
HCRYPTHASH hCryptHash = NULL;
HCRYPTKEY hCryptKey = NULL;
do
{
// 获取CSP句柄
bRet = ::CryptAcquireContext(&hCryptProv, NULL, NULL, PROV_RSA_AES, CRYPT_VERIFYCONTEXT);
if (FALSE == bRet)
{
ShowError("CryptAcquireContext");
break;
}
// 创建HASH对象
bRet = ::CryptCreateHash(hCryptProv, CALG_MD5, NULL, 0, &hCryptHash);
if (FALSE == bRet)
{
ShowError("CryptCreateHash");
break;
}
// 对密钥进行HASH计算
bRet = ::CryptHashData(hCryptHash, pPassword, dwPasswordLength, 0);
if (FALSE == bRet)
{
ShowError("CryptHashData");
break;
}
// 使用HASH来生成密钥
bRet = ::CryptDeriveKey(hCryptProv, CALG_AES_128, hCryptHash, CRYPT_EXPORTABLE, &hCryptKey);
if (FALSE == bRet)
{
ShowError("CryptDeriveKey");
break;
}
// 解密数据
bRet = ::CryptDecrypt(hCryptKey, NULL, TRUE, 0, pData, &dwDataLength);
if (FALSE == bRet)
{
ShowError("CryptDecrypt");
break;
}
} while (FALSE);
// 关闭释放
if (hCryptKey)
{
::CryptDestroyKey(hCryptKey);
}
if (hCryptHash)
{
::CryptDestroyHash(hCryptHash);
}
if (hCryptProv)
{
::CryptReleaseContext(hCryptProv, 0);
}
return bRet;
}
```
### 测试
直接运行上述程序设置基础密钥为16字节的字符串“DemonGanDemonGan”加密的数据内容为28字节的字符串“Whar is your name? DemonGan”。那么AES加解密的结果如图9-2所示。
![2](demonganpaperimage/1006/2.jpg)
# 9.1.3 RSA加解密
RSA是一种非对称加密算法加密密钥和解密密钥不相同。RSA非对称加密算法的安全性非常高对极大整数做因数分解的难度决定了RSA算法的可靠性。到目前为止世界上还没有任何可靠的攻击RSA算法的方式。只要其密钥的长度足够长用RSA加密的信息实际上是不能被解破的。
由于RSA进行的都是大数计算使得RSA无论是软件还是硬件实现速度一直是RSA的缺陷。RSA的速度是对应同样安全级别的对称密码算法的1/1000左右所以一般只用于少量数据加密。
### 函数介绍
#### CryptGenKey函数
> 随机生成加密会话密钥或公钥/私钥对密钥或密钥对的句柄在phKey中返回。
>
> ```c++
> BOOL WINAPI CryptGenKey(
> _In_ HCRYPTPROV hProv,
> _In_ ALG_ID Algid,
> _In_ DWORD dwFlags,
> _Out_ HCRYPTKEY *phKey
> );
> ```
>
> 参数
>
> hProv [in]
> 通过调用CryptAcquireContext创建的加密服务提供程序CSP的句柄。
> Algid [in]
> 标识要为其生成密钥的算法的ALG_ID值。 AT_KEYEXCHANGE表示生成的是交换密钥对。
>
> dwFlags [in]
> 指定生成的密钥的类型。会话密钥RSA签名密钥和RSA密钥交换密钥的大小可以在密钥生成时设置。CRYPT_EXPORTABLE表示密钥对就可以用CryptExportKey函数导出。
>
> phKey [out]
> 密钥或者密钥对的句柄。在完成密钥使用后通过调用CryptDestroyKey函数来删除密钥的句柄。
>
> 返回值
>
> 如果函数成功函数返回TRUE。
> 如果该功能失败则返回FALSE。有关扩展错误信息请调用GetLastError。
#### CryptExportKey函数
> 以安全的方式从加密服务提供程序CSP中导出加密密钥或密钥对。
>
> ```c++
> BOOL WINAPI CryptExportKey(
> _In_ HCRYPTKEY hKey,
> _In_ HCRYPTKEY hExpKey,
> _In_ DWORD dwBlobType,
> _In_ DWORD dwFlags,
> _Out_ BYTE *pbData,
> _Inout_ DWORD *pdwDataLen
> );
> ```
>
> 参数
>
> hKey [in]
> 要导出的密钥的句柄。
> hExpKey [in]
> 目标用户的密钥的句柄。通常置为NULL。
>
> dwBlobType [in]
> 指定要在pbData中导出的键BLOB的类型。PUBLICKEYBLOB表示导出公钥PRIVATEKEYBLOB表示导出私钥。
>
> dwFlags [in]
> 为函数指定附加选项。该参数通常置为0。
>
> pbData [out]
> 指向接收关键BLOB数据的缓冲区的指针。
> 如果此参数为NULL则所需的缓冲区大小将放置在pdwDataLen参数指向的值中。
> pdwDataLen [in, out]
> 指向DWORD值的指针该值在入口处包含由pbData参数指向的缓冲区的大小以字节为单位。当函数返回时该值包含存储在缓冲区中的字节数。
>
> 要检索所需的pbData缓冲区大小请为pbData传递NULL。所需的缓冲区大小将放置在此参数指向的值中。
>
> 返回值
>
> 如果函数成功函数返回TRUE。
> 如果该功能失败则返回FALSE。有关扩展错误信息请调用GetLastError。
#### CryptImportKey函数
> 将密钥从密钥BLOB导入到密码服务提供者CSP中。
>
> ```c++
> BOOL WINAPI CryptImportKey(
> _In_ HCRYPTPROV hProv,
> _In_ BYTE *pbData,
> _In_ DWORD dwDataLen,
> _In_ HCRYPTKEY hPubKey,
> _In_ DWORD dwFlags,
> _Out_ HCRYPTKEY *phKey
> );
> ```
>
> 参数
>
> hProv [in]
> 使用CryptAcquireContext函数获取CSP的句柄。
> pbData [in]
> 一个BYTE数组此密钥BLOB由CryptExportKey函数创建。
> dwDataLen [in]
> 包含关键BLOB的长度以字节为单位
> hPubKey [in]
> 解密存储在pbData中的密钥的加密密钥句柄。
>
> dwFlags [in]
> 目前仅在PRIVATEKEYBLOB形式的公钥/私钥对导入到CSP中时使用。
>
> phKey [out]
> 指向接收导入的键的句柄的HCRYPTKEY值的指针。完成密钥的使用后通过调用CryptDestroyKey函数来释放句柄。
>
> 返回值
>
> 如果函数成功函数返回TRUE。
> 如果该功能失败则返回FALSE。有关扩展错误信息请调用GetLastError。
### 实现过程
对于RSA非对称加密码算法通常情况下公钥用来加密数据私钥用来解密数据。所以在进行数据加解密之前需要先产生公钥和私钥密钥对。公钥和私钥是成对出现的使用公钥加密的数据只能用唯一的私钥来解密。
那么生成RSA公钥私钥密钥对的具体实现流程如下所示。
首先任何程序在使用CryptoAPI函数来对数据计算或是加解密之前都需要先调用CryptAcquireContext函数来获取加密服务提供者CSP的句柄。由于本程序实现的是使用RSA非对称加密算法加解密数据所以将提供者类型设置为PROV_RSA_FULL该类型支持RSA非对称加密算法。当不在使用CSP的时候可以通过调用CryptReleaseContext函数释放CSP句柄。
然后就可以直接调用CryptGenKey函数随机生成AT_KEYEXCHANGE交换密钥对并设置生成的密钥对类型为CRYPT_EXPORTABLE可导出的可以使用CryptExportKey函数导出密钥。在不需要使用密钥句柄之后可以通过调用CryptDestroyKey函数来删除密钥的句柄。
经过上述两步操作RSA密钥对就已经成功生成。但是为了方便后续使用公钥和私钥密钥对所以需要通过CryptExportKey函数来对密钥进行导出。由于密钥长度都不是固定的所以在获取密钥之前应该先确定密钥的长度。通过将输出缓冲区置为NULL即可返回实际所需的缓冲区大小以此来申请足够的密钥缓冲区。若要导出公钥则将导出类型置为PUBLICKEYBLOB若要导出私钥则将导出类型置为PRIVATEKEYBLOB。
那么生成RSA公私密钥对并导出的具体实现代码如下所示。
```c++
// 生成公钥和私钥
BOOL GenerateKey(BYTE **ppPublicKey, DWORD *pdwPublicKeyLength, BYTE **ppPrivateKey, DWORD *pdwPrivateKeyLength)
{
BOOL bRet = TRUE;
HCRYPTPROV hCryptProv = NULL;
HCRYPTKEY hCryptKey = NULL;
BYTE *pPublicKey = NULL;
DWORD dwPublicKeyLength = 0;
BYTE *pPrivateKey = NULL;
DWORD dwPrivateKeyLength = 0;
do
{
// 获取CSP句柄
bRet = ::CryptAcquireContext(&hCryptProv, NULL, NULL, PROV_RSA_FULL, 0);
if (FALSE == bRet)
{
ShowError("CryptAcquireContext");
break;
}
// 生成公私密钥对
bRet = ::CryptGenKey(hCryptProv, AT_KEYEXCHANGE, CRYPT_EXPORTABLE, &hCryptKey);
if (FALSE == bRet)
{
ShowError("CryptGenKey");
break;
}
// 获取公钥密钥的长度和内容
bRet = ::CryptExportKey(hCryptKey, NULL, PUBLICKEYBLOB, 0, NULL, &dwPublicKeyLength);
if (FALSE == bRet)
{
ShowError("CryptExportKey");
break;
}
pPublicKey = new BYTE[dwPublicKeyLength];
::RtlZeroMemory(pPublicKey, dwPublicKeyLength);
bRet = ::CryptExportKey(hCryptKey, NULL, PUBLICKEYBLOB, 0, pPublicKey, &dwPublicKeyLength);
if (FALSE == bRet)
{
ShowError("CryptExportKey");
break;
}
// 获取私钥密钥的长度和内容
bRet = ::CryptExportKey(hCryptKey, NULL, PRIVATEKEYBLOB, 0, NULL, &dwPrivateKeyLength);
if (FALSE == bRet)
{
ShowError("CryptExportKey");
break;
}
pPrivateKey = new BYTE[dwPrivateKeyLength];
::RtlZeroMemory(pPrivateKey, dwPrivateKeyLength);
bRet = ::CryptExportKey(hCryptKey, NULL, PRIVATEKEYBLOB, 0, pPrivateKey, &dwPrivateKeyLength);
if (FALSE == bRet)
{
ShowError("CryptExportKey");
break;
}
// 返回数据
*ppPublicKey = pPublicKey;
*pdwPublicKeyLength = dwPublicKeyLength;
*ppPrivateKey = pPrivateKey;
*pdwPrivateKeyLength = dwPrivateKeyLength;
} while (FALSE);
// 释放关闭
if (hCryptKey)
{
::CryptDestroyKey(hCryptKey);
}
if (hCryptProv)
{
::CryptReleaseContext(hCryptProv, 0);
}
return bRet;
}
```
在获取公私密钥对之后就可以对使用公钥来对数据进行加密处理。使用的加密函数是之前介绍过的CryptEncrypt函数那么具体的RSA公钥加密数据的具体实现流程如下所示。
首先依然是通过调用CryptAcquireContext来获取加密服务提供者CSP的句柄。由于本程序实现的是使用RSA非对称加密算法加密数据所以将提供者类型设置为PROV_RSA_FULL该类型支持RSA非对称加密算法。当不在使用CSP的时候可以通过调用CryptReleaseContext函数释放CSP句柄。
然后需要把公钥导入到CSP中以方便后续进行加密操作。通过调用CryptImportKey函数来实现将密钥导入CSP中并获取导入的公钥密钥句柄。在不再使用密钥句柄的时候可以通过调用CryptDestroyKey函数来释放句柄。
最后就可以直接通过调用CryptEncrypt函数来对数据进行加密数据的输入和密文的输出使用同一个缓冲区所以一定要确保缓冲区足够大。RSA非对称加密算法也是一种分组加密算法所以通过对Final参数来指定加密数据是否是最后一组加密数据TRUE表示是最后一组加密数据FALSE则不是。
那么使用RSA公钥加密数据的具体实现代码如下所示。
```c++
// 公钥加密数据
BOOL RsaEncrypt(BYTE *pPublicKey, DWORD dwPublicKeyLength, BYTE *pData, DWORD &dwDataLength, DWORD dwBufferLength)
{
BOOL bRet = TRUE;
HCRYPTPROV hCryptProv = NULL;
HCRYPTKEY hCryptKey = NULL;
do
{
// 获取CSP句柄
bRet = ::CryptAcquireContext(&hCryptProv, NULL, NULL, PROV_RSA_FULL, 0);
if (FALSE == bRet)
{
ShowError("CryptAcquireContext");
break;
}
// 导入公钥
bRet = ::CryptImportKey(hCryptProv, pPublicKey, dwPublicKeyLength, NULL, 0, &hCryptKey);
if (FALSE == bRet)
{
ShowError("CryptImportKey");
break;
}
// 加密数据
bRet = ::CryptEncrypt(hCryptKey, NULL, TRUE, 0, pData, &dwDataLength, dwBufferLength);
if (FALSE == bRet)
{
ShowError("CryptImportKey");
break;
}
} while (FALSE);
// 释放并关闭
if (hCryptKey)
{
::CryptDestroyKey(hCryptKey);
}
if (hCryptProv)
{
::CryptReleaseContext(hCryptProv, 0);
}
return bRet;
}
```
使用RSA私钥来对密文进行解密的实现流程与使用公钥加密数据的流程很相似同样是首先通过调用CryptAcquireContext函数来获取CSP的句柄并指定提供者类型为PROV_RSA_FULL。在不再使用CSP句柄的时候再调用CryptReleaseContext函数释放CSP句柄。然后调用CryptImportKey函数将RSA私钥导入CSP中并获取私钥密钥的句柄方便后续的数据解密工作。最后通过CryptDecrypt函数完成数据解密工作对于Final的参数要与加密操作保持一致。
那么使用RSA私钥解密数据的具体实现代码如下所示。
```c++
// 私钥解密数据
BOOL RsaDecrypt(BYTE *pPrivateKey, DWORD dwProvateKeyLength, BYTE *pData, DWORD &dwDataLength)
{
BOOL bRet = TRUE;
HCRYPTPROV hCryptProv = NULL;
HCRYPTKEY hCryptKey = NULL;
do
{
// 获取CSP句柄
bRet = ::CryptAcquireContext(&hCryptProv, NULL, NULL, PROV_RSA_FULL, 0);
if (FALSE == bRet)
{
ShowError("CryptAcquireContext");
break;
}
// 导入私钥
bRet = ::CryptImportKey(hCryptProv, pPrivateKey, dwProvateKeyLength, NULL, 0, &hCryptKey);
if (FALSE == bRet)
{
ShowError("CryptImportKey");
break;
}
// 解密数据
bRet = ::CryptDecrypt(hCryptKey, NULL, TRUE, 0, pData, &dwDataLength);
if (FALSE == bRet)
{
ShowError("CryptDecrypt");
break;
}
} while (FALSE);
// 释放并关闭
if (hCryptKey)
{
::CryptDestroyKey(hCryptKey);
}
if (hCryptProv)
{
::CryptReleaseContext(hCryptProv, 0);
}
return bRet;
}
```
### 测试
直接运行上述程序先随机生成RSA公私密钥对然后对28字节数据“What is your name? DemonGan”使用RSA公钥进行加密得到密文后再使用RSA私钥对密文数据进行解密。公私密钥对以及RSA加解密的结果如图9-3所示。
![3](demonganpaperimage/1006/3.jpg)
# 9.1.4 小结
计算HASH的操作步骤主要是创建空HASH对象然后将数据添加到HASH对象中并计算HASH值最后就可以获取HASH值。因为不同HASH算法它的HASH值长度也不同所以在调用CryptGetHashParam函数获取HASH值之前应先获取HASH值的大小已申请足够的缓冲区。
AES加解密过程中并没有直接使用明文密码来加密而是计算明文密码的MD5值以此作为基础密码通过CryptDeriveKey函数派生出AES的加密密钥。在使用128位AES对称加密算法CALG_AES_128对数据进行加密的时候由于AES是分组加密的所以分组长度为128位即16字节密钥的长度可以使用128位、192位或256位。在程序中之所以没有对加解密数据按16字节长度分组加解密是因为在调用CryptEncrypt函数和CryptDecrypt函数加解密的过程中参数Final一直置为TRUE表示该数据是加密或解密中的最后一个分组系统会自动填充分组。
RSA在生成公私密钥对之后调用CryptExportKey函数导出公钥和私钥的时候由于密钥长度并不是固定的所以需要先确定密钥长度大小以申请足够的密钥存放缓冲区存。