admin 管理员组

文章数量: 887017

开篇前言:

        本加解密系列文章的最大特点在于关注实际应用层面以及那些鲜有文章探讨的相关技术应用细节。所以对于网上如汗牛充栋般的众多数据安全定义类知识点,笔者往往会一笔带过。如需要,读者可以自行搜索这些资源。此外,为了帮助读者更好地理解每篇系列文章,笔者还会提供一些相关的“番外篇”介绍文章,供有兴趣的读者进一步拓展阅读。

        加解密技术在当前的数据安全领域具有重要意义,几乎所有IT行业的从业者都会在工作中接触到各种加解密相关技术。虽然大多数工程师不需要深入理解专业级别的加解密算法,也无需高深的数学基础,但了解一些经典的加解密算法如 AES、DES 和 RSA 等的实现原理和接口使用方法,对于提高数据安全的意识和工作效率仍然非常重要。前人大牛们已经为我们设计了许多现成的加解密库函数接口,例如 Windows CryptoAPI、Crypto++ 和 OpenSSL 等。所以对于大多数工程师而言,在执行与数据安全相关的工作时,更重要的是学会如何选择合适的接口、正确高效地调用接口,以及确保程序在各种客户环境中仍然能够实现设计的安全效果。

Windows CryptoAPI 实例介绍:

        实现一套复杂而严密的加解密算法对于常人来说的确非常困难,而过往很多程序自行开发的简单加解密代码又很容易被黑客破译。为了解决这个问题,Windows 提供的 CryptoAPI 可以让使用者在享用其强大加密功能的同时,而不必研究其高深的实现。让我们通过以下一段程序实例,从最常见的 Windows CryptoAPI 来开始介绍。

int main()
{
    HCRYPTPROV hCryptProv = NULL;
    HCRYPTKEY hKey = NULL;
    BYTE* pDataToEncrypt = (BYTE*)"Hello, World!";
    BYTE* pEncryptedData = NULL;
    DWORD encryptedDataLen = 0;
    BYTE* pDecryptedData = NULL;
    DWORD decryptedDataLen = 0;

    //Get the handler of Crypt Service Provider(CSP)
    if (!CryptAcquireContext(&hCryptProv, NULL, NULL, PROV_RSA_AES, 0)) {
        printf("CryptAcquireContext failed: %d\n", GetLastError());
        return 1;
    }

    //Generate AES key
    if (!CryptGenKey(hCryptProv, CALG_AES_256, 0, &hKey)) {
        printf("CryptGenKey failed: %d\n", GetLastError());
        CryptReleaseContext(hCryptProv, 0);
        return 1;
    }

    //Encrypt data
    if (EncryptData(hKey, pDataToEncrypt, strlen((char*)pDataToEncrypt) + 1, &pEncryptedData, &encryptedDataLen)) {
        printf("Data encrypted successfully.\n");
        printf("Encrypted data: ");
        for (DWORD i = 0; i < encryptedDataLen; i++)
        {
            printf("%02X", pEncryptedData[i]);
        }
        printf("\n");

        //Decrypt data
        if (DecryptData(hKey, pEncryptedData, encryptedDataLen, &pDecryptedData, &decryptedDataLen)) {
            printf("Decrypted data: %s\n", pDecryptedData);
            free(pDecryptedData);
        }

        free(pEncryptedData);
    }

    //Clean resources
    CryptDestroyKey(hKey);
    CryptReleaseContext(hCryptProv, 0);

    return 0;
}

        阅读这段 Windows Console Application main 函数,可以按照几个注释分步学习:(1)获取加解密上下文、(2)产生目标密钥、(3)加密明文、(4)解码密文和最后(5)释放相关资源。本文首先帮助读者通晓各 API 的调用方法和顺序,在后续的“番外篇”里会通过微软的一个强大的用于监视和记录 Windows 操作系统上的系统活动的高级工具(Process Monitor),来从“上帝视角”再次观察这个实例运行过程中的各个底层细节,以期加深理解。本文的另一个“番外篇”还会通过笔者收集的一些真实案例来介绍(Process Monitor)在解决生产环境数据安全相关问题时所起到的巨大帮助。

