轻松看懂的加解密系列(4) —— 改进版“AES对称加解密实例”(附源码)

图-1

     

        相比较之前的几个加解密实例,这一版使用 AES对称加解密实例所介绍的方法更具有实用性和参考价值,很适合保存客户本机的私密数据,不但用 Base64 Encode了自定义格式的密钥和密文合并字符串,增加了黑客解析出独立的密钥和密文的难度;而且还只能在执行加密操作的同一台机器上解密密文,实现了密钥隔离的策略;另外对其它的一些安全细节也做了增强处理。

新版 AES对称加解密实例直观感受:

        这一版实例的直观感受如下。如果本地你有一段文字需要加密,执行加密后会将密钥,密文和一些参数的长度信息按照自定义的格式合并为一个字符串,最后将字符串进行 Base64 Encode并保存在指定注册表路径下。【图-2】中可以看到当前机器的IP地址为【10.224.70.204】,PID为【18536】。

图-2

        在【图-3】中可以观察到,同一台机器下,新进程的 PID为【11616】,在执行解密操作后很顺利地将明文再次输出。

图-3

        如果有人偷偷把加密保存的内容和测试程序统统拷贝到另一台机器上执行解密操作,会发现如【图-4】所示,程序会提示导入密钥失败,这也就印证了本文开头所说的密钥隔离的策略。

图-4

改进后的源码分析(加密部分):

// Create customized CSP container with current user SID and defined container name
// To reduce the risk of missing local container file and enhance the security, acquire CSP with customized name
// If CryptAcquireContext failure first time, delete keyset and try to create a new one
if (true == GetProcessSID(strSID))
{
	strContainer = s_pszEnContainer;
	strContainer += _T("_");
	strContainer += strSID;
}

