Windows CryptoAPI密码学编程接口详解

CryptoAPI及下一代密码学编程接口是被广泛使用的一个密码学编程接口。密码学编程接口的相关知识点比较抽象,MSDN帮助文档也不完善,这让初学者感到非常困惑。为了帮助读者学好密码学编程接口,笔者会介绍一些背景知识,讲解API用法的时候,会酌情介绍其内部实现原理,并给出示例程序。

1.1 Windows CryptoAPI

CryptoAPI(简称CAPI)是Windows向应用程序开发人员提供的一套密码学编程接口,包括加密解密、身份认证和编码解码等。加密服务提供程序(Cryptographic Service Provider,简称CSP)也称加密服务提供商,指的是Microsoft或其他公司提供的包含密码算法的DLL模块,CryptoAPI的调用最终是由这些模块中的相关API 执行的。具体说来,CryptoAPI是由Advapi32.dll和Crypt32.dll导出的,应用程序对CryptoAPI的调用,会经过CryptoSPI(系统编程接口),然后CryptoSPI将调用传递到加密服务提供程序。应用程序开发人员可以使用CryptoAPI,而无需了解其底层实现细节,就像可以在不了解图形硬件配置的情况下可以使用图形库中的API一样。

现在,CryptoAPI已经被弃用,Microsoft建议使用密码算法种类更丰富、安全性更高的下一代密码学编程接口CNG。但是,作为曾被广泛使用的一个密码学编程接口,目前市面上仍然有许多此类应用程序,而且CryptoAPI和CNG API的使用方法是类似的,因此仍然有必要学习一下。

1.1.1 基本操作

执行具体的密码学计算时,调用对应的API即可,单纯学习一个API的使用,非常简单,但是可能还会涉及一些其他操作。例如,用于加密数据的API是CryptEncrypt,要使用对称密码,就需要有一个加密密钥,这涉及对称密码密钥的生成;生成密钥并对数据进行加密后,将来还需要解密数据,因此应该保存加密密钥,这涉及密钥的保存(导出);加密密钥应该使用公钥密码进行加密,以便可以安全地存储,或与其他用户进行交换,这涉及密钥对的生成和导出;通过调用相关API生成的密钥对会自动保存到密钥容器中,这涉及密钥容器和密钥数据库的一些知识;密码算法是由加密服务提供程序(CSP)提供的,因此还应该了解一些常用的CSP及其支持的密码算法。

枚举CSP与CSP类型

系统中提供了多个CSP,每个CSP都有一个名称,一个CSP的名称在系统中是唯一的。每个CSP都属于一个具体的CSP类型,但CSP类型不一定唯一,系统中可能有多个CSP同属于一种类型。CSP类型指的是共享数据格式和密码协议(Cryptographic Protocol)的一个CSP族。数据格式包括密码算法、密钥长度、操作模式和填充模式等。对于每种CSP类型,通常都会有一个默认CSP。

Microsoft提供的CSP及其类型和功能如下表所示。CSP类型给出的是“常量(值)”的形式,除了有一个值,每个CSP类型通常还有一个名称。

CSP名称类型和功能
Microsoft Base Cryptographic Provider v1.0类型:PROV_RSA_FULL(1) 基本加密服务提供程序。支持的算法类包括数据加密算法、散列算法、密钥交换算法和数字签名算法。具体支持的密码算法包括RC2、RC4、DES、SHA-1、MD2、MD4、MD5、MAC、HMAC、RSA_KEYX和RSA_SIGN
Microsoft Enhanced Cryptographic Provider v1.0类型:PROV_RSA_FULL(1) Microsoft Base Cryptographic Provider v1.0的增强版。支持更长的密钥,增加了对3DES TWO KEY和3DES算法的支持
Microsoft Strong Cryptographic Provider类型:PROV_RSA_FULL(1) Microsoft Base Cryptographic Provider v1.0的扩展版。具体支持的密码算法和Microsoft Enhanced Cryptographic Provider v1.0相同。是PROV_RSA_FULL类型的默认CSP
Microsoft Enhanced RSA and AES Cryptographic Provider类型:PROV_RSA_AES(24) 支持AES和SHA-2算法的Microsoft Enhanced Cryptographic Provider v1.0。和Microsoft Enhanced Cryptographic Provider v1.0相比,增加的密码算法包括SHA-256、SHA-384、SHA-512、AES-128、AES-192和AES-256。是PROV_RSA_AES类型的默认CSP
Microsoft RSA Signature Cryptographic Provider类型:PROV_RSA_SIG(2) 不支持
Microsoft RSA SChannel Cryptographic Provider类型:PROV_RSA_SCHANNEL(12) 支持的算法类包括数据加密算法、散列算法和密钥交换算法。具体支持的密码算法包括RC2、RC4、DES、3DES TWO KEY、3DES、AES-128、AES-256、SHA-1、MD5、MAC、HMAC和RSA_KEYX。是PROV_RSA_SCHANNEL类型的默认CSP
Microsoft Base DSS Cryptographic Provider类型:PROV_DSS(3) 支持的算法类包括散列算法和数字签名算法。具体支持的密码算法包括SHA-1、MD5和DSA_SIGN。是PROV_DSS类型的默认CSP
Microsoft Base DSS and Diffie-Hellman Cryptographic Provider类型:PROV_DSS_DH(13) Microsoft Base DSS Cryptographic Provider的超集。支持的算法类包括数据加密算法、散列算法、密钥交换算法和数字签名算法。具体支持的密码算法包括RC2、RC4、DES、CYLINK MEK、SHA-1、MD5、DH_KEYX和DSA_SIGN
Microsoft Enhanced DSS and Diffie-Hellman Cryptographic Provider类型:PROV_DSS_DH(13) Microsoft Base DSS and Diffie-Hellman Cryptographic Provider的增强版。支持更长的密钥,增加了对3DES TWO KEY和3DES算法的支持。是PROV_DSS_DH类型的默认CSP
Microsoft DH SChannel Cryptographic Provider类型:PROV_DH_SCHANNEL(18) 支持的算法类包括数据加密算法、散列算法、密钥交换算法和数字签名算法。具体支持的密码算法包括RC2、RC4、DES、3DES TWO KEY、3DES、CYLINK MEK、SHA-1、MD5、DH_KEYX和DSA_SIGN。是PROV_DH_SCHANNEL类型的默认CSP

CryptoAPI中的密码算法可以划分为4个算法类(Algorithm Class),分别是数据加密算法、散列算法、密钥交换算法和数字签名算法。数据加密算法使用对称密码对数据进行加密解密;散列算法可以将任意长度的消息压缩为较少字节数(通常小于 256 字节)的消息摘要,通常不需要使用密钥;密钥交换算法使用公钥加密对称密码的密钥,只有持有对应私钥的人才可以解密得到对称密码密钥,通过密钥交换算法,通信双方可以安全地进行对称密码密钥交换;数字签名算法使用私钥生成数字签名,其他人可以通过对应的公钥验证数字签名。

注册表HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\Defaults\Provider包含计算机上已安装CSP的子键,每个子键中包含表示DLL文件名和CSP类型等的键值项。

注册表HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\Defaults\Provider Types包含计算机上所有可用的CSP类型的子键,每个子键中包含表示CSP类型的名称的键值项,以及表示该CSP类型的默认CSP的键值项。

CryptEnumProviderTypes函数用于依次获取计算机上所有可用的CSP类型。函数原型:

BOOL CryptEnumProviderTypes(
   _In_       DWORD  dwIndex,       // 要获取的CSP类型的索引,初始设置为0,每次递增1
   _Reserved_ DWORD* pdwReserved,   // 保留参数,必须设置为NULL
   _In_       DWORD  dwFlags,       // 保留参数,必须设置为0
   _Out_      DWORD* pdwProvType,   // 用于返回CSP类型的值
   _Out_opt_  LPTSTR pszTypeName,   // 用于返回CSP类型的名称的缓冲区
   _Inout_    DWORD* pcbTypeName);  // pszTypeName参数所指向的缓冲区的大小,字节单位

其中pszTypeName和pcbTypeName两个参数的说明如下。

● _Out_opt_ LPTSTR pszTypeName

pszTypeName参数指向的缓冲区用于接收CSP类型的名称。如果将该参数设置为NULL,函数会通过pcbTypeName参数所指向的DWORD类型变量返回所需的缓冲区大小(字节单位,包括CSP类型名称字符串结尾的0),然后程序可以分配这个大小的缓冲区,并进行第二次调用。

● _Inout_ DWORD* pcbTypeName

pcbTypeName参数用于指定pszTypeName参数所指向的缓冲区的大小,字节单位。函数执行成功,该参数指向的DWORD类型变量的值更新为CSP类型名称字符串的实际大小(包括字符串结尾的0)。有的CSP类型没有名称,这种情况下,返回的DWORD值为0。

函数执行成功,返回值为TRUE;函数执行失败,则返回值为FALSE,可以通过调用GetLastError函数获取错误代码。

程序可以循环调用CryptEnumProviderTypes函数以依次获取计算机上所有可用的CSP类型,直到函数返回FALSE。

CryptEnumProviders函数用于依次获取计算机上所有可用的CSP。函数原型:

BOOL CryptEnumProviders(
   _In_       DWORD  dwIndex,       // 要获取的CSP的索引,初始设置为0,每次递增1
   _Reserved_ DWORD* pdwReserved,   // 保留参数,必须设置为NULL
   _In_       DWORD  dwFlags,       // 保留参数,必须设置为0
   _Out_      DWORD* pdwProvType,   // 用于返回CSP的类型值
   _Out_opt_  LPTSTR pszProvName,   // 用于返回CSP的名称的缓冲区
   _Inout_    DWORD* pcbProvName);  // pszProvName参数所指向的缓冲区的大小,字节单位

CryptEnumProviders函数的用法和CryptEnumProviderTypes是类似的:可以进行两次调用,第一次调用时只是获取CSP名称所需的缓冲区大小,第二次调用则是获取CSP的类型值与名称;程序可以循环调用CryptEnumProviders函数以依次获取计算机上所有可用的CSP,直到函数返回FALSE。

CryptGetDefaultProvider函数用于获取本地计算机或当前用户中指定CSP类型的默认CSP的名称。函数原型:

BOOL CryptGetDefaultProvider(
   _In_       DWORD  dwProvType,    // 指定CSP类型
   _Reserved_ DWORD* pdwReserved,   // 保留参数,必须设置为NULL
   _In_       DWORD  dwFlags,       // CRYPT_MACHINE_DEFAULT或CRYPT_USER_DEFAULT
   _Out_opt_  LPTSTR pszProvName,   // 用于返回指定CSP类型的默认CSP的名称的缓冲区
   _Inout_    DWORD* pcbProvName);  // pszProvName参数所指向的缓冲区的大小,字节单位

其中dwProvType和dwFlags两个参数的说明如下。

● _In_ DWORD dwProvType

dwProvType参数用于指定CSP类型,常见的CSP类型有PROV_RSA_FULL、PROV_DSS、PROV_RSA_SCHANNEL、PROV_DSS_DH、PROV_DH_SCHANNEL和PROV_RSA_AES等。

● _In_ DWORD dwFlags

dwFlags参数可以设置为CRYPT_MACHINE_DEFAULT或CRYPT_USER_DEFAULT,前者表示本地计算机,后者表示当前用户。

CryptGetDefaultProvider函数的用法和前面两个枚举函数是类似的,也是可以进行两次调用,第一次调用时只是获取CSP名称所需的缓冲区大小,第二次调用则是获取指定CSP类型的默认CSP的名称。

笔者计算机上获取到的所有可用的CSP类型及默认CSP的名称如下表所示。获取方法,循环调用CryptEnumProviderTypes函数依次获取计算机上所有可用的CSP类型,每获取到一个类型,调用CryptGetDefaultProvider函数获取该类型的默认CSP。具体请参见EnumCSPAndGetParam项目。

CSP类型默认CSP的名称
PROV_RSA_FULLMicrosoft Strong Cryptographic Provider
PROV_DSSMicrosoft Base DSS Cryptographic Provider
PROV_RSA_SCHANNELMicrosoft RSA SChannel Cryptographic Provider
PROV_DSS_DHMicrosoft Enhanced DSS and Diffie-Hellman Cryptographic Provider
PROV_DH_SCHANNELMicrosoft DH SChannel Cryptographic Provider
PROV_RSA_AESMicrosoft Enhanced RSA and AES Cryptographic Provider

获取未知大小的数据。有些函数会向程序指定的缓冲区返回大量数据,这些函数通常都会有缓冲区地址和缓冲区大小两个参数,表示缓冲区地址的参数名称通常以psz、pb或pv等作为前缀,表示缓冲区大小的参数名称通常以pcb或pdw等作为前缀,这些函数确定缓冲区大小的方法是类似的。

