RSA算法之实现篇(Markdown重新整理版)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qmickecs/article/details/73556098

序言

RSA中的密钥长度指的是公钥的长度,目前主流的公钥长度为1024、2048以及4096位。由于已经有768位公钥被成功分解的先例,所以低于1024位的公钥都被认为是不安全的。而C++自带的基本类型远远无法满足RSA的运算需求,所以RSA算法的实现必须依赖于高精度整型运算。

本文旨在介绍RSA算法的实现流程,不会对于涉及到的每一个算法进行深入介绍,如果需要进一步了解的可以参考本博客的其它相关文章。

GMP开发环境配置

本文使用的高精度运算库为GMP(GNU多精度运算库)

GMP简介

GMP(the GNU Multiple Precision arithmetic library)是著名的任意精度算术运算库,支持任意精度的整数、有理数以及浮点数的四则运算、求模、求幂、开方等基本运算,还支持部分数论相关运算。Maple、Mathematica等大型数学软件的高精度算术运算功能都是利用GMP实现的。

Linux

直接使用sudo apt-get install libgmp-dev或yum等命令即可从软件源安装GMP。如果要使用tarball方式安装,下载GMP的源代码压缩包后运行下面的命令即可

tar xzf gmp-X.Y.Z.tar.xz  
cd gmp-X.Y.Z
./configure  
make   
make check  
sudo make install  

Windows

Windows下GMP的配置相对麻烦些,需要使用MinGW。

首先下载MinGW的安装管理程序MinGW Installation Manager,然后在basic setup中,将下面标记出来的几项全部右键->”Mark for Installation”
Windows GMP1
然后点击Installation->Apply Changes使更改生效。

完成后,再点击All Packages,找到下图中标记出来的名字为mingw32-gmp,class属于dev的一项,同样Mark for Installation,然后Apply Changes。
Windows GMP2

GMP使用

要使用GMP,只需要包含头文件gmp.h,然后在使用gcc编译时加上参数-lgmp

GMP是一个基于C语言的开源库,其中包含了数种自定义数据类型,包括

  • mpz_t 多精度整型
  • mpq_t 多精度有理数
  • mpf_t 多精度浮点型

RSA中仅设计整数运算,所以我们只会用到mpz_t 类型。

扫描二维码关注公众号,回复: 3186621 查看本文章

GMP要求一个mpz_t类型变量在被使用前必须手动进行初始化,并且不允许对已经初始化的变量进行初始化。

下面是一些本文中使用到的部分函数,其他函数介绍以及用法请参考GMP官方文档:

mpz_t x
声明一个多精度整型变量x

void mpz_init (mpz_t x)
初始化x。任何一个mpz_t类型的变量在使用前都应该初始化。

void mpz_init_set_ui (mpz_t rop, unsigned long int op)
初始化rop,并将其值设置为op

int mpz_init_set_str (mpz_t rop, const char *str, int base)
初始化rop,并赋值rop = str,其中str是一个表示base进制整数的字符数组

void mpz_clear (mpz_t x)
释放x所占用的内存空间

void mpz_sub_ui (mpz_t rop, const mpz_t op1, unsigned long int op2)
计算op1 – op2,结果保存在rop中

void mpz_mul (mpz_t rop, const mpz_t op1, const mpz_t op2)
计算op1 * op2,结果保存在rop中

void gmp_randinit_default (gmp_randstate_t state)
设置state的随机数生成算法,默认为梅森旋转算法

void gmp_randseed_ui (gmp_randstate_t state, unsigned long int seed)
设置state的随机化种子为seed

void mpz_urandomb (mpz_t rop, gmp_randstate_t state, mp_bitcnt_t n)
根据state生成一个在范围0~2^n-1内均匀分布的整数,结果保存在rop中

char * mpz_get_str (char *str, int base, const mpz_t op)
将op以base进制的形式保存到字符数组中,该函数要求指针str为NULL(GMP会自动为其分配合适的空间),或者所指向的数组拥有足够存放op的空间

int gmp_printf (const char *fmt, …)
语法跟C语言中的标准输出函数printf类似。它在printf的基础上,增加了mpz_t等数据类型的格式化输出功能。fmt为输出格式,例如fmt=”%Zd”时,表示输出一个10进制的多精度整型。其后的所有参数为输出的内容。

