1. 前言
通过调用windows API使用Diffe-Hellman协议来进行密钥交换,写下这个博客以便今后需要是回来进行考古。在微软的MSDN文档中给出了详细的过程描述。
由于是一个协议涉及到双方之间的数据交换,网络通信是最常用的方式。相关的Sever/Client通行的框架在此给出在此给出。
2. DIffe-Hellman简单介绍
Diffe-Hellman协议。两个大素数g和p(p至少512 bits)。g和p这两个素数不需要保密,可以公开。通信双方选择大的随机数a和b,需要保密。计算:
A
=
g
a
m
o
d
p
B
=
g
b
m
o
d
p
\begin{aligned} A=g^a\mod p\\ B=g^b\mod p \end{aligned}
A=gamodpB=gbmodp
计算之后将A和B分别发往对方。则共享密钥为:
K
=
g
a
b
m
o
d
p
K=g^{ab}\mod p
K=gabmodp
A使用如下公式进行计算:
K
=
B
a
m
o
d
p
=
(
g
b
)
a
m
o
d
p
K=B^a\mod p=(g^b)^a\mod p\\
K=Bamodp=(gb)amodp
B使用如下公式进行计算:
K
=
A
b
m
o
d
p
=
(
g
a
)
b
m
o
d
p
K=A^b\mod p=(g^a)^b\mod p
K=Abmodp=(ga)bmodp
从Diffe-Hellman的理论公式来看当相应的大数运行都已经准备好时,实现起来应该是很简单的。
3. Server
3.1 生成Diffe-Hellman公私钥对
通过CryptAcquireContext函数的szProvider参数传入MS_DEF_DH_SCHANNEL_PROV,dwProvType参数传入PROV_DH_SCHANNEL即可获得一个Diffe-Hellman协议的CSP
CryptAcquireContext(&hProv, ContainName, MS_DEF_DH_SCHANNEL_PROV, PROV_DH_SCHANNEL, 0);
服务端需要生成P和G,所以在dwFlags参数选择了CRYPT_EXPORTABLE设置为可导出的形式,而不是像客户端中那样设置为CRYPT_PREGEN进行导入。
CryptGenKey(hProv, CALG_DH_EPHEM, CRYPT_EXPORTABLE, &hDHKey);
3.2 导出A,P和G
在MSDN文档中给出了Diffe-Hellman创建的公私钥结构。下图是截取其中的相关部分的内容。在服务端生成公钥A中只保存了一个A,即
g
a
m
o
d
p
g^a\mod p
gamodp,而P和G的相关信息没有保存在公钥中,所以需要单独将P和G从私钥中导出。发送过程中需要将公钥A,两个因子P和G分别发送。
导出公钥G可以直接使用CryptExportKey函数进行,将之前生成密钥是获得的句柄hDHkey作为目标参数传入,同时在dwBlobType参数中选择PUBLICKEYBLOB选择导出的密钥类型为公钥。为了确保这里能够顺利导出前面在使用CryptGenKey函数时需要CRYPT_EXPORTABLE参数被顺利传递。
CryptExportKey(hDHKey, NULL, PUBLICKEYBLOB, 0, pbPublicKey, &dwDataLen);
私钥中的内容不应该全部的导出,里面存在了X等一些关键数据,这些是在进行传输时不需要的信息。这里选择使用CryptGetKeyParam函数来将私钥中的P和G导出。只需要将函数的第二个参数设置为KP_P或KP_G就能够将相关部分导出。
CryptGetKeyParam(hDHKey, KP_P, pbPKey, &dwDataLen, 0);
3.3 导入客户端公钥B
通过TCP网络接收到了客户端传递过来的公钥B,需要在服务端计算出K,导入的过程中遇到了一个问题:由于服务端需要通过线程进行多任务处理,所以密钥生成和密钥导入分别位于两个函数中,在设计函数接口时没有预留给CSP句柄,所以在导入是又通过CryptAcquireContext函数来申请句柄,但是会出现NET_BAD_KEY错误代码。
出现上述问题的原因可以在MSDN关于CryptAcquireContextA描述中找到。
通过CryptAcquireContext函数是无法重新获得操作私钥的权限句柄的,所以需将之前获得的CSP句柄传递给导入部分的函数,由于对于函数接口的更改是比较麻烦的,所以就直接使用全局变量来传递CSP了。
HCRYPTKEY hDHKey;
HCRYPTPROV hProv = NULL;
只要对于CSP的获取没有问题就不会出现可以很容易的将客户端的公钥B导入到服务端中,形成他们之间的共享密钥K。
CryptImportKey函数的第四个参数hPubKey传递之前那保存了服务端自身私钥的句柄即可。
CryptImportKey(hProv, pbYKey->pbData, pbYKey->length, hDHKey, 0, &hAESKey)
4. Client
4.1 导入P、G,生成X
P、G的获取可以通过各种形式,只要能够传递即可,在协议中这两个数值是不用进行保护的,可以进行公开,这里是使用TCP的方式进行传输。
通过CryptAcquireContext函数的szProvider参数传入MS_DEF_DH_SCHANNEL_PROV,dwProvType参数传入PROV_DH_SCHANNEL即可获得一个Diffe-Hellman协议的CSP
CryptAcquireContext(&hProv, ContainName, MS_DEF_DH_SCHANNEL_PROV, PROV_DH_SCHANNEL, 0);
由于P和G是服务端生成并公开的,客户端生成密钥是应该采用CRYPT_PREGEN的预定义形式,随后需要将相应的参数导入到密钥之中。
CryptGenKey(hProv, CALG_DH_EPHEM, CRYPT_PREGEN, &hDHKey);
将从服务端获取到的P存储到一个CRYPT_DATA_BLOB的结构体中,该结构体将cbData设置为大小,pbData指向数据所在的缓冲区。
CRYPT_DATA_BLOB blob;
blob.cbData = iResult, blob.pbData = (BYTE*)buf;
CryptSetKeyParam(hDHKey, KP_P, (BYTE*)&blob, 0);
G和X的导入与P相似只是将KP_P参数替换为了KP_G和KP_X。由于X参数是随机生成的,所以在导入X参数是可以将第三个参数设置为NULL,这样CSP就会随机生成一个X,同时计算出 B = g x m o d p B=g^x\mod p B=gxmodp的结果。
4.2 导入服务端公钥A
在客户端导入服务端的公钥A时使用CryptImportKey,将hPubKey参数设置为被导入的地方,也就是之前通过CryptGenKey生成的句柄。
CryptImportKey(hProv, blob.pbData, blob.cbData, hDHKey, 0, &hAESKey);
导入之后会自动计算出他们之间的协商密钥K,这里的密钥类型为CALG_AGREEDKEY_ANY不能够直接使用需要为其设置加密类型,一开始我向设置为AES的加密类型但是在给出的PROV_DH_SCHANNEL CSP没有支持AES最后选择了与MSDN例子中相同的RC4,这里的hAESKey的命名就没有进行更改。
ALG_ID algid = CALG_RC4;
CryptSetKeyParam(hAESKey, KP_ALGID, (PBYTE)&algid, 0);
导入了服务端的公钥A之后,在客户端就计算出了会话密钥K,这个密钥设置ALG_ID之后就能够作加解密密钥进行使用,使用的方式可以直接调用API接口CryptEecrypt和CryptDecrypt函数进行。
CryptEncrypt(hKey, NULL, true, 0, (BYTE*)buf, &dwLen, 512);
CryptDecrypt(hAESKey, NULL, true, 0, (BYTE*)buf, &dwDataLen);
5. 完整代码
完整的代码由于使用TCP的方式传输数据,整体较长,故不贴在此处了。代码的链接地址在这,有需求的可以到Gitee上获取。