# 背景 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函数导出公钥和私钥的时候,由于密钥长度并不是固定的,所以需要先确定密钥长度大小,以申请足够的密钥存放缓冲区存。