int mpz_probab_prime_p (const mpz_t n, int reps)
检测n是否为素数。该函数首先对n进行试除,然后使用米勒-拉宾素性检测对n进行测试,reps表示进行检测的次数。如果n为素数,返回2;如果n可能为素数,返回1;如果n为合数,返回0。

可以看到,GMP中的算术函数通常将保存输出结果的变量作为第一个参数,其后的参数为操作数。

下面是一个用GMP计算1+1的程序:

#include <gmp.h>  

int main()  
{  
    mpz_t a, b, c;  
    mpz_init_set_ui(a, 1);  //a = 1  
    mpz_init_set_ui(b, 1);  //b = 1  
    mpz_init(c);  

    mpz_add(c, a, b);  //c = a + b  

    return 0;  
}  

RSA算法实现

RSA算法大体可以分为三个部分:

  • 生成密钥对
  • 加密
  • 解密

生成密钥对

其中生成密钥对包括以下步骤:

  • 随机生成两个足够大的素数 p,q
  • 计算公共模数 n=pq
  • 计算欧拉函数 φ(n)=(p1)(q1)
  • 选取一较小的与φ(n)互质的正整数e作为公共指数。则数对 (n,e) 为密钥对中的公钥
  • 计算 d=e1modφ(n) ,则数对 (n,d) 为密钥对中的私钥

第一步,随机素数生成

根据著名的素数定理,我们可以知道,随机选取一个正整数n,它是素数的概率为 1ln(n) ,这个概率并不算小,所以我们用最暴力的方法选取素数:随机选取一个很大的正整数,检测它是否为素数,如果它不是素数,那我们就可以逐个测试它邻近的奇数,直到找到一个素数为止。

比如,我们需要生成一个长度为1024位的素数,那我们先随机选取一个长度为1024位的正整数,它是素数的概率约为 1ln(21024)1710 ,排除掉偶数,选中素数的概率约为三百五十分之一。

这样问题就转移到如何测试一个正整数是否为素数上了。

目前最常用的素性检测方法是米勒-拉宾素性检测法,这里我们使用GMP中的素数检测函数。

int mpz_probab_prime_p (const mpz_t n, int reps)

代码实现:

gmp_randstate_t grt;  
gmp_randinit_default(grt); //设置随机数生成算法为默认  
gmp_randseed_ui(grt, time(NULL));   //设置随机化种子为当前时间,这几条语句的作用相当于标准C中的srand(time(NULL));  

mpz_t key_p, key_q;  
mpz_init(key_p);  
mpz_init(key_q);    //一个mpz_t类型的变量必须在初始化后才能被使用  

mpz_urandomb(key_p, grt, 1024);  
mpz_urandomb(key_q, grt, 1024); //随机生成一个在0~2^1024-1之间的随机数  

if(mpz_even_p(key_p))  
    mpz_add_ui(key_p, key_p, 1);  
if(mpz_even_p(key_q))  
    mpz_add_ui(key_q, key_q, 1);    //如果生成的随机数为偶数,则加一  

while(!mpz_probab_prime_p(key_p, 25) > 0)  //逐个检查比p大的奇数是否为素数  
    mpz_add_ui(key_p, key_p, 2);  
while(!mpz_probab_prime_p(key_q, 25) > 0)  
    mpz_add_ui(key_q, key_q, 2);  

gmp_printf("%ZX\n", key_p);   //以十六进制的形式输出生成的素数  
gmp_printf("%ZX\n", key_q);  

第二步为简单的乘法运算,直接调用函数void mpz_mul(rop, op1, op2)

mpz_t key_n;  
mpz_init(key_n);  

mpz_mul(key_n, key_p, key_q); //计算p * q,并将结果储存在key_n中  

第三步计算欧拉函数值也只是减法和乘法运算

mpz_t key_f;  
mpz_init(key_f);  

mpz_sub_ui(key_p, key_p, 1);    //p=p-1  
mpz_sub_ui(key_q, key_q, 1);    //q=q-1  
mpz_mul(key_f, key_p, key_q);   //计算(p - 1) * (q - 1),并将结果储存在key_f中  

