基于PKCS#3与对称加密的密文传输方式

场景1:

在QQ空间的秘密中吐槽时发现自己的好友列表中有一个十分聊得来的人,尽管你们都不知道对方是谁,却还是迫不及待的想要交换自己的QQ,但是你们又都不想把自己的QQ写在这个大家都能看到的地方,毕竟刚才的吐槽可能会让其他人对你留下不好的印象。

场景2:

在某个没有私聊功能的论坛上,你看到了某个人拥有你想要的资源,而他也正好有你想要的资源,于是你们决定互换一波资源,方式就是交换邮箱账号,但是你们又都不希望邮箱明文被其他围观群众看到,不管是因为安全考虑还是某种习惯。

总之,总结一下这两种场景的共同点,就是没有私密的信息交换通道,所以不得不正在公开通道上进行信息传输,如果信息比较隐私的话,可能通信就无法实现了,不过利用PKCS#3协议的原理,再配合对称加密算法,我们就能够不便捷但是安全的传输这些信息了。

一、原理:

PKCS#3是Diffie-Hellman密钥协议标准。PKCS#3描述了一种实现Diffie- Hellman密钥协议的方法,其步骤如下:

  1. 选取大素数p和它的一个生成元g,这些参数公开
  2. A选择随机数 X a X_a Xa,B选择随机数 X b X_b Xb
  3. 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
  4. 交换 Y a Y_a Ya Y b Y_b Yb
  5. 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语言版本来讲解一下整个流程吧。

根据之前的原理可以看出,关键的计算有如下四个:

  1. p和g的产生
  2. x的产生和y的计算
  3. k的计算
  4. 对称加解密运算

所以我也按照这四个关键运算来完成了四个c文件,编译生成了四个组件,接下来就对这四个文件逐一说明,文件中用到的API都是参考这个博主的博文的:

Openssl中的BIGNUM运算函数(整理)

1.p和g的产生

p是一个大素数,在openssl中有大素数生成相关的API,但是似乎没有专门计算生成元g的API,生成元的概念我就不解释了,可以直接百度百科找,这里我找到了一个计算快速计算g的公式,原文链接如下:

生成元快速计算方法一

取安全素数 P 使P = 2Q+1 P Q都必须为素数, 如最小的安全素数为 (5,7,11……) Q 对应就为 (2,3,5……)

取任意数 a 同时满足如下条件:

  1. a 2 a^2 a2 mod P !=1

  2. 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

可以看到如下的结果:

1.png

此时生成了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

得到如下输出:

2

此时B可以顺手计算出K:

pkcs3.sh -k

3

然后把Y传给A。

在A文件夹中:
echo DD5FAF629C31CCD35D26957BEA32BD2A>num/Yb.txt
pkcs3.sh -k

4

可以看到,A和B产生的密钥是一样的,接下来就可以用这个密钥来加密了。

pkcs3.sh -e 123456789

5

然后把密文发给B。

在B文件夹中:
pkcs3.sh -d etdtuBoNedbR89lOaP2DV1B9IQ==

6

可以看到,这里成功解析出了A发过来的密文,即成功交换了隐私信息。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值