do
{
	if (!CryptAcquireContext(&hCryptProv, strContainer.c_str(), MS_ENH_RSA_AES_PROV, PROV_RSA_AES, dwFlags))
	{
		ShowError("CryptAcquireContext");

		if (!CryptAcquireContext(&hCryptProv, strContainer.c_str(), MS_ENH_RSA_AES_PROV, PROV_RSA_AES, CRYPT_DELETEKEYSET | dwFlags))
		{
			ShowError("CryptAcquireContext with CRYPT_DELETEKEYSET");
		}

		if (!CryptAcquireContext(&hCryptProv, strContainer.c_str(), MS_ENH_RSA_AES_PROV, PROV_RSA_AES, CRYPT_NEWKEYSET | dwFlags))
		{
			ShowError("CryptAcquireContext with CRYPT_DELETEKEYSET");
			return;
		}
	}

	// Cenerate exportable random session key
	DWORD dwFlag = CRYPT_EXPORTABLE;
	ALG_ID idAlg = CALG_AES_128;
	if (!CryptGenKey(hCryptProv, idAlg, dwFlag, &hSessionKey))
	{
		ShowError("CryptGenKey");
		return;
	}

	// Use CryptExportKey with CryptGetUserKey to export key that associated with specific users and computers
	if (!CryptGetUserKey(hCryptProv, AT_KEYEXCHANGE, &hXchgKey))
	{
		ShowError("CryptGetUserKey");

		if (!CryptGenKey(hCryptProv, AT_KEYEXCHANGE, 0, &hXchgKey))
		{
			ShowError("CryptGenKey with AT_KEYEXCHANGE");
			return;
		}
	}

	DWORD dwKeyBlobLen = 0;
	if (!CryptExportKey(hSessionKey, hXchgKey, SIMPLEBLOB, 0, NULL, &dwKeyBlobLen))
	{
		ShowError("CryptExportKey to get key blob length");
		return;
	}

	lpPubKey = new BYTE[dwKeyBlobLen];
	if (!CryptExportKey(hSessionKey, hXchgKey, SIMPLEBLOB, 0, lpPubKey, &dwKeyBlobLen))
	{
		ShowError("CryptExportKey to get key");
		break;
	}

        与之前几个例程最大的不同之一在于,调用 CryptAcquireContext 接口时,指定了“pszContainer”(密钥容器的名称)和 “pszProvider”(密码学服务提供程序的名称)。这使得本例程不再使用默认容器和默认提供程序,而是使用自定义的名称创建属于自己的密钥文件。并且还增加了遇到密钥文件丢失意外时的冗余处理,即会尝试再次创建新的自定义密钥文件。其中自定义的密钥容器名称包括“自定义的容器名字”和“本机当前用户的 SID”两部分,增加了唯一性和安全性。如【图-5】所示:

图-5 自定义的密钥文件中就包含自定义密钥容器名字

       

        还记得在之前的文章中曾经介绍过有客户的 VDI虚拟机由于配置原因而导致默认密钥文件丢失的问题吧?如果你的程序采用了自定义的密钥文件,并且增加了对密钥文件的冗余处理,那么在一定程度上会增加你程序的健壮性【图-6】。这也是产品级软件在研发阶段需要注意的问题,毕竟客户花银子买了你的软件,理应为客户提供更流畅的服务,而不是动辄就弹个提示框告诉客户,这也不行,那也不行。

图-6 原密钥文件名被改,程序自动创建新密钥文件,提高了软件的健壮性

        实现密钥隔离策略的手段在于使用了 CryptGenKey 和 CryptGetUserKey 接口。CryptGetUserKey 函数与 AT_KEYEXCHANGE 参数一起使用的主要目的是获取用户的用于加密和解密操作的非对称密钥。通过 CryptGetUserKey 获取的密钥通常与特定的用户帐户和计算机相关联,因此无法在不同的计算机上使用相同的密钥。这是因为密钥的生成和存储通常受到操作系统和硬件安全性的保护,而且密钥的使用需要用户的身份验证。

    CryptExportKey 函数用于导出密钥,以便将其存储在外部介质中。通常使用 SIMPLEBLOB 指定要导出的密钥的格式。 本质上导出的密钥是按字节存储在内存中的一段内容,所以我们要在栈上申请一个足够长的新缓冲区来暂时保存,并在随后的 CryptEncrypt 加密过程中使用它。

if (!CryptEncrypt(hSessionKey, 0, TRUE, 0, (LPBYTE)lpszSrcTemp, &dwUsedBuffSize, dwTotolBuffSize))
{
	ShowError("CryptEncrypt");
	break;
}

DWORD dwTotalSize = 0;
char* lpByteBase64 = NULL;
DWORD dwByteBase64 = 0;

// Customized format key, ciphertext and related string length
// KeyLen(4) + TotalLen(4) + Key + ciphertext
dwTotalSize = 8 + dwKeyBlobLen + dwUsedBuffSize;
lpEncryptBuf = new BYTE[dwTotalSize];
ZeroMemory(lpEncryptBuf, dwTotalSize);
memcpy_s(lpEncryptBuf, 4, &dwKeyBlobLen, 4);
memcpy_s(lpEncryptBuf + 4, 4, &dwTotalSize, 4);
memcpy_s(lpEncryptBuf + 8, dwKeyBlobLen, lpPubKey, dwKeyBlobLen);
memcpy_s(lpEncryptBuf + 8 + dwKeyBlobLen, dwUsedBuffSize, lpszSrcTemp, dwUsedBuffSize);
lpByteBase64 = new CHAR[dwTotalSize * 2];

if (NULL == lpByteBase64)
{
	ShowError("lpByteBase64 is null");
	break;
}

ZeroMemory(lpByteBase64, dwTotalSize * 2);
dwByteBase64 = Base64Encode(lpByteBase64, (CHAR*)lpEncryptBuf, dwTotalSize);

        本例中自定义的密钥和密文合并字符串格式为【密钥长度(4) + 整个缓冲区长度(4) + 密钥内容 + 密文内容】,在获取到密钥和密文之后,申请一个足够长的缓冲区(按BTYE为单位申请)来存储以上四部分内容。其中开头的8个字节用来存储两个DWORD类型,分别为密钥长度和整个缓冲区长度。后续分别存储的密钥内容和密文内容,密文的长度就是整个缓冲区的长度减去8,再减去密钥的长度,如【图-7】所示。最后整个缓冲区内容可以在 Base64 Encode之后被持久化到文件或注册表中。

图-7 程序正在按格式将解密接口返回的密文复制到准备持久化的缓冲区中

改进后的源码分析(解密部分):

lpSrcBuf = new BYTE[nLen];
int iTotalSrcLen = 0;
if (lpSrcBuf != NULL)
{
	ZeroMemory(lpSrcBuf, nLen);
	iTotalSrcLen = Base64Decode((char*)lpSrcBuf, lpszSource, strlen_safe(lpszSource));
}
else
{
	AfxMessageBox(_T("OnBnClickedBtnAesDe lpSrcBuf is NULL"), MB_ICONERROR);
	break;
}

std::wstring strSID;
std::wstring strContainer;
if (true == GetProcessSID(strSID))
{
	strContainer = s_pszDeContainer;
	strContainer += _T("_");
	strContainer += strSID;
}

if (!CryptAcquireContext(&hCryptProv, strContainer.c_str(), MS_ENH_RSA_AES_PROV, PROV_RSA_AES, dwFlags))
{
	ShowError("CryptAcquireContext");
	break;
}

// Adopt customized format key, ciphertext and related string length
// KeyLen(4) + TotalLen(4) + Key + PlainText
DWORD dwBlobSize = 0;
memcpy_s(&dwBlobSize, 4, lpSrcBuf, 4);
if (dwBlobSize >= iTotalSrcLen - 8)
{
	AfxMessageBox(_T("OnBnClickedBtnAesDe: error in dwBlobSize >= iTotalSrcLen - 8"), MB_ICONERROR);
	break;
}

DWORD dwTotalSize = 0;
memcpy_s(&dwTotalSize, 4, lpSrcBuf + 4, 4);
if (iTotalSrcLen != dwTotalSize)
{
	AfxMessageBox(_T("OnBnClickedBtnAesDe: error in iTotalSrcLen != dwTotalSize"), MB_ICONERROR);
	break;
}

if (!CryptImportKey(hCryptProv, lpSrcBuf + 8, dwBlobSize, 0, 0, &hSessionKey))
{
	ShowError("CryptImportKey");
	break;
}

LPBYTE lpSrcEncrpt = lpSrcBuf + 8 + dwBlobSize;
DWORD dwEncrptBufSize = dwTotalSize - 8 - dwBlobSize;
if (!CryptDecrypt(hSessionKey, 0, TRUE, 0, lpSrcEncrpt, &dwEncrptBufSize))
{
	ShowError("CryptDecrypt");
	break;
}
图-8 解密函数将解密后的内容写回了缓冲区

        如【图-8】所示,异步的解密程序在读出持久化的内容后,按照约定好的格式反向读出密钥和密文,再通过自定义的密钥容器来执行解密操作。可以很清楚地看到,内存中解密后的内容的编码,正是明文字符对应的 UTF-8 格式的编码。如果输出的地方需要宽字符集,那么还要对明文内容做一次 UTF8 的转码操作。

表-1 解密后明文对应的UTF-8格式编码
字符编码10进制编码16进制Unicode编码10进制Unicode编码16进制
15041964E585AC20844516C
15041923E58583208035143
250325032
048304830
250325032
351335133
15055284E5B9B4241805E74
957395739
15113352E69C88263766708
149314931
755375537
15112101E697A52608565E5
250325032
048304830
15112118E697B62610265F6
351335133
351335133
15042694E58886209985206

例程源代码请访问:https://github.com/345967018/WBBestPractice028CryptoApiRsaMfcTestEnhanced.git

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值