第四步,选取一个正整数e,并输出公钥(n, e)
公共指数常取3, 17和65537三个值,一般我们直接取e=65537。

mpz_t key_e;  
mpz_init_set_ui(key_e, 65537);//初始化并设置e为65537  

gmp_printf("%s (%ZX, %ZX)\n", "public key is:", key_n, key_e); //输出公钥(n, e)  

第五步求e在模φ(n)下的乘法逆元(也被称为数论倒数)d,

这里使用GMP中的求数论倒数的函数
int mpz_invert(mpz_t rop, const mpz_t op1, const mpz_t op2)

mpz_t key_d;  
mpz_init(key_d);  

mpz_invert(key_d, key_e, key_f); //求e的数论倒数d  

gmp_printf("%s (%ZX, %ZX)\n", "private key is:", key_n, key_e);//输出私钥(n, d)  

加密与解密

加密就是计算

C=Memodn

其中M为明文,(n, e)为公钥,C为密文

而解密则是计算

M=Cdmodn

其中C为密文,(n, d)为私钥,M为明文

可以看到,加密跟解密都是对以下函数进行求值:

fb(a)=abmodn

这种形如 abmodn 的运算,我们称之为模幂运算。模幂运算在密码学中具有十分重要的意义,除了RSA加密外,离散对数加密等常用的加密方法里都有模幂运算。
对于模幂运算,直觉上是先计算a的b次幂,然后再对其取模。但如果模幂运算中的a或者b特别大,那求幂运算将非常困难。不过,根据模运算的特点,我们并不需要直接计算出a的b次方的值。这种算法叫快速模幂。

快速模幂

假设我们正在加密消息M = 3,取公共指数e = 11,公共模数n = 5。
11的二进制形式为1011,那么我们可以把它变成以下形式:
311=383231

由模运算的性质可知,我们只需要利用反复平方法计算

38mod5 32mod5 3mod5 的值,即可计算出 311mod5 的值。

计算3 ^11 mod 5的值,我们首先计算

31mod532mod534mod538mod5=3=(31mod5)2mod5=4=(32mod5)2mod5=1=(34mod5)2mod5=1

由于 311=383231

311mod5=(383231)mod5=[(38mod5)(32mod5)(31mod5)]mod5=(143)mod5=2

这也是为什么之前选取公共指数e的时候,要选择3、17或65537的原因,它们的二进制形式分别为11, 1001, 10000000000000001。在下面的代码中,可以很直观地看到,快速模幂运算中,指数(二进制形式)中1的个数越少,计算效率越高。

快速模幂算法的代码实现:

void mod_exp(mpz_t result, const mpz_t exponent, const mpz_t base, const mpz_t n)  
{  
    char exp[2048 + 10];  
    mpz_get_str(exponent, 2, exp); //把指数e转化为二进制并储存到字符数组exp中  

    mpz_t x, power;  
    mpz_init(power);   
    mpz_init_set_ui(x, 1);  // x = 1  
    mpz_mod(power, base, n); //power = base mod n  

    for(int i = strlen(exp) - 1; i >= 0; i--)  
    {  
        if(exp[i] == '1')  
        {  
            mpz_mul(x, x, power);    
            mpz_mod(x, x, n);   //x = x * power mod n  
        }  
        mpz_mul(power, power, power);  
        mpz_mod(power, power, n);  //power = power^2 mod n  
    }  
    mpz_set(result, x); //返回结果  
}  

那么加密过程的代码实现为:

mpz_t M, C;  
mpz_init(C);  
mpz_init_set_ui(M, 123456789);//假设明文为整数123456789  

mod_exp(C, e, M, n);//加密函数,C = M^e mod n  

解密:

mpz_t M;  
mpz_init(M);  

mod_exp(M, d, C, n);//解密函数,M = C^d mod n  

总结

下面是一个完整的使用RSA算法进行加密解密的程序代码,该代码在Ubuntu14.04 + g++4.8 + GMP6.0.0a下通过编译并正常运行

#include <cstdio>
#include <ctime>
#include <cstring>
#include <cstdlib>
#include <iostream>
#include <gmp.h>

#define KEY_LENGTH 2048  //公钥的长度
#define BASE 16    //输入输出的数字进制

using namespace std;