为了分配合适大小的缓冲区,可以进行两次函数调用,第一次调用时,将缓冲区地址参数设置为NULL,这时函数调用会成功,函数会通过缓冲区大小参数所指向的变量返回所需的缓冲区大小(字节单位);然后程序可以分配该大小的缓冲区,并进行第二次函数调用以获取数据,第二次调用成功返回后,缓冲区大小参数所指向的变量的值更新为所返回数据的实际大小。如果表示缓冲区大小的参数名称以pcch或cch等作为前缀,ch表示char,字符的意思,这时的缓冲区大小以字符为单位(Unicode或ASCII)。

如果缓冲区地址参数所指定的缓冲区的大小不足以容纳返回的数据,函数调用会失败,错误代码为ERROR_MORE_DATA,不过,函数依然会通过缓冲区大小参数所指向的变量返回所需的缓冲区大小。

有时候也可以只进行一次函数调用,只需要确保缓冲区大小不小于实际需要即可,函数返回后,缓冲区大小参数所指向的变量的值会更新为所返回数据的实际大小。

密钥与密钥容器

用于对称密码的密钥称为对称密钥。用于通信会话的一次性的对称密钥称为会话密钥。本书不区分这两种密钥,统一称为对称密钥。

CryptoAPI中有两种类型的密钥对,交换密钥对(Exchange Key Pair)和签名密钥对(Signature Key Pair)。交换密钥对是指用于加密解密对称密钥的密钥对,以便可以安全地存储对称密钥,或与其他用户进行交换。签名密钥对是指用于生成和验证数字签名的密钥对。在CryptoAPI中,密钥对类型称为密钥规范(Key Specification),使用常量AT_KEYEXCHANGE表示交换密钥对,常量AT_SIGNATURE表示签名密钥对。

存储用户的持久密钥(Persistent Key)的数据库称为密钥数据库(Key Database)。密钥数据库可以存储在硬件内部(例如智能卡)、文件或系统注册表。一个密钥数据库中可以包含多个密钥容器(Key Container),密钥容器用于存储用户的密钥对。对于每种密钥规范,一个密钥容器中只存储一个密钥对,即最多存储一个交换密钥对和一个签名密钥对。CryptoAPI的密钥数据库(密钥容器)不支持存储对称密钥。

在计算机中,密钥容器是一个文件,存储位置如下表所示。

持久密钥分类目录
用户专用%APPDATA%\Microsoft\Crypto\RSA\User SID\ %APPDATA%\Microsoft\Crypto\DSS\User SID\
本地系统专用%ALLUSERSPROFILE%\Application Data\Microsoft\Crypto\RSA\S-1-5-18\ %ALLUSERSPROFILE%\Application Data\Microsoft\Crypto\DSS\S-1-5-18\
本地服务专用%ALLUSERSPROFILE%\Application Data\Microsoft\Crypto\RSA\S-1-5-19\ %ALLUSERSPROFILE%\Application Data\Microsoft\Crypto\DSS\S-1-5-19\
网络服务专用%ALLUSERSPROFILE%\Application Data\Microsoft\Crypto\RSA\S-1-5-20\ %ALLUSERSPROFILE%\Application Data\Microsoft\Crypto\DSS\S-1-5-20\
共享专用%ALLUSERSPROFILE%\Application Data\Microsoft\Crypto\RSA\MachineKeys %ALLUSERSPROFILE%\Application Data\Microsoft\Crypto\DSS\MachineKeys

密钥容器有两个名称,一个是应用程序使用的名称,一个是CSP内部使用的唯一名称,为避免歧义,我们将前者称为密钥容器名称,将后者称为密钥容器唯一名称。密钥容器是存储在相关目录中的一个文件,文件名使用的是唯一名称。密钥容器唯一名称通常是“密钥容器名称的MD5值_MachineGuid”的形式。MachineGuid取自注册表HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\MachineGuid键值项。关于密钥容器名称的MD5值,将密钥容器名称转换为小写并添加一个0字符(即密钥容器名称的ASCII字符串),计算该ASCII字符串的MD5,然后将16字节的MD5划分为4个DWORD值(小尾方式,即每4字节一组进行反转)即可得到。例如,设密钥容器名称为“37611”,MD5为8566DEB6BAA1CC06684D149F93B77DAE,划分为4个DWORD值之后就是B6DE668506CCA1BA9F144D68AE7DB793。

在CSP内部,通常会将应用程序传递过来的密钥容器名称转换为唯一名称,以定位文件系统中的密钥容器。密钥容器的存储位置分为RSA和DSS两个子目录,RSA算法所用的密钥对存储在RSA目录,数字签名标准算法所用的密钥对存储在DSS目录。支持RSA算法的所有CSP使用同一个RSA目录,支持数字签名标准算法的所有CSP使用同一个DSS目录。

通过CSP名称和密钥容器名称,可以唯一地确定文件系统中的一个密钥容器,进而可以唯一地确定其中的交换密钥对或签名密钥对。如果多个CSP使用相同的密钥容器名称,那么它们可能访问的是同一个密钥容器文件,一个CSP可能会修改或销毁另一个CSP正在使用的密钥对。实际编程时,应该避免出现多个CSP或应用程序使用同一密钥容器的情况,程序应该根据需要使用的密码算法选择一个恰当的CSP,并使用一个不大可能被其他程序使用的密钥容器名称(例如生成一个GUID作为名称)。如果应用程序需要同时使用多个CSP,每个CSP应该使用不同的密钥容器名称。

通过调用CryptGenKey函数可以生成一个随机对称密钥、交换密钥对或签名密钥对。生成的交换密钥对和签名密钥对会自动保存到所指定的密钥容器中,如果密钥容器中先前已经存在同种密钥规范的密钥对,则会被覆盖,因为对于每种密钥规范,一个密钥容器中只存储一个密钥对。生成的对称密钥不会保存到密钥容器,因为密钥容器存储的是用户的密钥对。

程序可以通过调用CryptExportKey函数将生成的对称密钥或密钥对导出为密钥BLOB(Key BLOB)格式并存盘。对称密钥通常都需要及时进行导出以传输或备份,因为密钥是随机生成的,在当前程序会话中生成对称密钥并加密数据,以后解密数据时必须使用同一个对称密钥。

BLOB是用于存储特定格式数据的一种数据结构。密钥BLOB是通过调用CryptExportKey函数从CSP中导出现有密钥来创建的,以后可以通过调用CryptImportKey函数将保存的密钥BLOB导入到CSP中。通过调用CryptImportKey函数导入到CSP中的交换密钥对和签名密钥对会同时自动保存到所指定的密钥容器中,如果密钥容器中先前已经存在同种密钥规范的密钥对,则会被覆盖。通过调用CryptImportKey函数导入到CSP中的对称密钥不会保存到密钥容器。

密钥BLOB分为简单密钥BLOB、私钥BLOB和公钥BLOB等。简单密钥BLOB(Simple Key BLOB)指的是用交换密钥对的公钥加密的对称密钥的BLOB,用于存储对称密钥或将对称密钥传输给另一个用户。私钥BLOB(Private Key BLOB)指的是完整密钥对的BLOB,通常会使用对称密码进行加密,用于将密钥对传输给另一个用户。公钥BLOB(Public Key BLOB)指的是密钥对中的公钥的BLOB,公钥BLOB不会被加密,因为公钥不需要保密。

获取CSP句柄

使用CryptoAPI的第一个步骤是调用CryptAcquireContext函数获取CSP句柄。CryptAcquireContext函数内部会调用LoadLibraryEx函数加载指定CSP对应的DLL,并在这个CSP(或者说这个CSP对应的密钥容器目录)中查找具有指定名称的密钥容器,如果查找成功,说明具备加密环境(Cryptographic Context),然后做一些初始化工作,接下来可以使用这个CSP提供的密码算法。读者可能会疑惑,只要成功加载指定CSP对应的DLL,就可以使用其中的密码算法,为什么还要查找密钥容器呢?一些密码算法需要使用密钥对,而密钥对存储在密钥容器中,只有确定存在指定的密钥容器才可以进行后续的操作。函数执行成功,会通过phProv参数指向的HCRYPTPROV类型变量返回一个句柄,这个句柄是表示指定CSP和密钥容器的句柄,但习惯上将其简称为CSP句柄。如果需要,程序可以多次调用CryptAcquireContext函数基于同一CSP打开多个不同名称的密钥容器。

除了可以准备加密环境并返回一个CSP句柄,通过对CryptAcquireContext函数的dwFlags参数设置不同的值,该函数还可以创建一个新的密钥容器,或删除指定的密钥容器,或创建并使用临时密钥容器(仅存在于内存中,生成的密钥对不会被持久化存储)。

CryptAcquireContext函数原型:

BOOL CryptAcquireContext(
   _Out_    HCRYPTPROV* phProv,        // 通过该参数返回一个CSP句柄
   _In_opt_ LPCTSTR     pszContainer,  // 指定密钥容器的名称
   _In_opt_ LPCTSTR     pszProvider,   // 指定CSP的名称
   _In_     DWORD       dwProvType,    // 指定CSP的类型
   _In_     DWORD       dwFlags);      // 标志值,通常可以设置为0

各个参数的说明如下。

● _Out_ HCRYPTPROV* phProv

phProv参数设置为一个指向HCRYPTPROV类型变量的指针,函数执行成功,会通过该参数返回一个有效的CSP句柄值,返回的句柄可以用于下一步的使用所选CSP的CryptoAPI函数调用。

● _In_opt_ LPCTSTR pszContainer

pszContainer参数用于指定密钥容器的名称。如果将该参数设置为NULL,表示使用默认密钥容器。默认密钥容器通常以当前用户名为名称。

当dwFlags参数设置为CRYPT_NEWKEYSET时,表示创建一个该参数所指定名称的密钥容器,如果将该参数设置为NULL,则创建一个默认密钥容器。当dwFlags参数设置为CRYPT_DELETEKEYSET时,表示删除该参数所指定名称的密钥容器,如果将该参数设置为NULL,则删除默认密钥容器,密钥容器中的所有密钥对都会被同时删除。当dwFlags参数设置为CRYPT_VERIFYCONTEXT时,表示创建并使用临时密钥容器,对于基于文件的CSP,必须将该参数设置为NULL;对于基于硬件的CSP,可以将该参数设置为NULL或空字符串,表示不需要访问任何密钥,也可以设置一个确定的名称,表示访问密钥容器中的公开信息。

注意,除了当dwFlags参数设置为CRYPT_VERIFYCONTEXT并且是基于文件的CSP时,通常不建议将该参数设置为NULL以使用默认密钥容器,因为当多个CSP或应用程序使用同一个密钥容器时可能会引发访问冲突。

● _In_opt_ LPCTSTR pszProvider

pszProvider参数用于指定CSP的名称。

● _In_ DWORD dwProvType

dwProvType参数用于指定CSP的类型。如果程序通过dwProvType和pszProvider两个参数同时指定了CSP类型和名称,函数会尝试查找具有这两个参数所描述特征的CSP,如果找到了符合条件的CSP,再在这个CSP中查找具有pszContainer参数所指定名称的密钥容器。如果程序通过dwProvType参数指定了CSP类型,但没有指定CSP名称(pszProvider参数设置为NULL),表示使用当前用户或本地计算机的所指定CSP类型的默认CSP。需要注意的是,不同用户或不同版本操作系统中的默认CSP可能不一致。

● _In_ DWORD dwFlags

dwFlags参数是一个标志值,通常可以设置为0;如果需要,可以设置为一些标志的组合。常用的标志及含义如下表所示。

