场景1:
在QQ空间的秘密中吐槽时发现自己的好友列表中有一个十分聊得来的人,尽管你们都不知道对方是谁,却还是迫不及待的想要交换自己的QQ,但是你们又都不想把自己的QQ写在这个大家都能看到的地方,毕竟刚才的吐槽可能会让其他人对你留下不好的印象。
场景2:
在某个没有私聊功能的论坛上,你看到了某个人拥有你想要的资源,而他也正好有你想要的资源,于是你们决定互换一波资源,方式就是交换邮箱账号,但是你们又都不希望邮箱明文被其他围观群众看到,不管是因为安全考虑还是某种习惯。
总之,总结一下这两种场景的共同点,就是没有私密的信息交换通道,所以不得不正在公开通道上进行信息传输,如果信息比较隐私的话,可能通信就无法实现了,不过利用PKCS#3协议的原理,再配合对称加密算法,我们就能够不便捷但是安全的传输这些信息了。
一、原理:
PKCS#3是Diffie-Hellman密钥协议标准。PKCS#3描述了一种实现Diffie- Hellman密钥协议的方法,其步骤如下:
- 选取大素数p和它的一个生成元g,这些参数公开
- A选择随机数 X a X_a Xa,B选择随机数 X b X_b Xb
- A计算 Y a Ya Ya = g X a    m o d    p g^{X_a}\; mod\;p gXamodp,B计算 Y b Yb Yb= g X b    m o d    p    g^{X_b}\;mod\; p\; gXbmodp
- 交换 Y a Y_a Ya, Y b Y_b Yb
- A计算K= Y b X a    m o d    p Y_b^{X_a}\;mod\;p YbXamodp B计算K’= Y a X b    m o d    p Y_a^{X_b}\;mod\;p YaXbmodp
事实上K=K’,这样也就完成了密钥的交换,其安全性由大数分解难题保证。
传输完K之后,剩下的事情就是对称加密了,有很多很好的对称加密算法可供选用,因此关键在于密钥K的传输。
二、实现:
我实现了两个版本的程序,一个利用C语言调用openssl库实现整个流程,再配合Shell脚本进行调用,另一个是用JS配合一些现成的大数运算库实现的,尽管后者可能对于我之前所说的场景更加实用,但是使用openssl库来做更好一些,因为它更加完整和强大,这里就拿C语言版本来讲解一下整个流程吧。
根据之前的原理可以看出,关键的计算有如下四个:
- p和g的产生
- x的产生和y的计算
- k的计算
- 对称加解密运算
所以我也按照这四个关键运算来完成了四个c文件,编译生成了四个组件,接下来就对这四个文件逐一说明,文件中用到的API都是参考这个博主的博文的:
1.p和g的产生
p是一个大素数,在openssl中有大素数生成相关的API,但是似乎没有专门计算生成元g的API,生成元的概念我就不解释了,可以直接百度百科找,这里我找到了一个计算快速计算g的公式,原文链接如下:
取安全素数 P 使P = 2Q+1 P Q都必须为素数, 如最小的安全素数为 (5,7,11……) Q 对应就为 (2,3,5……)
取任意数 a 同时满足如下条件:
a 2 a^2 a2 mod P !=1
a Q a^Q aQ mod P !=1
此时,a就是一个生成元
所以,我们可以利用openssl中的API产生安全素数P,然后计算出Q,已知g最小为2,因此设置g为2,然后用这两个公式去验证g是不是生成元,如果不是就让g加一,直到最后两个式子都满足为止,通常情况下,这个g都是非常小的,所以不会耗费太长时间。
按照这个算法写出程序,最后打印出计算出的p和g对应的十六进制字符串。
#include <stdio.h>
#include <openssl/bn.h>
/** 根据P和Q计算生成元
* @param P 安全大素数
* @param Q (P-1)/2
* @param one 值为1的大数结构
* @param two 值为2的大数结构
* @return 计算所得的生成元
*/
//获取生成元g
BIGNUM * get_g(BIGNUM *P,BIGNUM *Q,const BIGNUM *one,const BIGNUM *two)
{
BN_CTX *ctx=BN_CTX_new();
BIGNUM *g=BN_new(); //生成元g
BN_set_word(g,2); //设置初始值为2
// 1.a^2 mod P !=1
// 2.a^Q mod P !=1
BIGNUM *condi1=BN_new(); //条件1
BN_mod_exp(condi1,g,two,P,ctx); //计算条件1
BIGNUM *condi2=BN_new(); //条件2
BN_mod_exp(condi2,g,Q,P,ctx); //计算条件2
while(BN_is_one(condi1)||BN_is_one(condi2)) //如果有一个条件不满足
{
BN_add(g,g,one); //加1
BN_mod_exp(condi1,g,two,P,ctx); //计算条件1
BN_mod_exp(condi2,g,Q,P,ctx); //计算条件2
}
BN_CTX_free(ctx);
BN_free(condi1);
BN_free(condi2);
return g;
}
//主函数
int main(int argc,char ** argv)
{
BIGNUM *P=BN_new();
BIGNUM *Q=BN_new();
BIGNUM *rem=BN_new();
BN_CTX *ctx=BN_CTX_new();
BN_generate_prime(P,128,1,NULL,NULL,NULL,NULL); //生成一个安全素数P
BIGNUM *two=BN_new(); //2
BIGNUM *one=BN_new(); //1
BN_set_word(two,2); //设置为2
BN_one(one); //设置为1
BN_sub(Q,P,one); //P-1
BN_div(Q,rem,Q,two,ctx);//计算出Q
BIGNUM *g=get_g(P,Q,one,two); //计算生成元g
printf("P g\n");
printf("%s\n",BN_bn2hex(P)); //打印出数字对应的16进制
printf("%s\n",BN_bn2hex(g));
BN_free(P); //释放资源
BN_free(Q);
BN_free(two);
BN_free(one);
BN_free(rem);
BN_free(g);
BN_CTX_free(ctx);
return 0;
}
2.x的产生和y的计算
这一步的运算需要用到刚才生成的g和P,所以要将他们作为调用参数输入进来。
X是一个随机数,不一定是一个素数,所以这里只需要调用openssl的随机数生成API即可,这里唯一要关注的就是X的位数,这里我选择和P等长,然后利用X和调用参数输入的g和P一起计算出Y即可。
#include <stdio.h>
#include <openssl/bn.h>
int main(int argc,char ** argv)
{
if(argc<3)
{
printf("用法:%s P g\n",argv[0]);
return 0;
}
char *gs=argv[2]; //设置生成元字符串
char *Ps=argv[1]; //设置安全素数字符串
BIGNUM *g=BN_new(); //生成元g
BIGNUM *P=BN_new(); //安全素数P
BIGNUM *X=BN_new(); //自己的随机数X
BIGNUM *Y=BN_new(); //要交换的数Y
BN_CTX *ctx=BN_CTX_new();
BN_hex2bn(&g,gs); //设置生成元g
BN_hex2bn(&P,Ps); //设置素数P
BN_rand(X,128,0,0); //生成128位的X
BN_mod_exp(Y,g,X,P,ctx); //计算Y
printf("X Y\n");
printf("%s\n%s\n",BN_bn2hex(X),BN_bn2hex(Y)); //输出X和Y
BN_free(X);
BN_free(g);
BN_free(Y);
BN_free(P); //释放
BN_CTX_free(ctx);
return 0;
}
3.K的计算
K的计算需要自己生成的X和对方生成的Y,以及大素数P,作为参数传入程序之后只需要进行大数的模运算即可。
#include <stdio.h>
#include <openssl/bn.h>
int main(int argc,char ** argv)
{
if(argc<4)
{
printf("用法:%s Xa Yb P\n",argv[0]);
return 0;
}
char *Xas=argv[1]; //自己的X字符串
char *Ybs=argv[2]; //对方的Y字符串
char *Ps=argv[3]; //大素数
BIGNUM *Xa=BN_new(); //X
BIGNUM *Yb=BN_new(); //对方的Y
BIGNUM *P=BN_new(); //大素数P
BIGNUM *K=BN_new(); //密钥K
BN_CTX *ctx=BN_CTX_new();
BN_hex2bn(&Xa,Xas); //设置X
BN_hex2bn(&Yb,Ybs); //设置对方Y
BN_hex2bn(&P,Ps); //设置素数P
BN_mod_exp(K,Yb,Xa,P,ctx); //计算K
printf("K\n");
printf("%s\n",BN_bn2hex(K)); //打印K
BN_free(Xa); //释放
BN_free(Yb);
BN_free(K);
BN_free(P);
BN_CTX_free(ctx);
return 0;
}
4.对称加密
这里我选择了128位的AES算法,因为密钥是32个十六进制数,刚好是128位,由于加密的信息应该不会很长所以使用的是ECB模式。
要注意的就是输出的未必是可见字符,所以可能无法放在网页输入框中进行传输,所以这里使用base64编码,将其转变为可见字符。
#include <stdio.h>
#include <string.h>
#include "openssl/aes.h"
#include <openssl/pem.h>
#include <openssl/bio.h>
/** 根据十六进制字符c计算出其对应的数值
* @param c 十六进制字符
* @return 字符对应的值
*/
unsigned char get_value(unsigned char c)
{
if(c>='0'&&c<='9')
return c-'0';
else if(c>='A'&&c<='Z')
return c-'A'+10;
else
return c-'a'+10;
}
/** 根据字符串设置密钥,密钥中的字符未必是可见字符
* @param key_str 十六进制字符串
* @param key 密钥字符串
*/
void set_key(char * key_str,unsigned char key[17])
{
unsigned char temp[2];
for(int i=0;i<16;i++)
{
strncpy(temp,key_str+i*2,2);
key[i]=get_value(temp[0])*16+get_value(temp[1]);
//printf("temp[0]=%d temp[1]=%d value=%d\n",temp[0],temp[1],key[i]);
}
key[16]='\0';
}
/** 对字符串进行base64编码
* @param in_str 待编码字符串
* @param in_len 待编码字符串长度
* @param out_str 编码后的输出字符串
* @return 输出字符串
*/
int base64_encode(char *in_str, int in_len, char *out_str)
{
BIO *b64, *bio;
BUF_MEM *bptr = NULL;
size_t size = 0;
if (in_str == NULL || out_str == NULL)
return -1;
b64 = BIO_new(BIO_f_base64());
bio = BIO_new(BIO_s_mem());
bio = BIO_push(b64, bio);
BIO_write(bio, in_str, in_len);
BIO_flush(bio);
BIO_get_mem_ptr(bio, &bptr);
memcpy(out_str, bptr->data, bptr->length);
out_str[bptr->length] = '\0';
size = bptr->length;
BIO_free_all(bio);
return size;
}
/** 对字符串进行base64解码
* @param in_str 待解码字符串
* @param in_len 待解码字符串长度
* @param out_str 解码后的输出字符串
* @return 输出字符串
*/
int base64_decode(char *in_str, int in_len, char *out_str)
{
BIO *b64, *bio;
BUF_MEM *bptr = NULL;
int counts;
int size = 0;
if (in_str == NULL || out_str == NULL)
return -1;
b64 = BIO_new(BIO_f_base64());
BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL);
bio = BIO_new_mem_buf(in_str, in_len);
bio = BIO_push(b64, bio);
size = BIO_read(bio, out_str, in_len);
out_str[size] = '\0';
BIO_free_all(bio);
return size;
}
int main(int argc,char **argv)
{
if(argc<4)
{
printf("用法:%s (-e/-d) key plaintext\n",argv[0]);
return 0;
}
unsigned char key[16+1];// 128bits key (应该是真正的随机数才好)
set_key(argv[2],key); //设置k
char * text=argv[3]; //明文或者密文
char base[1000]; //base64编码
char ct[1000]; // 密文
char pt[1000]; // 解密后的明文
AES_KEY k;
// single blcok test
if(strcmp(argv[1],"-e")==0) //如果需要加密
{
AES_set_encrypt_key(key, 16*8, &k); //设置密钥
AES_encrypt((unsigned char*)text, (unsigned char*)ct, &k); //AES加密
base64_encode(ct, strlen(ct),base); //base64编码加密后输出的字符串
printf("%s",base); //输出最终的字符串
}
else
{
base64_decode(text,strlen(text),ct); //base64 解码
AES_set_decrypt_key(key, 16*8, &k); //设置密钥
AES_decrypt((unsigned char*)ct, (unsigned char*)pt, &k); //AES解密
printf("%s\n",pt); //输出最终的字符串
}
return 0;
}
三、编译与脚本组织
这里编译只需要注意加上-l crypto选项引用动态链接库即可,这里由于文件较少,而且Makefile语法比较生疏,所以写了一个shell脚本来完成四个文件的编译:
gcc endecode.c -o endecode -l crypto
gcc get_k.c -o get_k -l crypto
gcc get_x_y.c -o get_x_y -l crypto
gcc get_g_p.c -o get_g_p -l crypto
生成各组件之后,就可以使用shell脚本对整个过程进行组织了。
脚本要做的就是调用之前编译生成的四个程序来实现整个流程,由于脚本实现文件的写入读取非常方便,所以可以避免用户直接调用程序的复杂性,脚本运行的过程中会在当前目录生成一个num文件夹,并将数字的值写入num文件夹中的文件中。
这里脚本还不算是完美,比如如果想要把对方发过来的g、y、p放入num文件夹中,必须手动来完成,如:
echo xxxxxxxx > ./num/X.txt
不过也可以通过直接对脚本的修改来支持对参数的直接录入。
目前脚本中包含5个选项:
- -i 初始化,生成p、g、x、y,并写入num文件夹下对应名称的文件中。
- -x 在已经生成过p和g的情况下生成x和y,并写入num文件夹下对应名称的文件中。
- -k 在已经有p、g、x、y和对方的y的情况下,计算出密钥k并写入num文件夹下对应名称的文件中。
- -e 在已经有k的情况下,加密参数。
- -d 在已经有k的情况下,解密参数。
#!/bin/bash
#文件名: pkcs3.sh
#用途:PKCS3协议实验
if [ $# -lt 1 ];
then
echo 用法:$0 选项
exit 0
fi
base_dir=`cd $(dirname $0); pwd -P`
cur_dir=`pwd`
if [ ! -d ./num ]; #如果目录下没有num文件夹
then
mkdir num; #新建一个文件夹
fi
case $1 in #选择参数
-i ) # 初始化p、g、x、y,在当前目录下生成
pg=(`$base_dir/get_g_p|awk 'NR>1'|xargs`)
echo P is ${pg[0]}
echo ${pg[0]}>$cur_dir/num/p.txt
echo g is ${pg[1]}
echo ${pg[1]}>$cur_dir/num/g.txt
xy=(`$base_dir/get_x_y ${pg[0]} ${pg[1]}|awk 'NR>1'|xargs`)
echo X is ${xy[0]}
echo ${xy[0]}>$cur_dir/num/X.txt
echo Y is ${xy[1]}
echo ${xy[1]}>$cur_dir/num/Y.txt
exit 0;;
-x ) #已有g和P,获取x和y
if [[ ! -e $cur_dir/num/p.txt ]] || [[ ! -e $cur_dir/num/g.txt ]]; #如果缺少大素数或者生成元
then
echo 缺少运算关键文件!
exit 0;
fi
P=`cat $cur_dir/num/p.txt` #获取P
g=`cat $cur_dir/num/g.txt` #获取g
xy=(`$base_dir/get_x_y $P $g|awk 'NR>1'|xargs`)
echo X is ${xy[0]}
echo ${xy[0]}>$cur_dir/num/X.txt
echo Y is ${xy[1]}
echo ${xy[1]}>$cur_dir/num/Y.txt
exit 0;;
-k ) #计算k
if [[ ! -e $cur_dir/num/p.txt ]] || [[ ! -e $cur_dir/num/X.txt ]] || [[ ! -e $cur_dir/num/Yb.txt ]]; #如果缺少大素数、X或者对方的Y
then
echo 缺少运算关键文件!
exit 0;
fi
P=`cat $cur_dir/num/p.txt` #获取P
X=`cat $cur_dir/num/X.txt` #获取X
Yb=`cat $cur_dir/num/Yb.txt` #获取Yb
k=`$base_dir/get_k $X $Yb $P|awk 'NR>1'|xargs` #计算K
echo k is $k
echo $k>$cur_dir/num/K.txt #输出k到文件中
exit 0;;
-e ) #加密
if [[ ! -e $cur_dir/num/K.txt ]]; #如果缺少K
then
echo 请先生成密钥!
exit 0;
fi
K=`cat $cur_dir/num/K.txt`
echo `$base_dir/endecode -e $K $2`
exit 0;;
-d ) #加密
if [[ ! -e $cur_dir/num/K.txt ]]; #如果缺少K
then
echo 请先生成密钥!
exit 0;
fi
K=`cat $cur_dir/num/K.txt`
echo `$base_dir/endecode -d $K $2`
exit 0;;
esac
四、运行
这里建立两个文件夹,分别为A和B,然后再进行运行演示,在此之前我已经把脚本及程序所在的目录添加到系统路径中,因此在任何位置都能够访问这个脚本了。
建立A和B两文件夹:
mkdir A
mkdir B
在A文件夹中:
pkcs3.sh -i
可以看到如下的结果:
此时生成了P和g,以及A的 X a X_a Xa和 Y a Y_a Ya,此时,需要将P、g、 Y a Y_a Ya发送给B。
在B文件夹中:
mkdir num
echo E5695D30DF0D3DEBD4C971CB5597DC6F>num/p.txt
echo "05">num/g.txt
echo 2EC8DD0F35739463A69FED9243AAF0E4>num/Yb.txt
pkcs3.sh -x
得到如下输出:
此时B可以顺手计算出K:
pkcs3.sh -k
然后把Y传给A。
在A文件夹中:
echo DD5FAF629C31CCD35D26957BEA32BD2A>num/Yb.txt
pkcs3.sh -k
可以看到,A和B产生的密钥是一样的,接下来就可以用这个密钥来加密了。
pkcs3.sh -e 123456789
然后把密文发给B。
在B文件夹中:
pkcs3.sh -d etdtuBoNedbR89lOaP2DV1B9IQ==
可以看到,这里成功解析出了A发过来的密文,即成功交换了隐私信息。