struct key_pair
{
    char * n;
    char * d;
    int e;
};

//生成两个大素数
mpz_t * gen_primes()
{                                       
    gmp_randstate_t grt;                
    gmp_randinit_default(grt);  
    gmp_randseed_ui(grt, time(NULL));   

    mpz_t key_p, key_q;
    mpz_init(key_p);
    mpz_init(key_q);

    mpz_urandomb(key_p, grt, KEY_LENGTH / 2);       
    mpz_urandomb(key_q, grt, KEY_LENGTH / 2);   //随机生成两个大整数

    mpz_t * result = new mpz_t[2];
    mpz_init(result[0]);
    mpz_init(result[1]);

    mpz_nextprime(result[0], key_p);  //使用GMP自带的素数生成函数
    mpz_nextprime(result[1], key_q);

    mpz_clear(key_p);
    mpz_clear(key_q);

    return result;  
}

//生成密钥对
key_pair * gen_key_pair()
{
    mpz_t * primes = gen_primes();

    mpz_t key_n, key_e, key_f;
    mpz_init(key_n);
    mpz_init(key_f);
    mpz_init_set_ui(key_e, 65537);  //设置e为65537

    mpz_mul(key_n, primes[0], primes[1]);       //计算n=p*q
    mpz_sub_ui(primes[0], primes[0], 1);        //p=p-1
    mpz_sub_ui(primes[1], primes[1], 1);        //q=q-1
    mpz_mul(key_f, primes[0], primes[1]);       //计算欧拉函数φ(n)=(p-1)*(q-1)

    mpz_t key_d;    
    mpz_init(key_d);
    mpz_invert(key_d, key_e, key_f);   //计算数论倒数

    key_pair * result = new key_pair;

    char * buf_n = new char[KEY_LENGTH + 10];
    char * buf_d = new char[KEY_LENGTH + 10];

    mpz_get_str(buf_n, BASE, key_n);
    result->n = buf_n;
    mpz_get_str(buf_d, BASE, key_d);
    result->d = buf_d;
    result->e = 65537;

    mpz_clear(primes[0]);   //释放内存
    mpz_clear(primes[1]);
    mpz_clear(key_n);
    mpz_clear(key_d);
    mpz_clear(key_e);
    mpz_clear(key_f);
    delete []primes;

    return result;
}

//加密函数
char * encrypt(const char * plain_text, const char * key_n, int key_e)  
{
    mpz_t M, C, n;
    mpz_init_set_str(M, plain_text, BASE); 
    mpz_init_set_str(n, key_n, BASE);
    mpz_init_set_ui(C, 0);

    mpz_powm_ui(C, M, key_e, n);    //使用GMP中模幂计算函数

    char * result = new char[KEY_LENGTH + 10];
    mpz_get_str(result, BASE, C);

    return result;
}

//解密函数
char * decrypt(const char * cipher_text, const char * key_n, const char * key_d)  
{
    mpz_t M, C, n, d;
    mpz_init_set_str(C, cipher_text, BASE); 
    mpz_init_set_str(n, key_n, BASE);
    mpz_init_set_str(d, key_d, BASE);
    mpz_init(M);

    mpz_powm(M, C, d, n);   //使用GMP中的模幂计算函数

    char * result = new char[KEY_LENGTH + 10];
    mpz_get_str(result, BASE, M);

    return result;
}

int main()
{       
    key_pair * p = gen_key_pair();

    cout<<"n = "<<p->n<<endl;
    cout<<"d = "<<p->d<<endl;
    cout<<"e = "<<p->e<<endl;

    char buf[KEY_LENGTH + 10];
    cout<<"请输入要加密的数字,二进制长度不超过"<<KEY_LENGTH<<endl;
    cin>>buf;

    char * cipher_text = encrypt(buf, p->n, p->e);
    cout<<"密文为:"<<cipher_text<<endl;
    char * plain_text = decrypt(cipher_text, p->n, p->d);
    cout<<"明文为:"<<plain_text<<endl;

    if(strcmp(buf, plain_text) != 0)
        cout<<"无法解密"<<endl;
    else
        cout<<"解密成功"<<endl;

    return 0;
}

猜你喜欢

转载自blog.csdn.net/qmickecs/article/details/73556098