使用 CryptAPI 进行数据加密

CryptAPI 简述

CryptAPI 是微软提供的数据加密套件,使用该套件可以实现对称加密、非对称加密、哈希算法及数字签名等。

CSP 基础

CSP(Cryptographic Service Providers),加密服务提供者,是独立的加密模块。CSP 内部定义了特定的算法(例如 DES、AES 等),实际上数据的加密与解密便是通过 CSP 完成的。

CSP 中包含三个重要的概念:Key Container,Cryptographic Provider Name 和 Cryptographic Provider Type。

  • Key Container:用于存放加密密钥的区域,使用字符串进行标识,每个 CSP 可以包含多个密钥容器
  • Provider Name:Service Provider 的名称,用于区分不同的加密实现模块
  • Provider Type:用于标识 Serivce Provider 提供的功能和加密算法类型

CSP 模块一般以 DLL 的形式存在,并向外暴露统一的函数接口,CryptAPI 通过载入不同的 CSP 模块,调用相同规范的函数,便可以实现各种各样的加密运算。

加密流程

首先,必须通过 CryptAcquireContext 函数来初始化一个 CSP 模块,CryptAcquireContext 函数定义如下:

BOOL WINAPI CryptAcquireContext(
    HCRYPTPROV *phProv,
    LPCTSTR    pszContainer,
    LPCTSTR    pszProvider,
    DWORD      dwProvType,
    DWORD      dwFlags
);

pszContainer 用于指定 CSP 存放密钥的容器名称,可以自定义,也可以传入 NULL 使用默认的密钥容器。pszProvider 和 dwProvType 分别对应 CSP 的名称和类型。例如 MS_DEF_PROV 模块提供了公钥加密算法,其算法类型为 RSA。最后的 dwFlags 可以设置为 0。

函数调用成功后,CSP 模块的句柄会保存到 hProv 参数中。

接着,将用户明文密码转换为 Hash 进行存储,这里的 Hash 算法只用于加密密码,而非用户数据。使用 CryptCreateHash 获取一个 Hash 算法对象:

BOOL WINAPI CryptCreateHash(
    HCRYPTPROV hProv,
    ALG_ID     Algid,
    HCRYPTKEY  hKey,
    DWORD      dwFlags,
    HCRYPTHASH *phHash
);

hProv 为 CryptAcquireContext 获取的 CSP 句柄,Algid 为 Hash 算法类型,hKey 和 dwFlags 默认为 0,成功后 Hash 对象句柄存放于 hHash 中。

获取 Hash 算法对象句柄后,使用 CryptHashData 将密码更新到 Hash 对象中:

BOOL WINAPI CryptHashData(
    HCRYPTHASH hHash,
    BYTE       *pbData,
    DWORD      dwDataLen,
    DWORD      dwFlags
);

得到密文 Hash 后,使用函数 CryptDeriveKey 创建一个密钥对象,该密钥存放于 CSP 的密钥容器中,加密与解密使用该密钥来进行:

BOOL WINAPI CryptDeriveKey(
    HCRYPTPROV hProv,
    ALG_ID     Algid,
    HCRYPTHASH hBaseData,
    DWORD      dwFlags,
    HCRYPTKEY  *phKey
);

Algid 指定一个加密算法,hBaseData 是加密的 Hash 对象,dwFlags 可以设置为 CRYPT_EXPORTABLE,这样后期可以把密钥导出,当然使用原明文密码也是可以解密的。

成功后 hKey 便最终的密钥对象。

跋山涉水获取到密钥对象后,就可以使用 CryptEncrypt 和 CryptDecrypt 对数据进行加密,CryptEncrypt 定义如下:

BOOL WINAPI CryptEncrypt(
    HCRYPTKEY  hKey,
    HCRYPTHASH hHash,
    BOOL       Final,
    DWORD      dwFlags,
    BYTE       *pbData,
    DWORD      *pdwDataLen,
    DWORD      dwBufLen
);