标志含义
CRYPT_VERIFYCONTEXT表示创建并使用临时密钥容器,设置该标志后,无法访问持久私钥,临时密钥容器将会在调用 CryptReleaseContext 函数释放CSP句柄后被删除。适用于使用临时密钥或不需要访问持久私钥的应用程序,例如只是用于计算散列值、对称密码加密解密、公钥密码加密或数字签名验证的应用程序。数字签名验证需要使用公钥,但程序可以随意访问公钥,不受任何限制。只有执行公钥密码解密或生成数字签名时才需要使用私钥。 对于基于文件的CSP,当设置该标志时,必须将pszContainer参数设置为NULL。 对于基于硬件的CSP(例如智能卡),当设置该标志时:如果将pszContainer参数设置为NULL或空字符串,表示不需要访问任何密钥,并且不应该向用户显示UI(用户界面),通常用于连接到CSP以查询其功能时;如果将pszContainer参数设置为一个确定的名称,表示访问密钥容器中的公开信息。 一些CSP在授予用户对密钥容器中私钥的访问权限前,私钥可能设置了保护,需要用户输入密码才能使用,但是如果指定了该标志,则表示不需要访问私钥,并且可以绕过UI
CRYPT_NEWKEYSET使用pszContainer参数指定的名称创建一个新的密钥容器,如果pszContainer参数为NULL,则创建一个新的默认密钥容器
CRYPT_DELETEKEYSET删除pszContainer参数指定的密钥容器,如果pszContainer参数为NULL,则删除默认密钥容器,密钥容器中的所有密钥对都会被同时删除
CRYPT_MACHINE_KEYSET该标志可以和其他所有标志组合使用,表示使用机器级密钥容器,即表示pszContainer参数指定的是一个机器级密钥容器名称。 密钥容器可以分为机器级(machine-level)和用户级(user-level),默认情况下为用户级。用户级的密钥容器存储在用户配置文件中,只有特定用户才可以访问;机器级的密钥容器存储在本地计算机中所有用户都可以访问的一个位置,所有用户都有可能具有访问权限。 对用户级密钥容器的访问:由管理员在没有设置该标志的情况下创建的密钥容器,只能由创建密钥容器的用户和具有管理权限的用户访问;由非管理员用户在没有设置该标志的情况下创建的密钥容器,只能由创建密钥容器的用户和本地系统帐户访问。 系统对机器级密钥容器添加了访问控制,因此对机器级密钥容器的访问和上面介绍的对用户级密钥容器的访问情形是相同的,但是,可以通过调用CryptSetProvParam函数(dwParam参数设置为PP_KEYSET_SEC_DESCR)授予对机器级密钥容器的访问权限,以达到所有用户都可以访问的目的。 如果不是仅针对特定用户的需要使用私钥的操作,而是面向全局的,应该使用该标志。应该使用该标志的一些特定场景包括服务程序、ASP页面下运行的组件和Microsoft Transaction Server(MTS)组件等。这些场景的安全环境可能无法访问用户配置文件,例如,MTS客户端可以模拟用户,但由于用户没有登录,用户的配置文件不可用,在ASP页面下运行的组件也是如此
CRYPT_SILENT保持静默,不显示任何UI,适用于不需要显示UI的CSP。如果后续使用该CSP句柄的其他函数调用要求显示UI,那么这些函数调用会失败(错误代码为NTE_SILENT_CONTEXT)

函数执行成功,返回值为TRUE;函数执行失败,则返回值为FALSE,可以通过调用GetLastError函数获取错误代码。

一些常见的错误代码及含义如下表所示,其中,以NTE开头的错误代码是由使用的CSP生成的。

错误代码含义
ERROR_BUSY当dwFlags参数设置为CRYPT_DELETEKEYSET以删除密钥容器,而另一个进程正在使用这个密钥容器时
NTE_BAD_FLAGSdwFlags参数具有无效的值
NTE_BAD_KEYSET_PARAMpszContainer或pszProvider参数具有无效的值
NTE_PROV_TYPE_NOT_DEF没有找到dwProvType参数所指定类型的CSP
NTE_BAD_KEYSET表示无法打开密钥容器,可能是密钥容器不存在,这时应将dwFlags参数设置为CRYPT_NEWKEYSET以创建一个新的密钥容器;该错误代码还可以表示拒绝访问指定的密钥容器,程序可以通过调用CryptSetProvParam函数授予对指定密钥容器的访问权限
NTE_KEYSET_NOT_DEF请求的CSP不存在

总结一下。如果不需要访问持久私钥,可以为dwFlags参数指定CRYPT_VERIFYCONTEXT标志,以创建并使用临时密钥容器,这时必须将pszContainer参数设置为NULL(对于基于文件的CSP)。如果需要访问持久私钥,最好使用一个不大可能被其他应用程序使用的密钥容器名称,dwFlags参数通常可以设置为0;如果函数执行失败并返回错误代码NTE_BAD_KEYSET,可以给dwFlags参数加上CRYPT_NEWKEYSET标志并进行重新调用,以创建一个新的密钥容器。

CryptAcquireContext函数执行成功,会通过phProv参数所指向的HCRYPTPROV类型变量返回一个CSP句柄。当不再需要返回的CSP句柄时,应该调用CryptReleaseContext函数将其释放。如果dwFlags参数设置为CRYPT_DELETEKEYSET,表示删除指定的密钥容器,系统会同时释放CSP句柄,这种情况下不需要调用CryptReleaseContext函数。

获取CSP属性

CryptGetProvParam函数用于获取CSP的指定属性。函数原型:

BOOL CryptGetProvParam(
   _In_      HCRYPTPROV  hProv,        // CryptAcquireContext函数返回的CSP句柄
   _In_      DWORD       dwParam,      // 表示要获取的属性的常量标志
   _Out_opt_ BYTE*       pbData,       // 用于接收返回的属性值的缓冲区指针
   _Inout_   DWORD*      pdwDataLen,   // pbData所指向的缓冲区的大小,字节单位
   _In_      DWORD       dwFlags);     // 标志值,通常可以设置为0

几个参数的说明如下。

● _In_ DWORD dwParam

dwParam参数用于指定要获取的属性。常用的属性常量标志及含义如下表所示。

标志含义
PP_CONTAINER获取当前密钥容器的名称。函数返回后,pbData参数是一个表示密钥容器名称的CHAR类型字符串指针。返回的密钥容器名称和调用CryptAcquireContext函数时通过pszContainer参数所指定的名称相同。通过使用该标志可以查询默认密钥容器的名称
PP_UNIQUE_CONTAINER获取当前密钥容器的唯一名称。函数返回后,pbData参数是一个表示密钥容器唯一名称的CHAR类型字符串指针。密钥容器唯一名称是“密钥容器名称MD5值的变换_MachineGuid”的形式。 对于一些CSP来说,通过该标志获取到的名称和通过PP_CONTAINER标志获取到的名称可能是相同的
PP_NAME获取CSP的名称。函数返回后,pbData参数是一个表示CSP名称的CHAR类型字符串指针。返回的CSP名称和调用CryptAcquireContext函数时通过pszProvider参数所指定的名称相同
PP_PROVTYPE获取CSP类型。函数返回后,pbData参数是一个表示CSP类型的DWORD类型值的指针
PP_ENUMALGS枚举CSP中所有可用的密码算法,每枚举到一个算法,pbData参数是一个指向PROV_ENUMALGS结构的指针,该结构包括算法ID、默认密钥长度、算法名称和算法名称的长度。枚举第一个算法时,函数调用中的dwFlags参数必须包含CRYPT_FIRST标志,成功获取到第一个算法后,将dwFlags参数更改为包含CRYPT_NEXT标志,并循环调用函数以获取所有后续算法,直到函数返回FALSE(错误代码为ERROR_NO_MORE_ITEMS)。该函数不是线程安全的,如果在多线程环境中调用该函数,可能无法枚举到所有可用的密码算法
PP_ENUMALGS_EX和上面PP_ENUMALGS标志的用法是一样的,只不过返回的是算法信息更为丰富的PROV_ENUMALGS_EX结构,除了包括PROV_ENUMALGS结构的4个字段以外,PROV_ENUMALGS_EX结构还包括算法的最小和最大密钥长度、算法支持的协议、算法的长名称以及算法长名称的长度这5个字段
PP_ENUMCONTAINERS枚举CSP中所有的用户级密钥容器,每枚举到一个密钥容器,pbData参数是一个表示密钥容器名称的CHAR类型字符串指针。枚举方法和PP_ENUMALGS以及PP_ENUMALGS_EX是一样的,枚举第一个密钥容器时,函数调用中的dwFlags参数必须包含CRYPT_FIRST标志,后续循环调用则更改为包含CRYPT_NEXT标志,直到函数返回FALSE(错误代码为ERROR_NO_MORE_ITEMS)。 如果需要枚举所有的机器级密钥容器,调用CryptAcquireContext函数获取CSP句柄时,dwFlags参数需要包含CRYPT_MACHINE_KEYSET标志。同样,该函数不是线程安全的,如果在多线程环境中调用该函数,可能无法枚举到所有的用户级或机器级密钥容器
PP_KEYSET_TYPE查询hProv参数指定的CSP是不是使用的机器级密钥容器。函数返回后,pbData参数是一个指向DWORD类型值的指针,如果DWORD值为0,表示用户级,如果DWORD值为CRYPT_MACHINE_KEYSET,表示机器级
PP_KEYSPEC查询CSP支持的密钥规范。函数返回后,pbData参数是一个指向DWORD类型值的指针,这个DWORD值是密钥规范标志的组合,例如AT_KEYEXCHANGE | AT_SIGNATURE
PP_KEYX_KEYSIZE_INC获取密钥交换算法的密钥增量长度(位数)。函数返回后,pbData参数是一个表示密钥增量长度的DWORD类型值的指针。通过该标志获取的信息可以和通过PP_ENUMALGS_EX标志获取的信息结合起来使用,PROV_ENUMALGS_EX结构有算法的最小和最大密钥长度,假设最小为512位,最大为1024位,密钥增量长度为64位,那么512,576,640,…,1024都是有效的密钥长度,这些密钥长度可以在调用CryptGenKey函数生成密钥时使用,以指定所生成密钥的长度
PP_SIG_KEYSIZE_INC获取数字签名算法的密钥增量长度(位数),用法同PP_KEYX_KEYSIZE_INC
PP_KEYSET_SEC_DESCR获取密钥容器的安全描述符,函数调用中的dwFlags参数必须设置合适的表示所请求安全信息的SECURITY_INFORMATION位标志。函数返回后,pbData参数是一个指向SECURITY_DESCRIPTOR结构的指针

● _Out_opt_ BYTE* pbData

pbData参数指向的缓冲区用于接收返回的属性值,所返回数据的格式因dwParam参数所指定的标志而异。为了分配合适大小的缓冲区,可以进行两次函数调用。

需要注意的是,当dwParam参数设置为PP_ENUMALGS、PP_ENUMALGS_EX或PP_ENUMCONTAINERS时,如果将该参数设置为NULL(或缓冲区太小时),那么通过pdwDataLen参数所指向的DWORD类型变量返回的是枚举列表中最大项所需的缓冲区大小,或CSP允许的最大大小,而不是当前读取的项的大小,就是说只需要在枚举第一项时(dwFlags参数包含CRYPT_FIRST标志)获取一次缓冲区大小即可。

● _Inout_ DWORD* pdwDataLen

pdwDataLen参数用于指定pbData参数所指向的缓冲区的大小。函数成功获取到数据并返回后,该参数的值会更新为实际获取到的数据的大小,字节单位。

● _In_ DWORD dwFlags

dwFlags参数是一个标志值,通常可以设置为0。

当dwParam参数设置为PP_KEYSET_SEC_DESCR以获取密钥容器的安全描述符时,应该为该参数设置合适的表示所请求安全信息的SECURITY_INFORMATION位标志。可以使用的SECURITY_INFORMATION位标志及含义:OWNER_SECURITY_INFORMATION,引用对象的所有者安全标识符(SID);GROUP_SECURITY_INFORMATION,引用对象的主组安全标识符(SID);DACL_SECURITY_INFORMATION,引用对象的自主访问控制列表(DACL);SACL_SECURITY_INFORMATION,引用对象的系统访问控制列表(SACL)。

当dwParam参数设置为PP_ENUMALGS、PP_ENUMALGS_EX或PP_ENUMCONTAINERS时,要枚举第一个元素,该参数必须包含CRYPT_FIRST标志,要枚举后续元素,则将该参数更改为包含CRYPT_NEXT标志。

GET_ALG_CLASS(x)宏可以确定dwParam设置为PP_ENUMALGS或PP_ENUMALGS_EX时枚举到的密码算法所属的算法类,参数x是表示密码算法ID的ALG_ID类型值,该宏返回一个算法类代码。算法类代码包括ALG_CLASS_DATA_ENCRYPT、ALG_CLASS_HASH、ALG_CLASS_KEY_EXCHANGE和ALG_CLASS_SIGNATURE,分别表示数据加密算法、散列算法、密钥交换算法和数字签名算法。

在EnumCSPAndGetParam项目中,循环调用CryptEnumProviders函数依次获取计算机上所有可用的CSP,每获取到一个,调用CryptGetProvParam函数并为dwParam参数指定PP_ENUMALGS_EX标志以枚举该CSP中所有可用的密码算法。为了读者编程方便,这里将每种CSP支持的密码算法按算法类划分,即CSP中属于各个算法类的密码算法分别有哪几个,具体请参见CSPAlg.bmp。

1.1.2 生成、导出和导入密钥

CryptGetUserKey函数用于获取密钥容器中用户的交换密钥对或签名密钥对,该函数仅由密钥对的所有者使用。函数原型:

BOOL CryptGetUserKey(
   _In_  HCRYPTPROV hProv,       // CryptAcquireContext函数返回的CSP句柄
   _In_  DWORD      dwKeySpec,   // 密钥规范,AT_SIGNATURE或AT_KEYEXCHANGE
   _Out_ HCRYPTKEY* phUserKey);  // 返回一个密钥句柄

函数执行成功,会通过phUserKey参数所指向的HCRYPTKEY类型变量返回一个密钥句柄。当不再需要返回的密钥句柄时,应该调用CryptDestroyKey函数将其释放。

函数执行成功,返回值为TRUE;函数执行失败,则返回值为FALSE,可以通过调用GetLastError函数获取错误代码。几个常见的错误代码及含义:ERROR_INVALID_HANDLE,其中一个参数指定的句柄无效;ERROR_INVALID_PARAMETER,其中一个参数包含无效的值,通常是无效指针;NTE_NO_KEY,dwKeySpec参数所指定密钥规范的密钥对不存在。

CryptGenKey函数用于生成一个随机对称密钥或密钥对,并返回一个密钥句柄。CryptGenKey函数内部会调用随机数生成函数。通过CryptGenKey函数生成的交换密钥对和签名密钥对都会自动保存到密钥容器中,如果密钥容器中先前已经存在同种密钥规范的密钥对,则会被覆盖,因为对于每种密钥规范,一个密钥容器只存储一个密钥对。生成的对称密钥不会保存到密钥容器,因为密钥容器存储的是用户的密钥对。

说明:将对称密钥句柄和密钥对句柄统称为密钥句柄;有时候为了特指密钥对中的公钥或私钥,可能会将密钥对句柄称为公钥句柄或私钥句柄。

CryptGenKey函数原型:

BOOL CryptGenKey(
   _In_  HCRYPTPROV hProv,       // CryptAcquireContext函数返回的CSP句柄
   _In_  ALG_ID     Algid,       // 指定要为哪个对称密码生成对称密钥,或指定密钥规范
   _In_  DWORD      dwFlags,     // 高16位指定密钥长度,低16位可以设置为0或标志的组合
   _Out_ HCRYPTKEY* phKey);      // 返回一个密钥句柄

几个参数的说明如下。

● _In_ ALG_ID Algid

如果是生成对称密钥,Algid参数应设置为一个ALG_ID值,用以指定生成的对称密钥所用于的对称密码算法。例如,常量CALG_DES表示DES算法,CALG_AES_256表示AES-256算法等。需要注意的是,指定的算法必须是当前CSP所支持的。通过该参数指定的加密算法ID会与密钥捆绑,后续调用CryptEncrypt一类的函数执行实际的数据加密操作时,不需要再指定加密算法。

如果是生成密钥对,不需要指定算法ID,这时Algid参数用于指定密钥规范,可以设置为AT_KEYEXCHANGE,表示生成交换密钥对,或设置为AT_SIGNATURE,表示生成签名密钥对。不同CSP的密钥交换算法和数字签名算法有所不同。Microsoft Base Cryptographic Provider v1.0、Microsoft Enhanced Cryptographic Provider v1.0、Microsoft Strong Cryptographic Provider和Microsoft Enhanced RSA and AES Cryptographic Provider的密钥交换算法ID为CALG_RSA_KEYX(值为0x0000A400,RSA_KEYX算法),数字签名算法ID为CALG_RSA_SIGN(值为0x00002400,RSA_SIGN算法);Microsoft Base DSS and Diffie-Hellman Cryptographic Provider、Microsoft Enhanced DSS and Diffie-Hellman Cryptographic Provider和Microsoft DH SChannel Cryptographic Provider的密钥交换算法ID为CALG_DH_SF(值为0x0000AA01,DH_KEYX算法),数字签名算法ID为CALG_DSS_SIGN(值为0x00002200,DSA_SIGN算法)。程序可以通过调用CryptGetKeyParam函数并为dwParam参数指定KP_ALGID标志以获取密钥所用于的算法ID。

● _In_ DWORD dwFlags

dwFlags参数的高16位用于指定密钥长度,低16位可以设置为0或一些标志的组合。

假设需要生成2048(0x0800)位的RSA签名密钥对,可以将该参数设置为0x08000000,如果需要,再按位或上一些标志(低16位所用的那些标志)。如果将该参数的高16位设置为0,表示使用默认密钥长度。默认CSP和默认密钥长度可能会在不同的操作系统版本之间发生变化,因此,加密和解密时应该使用相同的CSP,并且始终通过dwFlags参数显式设置一个明确的密钥长度,以确保在不同操作系统平台上的互操作性。

该参数的低16位可以设置为0或一些标志的组合。一些常用的标志及含义如下表所示。

标志含义
CRYPT_ARCHIVABLE新生成的密钥可以进行导出,直到调用CryptDestroyKey函数释放密钥句柄为止,密钥句柄释放后,无法再导出密钥。大部分CSP都不支持该标志,函数调用会失败(错误代码NTE_BAD_FLAGS)。如果需要导出密钥,可以使用下面的CRYPT_EXPORTABLE标志
CRYPT_EXPORTABLE设置该标志后,可以通过调用CryptExportKey函数将密钥导出为密钥BLOB格式,否则,密钥不可导出。对于对称密钥来说,通常需要设置该标志,否则对称密钥仅在当前程序会话中可用;对于密钥对来说,如果需要进行传输或备份,则可以设置该标志。注意,密钥对中的公钥是始终可以导出的,不受该标志的影响
CRYPT_USER_PROTECTED设置中等密钥保护。设置该标志后,当某些操作试图使用此密钥时,会通过对话框或其他方式通知用户。需要注意的是,具体保护行为由所使用的CSP决定。如果获取CSP句柄时CryptAcquireContext函数的dwFlags参数指定了CRYPT_SILENT标志,那么指定该标志会导致CryptGenKey函数调用失败(错误代码NTE_SILENT_CONTEXT)
CRYPT_FORCE_KEY_PROTECTION_HIGH设置强密钥保护。设置该标志后,在生成密钥时,会提示用户为密钥设定一个密码,以后无论何时使用此密钥,都会提示用户输入密码。需要注意的是,具体保护行为由所使用的CSP决定。如果获取CSP句柄时CryptAcquireContext函数的dwFlags参数指定了CRYPT_VERIFYCONTEXT或CRYPT_SILENT标志,那么指定该标志会导致CryptGenKey函数调用失败(错误代码NTE_SILENT_CONTEXT)

● _Out_ HCRYPTKEY* phKey

函数执行成功,会通过phKey参数指向的HCRYPTKEY类型变量返回一个密钥句柄。当不再需要返回的密钥句柄时,应该调用CryptDestroyKey函数将其释放。

函数执行成功,返回值为TRUE;函数执行失败,则返回值为FALSE,可以通过调用GetLastError函数获取错误代码。几个常见的错误代码及含义:NTE_BAD_ALGID,Algid参数指定了CSP 不支持的算法;NTE_BAD_FLAGS,dwFlags参数包含无效的值;NTE_PERM,调用CryptAcquireContext函数时dwFlags参数指定了CRYPT_VERIFYCONTEXT标志(设置该标志后,无法访问持久私钥),无法生成密钥对;NTE_SILENT_CONTEXT,CSP无法执行该操作,因为要求保持静默。

CryptExportKey函数用于将对称密钥、公钥或密钥对导出为密钥BLOB格式。以后需要时可以通过调用CryptImportKey函数将密钥BLOB导入CSP。CryptExportKey函数原型:

BOOL CryptExportKey(
   _In_      HCRYPTKEY hKey,        // 要导出的密钥的句柄
   _In_      HCRYPTKEY hExpKey,     // 加密密钥句柄,密钥BLOB的密钥数据使用此密钥加密
   _In_      DWORD     dwBlobType,  // 所导出的密钥BLOB的类型
   _In_      DWORD     dwFlags,     // 标志值,通常可以设置为0
   _Out_opt_ BYTE*     pbData,      // 用于接收返回的密钥BLOB的缓冲区
   _Inout_   DWORD*    pdwDataLen); // pbData参数所指向的缓冲区的大小,字节单位

几个参数的说明如下。

● _In_ HCRYPTKEY hExpKey

hExpKey参数用于指定加密密钥句柄,密钥BLOB的密钥数据使用这个密钥进行加密。通过对导出的密钥进行加密,可以确保只有预期用户才能使用所导出的密钥BLOB。需要注意的是,hExpKey和hKey必须来自同一个CSP。

有的CSP可能会修改hExpKey参数,如果稍后还需要使用这个密钥句柄,应该调用CryptDuplicateKey函数复制一份。当不再需要复制的密钥句柄时,应该调用CryptDestroyKey函数将其释放。

● _In_ DWORD dwBlobType

dwBlobType参数用于指定所导出的密钥BLOB的类型。常用的表示密钥BLOB类型的标志及含义如下表所示。

标志含义
SIMPLEBLOB将对称密钥导出为简单密钥BLOB,hExpKey参数必须设置为一个公钥句柄。注意,这里的公钥句柄就是指交换密钥对句柄,函数会使用密钥对中的公钥加密对称密钥
PLAINTEXTKEYBLOB将对称密钥导出为明文形式的密钥BLOB(明文密钥BLOB),hExpKey参数必须设置为NULL
OPAQUEKEYBLOB以特定于CSP的格式导出对称密钥,具体格式由CSP决定,以后只能导入同一个CSP
PUBLICKEYBLOB导出为公钥BLOB,hExpKey参数必须设置为NULL
PRIVATEKEYBLOB导出为私钥BLOB,hExpKey参数可以设置为一个对称密钥句柄。即使导出为私钥BLOB,有的CSP也允许hExpKey参数设置为NULL,这种情况下,会导出明文形式的私钥BLOB,应用程序可以再手动加密以施加保护

● _In_ DWORD dwFlags

dwFlags参数是一个标志值,通常可以设置为0;如果需要,可以设置为一些标志的组合。可以使用的标志及含义:CRYPT_BLOB_VER3,导出BLOB V3类型(默认是V2);CRYPT_DESTROYKEY,销毁OPAQUEKEYBLOB中的原始密钥,仅用于Schannel类型的CSP;CRYPT_OAEP,导出SIMPLEBLOB类型时,使用PKCS#1 V2填充(RSA);CRYPT_SSL2_FALLBACK,RSA加密分组填充的前8个字节设置为0x03,不能设置为随机数据,这可以防止版本回退攻击,仅用于Schannel类型的CSP。

● _Out_opt_ BYTE* pbData

pbData参数指向的缓冲区用于接收返回的密钥BLOB。为了分配合适大小的缓冲区,可以进行两次函数调用。程序可以通过调用相关文件函数将返回的密钥BLOB数据写入文件以保存。

● _Inout_ DWORD* pdwDataLen

pdwDataLen参数用于指定pbData参数所指向的缓冲区的大小,字节单位。函数执行成功,该参数指向的DWORD类型变量的值会更新为实际返回的密钥BLOB的大小。

函数执行成功,返回值为TRUE;函数执行失败,则返回值为FALSE,可以通过调用GetLastError函数获取错误代码。

一些常见的错误代码及含义如下表所示。

错误代码含义
NTE_BAD_KEYhKey和hExpKey参数指定的一个或两个密钥句柄无效
NTE_BAD_DATACSP不支持与要导出的公钥一起使用的算法,或者试图导出使用非公钥加密的对称密钥
NTE_BAD_KEY_STATE没有导出密钥的权限,即生成hKey时没有设置CRYPT_EXPORTABLE标志
NTE_BAD_PUBLIC_KEYhExpKey参数指定的公钥句柄无效
NTE_NO_KEY导出对称密钥时hExpKey参数没有指定公钥句柄

现在,读者一定很好奇密钥BLOB是什么样的?为了读者编程方便,下面给出简单密钥BLOB、明文密钥BLOB、私钥BLOB和公钥BLOB的数据结构定义。需要注意的是,Windows头文件中并没有这几个结构体的定义,需要时请自行定义。

typedef struct _SIMPLEKEYBLOBSTRUC {      // 结构大小0x10C字节
   PUBLICKEYSTRUC  publicKeyStruc;        // PUBLICKEYSTRUC结构
   ALG_ID          algID;                 // 加密对称密钥的算法ID,例如CALG_RSA_KEYX
   BYTE encryptedKey[RSA_KEY_LEN / 8];    // 已加密的对称密钥(已加密)
}SIMPLEKEYBLOBSTRUC, *PSIMPLEKEYBLOBSTRUC;

typedef struct _PLAINTEXTKEYBLOBSTRUC {   // 结构大小0xC字节,rgbKeyData字段不计入大小
   PUBLICKEYSTRUC  publicKeyStruc;        // PUBLICKEYSTRUC结构
   DWORD           dwKeySize;             // 密钥长度,字节单位
   BYTE            rgbKeyData[];          // 未加密的对称密钥
} PLAINTEXTKEYBLOBSTRUC, * PPLAINTEXTKEYBLOBSTRUC;