(1)CryptAcquireContext

    CryptAcquireContext 函数是 Windows Cryptographic API(CryptoAPI)中的一个函数,用于获取或创建一个加密服务提供程序(CSP)的上下文句柄。CSP是用于执行各种加密和安全操作的组件,例如生成密钥、加密和解密数据等。同微软许多库函数在执行相关命令之前需要进行上下文初始化调用一样,执行 CryptoAPIs,也首先需要在内部创建或获取一些资源、配置选项或环境设置,以确保后续函数的正确运行。CryptAcquireContext 也不例外,

BOOL CryptAcquireContext(
  HCRYPTPROV *phProv,         // 用于存储 CSP 句柄的指针
  LPCTSTR    pszContainer,    // CSP 容器名称(可选)
  LPCTSTR    pszProvider,     // CSP 提供程序名称(可选)
  DWORD      dwProvType,      // CSP 提供程序类型
  DWORD      dwFlags          // 标志,用于控制 CSP 的行为
);
  • phProv: 一个指向 HCRYPTPROV 类型指针的指针,用于接收 CSP 的句柄。

  • pszContainer: 可选参数,用于指定 CSP 容器的名称。如果为 NULL,将使用默认容器。后续系列程序实例将演示如何自定义此参数,当前实例使用默认容器。

  • pszProvider: 可选参数,用于指定 CSP 提供程序的名称。如果为 NULL,将使用默认提供程序,相当于设置为 MS_DEF_PROV(Microsoft Base Cryptographic Provider),后续系列程序实例将演示如何自定义此参数。

  • dwProvType: 一个 DWORD 值,用于指定要获取的加密服务提供程序(CSP)的类型。常见的类型包括 PROV_RSA_FULL、PROV_RSA_AES 等。当前实例选用了PROV_RSA_AES 是为了下一步创建一个 AES 密钥。

  • dwFlags: 用于控制 CSP 行为的标志。通常可以设置为 0

        作为IT从业人员,有必要至少记住 "RSA" 和 "AES" 这两种最常见的不同加密算法,以及它们不同的特性和用途(考数据安全必考之基础🙂)。

  • RSA 是一种非对称加密算法,由 Ron Rivest、Adi Shamir 和 Leonard Adleman 于 1977 年共同提出。它基于两个密钥对:公钥和私钥。公钥用于加密数据,私钥用于解密数据。RSA 主要用于数字签名、密钥交换。
  • AES(Advanced Encryption Standard)是一种对称加密算法,它使用相同的密钥用于加密和解密数据。由于其高效性和安全性,在文件加密、磁盘加密、网络通信等领域普遍使用。
  • 在实际应用中,它们经常一起使用。例如在 SSL/TLS 协议中,通常的做法是使用非对称加密算法(如 RSA)来进行初始握手,协商会话密钥,然后使用对称加密算法(如 AES)来加密实际的数据传输。这种组合充分利用了非对称加密和对称加密各自的优势:非对称加密用于安全地协商对称密钥,而对称加密用于高效地加密大量的数据。
(2)CryptGenKey

    CryptGenKey 是 Windows Cryptographic API(CryptoAPI)中的一个函数,用于生成对称密钥非对称密钥。这个函数可以用于生成加密和解密数据的密钥,以及用于数字签名和验证的密钥对

BOOL CryptGenKey(
  HCRYPTPROV hProv,
  ALG_ID    Algid,
  DWORD     dwFlags,
  HCRYPTKEY *phKey
);
  • hProv:用于生成密钥的加密服务提供程序(CSP)的句柄,也就是之前 CryptAcquireContext 函数获取的 CSP 句柄。

  • Algid:指定要生成的密钥的算法标识符(ALG_ID)。

  • dwFlags:生成密钥的标志。通常情况下,可以将其设置为零。

  • phKey:生成的密钥的句柄将存储在此处,接下来具体的加密和解密接口调用都需要用到此参数。

        这里需要扩展介绍一下的就是 Algid 参数了。不同的 ALG_ID 类型,表示不同的加密算法和哈希算法。具体使用哪种算法取决于不同的应用需求和安全性要求。

表1 - Algid 常见类型
对称加密算法(Symmetric Encryption Algorithms)

CALG_RC4:RC4 算法,一种流密码算法。

CALG_DES:DES 算法,一种分组密码算法。

CALG_AES:AES 算法,一种高级加密标准,支持128位、192位和256位密钥。

非对称加密算法(Asymmetric Encryption Algorithms)

CALG_RSA_KEYX:RSA 密钥交换算法,一种非对称加密算法。

CALG_DSS_SIGN:DSA 数字签名算法,一种非对称签名算法。

哈希算法(Hash Algorithms)