如果要计算数据的签名值,可以传入一个 hHash 对象(同密码 Hash 不是同个对象),否则为 NULL。

Final 指定是否为最后一个数据块,例如分块从文件中读取内容,最后的分块必须将该标志设置为 TRUE。

pbData 和 pdwDataLen 是原始数据内容和大小。

dwFlags 为系统保留,置零。

加密后的数据会覆盖到明文数据区域,即 pbData 指定的内存空间,密文大小到 dwDataLen 参数中,所以必须保证原始内容的内存区域可写,并将其大小指定到 dwBufLen 参数中。

当程序结束前,需要释放各个资源句柄。CryptDestroyHash 和 CryptDestroyKey 用于销毁 Hash 对象和密钥对象,最后,调用 CryptReleaseContext 释放整个 CSP 模块。

Talk is Cheap, Show Me The Code

下面代码通过 CryptAPI ,使用 AES-256 对称加密算法对用户输入的文本进行加密,并以 16 进制的结果进行输出:

#include <Windows.h>
#include <wincrypt.h>
#include <stdio.h>

int main(int argc, char** argv)
{
    int nRet = -1, i;
    HCRYPTPROV hProv;
    HCRYPTKEY hKey;
    HCRYPTHASH hHash;

    DWORD dwDataLen = 0;
    LPBYTE lpBuffer = NULL;
    LPBYTE lpData = NULL, lpPassword = NULL;
    
    if (argc < 3) {
        printf("Usage: EncryptData <password> <plain-text>\n");
        return nRet;
    }
        
    lpData = argv[2];
    lpPassword = argv[1];

    // 使用 MS_ENH_RSA_AES_PROV CSP 模块,其支持 AES 加密
    if (! CryptAcquireContext(&hProv, NULL, 
        MS_ENH_RSA_AES_PROV, PROV_RSA_AES, 0)) {
        printf("A CSP handle could not be acquired. Error code: %d\n", GetLastError());
        return nRet;
    }

    // 获取 MD5 哈希算法对象
    if (! CryptCreateHash(hProv, CALG_MD5, 0, 0, &hHash)) {
        printf("Cannot create the hash handle. Error code: %d\n", GetLastError());
        goto release;
    }

    // 使用 MD5 对用户密码进行加密
    if (!CryptHashData(hHash, lpPassword, strlen(lpPassword) * sizeof(char), 0)) {
        printf("The password cloud not be hashed. Error code: %d\n", GetLastError());
        goto release;
    }

    // 通过 Hash 密码生成密钥对象,加密类型为 AES-256-CBC
    if (! CryptDeriveKey(hProv, CALG_AES_256, hHash, CRYPT_EXPORTABLE, &hKey)) {
        printf("Cannot create the key handle. Error code: %d\n", GetLastError());
        goto release;
    }

    dwDataLen = strlen(lpData) * sizeof(char);
    lpBuffer = (LPBYTE) malloc(dwDataLen * 2);

    memcpy(lpBuffer, lpData, dwDataLen);

    // 加密数据
    if (! CryptEncrypt(hKey, 0, TRUE, 0, lpBuffer, &dwDataLen, dwDataLen * 2)) {
        printf("Failed to encrypt the special data. Error code: %d\n", GetLastError());
        goto release;
    }

    // 输出 16 进制格式密文
    for (i = 0; i < dwDataLen; i++) {
        printf("%02X", lpBuffer[i]);
    }
    printf("\n");
  
    free(lpBuffer);
        
release:
    if (hHash != NULL)
    {
        CryptDestroyHash(hHash);
    }

    if (hKey != NULL)
    {
        CryptDestroyKey(hKey);
    }

    if (hProv != NULL)
    {
      CryptReleaseContext(hProv, 0);
    }

    return 0;
}

执行结果:

enter image description here

参考资料

About Cryptography

Encrypting and Decrypting Data with the CryptoAPI

The Cryptography API, or How to Keep a SecretThe Cryptography API, or How to Keep a Secret

Using Symmetric Encryption with Microsoft’s CryptoAPI