typedef struct _PRIVATEKEYBLOBSTRUC {     // 结构大小0x494字节
   PUBLICKEYSTRUC  publicKeyStruc;        // PUBLICKEYSTRUC结构
   RSAPUBKEY       rsaPubKey;             // RSAPUBKEY结构(已加密)
   BYTE modulus[RSA_KEY_LEN / 8];         // 模数n(已加密)
   BYTE prime1[RSA_KEY_LEN / 16];         // 大素数p(已加密)
   BYTE prime2[RSA_KEY_LEN / 16];         // 大素数q(已加密)
   BYTE exponent1[RSA_KEY_LEN / 16];      // 指数1,值为d mod (p - 1)(已加密)
   BYTE exponent2[RSA_KEY_LEN / 16];      // 指数2,值为d mod (q - 1)(已加密)
   BYTE coefficient[RSA_KEY_LEN / 16];    // 系数,值为(q的乘法逆) mod p(已加密)
   BYTE privateExponent[RSA_KEY_LEN / 8]; // 私有指数d(已加密)
}PRIVATEKEYBLOBSTRUC, *PPRIVATEKEYBLOBSTRUC;

typedef struct _PUBLICKEYBLOBSTRUC {      // 结构大小0x114字节
   PUBLICKEYSTRUC  publicKeyStruc;        // PUBLICKEYSTRUC结构
   RSAPUBKEY       rsaPubKey;             // RSAPUBKEY结构
   BYTE modulus[RSA_KEY_LEN / 8];         // 模数n
}PUBLICKEYBLOBSTRUC, * PPUBLICKEYBLOBSTRUC;

这4个数据结构中标注“(已加密)”的字段表示其内容已经被加密,其他所有没有标注的字段都是没有加密的。PLAINTEXTKEYBLOBSTRUC结构的最后一个字段rgbKeyData是不完整类型(Incomplete Type),rgbKeyData数组称为灵活数组(Flexible Array)。不完整类型不占用结构体变量的空间,sizeof(PLAINTEXTKEYBLOBSTRUC)的值为12。

上面的数据结构中用到了PUBLICKEYSTRUC和RSAPUBKEY这两个结构,它们在wincrypt.h头文件中定义。

typedef struct _PUBLICKEYSTRUC { // 结构大小8字节
   BYTE   bType;        // 密钥BLOB的类型,参见CryptExportKey函数的dwBlobType参数
   BYTE   bVersion;     // 密钥BLOB的版本,2或3,3是数字签名标准(DSS)
   WORD   reserved;     // 保留字段,值为0
   ALG_ID aiKeyAlg;     // 密钥BLOB中的密钥所用于的加密算法
} BLOBHEADER, PUBLICKEYSTRUC;

typedef struct _RSAPUBKEY {      // 结构大小12字节
   DWORD magic;         // 公钥BLOB为RSA1(0x31415352),私钥BLOB为RSA2(0x32415352)
   DWORD bitlen;        // 模数n的位数,必须是8的倍数
   DWORD pubexp;        // 公共指数e
} RSAPUBKEY;

简单密钥BLOB指的是用交换密钥对的公钥加密的对称密钥的BLOB,用于存储对称密钥或将对称密钥传输给另一个用户。对称密钥还可以导出为明文密钥BLOB,调用CryptExportKey函数时将dwBlobType参数设置为PLAINTEXTKEYBLOB,同时hExpKey参数设置为NULL即可。私钥BLOB指的是完整密钥对的BLOB,通常会使用对称密码进行加密,用于将密钥对传输给另一个用户;如果希望导出明文形式的密钥对信息,调用CryptExportKey函数时将hExpKey参数设置为NULL即可,前提是所使用的CSP支持。公钥BLOB指的是密钥对中的公钥的BLOB,公钥BLOB不会被加密,因为公钥不需要保密。

在GenAndExportKey项目中,调用CryptGenKey函数生成一个DES对称密钥,分别通过调用CryptGetUserKey函数获取密钥容器中用户的交换密钥对和签名密钥对,如果不存在,则分别通过调用CryptGenKey函数生成一个。然后,通过调用CryptExportKey函数,分别将DES对称密钥导出为简单密钥BLOB和明文密钥BLOB,分别将交换密钥对导出为公钥BLOB、私钥BLOB(通过对称密码加密)和私钥BLOB(未通过对称密码加密),也同样将签名密钥对导出为这3种格式。程序还对导出的8个密钥BLOB分别进行了打印输出和存盘(保存到和可执行文件相同的目录下)。

上面列出的4种密钥BLOB结构,在将RSA_KEY_LEN常量设置为2048的情况下,同时给出了每种密钥BLOB的结构大小,这是通过sizeof运算符计算的结果。其中,sizeof(PRIVATEKEYBLOBSTRUC) = 0x494,所导出的私钥BLOB(未通过对称密码加密)的实际大小为 0x494,这和结构大小一致;但是,所导出的私钥BLOB(通过对称密码加密)的实际大小为0x498,多了4个字节。这是因为,加密的私钥BLOB会通过对称密码进行加密,如果对称密码是分组密码,默认情况下会采用CBC操作模式和PKCS#5填充模式,如果最后一个加密分组的长度小于分组长度,就需要进行填充。在CryptExportKey函数内部,会调用CryptGetKeyParam函数并为dwParam参数指定KP_BLOCKLEN标志以获取分组密码的分组长度(如果是流密码,获取到的值为0)。

注意,实际上CryptExportKey函数内部调用的并不是CryptGetKeyParam函数,而是间接调用的另一个具有相同功能和类似名称但更为底层的CPGetKeyParam函数,后者是前者的更底层实现。以PROV_RSA_FULL类型的默认CSP为例,CSP对应的DLL为rsaenh.dll。程序中的CryptGetKeyParam函数的调用关系为advapi32.CryptGetKeyParam(CryptGetKeyParamStub) → cryptsp.CryptGetKeyParam → rsaenh.CPGetKeyParam。
CryptExportKey函数的调用关系为advapi32.CryptExportKey(CryptExportKeyStub) → cryptsp.CryptExportKey → rsaenh.CPExportKey。
在CPExportKey函数中,会调用CPGetKeyParam。笔者所说的“在CryptExportKey函数内部,会调用CryptGetKeyParam函数”只是想简化问题,避免分散读者的注意力。但是,这样的说辞有失严谨,以后对于类似问题,我们取一个折中的说法——某某函数的底层函数。例如,在CryptExportKey函数内部,会调用CryptGetKeyParam函数的底层函数。

对于导出的密钥BLOB,以后可以通过调用CryptImportKey函数将其导入CSP。调用CryptImportKey函数时,需要确定所导入密钥BLOB的大小,自己去计算每种密钥BLOB的实际大小比较麻烦,甚至可能会出错,这里介绍一种更简单的方法。将密钥导出为密钥BLOB之后,保存文件之前,先将密钥BLOB的大小(可以是4字节的DWORD类型)写入文件,然后再写入密钥BLOB数据;调用CryptImportKey函数将保存的密钥BLOB导入CSP时,先读取4字节的密钥BLOB大小。

CryptImportKey函数用于将密钥BLOB导入CSP。通过该函数导入到CSP中的交换密钥对和签名密钥对都会同时自动保存到密钥容器中,如果密钥容器中先前已经存在同种密钥规范的密钥对,则会被覆盖。通过该函数导入到CSP中的对称密钥不会保存到密钥容器。函数原型:

BOOL CryptImportKey(
   _In_  HCRYPTPROV  hProv,      // CryptAcquireContext函数返回的CSP句柄
   _In_  CONST BYTE* pbData,     // 要导入的密钥BLOB的缓冲区指针
   _In_  DWORD       dwDataLen,  // 密钥BLOB的大小,字节单位
   _In_  HCRYPTKEY   hPubKey,    // 解密密钥句柄,密钥BLOB的密钥数据使用此密钥解密
   _In_  DWORD       dwFlags,    // 标志值,通常可以设置为0
   _Out_ HCRYPTKEY*  phKey);     // 返回所导入密钥的句柄

几个参数的说明。

● _In_ CONST BYTE* pbData

pbData参数设置为密钥BLOB数据缓冲区的指针。程序可以通过调用ReadFile函数从保存的密钥BLOB文件中读取数据到缓冲区。

● _In_ DWORD dwDataLen

dwDataLen参数用于指定密钥BLOB的大小,字节单位。

● _In_ HCRYPTKEY hPubKey

hPubKey参数用于指定解密密钥句柄,密钥句柄必须来自hProv参数指定的CSP,密钥BLOB的密钥数据使用这个密钥进行解密。当导入已加密的私钥BLOB时,必须通过该参数指定一个有效的对称密钥句柄。当导入简单密钥BLOB时,应该通过该参数指定一个有效的交换密钥对句柄;如果将该参数设置为NULL,函数会使用当前密钥容器中的交换密钥对进行解密,如果交换密钥对不存在或不配套,函数执行会失败。

有的CSP可能会修改hPubKey参数,如果稍后还需要使用这个密钥句柄,应该调用CryptDuplicateKey函数复制一份。当不再需要复制的密钥句柄时,应该调用CryptDestroyKey函数将其释放。

● _In_ DWORD dwFlags

dwFlags参数是一个标志值,通常可以设置为0;如果需要,可以设置为一些标志的组合。几个常用的标志及含义:CRYPT_EXPORTABLE,导入的密钥可以导出,设置该标志后,导入的密钥可以通过调用CryptExportKey函数进行导出,否则导出会失败;CRYPT_OAEP,导入SIMPLEBLOB类型时,检查PKCS#1 V2填充;CRYPT_USER_PROTECTED,同CryptGenKey函数中dwFlags参数的同名标志,设置中等密钥保护。

● _Out_ HCRYPTKEY* phKey

函数执行成功,会通过phKey参数指向的HCRYPTKEY类型变量返回所导入密钥的句柄。当不再需要返回的密钥句柄时,应该调用CryptDestroyKey函数将其释放。

函数执行成功,返回值为TRUE;函数执行失败,则返回值为FALSE,可以通过调用GetLastError函数获取错误代码。

一些常见的错误代码及含义如下表所示。

错误代码含义
NTE_BAD_ALGID要导入的简单密钥BLOB没有使用预期的密钥交换算法加密
NTE_BAD_DATA不支持与要导入的公钥一起使用的算法,或者试图导入不是通过当前密钥容器中交换密钥对加密的对称密钥,或者导入已加密的私钥BLOB时没有指定对称密钥句柄
NTE_NO_KEY密钥不存在,密钥容器中没有用于解密简单密钥BLOB的交换密钥对

CryptDuplicateKey函数可以精确地复制密钥和密钥的状态(State)。通俗地说,CryptDuplicateKey函数用于复制一份密钥句柄,包括密钥和密钥的状态。状态是指与加密实体(例如密钥对象或散列对象等)关联的所有持久属性的集合,可以包括正在使用的初始化向量、密码算法以及已经计算过的值等。读者只需要明白三点:在使用方面,复制的密钥句柄和原密钥句柄没有区别;复制的密钥句柄和原密钥句柄相互独立,它们之间不会共享状态,释放一个密钥句柄不会影响到另一个密钥句柄的使用;当不再需要复制的密钥句柄时,同样应该调用CryptDestroyKey函数将其释放。函数原型:

BOOL CryptDuplicateKey(
   _In_       HCRYPTKEY  hKey,         // 要复制的密钥句柄
   _Reserved_ DWORD*     pdwReserved,  // 保留参数,必须设置为NULL
   _In_       DWORD      dwFlags,      // 保留参数,必须设置为0
   _Out_      HCRYPTKEY* phKey);       // 返回hKey的副本

CryptGenRandom函数用于生成指定字节的随机数。通常用于生成随机的初始化向量和盐值(Salt Value)。函数原型:

BOOL CryptGenRandom(
   _In_    HCRYPTPROV hProv,     // CryptAcquireContext函数返回的CSP句柄
   _In_    DWORD      dwLen,     // 生成的随机数的字节数
   _Inout_ BYTE*      pbBuffer); // 接收生成的随机数的缓冲区,缓冲区大小至少为dwLen

pbBuffer参数是用于接收生成的随机数的缓冲区。如果应用程序可以访问到一个好的随机源,那么可以在调用该函数之前用一些随机数据填充pbBuffer缓冲区,函数会根据这些数据进一步随机化其内部种子;否则,可以省略掉初始化pbBuffer缓冲区的步骤。