CALG_MD5:MD5 哈希算法。

CALG_SHA:SHA-1 哈希算法。

CALG_SHA_256:SHA-256 哈希算法。

        至于表1中的 CALC_AES 参数和实例中所使用的 CALG_AES_256 参数有什么区别和联系,聪明如你,对比以下 wincrypt.h #define 一定一眼就能看明白了。

#define ALG_SID_AES_128                 14
#define ALG_SID_AES_192                 15
#define ALG_SID_AES_256                 16
#define ALG_SID_AES                     17

#define CALG_AES_128            (ALG_CLASS_DATA_ENCRYPT|ALG_TYPE_BLOCK|ALG_SID_AES_128)
#define CALG_AES_192            (ALG_CLASS_DATA_ENCRYPT|ALG_TYPE_BLOCK|ALG_SID_AES_192)
#define CALG_AES_256            (ALG_CLASS_DATA_ENCRYPT|ALG_TYPE_BLOCK|ALG_SID_AES_256)
#define CALG_AES                (ALG_CLASS_DATA_ENCRYPT|ALG_TYPE_BLOCK|ALG_SID_AES)
(3)CryptEncrypt

    CryptEncrypt 是 Windows Cryptographic API(CryptoAPI)中用于对数据进行加密的函数。它允许使用已经生成的密钥来加密数据。其中重点需要讲解的是后三个参数。

BOOL CryptEncrypt(
  HCRYPTKEY hKey,
  HCRYPTHASH hHash,
  BOOL      Final,
  DWORD     dwFlags,
  BYTE      *pbData,
  DWORD     *pdwDataLen,
  DWORD     dwBufLen
);
  • hKey:用于加密数据的密钥的句柄。这可以是对称密钥或非对称密钥,具体取决于加密操作的类型。如本例使用对称 AES 密钥来直接加密明文。如果使用非对称 RSA 密钥,一般加密时用公钥,未来用私钥解密。

  • hHash:用于数据签名的哈希对象的句柄。通常情况下,可以将其设置为 NULL,除非您需要对数据进行签名。

  • Final:指示是否是最后一次加密。如果是最后一次加密,设置为 TRUE,否则设置为 FALSE。后续系列会介绍如何分块加密。

  • dwFlags:加密操作的标志。通常情况下,可以将其设置为零。

  • pbData在加密之前,指向将要被加密的明文。加密完成之后,所指向的地址已经存入了被加密后的密文。

  • pdwDataLen:指向一个变量的指针,用于存储加密后数据的实际长度

  • dwBufLen:指定 pbData 缓冲区的大小,要确保其大小能足够承载加密后的密文长度,以防止缓冲区溢出。例如本例在使用 AES 256 位密钥和块加密模式时,密文长度通常会比明文长度略长,因为 AES 加密通常使用填充(例如 PKCS7 填充)来处理不满一个块的数据。填充会增加密文的长度,使其达到一个完整块的大小。如果使用块加密模式,每个块的大小通常是 128 位(16 字节)。所以在本例封装的加密函数(EncryptData)中可以看到,第一次将接收密文的缓冲区长度设置为输入的明文长度加16,如果执行首轮 CryptEncrypt 失败后,GetLastError() 返回 ERROR_MORE_DATA,则意味着没有设置足够的缓冲区长度来接收密文。好在此时 pdwDataLen 参数(也就是本例当中的 dataLen)已经得到了加密后数据的实际长度,所以第二次将接收密文的缓冲区长度设置为 “dataLen + 1”,以保证第二次成功执行 CryptEncrypt。

