最近作业需要自己动手实现1024位的RSA,其中需要用到大数运算操作,参考了《信息安全数学基础》和《密码学C/C++实现》中的算法和部分程序,自己动手写了一次大数运算库,效率不是很高,希望大佬们指点指点。本部分是大数运算部分,下一篇博文将设计RSA部分,涉及加密和解密。
实现方法,是采用uint32_t类型来凑,32个uint32_t附加一个uint_t表示大数实际有效位有多少个uint32_t,就凑成了一个1024位的大整数类型BN。BN[0]中存放的是长度信息,BN[1]开始是第一位,直到BN[32]是第32位。类似的方法,定义一个BND,位数是2048+32位,因为虽然规模是1024位,在做乘法尤其模幂的时候可能会出现2048位的中间结果。
在文末给出了本文的完整代码的github链接和《密码学C/C++实现》的链接,自认为代码写的缺点很多,甚至可能有很多Bug,欢迎大神提供编程建议。
P.S.第一次写博文,可能排版不是很好,也希望大神提供建议~(^o^)~
在bignum.h文件中,定义了大数运算的数据结构和一些宏及函数定义。浏览它可以有一个总体的把握。
#define BITPERDIGIT 32//每个小块是uint32,32位顺序和人期望是一致的,当黑盒就好
#define BASE 0x100000000U//每个小块uint32,基数就是2^32,可以在模的时候用上
#define BNMAXDGT 32//最多32“位”个32位
#define ALLONE 0xffffffffU
#define ALLONEL 0xffffffffUL
typedef uint32_t BN[BNMAXDGT + 1];//BN是1024+32位的
#define BNMAXBIT BNMAXDGT<<5 //BN最大能处理1024位
#define BNDMAXBIT BNMAXBIT<<1 //BND最大能处理2048位
typedef uint32_t BND[1 + (BNMAXDGT << 1)];//BND能处理2048位,长度是2048+32
#define LDBITPERDGT 5
#define BNSIZE sizeof(BN)//BN占用的字节数
//一些常量
BN ZERO_BN = { 0 };//0
BN ONE_BN = { 1,1 };//1
BN TWO_BN = { 1,2 }; //2,看上去第一位就是长度为,赋值的时候是[0]
//比较大小
#define MIN(a,b) ((a)<(b)?(a):(b))
//获得位数,[0]里面存的是uint32格式的位数
#define DIGITS_B(n_b) ((uint32_t)*(n_b))
//设置位数,置[0]为想要的位数
#define SETDIGITS_B(n_b, b) (*(n_b) = (uint32_t)(b))
//指向最高位,用指针加法
#define MSDPTR_B(n_b) ((n_b) + DIGITS_B (n_b))
//指向最低位,[0]是位数,[1]才是数字
#define LSDPTR_B(n_b) ((n_b)+1)
//置为全0,位数置为0就可以了,0位,以后可以覆盖
#define SETZERO_B(n_b) (*(n_b) = 0)
//需要使用的一些函数
//增加1位位数
inline void INCDIGITS_B(BN n_b)
{
*(n_b) = *(n_b)+1;
}
//减少1位位数
inline void DECDIGITS_B(BN n_b)
{
if (DIGITS_B(n_b) > 0)
*(n_b) = *(n_b)-1;
}
//先是32“位”的,消除前导0,只要有32为基数的位数,且那一位为0,就位数减去1位,获得实际“位”数
inline void RMLDZRS_B(BN n_b)
{
while ((DIGITS_B(n_b) > 0) && (*MSDPTR_B(n_b) == 0))
{
DECDIGITS_B(n_b);
}
}
//设置一个uint32数为大数,只占1“位”
inline void SETONEBIT_B(BN num, uint32_t u)
{
*LSDPTR_B(num) = u;
SETDIGITS_B(num, 1);
RMLDZRS_B(num);
}
//拷贝src_b到dest_b中,可以复制BND,与结构无关
inline void cpy_b(BN dest_b, BN src_b)
{
uint32_t *lastsrc_b = MSDPTR_B(src_b);//最高位,从高到低复制
*dest_b = *src_b;//复制位数
while ((*lastsrc_b == 0U) && (*dest_b > 0))//去掉前导0
{
lastsrc_b = lastsrc_b - 1;//下一位
*dest_b = *dest_b - 1;//位数-1,去掉前导0
}
while (src_b < lastsrc_b)//复制有值的,lastsrc_b此时指向有数值的最高位
{
dest_b++; src_b++;
*dest_b = *src_b;
}
}
//比较大小两个大数大小,相等返回0,大于返回1,小于返回-1
inline int cmp_b(BN a, BN b)
{
uint32_t *msdptra_l, *msdptrb_l;
int la = (int)DIGITS_B(a);
int lb = (int)DIGITS_B(b);
if (la == 0 && lb == 0)//位数都是0,相等
{
return 0;
}
while (a[la] == 0 && la > 0)//消除a_l前导位的0
{
--la;
}
while (b[lb] == 0 && lb > 0)//消除b_l前导位的0
{
--lb;
}
if (la == 0 && lb == 0)//消除前导位的0以后都是0
{
return 0;
}
if (la > lb)//a的位数比b大
{
return 1;
}
if (la < lb)//a的位数比b小
{
return -1;
}
//位数一样时
msdptra_l = a + la;//指向有效最高位
msdptrb_l = b + lb;//指向有效最高位
while ((*msdptra_l == *msdptrb_l) && (msdptra_l > a))//一位位比较,如果该位数字一样下一位
{
msdptra_l--;
msdptrb_l--;
}
if (msdptra_l == a)//到末位都相同
{
return 0;
}
if (*msdptra_l > *msdptrb_l)//没到末位,此时比较出了a>b
{
return 1;
}
else//没到末位,此时比较出了a
{
return -1;
}
}
//设置每一位为0xffffff来实现小减大取模
inline void setmax_b(BN a)
{
uint32_t *aptr = a;
uint32_t *maptr = a + BNMAXDGT;//指向最高[31]
while (++aptr <= maptr)//全部为0xffffffff全1
{
*aptr = ALLONE;//全1最大
}
SETDIGITS_B(a, BNMAXDGT);//32“位”全1
}
//加法
int add_b(BN a, BN b, BN sum);//a+b=sum,会模掉1024位
void add(BN a, BN b, BN sum);//add的另一个形式,区别是sum可能比预料的多一位,在求模加的时候用得到,没有模1024位
int adduint_b(BN a, uint32_t b, BN sum);//a加一个uint32形式的b,a+b=sum
int adduint_b(uint32_t a, BN b, BN &sum);
//减法
int sub_b(BN a, BN b, BN result);//result=a-b
int subuint_b(BN a, uint32_t b, BN &result);//减一个uint32
void sub(BN a, BN b, BN result);//没有模1024位的减法,会用的到
//乘法
int mul_b(BN a, BN b, BN &result);//result = a*b 留下1024位
int mul(BN a, BN b, BN result);//没有模2^32^32的乘法
//除法
int div_b(BN a, BN b, BN q, BN rem);//a=qb+rem,商是q,余数是rem,a可以是BND,应付模幂运算
int remdiv_b(BN a, BN b, BN & rem);//取余数除法,a/b的余数是rem
//取模
int modn_b(BN a, BN n, BN & rem);//rem=a mod n
int modadd_b(BN a, BN b, BN n, BN & result);//result=a+b(mod n)
void modsub_b(BN a, BN b, BN n, BN &result);//result = a - b(mod n)
void modmul(BN a, BN b, BN n, BN & result);//模乘,result=a*b (mod n )
//信安数学中的函数
int gcd_b(BN a, BN b, BN & result);//求公因子,result=(a,b)
int inv_b(BN a, BN n, BN & x);//如果(a,n)=1,则有ax = 1 (mod n);否则异常没有逆元,返回0,显然逆元不可能为0
int modexp_b(BN b, BN n, BN m, BN & result);//模幂运算,用的模平方,result= b^n (mod m)
int fermat_b(BN a);//用费马小定理检测a是不是素数,选的是2/3/5/7,200位的a会出现误报,500位还没有发现误报
void crt_b(BN a, BN b, BN p, BN q, BN & result);//用中国剩余定理求模幂,a^b mod(p*q)
//位运算函数
int shl_b(BN a);//左移一位
int shr_b(BN a);//右移一位
uint32_t getbits_b(BN a);//获取a的二进制位数,a可以是BND形式,最大2048
//特殊操作的函数
void exclu();//求前几十个素数的乘积,存在文件中,便于后面找素数的时候利用乘积求gcd,多求gcd,少进行费马检测
int genBN(BN result, int bits);//利用sha-1结果产生bits个二进制位的大数result
void writerand(char * addr);//把随机数写到addr中,后面对它求哈希
int findprime(BN a, int bits);//寻找bits位素数a,会调用genBN产生一个大数,然后利用exclu()结果进行调整才去费马
void genpq(char * p_path, char * q_path);//产生私钥p和q存在p_path和q_path文件中
//SHA-1的函数和常数
uint32_t H0 = 0x67452301;
uint32_t H1 = 0xEFCDAB89;
uint32_t H2 = 0x98BADCFE;
uint32_t H3 = 0x10325476;
uint32_t H4 = 0xC3D2E1F0;
const uint32_t K0 = 0x5A827999;
const uint32_t K1 = 0x6ED9EBA1;
const uint32_t K2 = 0x8F1BBCDC;
const uint32_t K3 = 0xCA62C1D6;
void subround(uint32_t & A, uint32_t & B, uint32_t & C, uint32_t & D, uint32_t & E, uint32_t &W, uint32_t K, int mode);
long long msgsize(char*plainaddr);
inline uint32_t cirleft(uint32_t word, int bit);
int mysha1(char *inputfileaddr, char *output);//每次调用用的是全局的,相当于一个状态向量
int SHA1(char *inputfileaddr, char *output);//每次调用里面会重新初始化向量
int checkresult(char * plainpath, char * decrypath);//检查明文和解密以后是否相同,相同返回1
//字符串和文件处理
string bn2str(BN bignum);//bignum转化为字符串形式返回,返回的不显示前导0,和正常预期是一样的,可以吞下BND
int str2bn(BN & bignum, string strbn);//字符串形式的转化为大数,只能转化为BN
int readbn(BN &bignum, string filename);//从文件filename中读取大数到bignum中,字符串是16进制的不能是0x开头
int writebn(string filename, BN bignum);//把大数bignum写入到文件filename中,不带前导0和前缀0x
加法设计算a+b返回结果result。定义了一个uint64_t的存储本位和和进位的carry,以及一个uint32_t数组temp。需要注意的地方是,从低位开始一位位往上加,处理好本位和和进位,本位和送到结果中的这一位,进位是加一次加的时候需要加上。对于第i位,定义的carry是uint64_t,不是二进制32位,初始化为0。carry=ai+bi+ci-1,ci-1是上一次计算carry时获得的进位。
不要误以为加法操作的次数由a和b的最小的“位数”决定,这里指的位数是以uint32_t为单位的(后面不强调二进制位数,说的一般都是这个位数)。还需要有一个额外的加法操作,因为最后加了以后,carry的高32位中,还存着最后一次加法的进位,把这个进位当成bi来看待,进行加法操作,可能不止加1次,因为万一a的高位是FFFFF…,这个进位导致了以后还有好多次加法。
需要注意下面的情况:
①结果可能是1025位的,这种情况下会留下1024位。所以结果先存在uint32_t数组temp中,temp比BN多1位。然后用aptr和bptr分别指向长和短的那个数的最低位,maptr和mbptr指向最高位。
②使用循环,来一位位加,中间结果存在carry中,carry是64位的,carry的高32位中存的是进位,低32位是本位和,把本位和赋值给和。
③最后可能会多了1位,这种情况下需要处理,如果超过了1024位,取1024位
实现的加法及变形
int add_b(BN a, BN b, BN sum);//a+b=sum,会模掉1024位
void add(BN a, BN b, BN sum);//add的另一个形式,区别是sum可能比预料的多一位,在求模加的时候用得到,没有模1024位
int adduint_b(BN a, uint32_t b, BN sum);//a加一个uint32形式的b,a+b=sum
int adduint_b(uint32_t a, BN b, BN &sum);
加法代码
int add_b(BN a, BN b, BN sum)
{
memset(sum, 0, BNSIZE);
uint32_t temps[BNMAXDGT + 2];//先不着急传结果,存在temps里面
memset(temps, 0, sizeof(temps));
uint32_t *aptr, *bptr, *sptr = LSDPTR_B(temps);//指向a,b,temp的末位
uint32_t *maptr, *mbptr;//指向a和b的最高有效位的指针
int flag = FLAG_OK;//初始化为OK
uint64_t carry = 0ULL;//低32位表示中间和,高32位表示进位
if (DIGITS_B(a) < DIGITS_B(b))//如果a的位数比b小,以b为参考做第一次循环
{
aptr = LSDPTR_B(b);//b是长的
maptr = MSDPTR_B(b);
bptr = LSDPTR_B(a);//a是短的
mbptr = MSDPTR_B(a);
SETDIGITS_B(temps, DIGITS_B(b));//要么是b的位数,要么比b的位数多一位,再说
}
else//其他情况a要么比b长,要么和b一样长
{
aptr = LSDPTR_B(a);//a是长的
maptr = MSDPTR_B(a);
bptr = LSDPTR_B(b);//b是短的
mbptr = MSDPTR_B(b);
SETDIGITS_B(temps, DIGITS_B(a));//要么是a的位数,要么比a的位数多一位,再说
}
while (bptr <= mbptr)//b的位数还没完,至少有1位吧
{
carry = (uint64_t)*aptr + (uint64_t)*bptr + (uint64_t)(uint32_t)(carry >> BITPERDIGIT);//a+b+(ci-1)
*sptr = (uint32_t)(carry);//低32位是中间和
aptr++; bptr++; sptr++;//下一位
}
while (aptr <= maptr)//a的位数或许还没有完结,继续把a算完
{
carry = (uint64_t)*aptr + (uint64_t)(uint32_t)(carry >> BITPERDIGIT);//a+(ci-1)
*sptr = (uint32_t)(carry);//低32位是中间和
aptr++; sptr++;//下一位,可能连续进位
}
if (carry&BASE)//如果最后carry有1位进位,和的“位”数比a多1“位”
{
*sptr = 1;//置1,就是1
SETDIGITS_B(temps, DIGITS_B(temps) + 1);//比之前多一位
}
if (DIGITS_B(temps) > (uint32_t)BNMAXDGT)//超过了32“位”,上溢出了
{
SETDIGITS_B(temps, BNMAXDGT);//先设置32“位”,可能有前导0,模掉大于32“位”的
RMLDZRS_B(temps);
flag = FLAG_OF;//加法上溢出了
}
cpy_b(sum, temps);
return flag;
}
与加法类似,减法也采用循环来实现,先判断a和b的大小,如果a>b就按正常的减,和加法类似,中间结果存在carry中,可以判断是否有借位,每次a的位减b的位时,还要减去上一次的借位。否则执行b-a,然后取1024位模。
实现的减法及变形
int sub_b(BN a, BN b, BN result);//result=a-b
int subuint_b(BN a, uint32_t b, BN &result);//减一个uint32
void sub(BN a, BN b, BN result);//没有模1024位的减法,会用的到
减法代码
int sub_b(BN a, BN b, BN result)
{
memset(result, 0, BNSIZE);
uint64_t carry = 0ULL;
uint32_t *aptr, *bptr, *rptr, *maptr, *mbptr;
int flag = FLAG_OK;//检验是否发生意外下溢
uint32_t a_t[BITPERDIGIT + 2];//多了1位
BN b_t;
memset(b_t, 0, sizeof(b_t));
cpy_b(a_t, a);
cpy_b(b_t, b);
aptr = LSDPTR_B(a_t);
bptr = LSDPTR_B(b_t);
rptr = LSDPTR_B(result);
maptr = MSDPTR_B(a_t);
mbptr = MSDPTR_B(b_t);
if (cmp_b(a_t, b_t) == -1)//如果a
{
setmax_b(a_t);
maptr = a_t + BNMAXDGT;//指向[31]
SETDIGITS_B(result, BNMAXDGT);//怕是结果也有这么多位,先设这么多最后消除
flag = FLAG_UF;//下溢了
}
else//没有发生下溢
{
SETDIGITS_B(result, DIGITS_B(a_t));//位数应该和a是一样的
}
while (bptr <= mbptr)//b还有位数,类比加法
{
carry = (uint64_t)*aptr - (uint64_t)*bptr - ((carry&BASE) >> BITPERDIGIT);//a-b-ci(可能之前借位了)
*rptr = (uint32_t)carry;
aptr++; bptr++; rptr++;
}
while (aptr <= maptr)//a还没有完,类比加法
{
carry = (uint64_t)*aptr - ((carry&BASE) >> BITPERDIGIT);//a-b-ci(可能之前借位了)
*rptr = (uint32_t)carry;
aptr++; rptr++;
}
RMLDZRS_B(result);//消除前导0
if (flag == FLAG_UF)//如果下溢了,更正一下
{
add_b(result, a, result);
add_b(result, ONE_BN, result);//(Nm-b+a)+1
}
return flag;
}
乘法计算a和b的乘积,结果存在result中,结果大于1024位就模掉1024位。计算的时候,也用uint64_t carry保存本位和和进位,结果先存在BND形式的变量中,因为结果很可能会超过1024位。
先完成(bn-1bn-2…b1)·a0,然后再进入循环,此时a[1]已经做了,从a[2]开始循环做乘法,加上上一次进位carry。循环完之后去掉前导0,判断是不是超过1024位,如果是,取1024位,然后复制到结果result中。
下面给出的乘法算法摘自《Cryptography in C and C++ (Second Edition)》
实现的乘法及变形
int mul_b(BN a, BN b, BN &result);//result = a*b 留下1024位
int mul(BN a, BN b, BN result);//没有模2^32^32的乘法
乘法代码
int mul_b(BN a, BN b, BN &result)
{
int flag = FLAG_OK;
uint32_t * aptr, *bptr, *maptr, *mbptr, *bcir, *acir, *rptr, *cptr;
uint32_t ta;//暂时存放a
uint64_t carry;//保存本位和进位
BND tempr;//存储最终结果,可能是两倍的BN长度,2048位,不会更长了!
memset(tempr, 0, sizeof(tempr));
BN aa, bb;
memset(aa, 0, BNSIZE);
memset(bb, 0, BNSIZE);
memset(result, 0, BNSIZE);
cpy_b(aa, a); cpy_b(bb, b);//复制
//RMLDZRS_B(aa);//去掉前导0
//RMLDZRS_B(bb);
if (DIGITS_B(aa) == 0 || DIGITS_B(bb) == 0)//有没有乘数为0的投机取巧
{
result[0] = 0; result[1] = 0;//结果为0
return flag;
}
if (DIGITS_B(aa) < DIGITS_B(bb))//如果a的长度比b的短,和加减一样处理
{
aptr = bb;
bptr = aa;
}
else
{
aptr = aa;
bptr = bb;
}
maptr = aptr + *aptr;//指向最高有效位
mbptr = bptr + *bptr;//指向最高有效位
carry = 0;
ta = *LSDPTR_B(aptr);//末位
//完成(bn-1bn-2...b1)·a0
for (bcir = LSDPTR_B(bptr), rptr = LSDPTR_B(tempr); bcir <= mbptr;
bcir++, rptr++)
{
carry = (uint64_t)ta * (uint64_t)*bcir + (uint64_t)(uint32_t)(carry >> BITPERDIGIT);
*rptr = (uint32_t)carry;
}
*rptr = (uint32_t)(carry >> BITPERDIGIT);//最后一次的进位
//循环,a[1]已经做了
for (cptr = LSDPTR_B(tempr) + 1, acir = LSDPTR_B(aptr) + 1;
acir <= maptr; cptr++, acir++)
{
carry = 0;
ta = *acir;
for (bcir = LSDPTR_B(bptr), rptr = cptr;
bcir <= mbptr; bcir++, rptr++)
{
carry = (uint64_t)ta * (uint64_t)*bcir + (uint64_t)*rptr + (uint64_t)(uint32_t)(carry >> BITPERDIGIT);
*rptr = (uint32_t)carry;
}
*rptr = (uint32_t)(carry >> BITPERDIGIT);//最后一次的进位
}
SETDIGITS_B(tempr, DIGITS_B(aptr) + DIGITS_B(bptr));//可能是a+b“位”
RMLDZRS_B(tempr);//去掉前导0
if (DIGITS_B(tempr) > (uint32_t)BNMAXDGT)//乘法溢出,超过了32“位”
{
SETDIGITS_B(tempr, BNMAXDGT);//先设置32“位”,可能有前导0,模掉大于32“位”的
RMLDZRS_B(tempr);
flag = FLAG_OF;//上溢出了
}
cpy_b(result, tempr);
return flag;
}
除法采用的是最简单的方法,计算机组成原理里面就学习到的最简单的除法。把a想象成二进制展开了,把b想象成二进制展开了,然后把b左移移到和a对齐,然后进行比较大小操作,一个个二进制位上商。注意的地方是,除法很可能会出现a是BND,然后商也是BND,所以为了能对齐,把a存在r_t中,把b存在BND格式的b_t中,方便移位对齐。
除法调用的减法是sub(),因为不能模1024位。除法调用的加法是adduint(),商每次加1以后左移1位或者不加左移1位。最后r_t中剩下的就是余数。这个除法效率不高,所以导致了程序整体不快。
除法如果使用SRT方法,在《密码学C/C++实现》的库flint中使用的就是这种方法,非常巧妙,效率非常的高。感兴趣的可以在weki上搜一搜 division algorithm,里面有简单的提到。之前计组课上,这学期密码学课上没有讲,看了许久只知道它会估计中间商,估计以后再调整,误差范围在[-2,+2]。算法大概知道是怎么回事,但是《密码学C/C++实现》里面的没看懂,也没想明白怎么实现,这段时间ddl太多了来不及想,希望大神能教教。
实现的除法及变形
int div_b(BN a, BN b, BN q, BN rem);//a=qb+rem,商是q,余数是rem,a可以是BND,应付模幂运算
int remdiv_b(BN a, BN b, BN & rem);//取余数除法,a/b的余数是rem
除法代码
int div_b(BN a, BN b, BN q, BN rem)
{
memset(rem, 0, sizeof(rem));
memset(q, 0, sizeof(q));
BND b_t;//临时存放b
BND tempq = { 0 };
BND tempsub = { 0 };//存放商和临时差
BND q_t = { 0 };//每个商
BND r_t;
cpy_b(r_t, a);
cpy_b(b_t, b);
if (DIGITS_B(b) == 0)//如果b是0,除0错误
return FLAG_DIVZERO;
else if (DIGITS_B(r_t) == 0)//如果a=0
{
SETZERO_B(q);
SETZERO_B(rem);
return FLAG_OK;
}
else if (cmp_b(r_t, b_t) == -1)//如果a
{
cpy_b(rem, r_t);
SETZERO_B(q);//商为0
return FLAG_OK;
}
else if (cmp_b(r_t, b_t) == 0)//如果a=b,返回1就好了
{
q[0] = 1; q[1] = 1;//商为1
SETZERO_B(rem);//余数为0
return FLAG_OK;
}
else if (DIGITS_B(r_t) == 0)//如果a=0,非常好
{
SETZERO_B(q);
SETZERO_B(rem);
return FLAG_OK;
}
//其它情况下
SETDIGITS_B(q_t, DIGITS_B(r_t) - DIGITS_B(b_t) + 1);
int abit = getbits_b(r_t);
int bbit = getbits_b(b);
int shiftnum = abit - bbit;
int subtimes = abit - bbit + 1;
for (int i = 0; i < shiftnum; i++)
shl_b(b_t);
for (int i = 0; i < subtimes; i++) {
if (cmp_b(r_t, b_t) >= 0)//必须有等号!!!!!
{
sub(r_t, b_t, tempsub);
cpy_b(r_t, tempsub);
shl_b(q_t);
adduint_b(q_t, 1U, tempq);//上1
cpy_b(q_t, tempq);
shr_b(b_t);
}
else
{
shl_b(q_t);//商0
shr_b(b_t);
}
}
cpy_b(q, q_t);
RMLDZRS_B(q);
cpy_b(rem, r_t);
RMLDZRS_B(rem);
return FLAG_OK;
}
有了加减乘除,就拥有了一切,取模变得太简单了!使用除法,取余数,就实现了取模!直接看代码,因为太精简了!其中之所以用了BND temp来存储中间中间结果,也是由于在模幂操作中会发生商超过1024位的情况,商会超过1024位,但是余数不会,取模,正如《密码学C/C++实现》中所说,起到了“百川归海”的作用。
//rem=a mod n
int modn_b(BN a, BN n, BN & rem)
{
int flag = FLAG_OK;
BND temp;
BN result;
memset(rem, 0, BNSIZE);
memset(temp, 0, sizeof(temp));
memset(result, 0, sizeof(result));
if (cmp_b(a, ZERO_BN) == 0)
cpy_b(rem, ZERO_BN);
else
{
flag = div_b(a, n, temp, result);
cpy_b(rem, result);
}
return flag;
}
实现的模除及变形
int modn_b(BN a, BN n, BN & rem);//rem=a mod n
int modadd_b(BN a, BN b, BN n, BN & result);//result=a+b(mod n)
void modsub_b(BN a, BN b, BN n, BN &result);//result = a - b(mod n)
void modmul(BN a, BN b, BN n, BN & result);//模乘,result=a*b (mod n )
有了加减乘除,甚至模除和模乘、模加、模减,还有什么做不到的呢?!数论函数主要有求最大公因子、求逆、模幂、素性检测、用中国剩余定理加速模幂(模n=p*q,p和q都是素数时)。
感觉这个没什么好说的,用欧几里得除法算,算到最后余数为0,上一个余数就是最大公因子。
求逆则是最大公因子反过程,最大公因子倒过来看,就是求逆的过程,在信安数学中,称为寻找“贝祖等式”。在实现上,《密码学C/C++实现》中的算法很巧妙,求x使得ax=1(mod n)。本来以为顺着思路要使用栈,但是却没有使用。算法如下[^1]:
①求逆时,对于inv_b(a,n,x),初始化u=1,g=a,v1=0,v3=n.
②用div_l(g,v3,q,t3)计算带余除法,g=q·v3+t3,令t1=u-q·v1 (mod n ),u=v1,g=v3,v1=t1,v3=t3。
③如果v3=0,则把u赋值给x,作为结果,否则回到步骤②。
使用模平方算法,简单,速度也不慢,计算bn mod m时:
①使用模平方算法,配合调用modmul()模乘函数;
②对指数n进行二进制展开,初始化a=1,b=b,m=m。
③对于n的二进制展开,从低到高一位位来判断,b=bb (mod m),如果这一位为1,则a=ab(mod n),否则a=a;
④最后n的二进制位都算完了,a就是结果。
对于一个数是不是素数,可以使用简单版本的素性检测,跑了许多次,只出现有一次生成了一个200位的数,费马检测判断为素数,但实际为合数。
fermat_b(BN a)函数选的测试参数t是2/3/5/7,先是求(a,2),(a,3),(a,5),(a,7)确认是互素的,然后再求ta-1 mod a,调用modexp()函数来求,其中t依次带入2/3/5/7,如果模幂结果不是1,就返回0(a是合数),否则返回1(a是素数)。
在RSA解密时,已知n=p*q,可以用中国剩余定理大幅提高运算速度。
求模幂,ab mod(p*q)
①先计算b1和b2,在每次求模幂前,可以用算b11=b mod φ§和b22=b mod φ(q):ab11 = b1 mod p ab22 = b2 mod q
②得到方程组 x=b1 mod p
x=b2 mod q
③求逆然后得到结果:
m1=p;m2=q m=m1*m2;
M1=m2=q M2=m1=p
M1*M1’=1 mod p M2*M2’=1 mod q
result= b1*M1’*M1 + b2*M2’*M2 mod(m)
int gcd_b(BN a, BN b, BN & result);//求公因子,result=(a,b)
int inv_b(BN a, BN n, BN & x);//如果(a,n)=1,则有ax = 1 (mod n);否则异常没有逆元,返回0,显然逆元不可能为0
int modexp_b(BN b, BN n, BN m, BN & result);//模幂运算,用的模平方,result= b^n (mod m)
int fermat_b(BN a);//用费马小定理检测a是不是素数,选的是2/3/5/7,200位的a会出现误报,500位还没有发现误报
void crt_b(BN a, BN b, BN p, BN q, BN & result);//用中国剩余定理求模幂,a^b mod(p*q)
也类似加法,用一个64位的carry保存中间结果,carry = ((uint64_t)(*aptr) << 1) | (carry >> BITPERDIGIT),每次carry为本位左移1位同时最右边1位也就是第1位为上一次左移到的第33位。然后再将低32位赋给a的对应位,进行循环。左移和加法一样最后检查carry的第33位是不是1,是的话,再次赋值,总位数加1。
与左移类似,只是会出现有一位移到后面去了,赋值本位的时候需要把最左那位赋值为上次移动到后面的那位。使用中间变量undercarry。
先去掉前导0,然后剩下的位数就是以232为基数的位数,这个位数先乘32,得到的总位数并不一定是最终结果,因为最高有效位位可能没有满,也可能满了,最高有效位满32个二进制位的时候这个就是最终结果。拿最高位high和bit32=80000000U比较,如果high小于它,总位数减1,high左移1个二进制位,进行循环,最后得到的就是真实的位内也不含前导0的二进制位数。
int shl_b(BN a);//左移一位
int shr_b(BN a);//右移一位
uint32_t getbits_b(BN a);//获取a的二进制位数,a可以是BND形式,最大2048
这个函数是为了后面找大素数时求随机大数和前几十个素数的公因子gcd用的,使用时需要手动进入修改产生对应大素数乘积个数,文件名也要修改,findprime中对应位置也需要修改。
①产生方法为,对于从低到高每一位(一位包含了32个二进制位),调用void writerand(char * addr)函数,往addr中写一个随机数,然后调用之前写的SHA-1函数,计算addr出随机数文件的哈希值,然后取32个二进制位(8个十六进制位,也就是8个哈希结果的字符)。
②最高位(最高的可能有1-32个二进制位)需要特殊处理,因为最高如果是要32位,产生的哈希值第一位如果是A以下,如0-9,那最高就没有32位,得到的大数会小于bits。这种情况下,特殊化处理,多次产生直到产生的是所要的为止。其它时候可以左右移位来满足最高位的需求。
该函数调用了genBN(BN result, int bits)函数产生随机数,调用了int fermat_b(BN a)费马检测函数对大数进行素性检测,同时它利用了exclu()函数预先产生的前几十个素数的乘积结果,利用了gcd_b(BN a, BN b, BN & result)函数求公因子。同时需要用到SETONEBIT_B(BN num, uint32_t u)函数设置大数num为一个uint32_t的数u。用到了加法函数add_b(BN a, BN b, BN sum),过程如下:
①首先读取前20个素数的乘积fac,第20个素数存在temp3中;
②随机产生满足位数要求的大数bignum;
③求公因子gcd_b(fac,bignum,gcd),如果gcd=1,就拿这个大数去做费马检测步骤(7)。如果gcd比第20个素数还要大,或者很不幸这个大数是偶数gcd=2或者gcd比temp3大,就回到②;
④对大数进行微调,初始化微调变量linshi=2,循环变量i=0;
⑤大数bignum加上linshi,得到调整后的adjnum,对adjnum和fac求最大公因子gcd。
⑥如果gcd为1,就进行费马检测,步骤⑦,linshi设为2。如果gcd不是1,且gcd
void exclu();//求前几十个素数的乘积,存在文件中,便于后面找素数的时候利用乘积求gcd,多求gcd,少进行费马检测
int genBN(BN result, int bits);//利用sha-1结果产生bits个二进制位的大数result
void writerand(char * addr);//把随机数写到addr中,后面对它求哈希
int findprime(BN a, int bits);//寻找bits位素数a,会调用genBN产生一个大数,然后利用exclu()结果进行调整才去费马
void genpq(char * p_path, char * q_path);//产生私钥p和q存在p_path和q_path文件中
字符串和文件处理没有纯用C语言,用了C++的string类,方便许多,也容易理解。
其中bn2str(BN bignum)返回的不显示前导0,和正常预期是一样的,可以吞下BND然后吐出字符串。内部的缓冲区char strbignum[520],如果是265大小,则只能吞下BN。需要检查的是,到底高位的位内有没有前导零,有的话需要用字符串处理的方式去掉,中间“位”有零也必须显示,而高位为了方便,不显示0,只显示有效数字。
str2bn(BN & bignum, string strbn)只能转化为BN,转的时候,倒着转,先找到字符串的末位,然后每次取8个字符,拼成32位,直到到了最高位如果剩余的不够8个字符,再拼出最高位。
string bn2str(BN bignum);//bignum转化为字符串形式返回,返回的不显示前导0,和正常预期是一样的,可以吞下BND
int str2bn(BN & bignum, string strbn);//字符串形式的转化为大数,只能转化为BN
int readbn(BN &bignum, string filename);//从文件filename中读取大数到bignum中,字符串是16进制的不能是0x开头
int writebn(string filename, BN bignum);//把大数bignum写入到文件filename中,不带前导0和前缀0x
本文实现的大数运算库bignum.h rsa.cpp中,支持1024位RSA的大数运算,如果需要使用它进行其它实验,可以在rsa.cpp中修改。
作者水平有限,参考了《密码学C/C++实现》中的一些方法和风格,不能说是完全原创!也欢迎大神指导,代码的确有很多地方写的不好,很多地方可以进行优化。
编译运行时,如果用VS进行速度优化,运行速度会大幅提高。
[1]本文中的代码可以在作者的github中找到:https://github.com/DXWEIE/RSA-and-large-number-operation.git
[2]《密码学C/C++实现第二版》英文版可以在这里获得:http://www.engineeringbookspdf.com/download/?file=4911
[3]《密码学C/C++实现第二版》源代码可以在这里获得:https://github.com/Apress/cryptography-in-c-cpp