软件随机数生成器的工作原理基本相同,从一个称为种子的随机数开始,然后基于种子生成伪随机数序列。软件随机数生成器中最困难的部分是获得一个真正随机的种子,这通常可以基于用户输入延迟,或者来自硬件组件的抖动等。与软件随机数生成器对应的,还有一个硬件随机数生成器,但目前所有CSP均不支持。很明显,CryptGenRandom函数属于软件随机数生成器,生成的是伪随机数,但是,比典型的软件随机数生成器(例如C编译器提供的生成器,srand和rand函数)生成的伪随机数更具随机性,该函数产生的伪随机数是密码学安全伪随机数。所谓密码学安全伪随机数是指根据随机算法和随机样本的一部分,不能有效地演算出随机样本的剩余部分。

关于对称密钥,除了可以通过调用CryptGenKey函数生成一个随机对称密钥,还可以通过调用CryptDeriveKey函数基于基础数据(Base Data,例如用户输入的密钥)派生出一个对称密钥。只要使用相同的CSP和基础数据,CryptDeriveKey函数就会产生相同的对称密钥。CryptDeriveKey函数仅用于派生对称密钥。函数原型:

BOOL CryptDeriveKey(
   _In_  HCRYPTPROV hProv,       // CryptAcquireContext函数返回的CSP句柄
   _In_  ALG_ID     Algid,       // 生成的对称密钥所用于的对称密码算法
   _In_  HCRYPTHASH hBaseData,   // 一个散列对象的句柄
   _In_  DWORD      dwFlags,     // 高16位指定密钥长度,低16位可以设置为0或标志的组合
   _Out_ HCRYPTKEY* phKey);      // 返回一个密钥句柄

该函数大部分参数的用法和CryptGenKey函数是一样的。下面仅对hBaseData和dwFlags两个参数作出一些说明。

● _In_ HCRYPTHASH hBaseData

hBaseData参数设置为一个散列句柄。调用CryptDeriveKey函数之前,程序需要调用CryptCreateHash函数创建一个散列对象(函数会返回一个散列句柄),调用CryptHashData函数将基础数据添加到散列对象中,然后将散列句柄传递给CryptDeriveKey函数。在CryptDeriveKey函数内部,会调用CryptGetHashParam函数的底层函数获取基础数据的散列值,然后从散列值中取所需的字节数作为对称密钥,如果密钥长度大于散列值的长度,那么还需要对散列值进行一些变换以生成所需字节数的对称密钥。计算散列值一节会详细介绍上面3个散列函数。

● _In_ DWORD dwFlags

dwFlags参数的高16位用于指定密钥长度;低16位可以设置为0或一些标志的组合。可以使用的标志有CRYPT_EXPORTABLE、CRYPT_CREATE_SALT、CRYPT_NO_SALT、CRYPT_UPDATE_KEY和CRYPT_SERVER。

函数执行成功,会通过phKey参数指向的HCRYPTKEY类型变量返回一个密钥句柄。当不再需要返回的密钥句柄时,应该调用CryptDestroyKey函数将其释放。

在ImportAndExportKey项目中,将GenAndExportKey程序导出的部分密钥BLOB通过调用CryptImportKey函数进行了导入,并再次导出。

如果重装系统或误删除密钥容器,其中保存的密钥对自然也会消失,保险起见,可以将密钥对导出为加密的私钥BLOB以备份。加密的私钥BLOB是使用对称密码加密的,调用CryptImportKey函数导入时需要有效的对称密钥句柄。因此,导出和导入私钥BLOB时,可以总是基于相同的基础数据(用户输入的同一个密钥)派生对称密钥,以确保可以正确解密私钥BLOB。

CryptGetKeyParam函数用于获取密钥的指定属性。函数原型:

BOOL CryptGetKeyParam(
   _In_      HCRYPTKEY hKey,        // 密钥句柄
   _In_      DWORD     dwParam,     // 表示要获取的属性的常量标志
   _Out_opt_ BYTE*     pbData,      // 接收返回的属性值的缓冲区指针
   _Inout_   DWORD*    pdwDataLen,  // pbData所指向的缓冲区的大小,可以两次函数调用
   _In_      DWORD     dwFlags);    // 保留参数,必须设置为0

dwParam参数用于指定要获取的属性。

对于所有密钥类型,常用的标志如下所示。

标志含义
KP_ALGID获取密钥所用于的算法ID。函数返回后,pbData参数是一个表示算法ID的ALG_ID类型值的指针
KP_BLOCKLEN当hKey参数设置为对称密钥句柄时,用于获取对称密码的分组长度(位数),对于流密码来说,获取到的值为0;当hKey参数设置为密钥对句柄时,用于获取公钥密码的加密粒度(位数),如果这个公钥密码不支持加密,那么获取到的值是未定义的。这两种情况下,函数返回后,pbData参数是一个表示分组长度/加密粒度的DWORD类型值的指针
KP_KEYLEN获取密钥的实际长度(位数)。函数返回后,pbData参数是一个表示密钥实际长度的DWORD类型值的指针。这里获取到的密钥长度和调用CryptGetProvParam函数时(为dwParam参数指定PP_ENUMALGS或PP_ENUMALGS_EX标志)获取到的长度不同,例如,通过CryptGetProvParam函数获取到的DES、3DES TWO KEY和3DES的密钥长度分别为56、112和168位,而这里获取到的则分别为64、128和192位,包含了奇偶校验位
KP_PERMISSIONS获取密钥权限。函数返回后,pbData参数是一个表示密钥权限的DWORD类型值的指针。密钥权限值可以是0,也可以是一些权限标志的组合。常用的权限标志及含义:CRYPT_READ,允许读取属性值;CRYPT_WRITE,允许设置属性值;CRYPT_ENCRYPT,允许加密;CRYPT_DECRYPT,允许解密;CRYPT_EXPORT,允许导出;CRYPT_EXPORT_KEY,允许密钥用于导出其他密钥;CRYPT_IMPORT_KEY,允许密钥用于导入其他密钥;CRYPT_MAC,允许密钥用于消息鉴别码(MAC)

如果hKey参数指定的是用于对称密码的对称密钥句柄,常用的标志如下所示。

标志含义
KP_IV获取初始化向量。初始化向量和和对称密码的分组长度相同。函数返回后,pbData参数是一个表示初始化向量的字节数组指针
KP_MODE获取操作模式。函数返回后,pbData参数是一个表示操作模式的DWORD类型值的指针。返回的DWORD值可以是CRYPT_MODE_ECB、CRYPT_MODE_CBC、CRYPT_MODE_CFB、CRYPT_MODE_OFB或CRYPT_MODE_CTS(密文窃取模式)
KP_PADDING获取填充模式。函数返回后,pbData参数是一个表示填充模式的DWORD类型值的指针。对称密码通常都是采用PKCS#5填充模式,因此返回的DWORD值为PKCS5_PADDING(1)
KP_MODE_BITS获取采用CFB或OFB模式时的反馈位数(处理单位)。函数返回后,pbData参数是一个表示反馈位数的DWORD类型值的指针

如果hKey参数指定的是用于数字签名标准算法(DSA)的密钥句柄,可以使用的标志如下所示。

标志含义
KP_P获取模素数p。函数返回后,pbData参数是一个表示模素数p的小尾方式值的指针
KP_Q获取模素数q。函数返回后,pbData参数是一个表示模素数q的小尾方式值的指针
KP_G获取生成器g。函数返回后,pbData参数是一个表示生成器g的小尾方式值的指针

如果hKey参数指定的是用于Diffie-Hellman算法或数字签名标准算法(DSA)的密钥句柄,可以使用KP_VERIFY_PARAMS标志,以验证Diffie-Hellman或DSA密钥的参数,这种情况下不使用pbData和pdwDataLen两个参数。如果密钥参数有效,函数返回值为TRUE,否则函数返回FALSE。

CryptSetKeyParam函数用于设置密钥的指定属性。该函数对于密钥属性的修改不会被持久化,仅在当前程序会话中有效。例如,在为对称密码生成对称密钥时,默认的初始化向量为0,程序可以通过调用CryptGenRandom函数生成所需字节的随机数,并将其设置为对称密钥的初始化向量。通常不允许修改密钥对的属性。函数原型:

BOOL CryptSetKeyParam(
   _In_ HCRYPTKEY   hKey,        // 密钥句柄
   _In_ DWORD       dwParam,     // 表示要设置的属性的常量标志
   _In_ CONST BYTE* pbData,      // 新的属性值的缓冲区指针
   _In_ DWORD       dwFlags);    // 通常可以设置为0

当dwParam参数设置为KP_ALGID以设置对称密钥所用于的对称密码算法ID时,dwFlags参数的含义和CryptGenKey函数的同名参数的含义相同,高16位指定密钥长度,低16位可以设置为0或标志的组合。其他情况下,不使用dwFlags参数。

1.1.3 加密解密

了解了一些基本概念和基础操作后,至于具体使用CSP提供的各种密码算法,笔者认为还是比较简单的。本节学习数据加密解密操作。

假设发信者已经获得了收信者的交换密钥对的公钥,可以按如下步骤进行对称密钥交换和发信操作。

  1. 发信者通过调用CryptGenKey函数生成一个随机对称密钥;
  2. 发信者使用对称密码加密消息;
  3. 发信者通过调用CryptExportKey函数将对称密钥导出为简单密钥BLOB,其中函数调用的hExpKey参数设置为收信者的交换密钥对公钥句柄;
  4. 发信者将已经加密的消息和简单密钥BLOB发送给收信者;
  5. 收信者通过调用CryptImportKey函数将简单密钥BLOB导入CSP,其中函数调用的hPubKey参数设置为自己的交换密钥对私钥句柄;
  6. 收信者使用对称密码解密消息。

CryptEncrypt函数用于对数据进行加密。如果为CryptEncrypt函数的hHash参数指定一个散列句柄,那么在对数据进行加密的同时,还可以计算数据的散列值。函数原型:

BOOL CryptEncrypt(
   _In_        HCRYPTKEY  hKey,        // 加密密钥句柄
   _In_opt_    HCRYPTHASH hHash,       // 散列句柄,如果不需要,可以设置为NULL
   _In_        BOOL       bFinal,      // 是否是待加密数据的最后一段,TRUE或FALSE
   _In_        DWORD      dwFlags,     // 保留参数
   _Inout_opt_ BYTE*      pbData,      // 待加密数据缓冲区的指针
   _Inout_     DWORD*     pdwDataLen,  // pbData缓冲区中待加密数据的大小,字节单位
   _In_        DWORD      dwBufLen);   // pbData缓冲区的大小,字节单位

各个参数的说明如下。

● _In_ HCRYPTKEY hKey

hKey参数用于指定加密密钥句柄。程序可以通过调用CryptGetUserKey、CryptGenKey、CryptDeriveKey或CryptImportKey等函数获取密钥句柄。调用CryptGenKey或CryptDeriveKey函数生成密钥时,已经指定密钥所用于的加密算法,加密算法ID会与密钥捆绑,因此这里不需要再次指定。

可以将该参数设置为一个对称密钥句柄,以使用对称密码进行加密。对于支持RSA_KEYX算法的CSP,还可以使用RSA公钥加密和RSA私钥解密,加密时使用PKCS#1填充,解密时,会验证填充。使用RSA公钥密码时,每次可以加密的明文数据的长度不能大于模数的长度减11字节,这11字节是PKCS#1填充的最小值,密文以小尾格式返回。

● _In_opt_ HCRYPTHASH hHash

如果需要同时计算数据的散列值,那么可以为hHash参数指定一个散列句柄。调用CryptEncrypt函数之前,需要通过调用CryptCreateHash函数创建一个散列对象(函数会返回一个散列句柄),然后将散列句柄传递给CryptEncrypt函数;在CryptEncrypt函数内部,会调用CryptHashData函数的底层函数将数据添加到散列对象中;当所有数据都加密完毕后,程序可以通过调用CryptGetHashParam函数获取最终散列值,还可以通过调用CryptSignHash函数对最终散列值进行数字签名等。如果不需要同时计算数据的散列值,可以将该参数设置为NULL。

● _In_ BOOL bFinal

bFinal参数用于指定是否是待加密数据的最后一段。如果需要加密大量数据,并且是使用分组密码,可以通过循环调用CryptEncrypt函数来分段完成,每次加密的数据长度为分组长度的整数倍。例如,假设需要加密99.9MB的数据,可以每次加密1MB(假设1MB是分组长度的整数倍),前99次函数调用时都将该参数设置为FALSE;加密最后0.9MB时,必须将该参数设置为TRUE,函数会将这最后一段填充为分组长度的整数倍,假设已经是分组长度的整数倍,那么会额外填充一个分组。如果是CBC操作模式,以后基于相同密钥的CryptEncrypt函数调用,反馈寄存器会被重置为初始状态。如果是流密码,加密最后一段时,也必须将该参数设置为TRUE,以后基于相同密钥的CryptEncrypt函数调用,相关状态会被重置为初始状态。需要注意的是,将该参数设置为FALSE时,必须确保通过*pdwDataLen指定的待加密数据的字节数为分组长度的整数倍。