BOOL EncryptData(HCRYPTKEY hKey, BYTE* pData, DWORD dataLen, BYTE** ppEncryptedData, DWORD* pEncryptedDataLen) 
{
    *pEncryptedDataLen = dataLen + 16;
    *ppEncryptedData = (BYTE*)malloc(*pEncryptedDataLen);
    ZeroMemory(*ppEncryptedData, *pEncryptedDataLen);
    if (*ppEncryptedData == NULL) 
    {
        printf("Memory allocation failed.\n");
        return FALSE;
    }

    memcpy(*ppEncryptedData, pData, dataLen);
    if (!CryptEncrypt(hKey, NULL, TRUE, 0, *ppEncryptedData, &dataLen, *pEncryptedDataLen))
    {
        printf("CryptEncrypt failed: %d\n", GetLastError());
        free(*ppEncryptedData);

        if (GetLastError() == ERROR_MORE_DATA)
        {
            *pEncryptedDataLen = dataLen + 1;
            *ppEncryptedData = (BYTE*)malloc(*pEncryptedDataLen);
            ZeroMemory(*ppEncryptedData, *pEncryptedDataLen);
            if (*ppEncryptedData == NULL)
            {
                printf("Memory allocation failed again.\n");
                return FALSE;
            }
            memcpy(*ppEncryptedData, pData, dataLen);
            if (!CryptEncrypt(hKey, NULL, TRUE, 0, *ppEncryptedData, &dataLen, *pEncryptedDataLen))
            {
                printf("CryptEncrypt failed again: %d\n", GetLastError());
                free(*ppEncryptedData);
                return FALSE;
            }
        }
        else
        {
            return FALSE;
        }
    }

    *pEncryptedDataLen = dataLen;

    return TRUE;
}

        本例加密函数(EncryptData)最后一步 “*pEncryptedDataLen = dataLen” 的目的在于将密文长度返回给 main 主程序,因为接下来的解密步骤需要此作为基础长度用于计算缓冲区申请。

(4)CryptDecrypt

    CryptDecrypt 函数是 Windows 操作系统提供的用于解密数据的API函数。它属于 Windows Cryptographic API(CryptoAPI)的一部分,在执行对称密钥和非对称密钥的解密操作时,步骤是不同的,后续系列实例会给与说明。

BOOL CryptDecrypt(
  HCRYPTKEY hKey,
  HCRYPTHASH hHash,
  BOOL      Final,
  DWORD     dwFlags,
  BYTE      *pbData,
  DWORD     *pdwDataLen
);
  • hKey:表示要用于解密的密钥的句柄(handle)。这个密钥必须是一个已经打开的对称密钥句柄。在本例中为了演示方便,就直接复用了加密时的密钥全局变量 hKey。
  • hHash:可选的哈希对象句柄。如果不使用哈希,可以将其设置为 NULL
  • Final:一个布尔值,指示是否是解密操作的最后一块数据。如果是最后一块数据,则为 TRUE,否则为 FALSE
  • dwFlags:用于指定其他解密选项的标志。可以将其设置为 0
  • pbData:一个指向要解密的数据的缓冲区的指针。所以本例封装的解密函数(DecryptData)一开始就通过加密时得到的具体缓冲区长度来申请接收解密数据的缓冲区长度。因为理论上密文数据的长度一般都会大于明文长度,所以这样做是比较保险的。
  • pdwDataLen:一个指向一个 DWORD 的指针,用于输入和输出数据的长度。在输入时,它表示 pbData 缓冲区的长度,输出时,它表示解密后数据的实际长度。在本例中通过 “*pDecryptedDataLen = encryptedDataLen” 直接将输入长度设置为密文长度。输出的明文长度最后正好用来控制打印明文内容的长度。
BOOL DecryptData(HCRYPTKEY hKey, BYTE* pEncryptedData, DWORD encryptedDataLen, BYTE** ppDecryptedData, DWORD* pDecryptedDataLen) 
{
    *ppDecryptedData = (BYTE*)malloc(encryptedDataLen);
    ZeroMemory(*ppDecryptedData, encryptedDataLen);
    if (*ppDecryptedData == NULL) 
    {
        printf("Memory allocation failed.\n");
        return FALSE;
    }

    memcpy(*ppDecryptedData, pEncryptedData, encryptedDataLen);
    *pDecryptedDataLen = encryptedDataLen;
    if (!CryptDecrypt(hKey, NULL, TRUE, 0, *ppDecryptedData, pDecryptedDataLen)) 
    {
        printf("CryptDecrypt failed: %d\n", GetLastError());
        free(*ppDecryptedData);
        return FALSE;
    }

    return TRUE;
}

(5)释放相关资源

        在加解密过程中创建的各种资源,比如加密服务提供程序(CSP)的上下文句柄、密钥的句柄等都需要通过对应的函数(CryptDestroyKeyCryptReleaseContext)来按创建倒序释放。此外由于本例没有使用智能指针,所有申请的内存空间一定确保执行了对应的释放操作。

执行结果的控制台输出

        至此一个极简版的 Windows CryptoAPI 实例介绍就算完成了。接下来的“番外篇”将通过微软的(Process Monitor)工具来揭示本例当中的更多底层实现细节。

本文标签: 看懂 实例 轻松 加解密 系列