● _Inout_opt_ BYTE* pbData

pbData参数设置为待加密数据缓冲区的指针。函数执行成功,对应的密文会写入该缓冲区。这种明文和生成的密文使用同一个缓冲区的方法称为就地加密。

● _Inout_ DWORD* pdwDataLen

pdwDataLen参数用于指定pbData缓冲区中待加密数据的字节数。函数执行成功,该参数指向的DWORD类型变量的值更新为实际写入pbData缓冲区的密文的字节数。

● _In_ DWORD dwBufLen

dwBufLen参数用于指定pbData缓冲区的大小,字节单位。

如果是分组密码,并且是最后一个分段,因为需要填充,所以返回的密文会比原始明文大,最多会大一个分组长度。如果将pbData参数设置为NULL,其他参数正常设置,函数会通过pdwDataLen参数所指向的DWORD类型变量返回密文所需的缓冲区大小。如果pbData缓冲区的大小不足以容纳返回的密文,函数调用会失败,错误代码为ERROR_MORE_DATA,这时,函数会通过pdwDataLen参数所指向的DWORD类型变量返回密文所需的缓冲区大小。如果是流密码,密文和明文的长度通常是一样的。如果觉得两次函数调用太麻烦,可以使用下面的方法。

为了提高效率,可以采用分段加密,即每次调用CryptEncrypt函数加密一个分段长度(分组长度的整数倍)的数据。程序可以通过调用CryptGetKeyParam函数并为dwParam参数指定KP_BLOCKLEN标志以获取分组密码的分组长度(如果是流密码,获取到的值为0),设获取到的分组长度为dwBlockLen,分段长度设置为dwSectionLen,将缓冲区的大小分配为dwSectionLen + dwBlockLen,这个缓冲区大小可以满足任何一段的加密需求。

例如下面的代码,这里省略了一些不重要的代码。

#define ONE_MB (1024 * 1024)

DWORD dwSectionLen  = 0;      // 分段长度
PBYTE pbBuf         = NULL;   // 缓冲区
DWORD dwBufLen      = 0;      // 缓冲区大小
DWORD dwBytes       = 0;      // 实际读取写入的字节数
BOOL  bFinal        = FALSE;  // 是否是最后一段

dwSectionLen = ONE_MB - ONE_MB % dwBlockLen;
if (dwBlockLen > 1)
   dwBufLen = dwSectionLen + dwBlockLen;
else
   dwBufLen = dwSectionLen;

pbBuf = new BYTE[dwBufLen];

do 
{
   ReadFile(hFileSour, pbBuf, dwSectionLen, &dwBytes, NULL);
   if (dwBytes < dwSectionLen)
      bFinal = TRUE;

   CryptEncrypt(hSessionKey, NULL, bFinal, 0, pbBuf, &dwBytes, dwBufLen);

   WriteFile(hFileDest, pbBuf, dwBytes, &dwBytes, NULL);
} while (!bFinal);

delete[]pbBuf;

如果待加密文件的长度正好是分段长度dwSectionLen的整数倍,那么ReadFile函数最后一次读取到的字节数为0。这没关系,如果CryptEncrypt函数的*pdwDataLen的值为0,并且bFinal参数设置为TRUE,这种情况下,函数会额外填充一个分组,并生成一个分组长度的密文,这部分密文也必须保存到文件中,以免以后无法正确解密。

CryptEncrypt函数不是线程安全的(可能和反馈寄存器的状态更新有关),如果在多线程环境中调用该函数,可能会返回不正确的加密结果。
实际上,对于CryptoAPI和下一代密码学编程接口CNG来说,如果是多线程编程,任何同时修改同一内存区域的API都不是线程安全的。

CryptDecrypt函数用于解密数据。函数原型:

BOOL CryptDecrypt(
   _In_     HCRYPTKEY  hKey,           // 解密密钥句柄
   _In_opt_ HCRYPTHASH hHash,          // 散列句柄,如果不需要,可以设置为NULL
   _In_     BOOL       bFinal,         // 是否是待解密数据的最后一段,TRUE或FALSE
   _In_     DWORD      dwFlags,        // 标志值,通常可以设置为0
   _Inout_  BYTE*      pbData,         // 待解密数据缓冲区的指针
   _Inout_  DWORD*     pdwDataLen);    // pbData缓冲区中待解密数据的大小,字节单位

该函数各个参数的用法和CryptEncrypt函数是类似的,这里不再重复。

如果需要接着计算已解密数据的散列值,那么可以为hHash参数指定一个散列句柄。当所有数据解密完毕时,可以通过调用CryptGetHashParam函数获取最终散列值,还可以通过调用CryptVerifySignature函数验证数字签名等。如果不需要同时计算已解密数据的散列值,可以将hHash参数设置为NULL。

在EncryptDecrypt项目中,用户可以选择一个文件进行加密,也可以选择一个已经加密的文件进行解密,所用的加密算法为AES-256。加密时,用户必须输入一个加密密钥,程序会根据用户输入的加密密钥派生对称密钥;默认的初始化向量为0,程序通过调用CryptGenRandom函数生成一个分组长度的随机数,调用CryptSetKeyParam函数将其设置为新的初始化向量;导出对称密钥为简单密钥BLOB,在将导出的密钥BLOB写入文件之前,先分别写入4字节的密钥BLOB长度、4字节的分组长度和一个分组长度的初始化向量。解密时,可以根据用户输入的加密密钥重新派生对称密钥,但是因为加密时已经保存了密钥BLOB及一些相关信息,因此这里选择通过读取文件的方式导入对称密钥。

1.1.4 计算散列值

计算数据散列值的基本步骤如下。

  1. 通过调用CryptCreateHash函数启动数据的散列操作,该函数会创建并返回一个散列句柄,返回的句柄可以用于后续调用CryptHashData和CryptHashSessionKey等函数。
  2. 通过调用CryptHashData函数将数据添加到散列对象以计算散列值。CryptHashData函数类似于密码散列函数中的压缩函数,如果需要计算大块数据的散列值,可以通过循环调用该函数来分段完成。
  3. 将所有数据都添加到散列对象后,可以执行以下操作:通过调用CryptGetHashParam函数获取最终散列值;通过调用CryptDeriveKey函数派生对称密钥;通过调用CryptSignHash函数对散列值进行数字签名;通过调用CryptVerifySignature函数验证数字签名。需要注意的是,调用上面4个函数中的任意一个后,无法再调用CryptHashData或CryptHashSessionKey函数继续添加数据,因为散列对象的状态已经改变,但可以重复调用上面4个函数。如果稍后还需要使用原散列句柄,可以通过调用CryptDuplicateHash函数复制一份散列句柄,函数会同时复制散列对象的状态。
  4. 调用CryptDestroyHash函数释放散列句柄。

CryptCreateHash函数用于启动数据的散列操作,函数会创建并返回一个散列句柄。返回的句柄可以用于后续的CryptHashData函数调用,将数据添加到散列对象以计算散列值;还可以用于CryptHashSessionKey函数调用,将对称密钥添加到散列对象以计算散列值。函数原型:

BOOL CryptCreateHash(
   _In_  HCRYPTPROV  hProv,      // CryptAcquireContext函数返回的CSP句柄
   _In_  ALG_ID      Algid,      // 一个ALG_ID值,用于指定要使用的散列算法
   _In_  HCRYPTKEY   hKey,       // 一个对称密钥句柄,如果不需要,应该设置为NULL
   _In_  DWORD       dwFlags,    // 保留参数,必须设置为0
   _Out_ HCRYPTHASH* phHash);    // 返回一个散列句柄

如果所选择的散列算法的类型是密钥散列算法(Keyed Hash Algorithm),例如消息鉴别码(MAC)或基于散列的消息鉴别码(HMAC)算法,那么需要通过hKey参数指定一个对称密钥句柄;否则,必须将该参数设置为NULL。

函数执行成功,会通过phHash参数所指向的HCRYPTHASH类型变量返回一个散列句柄。当不再需要返回的散列句柄时,应该调用CryptDestroyHash函数将其释放。

CryptHashData函数用于将数据添加到指定的散列对象以计算散列值。如果需要计算大块数据的散列值,可以通过循环调用该函数来分段完成。如果程序需要,还可以通过调用该函数将不连续的数据分批添加到散列对象以计算散列值。函数原型:

BOOL CryptHashData(
   _In_ HCRYPTHASH  hHash,       // CryptCreateHash函数返回的散列句柄
   _In_ CONST BYTE* pbData,      // 要添加到散列对象中的数据的缓冲区指针
   _In_ DWORD       dwDataLen,   // 要添加到散列对象中的数据的大小,字节单位
   _In_ DWORD       dwFlags);    // 保留参数,应该设置为0

循环调用CryptHashData函数分段计算散列值时,分段长度并不要求必须是散列算法分组长度的整数倍。假设设置的分段长度为1字节,通过循环调用CryptHashData函数也可以正确计算大块数据的散列值,函数会累积接收到的数据,达到分组长度才会执行压缩函数。频繁的函数调用会产生较大开销,为了提高效率,建议将分段长度设置为分组长度的整数倍。可以通过调用CryptGetHashParam函数(dwParam参数设置为HP_HASHSIZE)获取散列算法的分组长度。

CryptHashSessionKey函数用于将对称密钥添加到指定的散列对象以计算散列值。应用程序不需要获取具体的对称密钥数据,只需要将密钥句柄传递给函数,函数内部会将明文形式的对称密钥作为二进制数据(不是作为字符数组)以计算散列值。如果程序需要,CryptHashData和CryptHashSessionKey这两个函数可以穿插、混合使用。函数原型:

BOOL CryptHashSessionKey(
   _In_ HCRYPTHASH hHash,        // CryptCreateHash函数返回的散列句柄
   _In_ HCRYPTKEY  hKey,         // 对称密钥句柄
   _In_ DWORD      dwFlags);     // 标志值

dwFlags参数是一个标志值。如果将该参数设置为CRYPT_LITTLE_ENDIAN,密钥的字节以小尾形式进行散列,也就是从低地址开始依次取各个字节,通常应该设置该标志;如果设置为0,密钥的字节以大尾形式进行散列,会将密钥字节进行反转。例如对称密钥“9BAEF4983E76EFAB”,如果将该参数设置为CRYPT_LITTLE_ENDIAN,是对“9BAEF4983E76EFAB”这个二进制串进行散列;如果设置为0,是对“ABEF763E98F4AE9B”这个二进制串进行散列。

CryptGetHashParam函数用于获取指定散列对象的相关数据,包括最终散列值。函数原型:

BOOL CryptGetHashParam(
   _In_      HCRYPTHASH hHash,      // CryptCreateHash函数返回的散列句柄
   _In_      DWORD      dwParam,    // 指定要获取哪项数据的常量标志
   _Out_opt_ BYTE*      pbData,     // 接收返回的数据的缓冲区,数据的形式取决于dwParam
   _Inout_   DWORD*     pdwDataLen, // pbData所指向的缓冲区的大小,可以两次函数调用
   _In_      DWORD      dwFlags);   // 保留参数,必须设置为0

dwParam参数用于指定要获取哪项数据。可以使用的标志及含义:HP_ALGID,获取创建散列对象时指定的散列算法,函数返回后,pbData参数是一个表示算法ID的ALG_ID类型值的指针;HP_HASHSIZE,获取散列值的大小(字节数),这个值因不同的散列算法而异,函数返回后,pbData参数是一个表示散列值大小的DWORD类型值的指针;HP_HASHVAL,获取散列值,函数返回后,pbData参数是一个表示散列值的字节数组指针。在使用HP_HASHVAL标志获取散列值之前,可以先使用HP_HASHSIZE标志获取散列值的大小,以分配合适大小的缓冲区。

CryptSetHashParam函数用于设置指定散列对象的相关数据。函数原型:

BOOL CryptSetHashParam(
   _In_  HCRYPTHASH  hHash,         // CryptCreateHash函数返回的散列句柄
   _In_  DWORD       dwParam,       // 指定要设置哪项数据的常量标志
   _In_  CONST BYTE* pbData,        // 新的数据值的缓冲区
   _In_  DWORD       dwFlags);      // 保留参数,必须设置为0

dwParam参数用于指定要设置哪项数据。可以使用的标志及含义:HP_HMAC_INFO,用于HMAC算法,pbData参数是一个指向HMAC_INFO结构的指针;HP_HASHVAL,设置散列对象的散列值,pbData参数是一个包含散列值的字节数组指针。

CryptDuplicateHash函数用于复制一份散列句柄,包括散列对象的状态,这和CryptDuplicateKey函数的工作原理是类似的。有时候可能有这样的需求,在计算数据散列值的过程中,复制一份散列句柄,然后,继续向原句柄对应的散列对象添加数据,而向复制句柄对应的散列对象添加不同的数据,这样一来,就可以分别计算出开头内容相同但后续内容不同的两份数据的散列值。当不再需要复制的散列句柄时,应该调用CryptDestroyHash函数将其释放。函数原型:

BOOL CryptDuplicateHash(
   _In_       HCRYPTHASH  hHash,          // 要复制的散列句柄
   _Reserved_ DWORD*      pdwReserved,    // 保留参数,必须设置为NULL
   _In_       DWORD       dwFlags,        // 保留参数,必须设置为0
   _Out_      HCRYPTHASH* phHash);        // 返回hHash的副本

关于计算散列值的简单示例程序请参见GetFileHash项目,该程序可以计算用户所选择文件的MD2、MD4、MD5、SHA-1、SHA-256、SHA-384和SHA-512值。程序运行效果如下图所示。
在这里插入图片描述

为了提高效率,本程序采用了多线程技术,借助这个示例程序,讲解一下多线程编程时需要注意的问题。

用户选择一个文件,勾选所需的散列算法,单击“计算散列值”按钮之后,程序进入窗口过程case WM_COMMAND中的case IDC_BTN_GETHASH分支。在该分支中,简单判断一下用户是否已经输入文件名,分别调用IsDlgButtonChecked函数判断每个复选框的选中状态,如果已经选中,就为该复选框对应的散列算法创建一个子线程;然后,对于每个有效的线程句柄,分别调用WaitForSingleObject函数等待对应的子线程执行完毕;最后,显示最终散列值,关闭线程句柄。

在线程函数ThreadProc中,调用CryptCreateHash函数获取散列句柄时需要复选框对应的散列算法ID;调用CryptGetHashParam函数(HP_HASHSIZE)获取到散列值的大小后,散列值大小应该传递回主线程;调用CryptGetHashParam函数(HP_HASHVAL)获取到最终散列值后,最终散列值也应该传递回主线程。为此,主线程中调用CreateThread函数创建子线程时,通过函数的lpParameter参数传递一个自定义结构(HashInfo)变量的指针,HashInfo结构包含复选框对应的散列算法的详细信息。自定义结构HashInfo的定义如下所示。

typedef struct _HashInfo
{
   ALG_ID Algid;                    // 散列算法ID
   DWORD  dwHashSize;               // 散列值的大小
   CHAR   szHashName[16];           // 散列算法名称
   BYTE   bHash[SHA512_HASH_SIZE];  // 散列值,#define SHA512_HASH_SIZE 64
}HashInfo, *PHashInfo;

编译运行程序,程序失去响应!这里分析一下原因。在主线程中,调用CreateThread函数创建子线程,线程函数ThreadProc立即执行,然后主线程分别调用WaitForSingleObject函数等待所有线程函数返回,在所有子线程返回之前,主线程一直停留在case WM_COMMAND中的case IDC_BTN_GETHASH分支不返回。在线程函数ThreadProc中,调用GetWindowText函数获取用户输入的文件名,这本质上是通过向主线程发送WM_GETTEXT消息来实现的,而主线程正在等待子线程返回(正忙着呢),因此WM_GETTEXT消息得不到处理,程序陷入死锁!解决这个问题的一个方法就是删除线程函数中的GetWindowText函数调用,将主线程中获取到的文件名通过结构变量参数传递过来,即HashInfo结构中再添加一个文件名字段。

本程序还利用了线程函数的返回值,如果线程函数执行成功,返回值为1;如果计算散列值的过程中出现错误,则返回值为0。在主线程中通过调用GetExitCodeThread函数获取线程函数的返回值,并作出判断。

笔者选择了一个5.89GB的文件,多线程计算这7个散列值需要11分钟,这期间程序主界面没有响应!实际项目实践中,不应该在主线程中调用WaitForSingleObject函数无限等待,这对用户而言是不友好的。程序可以创建一个子线程ExtraThreadProc,然后在这个子线程中继续创建7个子线程ThreadProc(作为工作线程)以计算散列值,在ExtraThreadProc中等待所有工作线程返回。改进后的程序代码请参见GetFileHash2项目,这次,在ThreadProc中可以通过调用GetWindowText函数获取用户输入的文件名,现在的主线程可以正常响应各种消息。GetFileHash2项目还演示了线程间通信,当ExtraThreadProc工作完成,会向主线程发送自定义消息WM_COMPLEWORK告知自己已经完成工作。这是一个比较典型的多线程编程示例,希望对读者有用。

读者可能会疑惑,7个工作线程中,每一个都调用CreateFile函数打开文件,能不能在ExtraThreadProc中打开文件,然后所有工作线程共用同一个文件句柄呢?答案是可以,但每个线程使用一个文件句柄可能更方便。打开一个文件后,系统会为该文件维护一个文件指针,文件指针是一个64位的偏移值,它记录要读取或写入的下一个字节的位置。打开一个文件时,文件指针位于文件的开头,偏移量为0,每个读取或写入操作都会导致文件指针前进所读取或写入的字节数,例如,如果文件指针位于文件的开头,然后请求5字节的读取操作,那么文件指针将在读取操作之后立即位于偏移量5处。因此在循环读取或写入一个文件的时候,随着数据的不断读取或写入,文件指针会随之移动,程序员不需要关心文件指针的问题。如果多线程共用同一个文件句柄,只有一个公用的文件指针,必然会出现混乱,除非为每个线程记录文件偏移量(只是理论上可行,但笔者没有尝试过),或者使用内存映射文件技术。

最后,介绍一下用于对散列值进行数字签名的CryptSignHash函数,以及用于验证数字签名的CryptVerifySignature函数。

数字签名算法都是公钥密码,计算速度比较慢,因此CryptoAPI不允许直接对数据进行数字签名。程序可以通过调用CryptCreateHash函数获取散列句柄,通过调用CryptHashData(或CryptHashSessionKey)函数向散列对象添加数据,然后通过调用CryptSignHash函数对散列值进行数字签名。

验证数字签名时,首先需要调用CryptCreateHash和CryptHashData(或CryptHashSessionKey)函数重新计算数据的散列值,然后调用CryptVerifySignature函数验证数字签名。CryptVerifySignature函数会使用指定的公钥对数字签名进行解密,得到一个散列值,然后将解密得到的散列值与重新计算得到的散列值进行比较,如果两者相等,则验证通过,函数返回TRUE。

CryptSignHash和CryptVerifySignature这两个函数的函数原型如下。

BOOL CryptSignHash(
   _In_      HCRYPTHASH hHash,         // CryptCreateHash函数返回的散列句柄
   _In_      DWORD      dwKeySpec,     // 密钥规范,AT_KEYEXCHANGE或AT_SIGNATURE
   _In_opt_  LPCTSTR    szDescription, // 该参数不再使用,必须设置为NULL
   _In_      DWORD      dwFlags,       // 标志值,通常可以设置为0
   _Out_opt_ BYTE*      pbSignature,   // 接收签名数据的缓冲区指针,可以两次调用
   _Inout_   DWORD*     pdwSigLen);    // pbSignature所指向的缓冲区的大小,字节单位

BOOL CryptVerifySignature(
   _In_     HCRYPTHASH  hHash,         // CryptCreateHash函数返回的散列句柄
   _In_     CONST BYTE* pbSignature,   // 要验证的签名数据的缓冲区指针
   _In_     DWORD       dwSigLen,      // 要验证的签名数据的长度,字节单位
   _In_     HCRYPTKEY   hPubKey,       // 用于验证签名的公钥句柄
   _In_opt_ LPCTSTR     szDescription, // 该参数不再使用,必须设置为NULL
   _In_     DWORD       dwFlags);      // 标志值,通常可以设置为0

CryptSignHash函数的dwKeySpec参数用于指定密钥规范,可以设置为AT_KEYEXCHANGE,表示使用CSP当前密钥容器中交换密钥对的私钥对散列值进行数字签名,或设置为AT_SIGNATURE,表示使用CSP当前密钥容器中签名密钥对的私钥对散列值进行数字签名。

关于CryptSignHash和CryptVerifySignature这两个函数的简单示例程序请参见SignHashAndVerifySign解决方案下的SignHash和VerifySign两个项目。

在SignHash程序中,用户可以在编辑框中输入1~1023个字符,然后单击“计算散列值并进行数字签名”按钮。对散列值进行数字签名后,程序分别将公钥BLOB、签名数据和原数据这三项的长度,以及这三项数据本身写入文件保存。

用户也可以直接对一个现成的散列值进行数字签名,例如这里有一个SHA-256散列值“73C09BA5FF23AC01F2D6C45D8E10B1FB0E5371338858ACB5272839469D71BA3A”,现在需要生成数字签名。这种情况下,可以通过调用CryptSetHashParam函数(dwParam参数设置为HP_HASHVAL)直接设置散列对象的散列值,而不需要调用CryptHashData或CryptHashSessionKey函数向散列对象添加数据。

在VerifySign程序中,读取保存的文件获取各种信息,导入公钥BLOB,验证签名。

一个解决方案下面创建多个项目的方法,打开VS → 创建新项目(N) → 空白解决方案,单击“下一步”按钮,打开“配置新项目”对话框,输入解决方案名称(例如MySolution)和保存位置,然后单击“创建”按钮,这时会打开VS主界面,在“解决方案资源管理器”选项卡中可以看到刚刚新建的解决方案。现在,新建一个项目,打开“解决方案资源管理器”选项卡,右键“解决方案’MySolution’(0个项目)” → 添加(D) → 新建项目(N),打开“添加新项目”对话框,按照需要创建新项目即可,这里创建的项目名称为MyProject。添加多个项目后,想编译其中一个时,可以打开“解决方案资源管理器”选项卡,右键项目名称(例如MyProject) → 设为启动项目(A)即可。

示例项目下载地址:https://pan.baidu.com/s/1NDZu5KuA24wIJf8v8tdlkQ
提取码:1234

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
经常看到有人要找AES-GCM-128  这个算法加解密 网上相关的文档也较少其中在telegram登录首页就在使用该算法 应该让不少哥们头疼 其实这个加密常见于浏览器内置接口window.crypto.subtle 该接口不仅支持该类型的加密 且支持非常多的算法加密如RSA DES  等等  这里就演示AES-GCM-128 这个类型 crypto-AES-GCM-128调用例子 function ___crypto__test(keyData, iv, data) {     const format = "raw",         // keyData = new Uint8Array([23, 113, 57, 6, 35, -69, -60, 98, 84, -17, -125, -49, 18, 6, -92, 32]),         algorithm = "AES-GCM",         extractable = true,         usages = ["encrypt", "decrypt"];     // iv = new Uint8Array([47, 46, 123, 78, 36, 14, 109, 50, 121, 64, 11, 38]);     window.crypto.subtle.importKey(         format,         keyData,         algorithm,         extractable, usages     ).then(key =gt; {         window.crypto.subtle.encrypt({                 name: algorithm,                 iv: iv             },             key,             data         ).then(result =gt; {             console.log(Array.from(new Uint8Array((result))))         })     }) } console.log(___crypto__test(             new Uint8Array([23, 113, 57, 6, 35, -69, -60, 98, 84, -17, -125, -49, 18, 6, -92, 32]),                 new Uint8Array([47, 46, 123, 78, 36, 14, 109, 50, 121, 64, 11, 38]),             new Uint8Array([50, 49, 48]) )) crypto主要相关接口介绍 crypto.subtle.importKey const result = crypto.subtle.importKey(     format,     keyData,     algorithm,     extractable,     usages ); format  是一个字符串,描述要导入的密钥的数据格式。可以是以下之一:----------raw:原始格式。----------pkcs8:PKCS#8格式。----------spki:SubjectPublicKeyInfo格式。----------jwk:JSON Web密钥格式。 - keyData 是ArrayBuffer,TypedArray,a DataView或JSONWebKey包含给定格式的键的对象。 - algorithm  是一个字典对象,用于定义要导入的密钥的类型并提供额外的算法特定参数。对于RSASSA-PKCS1-v1_5,  RSA-PSS或  RSA-OAEP:传递RsaHashedImportParams对象。对于ECDSA或ECDH:传递  EcKeyImportParams对象。对于HMAC:传递一个HmacImportParams对象。对于AES-CTR,AES-CBC,AES-GCM或AES-KW:传递标识算法的字符串或形式为的对象{ "name": ALGORITHM },其中ALGORITHM 是算法的名称。对于PBKDF2  :传递字符串PBKDF2。 - extractable 是Boolean表明它是否将有可能使用到导出密钥SubtleCrypto.exportKey()或SubtleCrypto.wrapKey()。 - ke

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值