zoukankan      html  css  js  c++  java
  • 算法系列2《RSA》

         

    1. RSA介绍

        RSA公钥加密算法是1977年由Ron Rivest、Adi Shamirh和LenAdleman在(美国麻省理工学院)开发的。RSA取名来自开发他们三者的名字。RSA是眼下最有影响力的公钥加密算法,它可以抵抗到眼下为止已知的全部password攻击,已被ISO推荐为公钥数据加密标准。RSA算法基于一个十分简单的数论事实:将两个大素数相乘十分easy,但那时想要对其乘积进行因式分解却极其困难,因此可以将乘积公开作为加密密钥。

                        

        RSA的安全性依赖于大数的因子分解,但并没有从理论上证明破译RSA的难度与大数分解难度等价。即RSA的重大缺陷是无法从理论上把握它的保密性能怎样,并且password学界多数人士倾向于因子分解不是NPC问题。

     

        RSA的缺点主要有:A)产生密钥非常麻烦,受到素数产生技术的限制,因而难以做到一次一密。B)分组长度太大,为保证安全性,n 至少也要600BITS以上,使运算代价非常高,尤其是速度较慢,较对称password算法慢几个数量级;且随着大数分解技术的发展,这个长度还在添加�,不利于数据格式的标准化。眼下,SET(Secure Electronic Transaction)协议中要求CA採用2048BITS长的密钥,其它实体使用1024比特的密钥。C)RSA密钥长度随着保密级别提高,添加�非常快。下表列出了对同一安全级别所相应的密钥长度。

     

          保密级别         

            对称密钥长度(bit)         

              RSA密钥长度(bit)           

               ECC密钥长度(bit)           

              保密年限              

    80

    80

    1024

    160

    2010

    112

    112

    2048

    224

    2030

    128

    128

    3072

    256

    2040

    192

    192

    7680

    384

    2080

    256

    256

    15360

    512

    2120

     

     

        该可逆算法是经批准用于加密和生成数字签名的算法。公钥指数的值仅仅同意是3和216+1。该算法产生的密文及数字签名的长度与模长相等。

     

    描写叙述

    最大长度

    认证中心公钥模

    248字节

    发卡行公钥模

    248字节

    IC卡公钥模

    248字节

     

        认证中心公钥模的长度NCA,发卡行公钥模的长度NI,IC卡公钥模的长度NIC,必须满足NIC≤NI≤NCA和NPE≤NI≤NCA。

     

        注: IC卡中一个记录的长度最长不超过254字节(包含Tag和Length),因而实际IC卡公钥和发卡行公钥长度应小于最大长度248字节。命令数据长度最长为255字节,响应数据最长为256字节,动态签名数据作为IC卡响应数据,也限制了IC卡公钥的最大长度。

     


         如卡片支持DDA和CDA,包括IC卡证书的记录模板的长度,即IC卡公钥证书长度加上证书(’9F46’)和记录模板(’70’)的Tag和Length不超过254字节,则IC卡公钥长度不超过247字节,因而发卡行公钥长度最大长度也不超过247字节。

        依据发卡行应用数据长度不同,IC卡公钥最大长度在205到240之间。假设Generate AC命令响应包括其它可选数据,IC卡公钥最大长度还应减去这些数据的长度(包括Tag和Length)。假设卡片应用支持INTERNALAUTHENTICATION格式二,IC卡公钥最大长度还应减去7字节。

     

        在选择公钥模长时,应该考虑到比較密钥的生命周期同预期的因数分解进程。

        发卡行公钥指数和IC卡公钥指数的值由发卡行决定。认证中心,发卡行和IC卡公钥指数必须等于3或216+1。

        标识本数字签名算法的公钥算法标识必须编码为十六进制‘01’。





    2.RSA算法源代码


    <<RSA.h>>

    // LargeInt.h
    // 大整数类的声明,包括了质数检查,随机数生成,以及RSA密钥生成及加密解密函数。
    
    //////////////////////////////////////////////////////////////////////////////
    
    //RSA加密里所说的1024 位加密、2048位加密,是不是仅仅要 
    //CLargeInt::CreateRandom(e,1024); 
    //这句代码里的參数设置成1024、2048即可了? 
    //
    ////不是的,2048bit,是指P*Q的积是2048bit,也就是说,当你P和Q都设置为1024的时候,加密强度是2048bit,其实P和Q的位数能够不等,而且过分接近的P和Q将导致破解难度极大减少。
    //
    //
    //_data 和 _len 这两个变量究竟起什么作用? 
    //_data 占了4092个字节的大小,_len=64时,仅仅输出了512个字节。 
    ////_data和_len其实是模拟了一个动态数组,用来容纳长度不定的大整数。_data 占领的大小是预先设置好的,这个数值在LargeInt.h中有定义:
    //enum LIMIT{ LENGTH = 0x3FF,SMALLPRIMES = 5000};
    //LENGTH 能够依据须要改动。
    //_len是以4字节为单位的眼下已经占用的数组的长度,_len = 64就是当前大整数的长度是64*4 = 256字节
    //输出的字节是以16进制表示的字符串,每一个字节须要两个符号来表示,所以长度是256*2 = 512字节。
    
    
    
    //必须保证T_DWORD是4字节。
    typedef unsigned long T_DWORD;
    
    class CLargeInt  
    {
    public:
    	//说明:
    	//主要的操作函数之所以没有设置成运算符的重载,主要是由于,
    	//大数运算的过程中,常常须要额外的參数,比如错位相加减的offset值。
    	//以及某些额外的结果
    	//比如,除法运算的商和余数。
    	//为了代码尽量通用,都写成了静态成员函数。
    	//运算符的重载能够通过对这些静态成员函数进一步封装来实现。
    
    	//复制
    	static void Copy(const CLargeInt& source,CLargeInt& target);
    
    	//比較
    	static bool LargerEqual(const CLargeInt& source,const CLargeInt& target,T_DWORD offset = 0);
    	static bool Equal(const CLargeInt& source,const CLargeInt& target);
    
    	//和T_DWORD类型交互的底层操作函数
    	static void Mul(const CLargeInt& faciend,T_DWORD multiplier,CLargeInt& product);
    	static void Div(const CLargeInt& dividend,T_DWORD divisor,CLargeInt& quotient,T_DWORD& residual);
    
    	//移位:
    	//右移一位,用来代替 /2 操作。
    	static void RCR(CLargeInt& n);
    	//左移一位,用来代替 *2 操作。
    	static void RCL(CLargeInt& n);
    
    	//CLargeInt类型底层操作函数,
    	static void Add(const CLargeInt& augend, const CLargeInt& addend, CLargeInt& sum,T_DWORD offset = 0);
    	static void Sub(const CLargeInt& minuend,const CLargeInt& subtrahend, CLargeInt& difference,T_DWORD offset = 0);
    	static void Mul(const CLargeInt& faciend, const CLargeInt& multiplier, CLargeInt& product);
    	static void Div(const CLargeInt& dividend, const CLargeInt& divisor, CLargeInt& quotient,CLargeInt& residual);
    	static void ExpMod(const CLargeInt& source, const CLargeInt& exponent, const CLargeInt& modulo,CLargeInt& result);
    
    	//高阶的測试
    	static bool RabinMillerTest(const CLargeInt& source, const CLargeInt& base);
    	static bool Coprime(const CLargeInt &source, const CLargeInt &target);
    
    	//RSA算法相关函数:
    	static bool IsPrime(const CLargeInt &n);
    	static void CreatePrime(CLargeInt &n);
    	static bool RSACreate( const CLargeInt &p,const CLargeInt & q,const CLargeInt &e,CLargeInt &d,CLargeInt &n);		
    	static void RSAEncode( const CLargeInt &n,const CLargeInt &d,const CLargeInt &m,CLargeInt &c);
    	static void RSADecode( const CLargeInt &n,const CLargeInt &e,const CLargeInt &c,CLargeInt &m);
    	static string RSADecode(const char* N, const char* E, const char* I);
    	//建立随机数。
    	static void CreateRandom(CLargeInt &n,T_DWORD bitcount);
    public:
    	CLargeInt(T_DWORD value = 0);
    	CLargeInt(const char* str);
    	CLargeInt(const CLargeInt& other);
    
    	~CLargeInt();
    
    private:
    	enum LIMIT{ LENGTH = 0x3FF, SMALLPRIMES = 5000};
    	T_DWORD _len;
    	mutable T_DWORD _data[LENGTH]; //之所以用 mutable 是由于须要在计算中获取指针,而且有时还须要在不改变数值的情况下改动某些数组元素。
    
    	//辅助用的小质数表和相关的函数。
    	static std::vector<T_DWORD> _smallPrimes;
    	static void InitSmallPrimes(T_DWORD count = SMALLPRIMES);
    	static bool SmallPrimeTest(const CLargeInt& n);
    	string GetHexStr();
    };
    
    
    // LARGEINT_HEADER


    <<RSA.CPP>>


    std::vector<T_DWORD> CLargeInt::_smallPrimes;
    void CLargeInt::InitSmallPrimes(T_DWORD	count)
    {
    	if (!_smallPrimes.size()) //尚未初始化
    	{
    		_smallPrimes.reserve(count);
    		_smallPrimes.push_back(3);
    		_smallPrimes.push_back(5);
    		_smallPrimes.push_back(7);
    		_smallPrimes.push_back(11);
    		_smallPrimes.push_back(13);
    		T_DWORD	i = 0;
    		for( i = 17; _smallPrimes.size() < count; i+= 2)
    		{
    			bool bePrime = true;
    			for( T_DWORD j = 0; j <	_smallPrimes.size(); j++ )
    			{
    				if( ( _smallPrimes[j] *	_smallPrimes[j]	) > i )	//已经不可能了。
    					break;
    				if( (i % _smallPrimes[j]) == 0)	//能够整除
    					bePrime	= false;				
    			}
    			if( bePrime ) //是素数
    			{
    				_smallPrimes.push_back(i);
    			}
    		}
    	}
    }
    
    bool CLargeInt::SmallPrimeTest(const CLargeInt&	n)
    {
    	if (n._data[0] == 2 && n._len == 1) 
    	{
    		return true; //2是质数。
    	}
    
    	if (n._len == 1	&& n._data[0] <= _smallPrimes.back()) //整数比表中的数据小。
    	{
    		if (std::binary_search(_smallPrimes.begin(),_smallPrimes.end(),n._data[0])) //找到,n存在于质数表中。
    		{
    			return true; //通过測试,数n是质数。
    		}
    
    		return false; //没有通过,n是合数。
    	}
    	else
    	{
    		CLargeInt temp;
    		T_DWORD	r;
    		for( T_DWORD i = 0; i <	_smallPrimes.size(); i++)
    		{
    			Div(n,_smallPrimes[i],temp,r); //依次检查能否整除。
    			if( r == 0) return false; //能整除,说明 n 有小于它的质因子。n 是合数。
    		}
    
    		return true; //	n 没有表中已有的质因子,n有可能是质数。
    	}
    }
    
    bool CLargeInt::IsPrime(const CLargeInt	&n)
    {
    	InitSmallPrimes();
    	if (n._data[0]&1) //奇数
    	{
    		if (n._len == 1	&& n._data[0] <= _smallPrimes.back())
    		{
    			return SmallPrimeTest(n);  //是小质数表中的一个质数。
    		}
    		else
    		{
    			if( SmallPrimeTest(n) )	//通过小质数測试
    			{
    				for( int i = 0;	i < 5; i++)
    				{
    					CLargeInt a(_smallPrimes[ _smallPrimes.size() -	i]);
    					if( !RabinMillerTest(n,a) ) return false;
    				}
    
    				return true; //99.9%的可能是质数。
    			}
    			else 
    			{
    				return false;
    			}
    		}
    	}
    
    	return false;
    }
    
    void CLargeInt::CreatePrime(CLargeInt &n)
    {
    	n._data[0] |= 0x01;
    	while (!IsPrime(n))
    	{
    		Add(n, 2, n); //步进2,寻找最接近的下一个质数。这个算法并非寻找质数最好的算法,可是它能够保证寻找到相邻的质数。
    	}
    }
    
    CLargeInt::CLargeInt(T_DWORD value) : _len(1)
    {
    	_data[0] = value;
    }
    
    CLargeInt::CLargeInt(const CLargeInt& other)
    {
    	Copy(other,*this);
    }
    
    CLargeInt::CLargeInt(const char	* str):	_len(1)
    {
    	_data[0] = 0;
    
    	//if(str && strlen(str) >	2 && ( strncmp(str,"0x",2) == 0	|| strncmp(str,"0X",2) == 0 ) )
    	if (str)
    	{
    		CLargeInt temp;
    		char ch;
    		T_DWORD	n = 0;
    		const char * p = str;
    		while (*p)
    		{
    			ch	= *p;
    			if ((ch >= '0') && (ch <= '9'))
    			{
    				n = ch - '0';
    			}
    			else if ((ch >= 'a') && (ch <= 'f'))    
    			{
    				n = ch - 'a' + 10;       //a->f分别代表10->15   a的AscII为97,所以这里应该减去87
    			}
    			else if ((ch >= 'A') && (ch <= 'F'))
    			{
    				n = ch - 'A' + 10;       //同上
    			}
    			p++;
    			Mul(*this, 16, temp);
    			Add(temp, n, *this);
    		}
    	}
    	//else
    	//{//这里应该用来处理除了0x开头的16进制字符串的情况。
    	//	__asm int 3;
    	//}
    }
    
    CLargeInt::~CLargeInt()
    {
    
    }
    
    void CLargeInt::Copy(const CLargeInt& source, CLargeInt& target)
    {
    	if (&source != &target) //避免自身拷贝。
    	{
    		target._len = source._len;
    
    		//相信库函数能提供最高效的实现。
    		memcpy(target._data, source._data, source._len * 4);
    	}
    }
    
    bool CLargeInt::Equal(const CLargeInt& source, const CLargeInt& target)
    {
    	if (source._len	== target._len)
    	{
    		//相信库函数能提供最高效的实现。
    		return !(memcmp(source._data, target._data,source._len * 4));
    	}
    	else
    	{
    		return false;
    	}
    }
    
    bool CLargeInt::LargerEqual(const CLargeInt& source,const CLargeInt& target,T_DWORD offset )
    {
    	if( source._len	> (target._len + offset) ) return true;
    	else if( source._len < (target._len + offset) )	return false;
    	else
    	{
    		int end	= offset;
    		for( int i = source._len - 1; i	>= end ; i--)
    		{
    			T_DWORD	ii = i - offset;
    			if( source._data[i] > target._data[ii] ) return	true; //大于
    			else if( source._data[i] < target._data[ii] )	return false;
    		}
    		return true; //相等。
    	}
    
    }
    
    void CLargeInt::Add(const CLargeInt &augend, const CLargeInt &addend, CLargeInt	&sum,T_DWORD offset)
    {
    	T_DWORD	len,len1,len2; //len绝对不能<1,否则会出错。
    	T_DWORD	*p1 = 0;
    	T_DWORD	*p2 = 0;
    	T_DWORD	*p3 = 0;
    
    	if( augend._len	>= (addend._len+offset)	)
    	{
    		len = augend._len - offset;
    		len1 = addend._len;
    		len2 =	len - len1;
    		p1 = augend._data+offset;
    		p2 = addend._data;
    		p3 = sum._data+offset;
    	}
    	else
    	{
    		len = addend._len;
    		if( augend._len	<= offset )
    		{
    			augend._data[offset] = 0;
    			len1 = 1;
    			len2 = addend._len - len1;
    		}
    		else
    		{
    			len1 = augend._len - offset;
    			len2 = addend._len - len1;
    		}		
    		p1 = addend._data;
    		p2 = augend._data + offset;
    		p3 = sum._data + offset;
    	}
    
    	__asm
    	{
    		pushad;
    		//加载计算地址
    		mov esi,p1;
    		mov ebx,p2;
    		mov edi,p3;
    
    		mov eax,len1;
    		sal eax,2;
    
    		//计算末地址
    		add esi,eax;
    		add ebx,eax;
    		add edi,eax;
    
    		mov ecx,eax;
    		add ecx,4*7;
    		and ecx,0xFFFFFFE0; //ecx - ecx%32,这里的ecx就变成了偏移址,同一时候也是累加器。
    		neg ecx; //取负,用作累加。
    
    		sar eax,2; //回复到原来的长度
    
    		neg eax; //取负
    		and eax,dword ptr 7;
    		jz labeladd0; //假设低位是0,说明长度至少有8(由于长度为0是非法的)
    
    		mov edx,dword ptr 0xC;
    		mul edx; //eax变为相对偏移址。
    
    		lea edx,labeladd1; //加载起始地址
    		sub edx,dword ptr 0xC;
    		add eax,edx; //计算出跳转地址
    
    		clc; //清除标志位。
    		jmp eax; //跳转
    labeladdloop:
    		popf;
    labeladd0:
    		mov eax,[esi+ecx+0*4];
    		adc eax,[ebx+ecx+0*4];
    		mov [edi+ecx+0*4],eax;
    labeladd1:
    		mov eax,[esi+ecx+1*4];
    		adc eax,[ebx+ecx+1*4];
    		mov [edi+ecx+1*4],eax;
    		//labeladd2:
    		mov eax,[esi+ecx+2*4];
    		adc eax,[ebx+ecx+2*4];
    		mov [edi+ecx+2*4],eax;
    		//labeladd3:
    		mov eax,[esi+ecx+3*4];
    		adc eax,[ebx+ecx+3*4];
    		mov [edi+ecx+3*4],eax;
    		//labeladd4:
    		mov eax,[esi+ecx+4*4];
    		adc eax,[ebx+ecx+4*4];
    		mov [edi+ecx+4*4],eax;
    		//labeladd5:
    		mov eax,[esi+ecx+5*4];
    		adc eax,[ebx+ecx+5*4];
    		mov [edi+ecx+5*4],eax;
    		//labeladd6:
    		mov eax,[esi+ecx+6*4];
    		adc eax,[ebx+ecx+6*4];
    		mov [edi+ecx+6*4],eax;
    		//labeladd7:
    		mov eax,[esi+ecx+7*4];
    		adc eax,[ebx+ecx+7*4];
    		mov [edi+ecx+7*4],eax;
    		//labeladd8:
    		pushf;
    		add ecx,32;
    		jnz labeladdloop;
    
    		//执行到这里了,说明两数重合部分已经处理完了。開始处理两数相差的部分。
    
    		mov ecx,len2; //加载长度
    
    		xor ebx,ebx; //由于ebx指向的缓冲区废弃不用了,所以使用已经空暇的ebx寄存器。
    		cmp ebx,ecx; //推断ecx是否等于ebx(0)
    		jz labelcarry; //假设相等,说明已经处理完了,跳转到处理最后一次进位的地方。
    
    labelfix:
    		popf; //弹出上次的标志位。
    
    		mov eax,[esi+ebx*4];
    		adc eax,0;	
    		mov [edi+ebx*4],eax;
    
    		//假设这里没有进位,剩下的部分能够直接拷贝。拷贝的长度就是ecx-1
    		jnc labelcopy;
    
    		pushf; //保存标志位。
    
    		inc ebx; //步进
    		dec ecx;
    		jnz labelfix; //循环。
    
    labelcarry:
    		popf; //弹出标志位
    		jnc labelend; //没有进位就直接结束
    		//没有跳转说明有进位:
    		mov dword ptr [edi+ebx*4],dword	ptr 0x00000001;	//直接置1
    		lea eax,len;
    		inc [eax]; //总长度+1
    		inc ecx; //补一次。
    labelcopy:
    		dec ecx;
    		sal ecx,2;
    		cmp edi,esi; //比較源地址和目标地址,假设同样就跳过。
    		jz labelend;
    
    		sal ebx,2;
    		add ebx,4;
    
    		add edi,ebx;
    		add esi,ebx;
    
    		rep movs dword ptr [edi],dword ptr [esi];
    
    labelend:
    		popad;
    	}
    	sum._len = len + offset;	
    }
    
    void CLargeInt::Sub(const CLargeInt& minuend,const CLargeInt& subtrahend,CLargeInt& difference,T_DWORD offset)
    {
    	T_DWORD	len,len1,len2; //len绝对不能<1,否则会出错。
    	T_DWORD	*p1 = 0;
    	T_DWORD	*p2 = 0;
    	T_DWORD	*p3 = 0;
    
    	if( minuend._len >= (subtrahend._len+offset) )
    	{
    		len = minuend._len- offset;
    		len1 = subtrahend._len;
    		len2 =	len - len1;
    		p1 = minuend._data + offset;
    		p2 = subtrahend._data;
    		p3 = difference._data +	offset;
    	}
    	else
    	{
    		__asm int 3; //出错。
    	}
    
    	__asm
    	{
    		pushad;
    		//加载计算地址
    		mov esi,p1;
    		mov ebx,p2;
    		mov edi,p3;
    
    		mov eax,len1;
    		sal eax,2;
    
    		//计算末地址
    		add esi,eax;
    		add ebx,eax;
    		add edi,eax;
    
    		mov ecx,eax;
    		add ecx,4*7;
    		and ecx,0xFFFFFFE0; //ecx - ecx%32,这里的ecx就变成了偏移址,同一时候也是累加器。
    		neg ecx; //取负,用作累加。
    
    		sar eax,2; //回复到原来的长度
    
    		neg eax; //取负
    		and eax,dword ptr 7;
    		jz labeladd0; //假设低位是0,说明长度至少有8(由于长度为0是非法的)
    
    		mov edx,dword ptr 0xC;
    		mul edx; //eax变为相对偏移址。
    
    		lea edx,labeladd1; //加载起始地址
    		sub edx,dword ptr 0xC;
    		add eax,edx; //计算出跳转地址
    
    		clc; //清除标志位。
    		jmp eax; //跳转
    labeladdloop:
    		popf;
    labeladd0:
    		mov eax,[esi+ecx+0*4];
    		sbb eax,[ebx+ecx+0*4];
    		mov [edi+ecx+0*4],eax;
    labeladd1:
    		mov eax,[esi+ecx+1*4];
    		sbb eax,[ebx+ecx+1*4];
    		mov [edi+ecx+1*4],eax;
    		//labeladd2:
    		mov eax,[esi+ecx+2*4];
    		sbb eax,[ebx+ecx+2*4];
    		mov [edi+ecx+2*4],eax;
    		//labeladd3:
    		mov eax,[esi+ecx+3*4];
    		sbb eax,[ebx+ecx+3*4];
    		mov [edi+ecx+3*4],eax;
    		//labeladd4:
    		mov eax,[esi+ecx+4*4];
    		sbb eax,[ebx+ecx+4*4];
    		mov [edi+ecx+4*4],eax;
    		//labeladd5:
    		mov eax,[esi+ecx+5*4];
    		sbb eax,[ebx+ecx+5*4];
    		mov [edi+ecx+5*4],eax;
    		//labeladd6:
    		mov eax,[esi+ecx+6*4];
    		sbb eax,[ebx+ecx+6*4];
    		mov [edi+ecx+6*4],eax;
    		//labeladd7:
    		mov eax,[esi+ecx+7*4];
    		sbb eax,[ebx+ecx+7*4];
    		mov [edi+ecx+7*4],eax;
    		//labeladd8:
    		pushf;
    		add ecx,32;
    		jnz labeladdloop;
    
    		//执行到这里了,说明两数重合部分已经处理完了。開始处理两数相差的部分。
    
    		mov ecx,len2; //加载长度,
    		//仅仅处理p1的buf
    
    		xor ebx,ebx; //由于ebx指向的缓冲区废弃不用了,所以使用已经空暇的ebx寄存器。
    		cmp ebx,ecx; //推断ecx是否等于ebx
    		jz labelcarry; //假设相等,说明已经处理完了,跳转到处理最后一次进位的地方。
    
    labelfix:
    		popf; //弹出上次的标志位。
    
    		mov eax,[esi+ebx*4];
    		sbb eax,0;	
    		mov [edi+ebx*4],eax;
    
    		//假设这里没有进位,剩下的部分能够直接拷贝。拷贝的长度就是ecx-1
    		jnc labelcopy;
    
    		pushf; //保存标志位。
    
    		inc ebx; //步进
    		dec ecx;
    		jnz labelfix; //循环。
    
    labelcarry:
    		popf; //弹出标志位
    		jnc labelend; //没有进位就直接结束
    		//没有跳转说明有进位:
    		int 3; //说明减数不够,结果为负
    		mov dword ptr [edi+ebx*4],dword	ptr 0x00000001;	//直接置1
    		lea eax,len;
    		dec [eax]; //总长度-1
    		inc ecx; //补一次。
    labelcopy:
    		dec ecx;
    		sal ecx,2;
    		cmp edi,esi; //比較源地址和目标地址,假设同样就跳过。
    		jz labelend;
    
    		sal ebx,2;
    		add ebx,4;
    
    		add edi,ebx;
    		add esi,ebx;
    
    		rep movs dword ptr [edi],dword ptr [esi];
    
    labelend:
    		popad;
    	}
    	difference._len	= len+offset;
    	for( T_DWORD i = difference._len-1; i >	0; i--)
    	{
    		if( difference._data[i]	== 0 ) //末尾是0
    		{
    			difference._len--;
    		}
    		else break;
    	}
    }
    
    
    void CLargeInt::Mul(const CLargeInt& faciend,T_DWORD multiplier,CLargeInt& product)
    {
    	T_DWORD	len; //len绝对不能<1,否则会出错。
    	T_DWORD	*p1 = 0;
    	T_DWORD	*p2 = 0;
    	T_DWORD	*p3 = 0;
    
    	len = faciend._len;
    	p1 = faciend._data;
    	p2 = 0;
    	p3 = product._data;
    
    	memset(p3,0,(len+1)*4);
    	__asm
    	{
    		pushad;
    		//加载计算地址
    		mov esi,p1;
    		mov edi,p3;
    		mov ebx,multiplier
    
    			mov eax,len;
    		sal eax,2;
    
    		//计算末地址
    		add esi,eax;
    		add edi,eax;
    
    		mov ecx,eax;
    		add ecx,4*7;
    		and ecx,0xFFFFFFE0; //ecx - ecx%32,这里的ecx就变成了偏移址,同一时候也是累加器。
    		neg ecx; //取负,用作累加。
    
    		sar eax,2; //回复到原来的长度
    
    		neg eax; //取负
    		and eax,dword ptr 7;
    		jz labeladd0; //假设低位是0,说明长度至少有8(由于长度为0是非法的)
    
    		mov edx,dword ptr 0xe; //0xe是跳转地址的系数。
    		mul edx; //eax变为相对偏移址。
    
    		lea edx,labeladd1; //加载起始地址
    		sub edx,dword ptr 0xe;
    		add eax,edx; //计算出跳转地址
    
    		clc; //清除标志位。
    		jmp eax; //跳转
    labeladdloop:
    labeladd0:
    		mov eax,[esi+ecx+0*4];
    		mul ebx;
    		add [edi+ecx+0*4],eax;
    		adc [edi+ecx+1*4],edx;
    labeladd1:
    		mov eax,[esi+ecx+1*4];
    		mul ebx;
    		add [edi+ecx+1*4],eax;
    		adc [edi+ecx+2*4],edx;
    		//labeladd2:
    		mov eax,[esi+ecx+2*4];
    		mul ebx;
    		add [edi+ecx+2*4],eax;
    		adc [edi+ecx+3*4],edx;
    		//labeladd3:
    		mov eax,[esi+ecx+3*4];
    		mul ebx;
    		add [edi+ecx+3*4],eax;
    		adc [edi+ecx+4*4],edx;
    		//labeladd4:
    		mov eax,[esi+ecx+4*4];
    		mul ebx;
    		add [edi+ecx+4*4],eax;
    		adc [edi+ecx+5*4],edx;
    		//labeladd5:
    		mov eax,[esi+ecx+5*4];
    		mul ebx;
    		add [edi+ecx+5*4],eax;
    		adc [edi+ecx+6*4],edx;
    		//labeladd6:
    		mov eax,[esi+ecx+6*4];
    		mul ebx;
    		add [edi+ecx+6*4],eax;
    		adc [edi+ecx+7*4],edx;
    		//labeladd7:
    		mov eax,[esi+ecx+7*4];
    		mul ebx;
    		add [edi+ecx+7*4],eax;
    		adc [edi+ecx+8*4],edx;
    		//labeladd8:
    		add ecx,32;
    		jnz labeladdloop;
    
    		//labelend:
    		popad;
    	}
    	product._len = len+1;
    
    	for( T_DWORD i = product._len-1; i > 0;	i--)
    	{
    		if( product._data[i] ==	0 ) //末尾是0
    		{
    			product._len--;
    		}
    		else break;
    	}
    
    }
    
    void CLargeInt::RCR(CLargeInt& n)
    {
    	T_DWORD	len; //len绝对不能<1,否则会出错。
    	T_DWORD	*p1 = 0;	
    	len = n._len;
    	p1 = n._data;
    
    	__asm
    	{
    		pushad;		
    
    		//加载计算地址
    		mov esi,p1;
    
    		mov eax,len;
    		mov ecx,eax;
    
    		add ecx,dword ptr 15;
    		and ecx,dword ptr 0xFFFFFFF0;
    
    		mov eax,ecx;
    		sal ecx,dword ptr 2;
    
    		sub eax,len; //求初始偏移。
    
    		mov ebx,dword ptr 4; //偏移倍率
    		mul ebx;		
    
    		lea ebx,label0;
    		add eax,ebx;
    		clc;
    		jmp eax;
    labelloop:
    		popf;
    label0:
    		rcr dword ptr [esi + ecx - 1*4],1;
    		//label1:
    		rcr dword ptr [esi + ecx - 2*4],1;
    		//label2:
    		rcr dword ptr [esi + ecx - 3*4],1;
    		//label3:
    		rcr dword ptr [esi + ecx - 4*4],1;
    		//label4:
    		rcr dword ptr [esi + ecx - 5*4],1;
    		//label5:
    		rcr dword ptr [esi + ecx - 6*4],1;
    		//label6:
    		rcr dword ptr [esi + ecx - 7*4],1;
    		//label7:
    		rcr dword ptr [esi + ecx - 8*4],1;
    		//label8:
    		rcr dword ptr [esi + ecx - 9*4],1;
    		//label9:
    		rcr dword ptr [esi + ecx - 10*4],1;
    		//label10:
    		rcr dword ptr [esi + ecx - 11*4],1;
    		//label11:
    		rcr dword ptr [esi + ecx - 12*4],1;
    		//label12:
    		rcr dword ptr [esi + ecx - 13*4],1;
    		//label13:
    		rcr dword ptr [esi + ecx - 14*4],1;
    		//label14:
    		rcr dword ptr [esi + ecx - 15*4],1;
    		//label15:
    		rcr dword ptr [esi + ecx - 16*4],1;
    		//label16:
    		pushf;
    		sub ecx,dword ptr 64;
    		jnz labelloop;
    		popf;
    		popad;
    	}
    	for( T_DWORD i = n._len-1; i > 0; i--)
    	{
    		if( n._data[i] == 0 ) //末尾是0
    		{
    			n._len--;
    		}
    		else break;
    	}
    }
    
    void CLargeInt::RCL(CLargeInt& n)
    {
    	T_DWORD	len = n._len; //len绝对不能<1,否则会出错。
    	T_DWORD	*p1 = 0;
    
    	n._data[len] = 0;
    	n._len++;
    
    	len++;
    	p1 = n._data;
    
    	__asm
    	{
    		pushad;		
    
    		//加载计算地址
    		mov esi,p1;
    
    		mov eax,len;
    		mov ecx,eax;
    
    		sal eax,dword ptr 2;
    		add esi,eax;
    
    		add ecx,dword ptr 15;
    		and ecx,dword ptr 0xFFFFFFF0;
    
    		mov eax,ecx;
    		sal ecx,dword ptr 2;
    
    		neg ecx;
    
    		sub eax,len; //求初始偏移。
    		jz label0;
    
    		mov ebx,dword ptr 4; //偏移倍率
    		mul ebx;		
    
    		lea ebx,label1;
    		sub ebx,4;
    		add eax,ebx;
    		clc;
    		jmp eax;
    labelloop:
    		popf;
    label0:
    		rcl dword ptr [esi + ecx + 0*4],1;
    label1:
    		rcl dword ptr [esi + ecx + 1*4],1;
    		//label2:
    		rcl dword ptr [esi + ecx + 2*4],1;
    		//label3:
    		rcl dword ptr [esi + ecx + 3*4],1;
    		//label4:
    		rcl dword ptr [esi + ecx + 4*4],1;
    		//label5:
    		rcl dword ptr [esi + ecx + 5*4],1;
    		//label6:
    		rcl dword ptr [esi + ecx + 6*4],1;
    		//label7:
    		rcl dword ptr [esi + ecx + 7*4],1;
    		//label8:
    		rcl dword ptr [esi + ecx + 8*4],1;
    		//label9:
    		rcl dword ptr [esi + ecx + 9*4],1;
    		//label10:
    		rcl dword ptr [esi + ecx + 10*4],1;
    		//label11:
    		rcl dword ptr [esi + ecx + 11*4],1;
    		//label12:
    		rcl dword ptr [esi + ecx + 12*4],1;
    		//label13:
    		rcl dword ptr [esi + ecx + 13*4],1;
    		//label14:
    		rcl dword ptr [esi + ecx + 14*4],1;
    		//label15:
    		rcl dword ptr [esi + ecx + 15*4],1;
    		//label16:
    		pushf;
    		add ecx,dword ptr 64;
    		jnz labelloop;
    		popf;
    		popad;
    	}
    	for( T_DWORD i = n._len-1; i > 0; i--)
    	{
    		if( n._data[i] == 0 ) //末尾是0
    		{
    			n._len--;
    		}
    		else break;
    	}
    }
    
    void CLargeInt::Mul(const CLargeInt& faciend, const CLargeInt& multiplier, CLargeInt& product)
    {
    	CLargeInt temp;
    	product._len = (faciend._len + multiplier._len);
    	memset(product._data, 0, product._len * 4);
    
    	for (T_DWORD i = 0; i <	multiplier._len; i++)
    	{
    		Mul(faciend,multiplier._data[i],temp);
    		Add(product,temp,product,i);
    	}
    	for (T_DWORD i = product._len - 1; i > 0;	i--)
    	{
    		if( product._data[i] ==	0 ) //末尾是0
    		{
    			product._len--;
    		}
    		else
    		{
    			break;
    		}
    	}
    }
    
    void CLargeInt::Div(const CLargeInt& dividend,T_DWORD divisor,CLargeInt& quotient,T_DWORD& residual)
    {
    	T_DWORD	len; //len绝对不能<1,否则会出错。
    	T_DWORD	*p1 = 0;
    	T_DWORD	*p2 = 0;
    
    	T_DWORD	*pr = &residual;
    
    	len = dividend._len;
    	quotient._len =	dividend._len;
    	p1 = dividend._data;
    	p2 = quotient._data;
    
    	__asm
    	{
    		pushad;		
    
    		//加载计算地址
    		mov esi,p1;
    		mov edi,p2;
    
    		mov eax,len;
    		mov ecx,eax;
    
    		add ecx,dword ptr 7;
    		and ecx,dword ptr 0xFFFFFFF8;
    
    		mov eax,ecx;
    		sal ecx,dword ptr 2;
    
    		sub eax,len; //求初始偏移。
    
    		mov ebx,dword ptr 0x0A;	//偏移倍率
    		mul ebx;		
    
    		lea edx,label0;
    		add eax,edx;
    
    		xor edx,edx;
    
    		mov ebx,divisor;
    
    		clc;
    		jmp eax;
    labelloop:
    
    label0:
    		mov eax,[esi + ecx - 1*4];
    		div ebx;
    		mov [edi + ecx - 1*4],eax;
    		//label1:
    		mov eax,[esi + ecx - 2*4];
    		div ebx;
    		mov [edi + ecx - 2*4],eax;
    		//label2:
    		mov eax,[esi + ecx - 3*4];
    		div ebx;
    		mov [edi + ecx - 3*4],eax;
    		//label3:
    		mov eax,[esi + ecx - 4*4];
    		div ebx;
    		mov [edi + ecx - 4*4],eax;
    		//label4:
    		mov eax,[esi + ecx - 5*4];
    		div ebx;
    		mov [edi + ecx - 5*4],eax;
    		//label5:
    		mov eax,[esi + ecx - 6*4];
    		div ebx;
    		mov [edi + ecx - 6*4],eax;
    		//label6:
    		mov eax,[esi + ecx - 7*4];
    		div ebx;
    		mov [edi + ecx - 7*4],eax;
    		//label7:
    		mov eax,[esi + ecx - 8*4];
    		div ebx;
    		mov [edi + ecx - 8*4],eax;
    		//label8:
    		sub ecx,dword ptr 32;
    		jnz labelloop;
    
    		//执行到这里说明已经计算完毕,最后保存余数。
    		mov ecx,pr;
    		mov [ecx],edx;
    		popad;
    	}
    
    	for (T_DWORD i = quotient._len-1; i > 0; i--)
    	{
    		if (quotient._data[i] == 0) //末尾是0
    		{
    			quotient._len--;
    		}
    		else 
    		{
    			break;
    		}
    	}
    }
    
    void CLargeInt::Div(const CLargeInt& dividend,const CLargeInt& divisor,CLargeInt& quotient,CLargeInt& residual)
    {
    	if( dividend._len < divisor._len )
    	{
    		quotient._len =	1;
    		quotient._data[0] = 0;
    		Copy(dividend,residual);
    	}
    	else
    	{
    		if( divisor._len == 1)
    		{			
    			Div(dividend,divisor._data[0],quotient,residual._data[0]);
    			residual._len =	1;
    		}
    		else
    		{
    			CLargeInt d,r;
    
    			//满位算法
    			T_DWORD	head = divisor._data[divisor._len-1];
    			__asm
    			{
    				pushad;
    				mov eax,head;
    				bsr ecx,eax;
    				mov edx,dword ptr 31;
    				sub edx,ecx;
    				mov ecx,edx;
    				mov eax,dword ptr 0x01;
    				sal eax,cl;
    				mov head,eax;
    				popad;
    			}
    
    			Mul(dividend,head,d);
    			Mul(divisor,head,r);
    
    			quotient._len =	d._len - r._len	+ 1;
    			quotient._data[quotient._len - 1] = 0;
    
    			T_DWORD	newhead	= r._data[r._len - 1];
    			//以上处理的目的是使得被除数最高位的1移动到DWORD的最高位。
    			//处理之后能够极大降低商的上下限之间的差距。
    
    			T_DWORD	highpart = 0;
    			T_DWORD	lowpart	 = 0;
    			T_DWORD	max = 0,min = 0;
    			T_DWORD	offset = 0;
    			CLargeInt temp;
    			for( T_DWORD i = d._len-1; i >=	r._len-1; i-- )
    			{				
    				offset = i - (r._len - 1);	
    				lowpart	= d._data[i];
    
    				__asm
    				{//这段汇编代码的作用是求商的上下限。min & max
    					pushad;
    					mov edx,highpart;
    					mov eax,lowpart;
    					mov ecx,newhead;
    					div ecx;
    					mov max,eax;
    
    					inc ecx;
    					jz _mov;
    					mov edx,highpart;
    					mov eax,lowpart;
    					div ecx;
    					mov edx,eax;
    _mov:
    					mov min,edx;
    					popad;
    				}
    
    				if (max == 0)
    				{
    					highpart = d._data[i];
    					quotient._data[offset] = 0;
    				}
    				else
    				{
    					Mul(r,max,temp);
    					if ( max	!= min )
    					{
    						while ( !LargerEqual(d,temp,offset) )
    						{//这里使用的事实上是试商法,
    							//由于max和min的差距已经非常小,所以这里不再折半试商。
    							max--;
    							Sub(temp,r,temp);
    						}
    					}	
    
    					quotient._data[offset] = max; //保存商。
    					Sub(d,temp,d,offset); //求本阶的余数。
    					highpart = d._data[i]; //加载余数。
    				}
    			}
    
    			//清除高位的0。
    			for(T_DWORD i = quotient._len-1; i > 0; i--)
    			{
    				if( quotient._data[i] == 0 ) //末尾是0
    				{
    					quotient._len--;
    				}
    				else break;
    			}
    			//除法完毕,计算余数:
    			Div(d,head,residual,newhead);
    
    			if ( newhead != 0 ) 
    			{
    				//余数不为0,说明出错。
    				__asm int 3;
    			}
    		}
    
    	}
    }
    
    void CLargeInt::ExpMod(const CLargeInt&	source,const CLargeInt&	exponent,const CLargeInt& modulo,CLargeInt& result)
    {
    	CLargeInt n,e,r(1),temp,temp1;
    	Copy(source,n);
    	Copy(exponent,e);
    
    	while( e._len >	1 || e._data[e._len - 1] > 1 )
    	{
    		if( e._data[0] &1 ) 
    		{
    			Mul(r,n,temp);
    			Div(temp,modulo,temp1,r);
    		}
    		RCR(e);
    		Mul(n,n,temp);
    		Div(temp,modulo,temp1,n);
    	}
    	Mul(r,n,temp);
    	Div(temp,modulo,temp1,result);
    }
    
    bool CLargeInt::RabinMillerTest(const CLargeInt& source,const CLargeInt& base)
    {
    	CLargeInt m,temp(1);
    
    	Sub(source,temp,m);
    
    	T_DWORD	count =	0;
    	while(!(m._data[0] & 0x01) ) 
    	{
    		count++;
    		RCR(m);
    	}
    	/*
    	这里凝视掉的是旧的算法,能够用后面的等效算法代替。
    	改变的算法使得计算ExpMod的次数由 count 降低到 1
    	尽管理论上计算次数大大降低了,可是实际效果似乎仅仅是降低了50%左右的运算量。
    
    	for( T_DWORD i = 0; i< count;	i++)
    	{
    	CLargeInt mod;
    	ExpMod(base,m,source,mod);
    	if( mod._data[0] == 1	&& mod._len == 1 ) 
    	{
    	return true;
    	}
    	else
    	{
    	Sub(source,mod,temp);
    	if(  temp._data[0] ==	1 && temp._len == 1) 
    	{
    	return true;
    	}
    	}
    	RCL(m);		
    	}
    	*/
    
    	CLargeInt mod;
    	ExpMod(base,m,source,mod);
    	if( mod._data[0] == 1 && mod._len == 1 ) 
    	{
    		return true;
    	}
    	else
    	{
    		Sub(source,mod,temp);
    		if(  temp._data[0] == 1	&& temp._len ==	1) 
    		{
    			return true;
    		}
    	}
    	for( T_DWORD i = 1; i< count; i++)
    	{
    		CLargeInt next,t;
    		Mul(mod,mod,next);
    		Div(next,source,t,mod);
    		if( mod._data[0] == 1 && mod._len == 1 ) 
    		{
    			return true;
    		}
    		else
    		{
    			Sub(source,mod,temp);
    			if(  temp._data[0] == 1	&& temp._len ==	1) 
    			{
    				return true;
    			}
    		}			
    	}
    
    	return false;
    }
    
    bool CLargeInt::RSACreate( const CLargeInt &p,const CLargeInt &	q,const	CLargeInt &e,CLargeInt &d,CLargeInt &n)
    {//由已知的P,Q,E计算N,D,完毕RSA密钥生成。
    	Mul(p,q,n);
    	CLargeInt a(e),b(n);
    	Sub(b,p,b);
    	Sub(b,q,b);
    	Add(b,1,b);
    
    	if( !Coprime(b,e) ) return false;
    	//求D的算法,通过辗转相除,终于求得须要的D值。
    	CLargeInt k1,c1;
    	Div(b,a,k1,c1);
    	if( c1._len == 1 && c1._data[0]	== 0 ) return false;
    	if( c1._len == 1 && c1._data[0]	== 1 ) 
    	{ 
    		CLargeInt temp;
    		Sub(e,1,temp);
    		Mul(temp,k1,d);
    		Add(d,1,d);
    		return true; 
    	}
    
    	CLargeInt k2,c2,ka2;
    	Div(a,c1,k2,c2);
    	if( c2._len == 1 && c2._data[0]	== 0 ) return false;
    	Mul(k1,k2,ka2);
    	Add(ka2,1,ka2);
    	if( c2._len == 1 && c2._data[0]	== 1 ) { Copy(ka2,d); return true; }
    
    
    	CLargeInt k3,c3,ka3;
    	Div(c1,c2,k3,c3);
    	if( c3._len == 1 && c3._data[0]	== 0 ) return false;
    	Mul(k3,ka2,ka3);
    	Add(ka3,k1,ka3);
    	if( c3._len == 1 && c3._data[0]	== 1 ) 
    	{ 
    		Copy(ka3,d);
    
    		CLargeInt temp,temp1,temp2;
    		Mul(d,e,temp);
    		Div(temp,b,temp1,temp2);
    		Add(temp2,1,temp2);
    		if( Equal(temp2,b) )
    		{
    			Mul(d,d,temp1);
    			Div(temp1,b,temp2,d);
    			Mul(d,e,temp1);
    			Div(temp1,b,temp2,d);
    		}
    		return true; 
    	}
    
    	CLargeInt kn_2(k2),cn_2(c2),ka_2(ka2);
    	CLargeInt kn_1(k3),cn_1(c3),ka_1(ka3);
    
    	CLargeInt kn,cn,kan;
    	do{
    		Div(cn_2,cn_1,kn,cn);
    		Mul(kn,ka_1,kan);
    		Add(kan,ka_2,kan);
    
    		if( cn._len == 1 && cn._data[0]	== 0 ) return false;
    		if( cn._len == 1 && cn._data[0]	== 1 ) 
    		{
    			Copy(kan,d);
    
    			CLargeInt temp,temp1,temp2;
    			Mul(d,e,temp);
    			Div(temp,b,temp1,temp2);
    			Add(temp2,1,temp2);
    			if( Equal(temp2,b) )
    			{
    				Mul(d,d,temp1);
    				Div(temp1,b,temp2,d);
    				Mul(d,e,temp1);
    				Div(temp1,b,temp2,d);
    			}
    			return true; 
    		}
    		Copy(kn_1,kn_2);
    		Copy(cn_1,cn_2);
    		Copy(ka_1,ka_2);
    
    		Copy(kn,kn_1);
    		Copy(cn,cn_1);
    		Copy(kan,ka_1);
    	} while (true);
    }
    
    void CLargeInt::RSAEncode( const CLargeInt &n,const CLargeInt &d,const CLargeInt &m,CLargeInt &c)
    {
    	ExpMod(m,d,n,c);
    }
    
    void CLargeInt::RSADecode( const CLargeInt &n,const CLargeInt &e,const CLargeInt &c,CLargeInt &m)
    {
    	ExpMod(c,e,n,m);
    }
    
    string CLargeInt::RSADecode(const char* N, const char* E, const char* I)
    {
    	CLargeInt o;
    	CLargeInt n(N);
    	CLargeInt i(I);
    	CLargeInt e(E);
    	RSADecode(n, e, i, o);
    	return o.GetHexStr();
    }
    
    string CLargeInt::GetHexStr()
    {
    	char buffer[128];
    	memset(buffer, 0x00, sizeof(buffer));
    	string strHex;
    
    	for (int i = _len - 1 ; i >= 0; i--)
    	{
    		char str[] = "%08X";
    		str[2] = '0' + sizeof(T_DWORD) * 2;
    		if ( i != (int)(_len - 1) )
    		{
    			sprintf(buffer, str, _data[i]);
    		}
    		else 
    		{
    			sprintf(buffer, str, _data[i]);
    		}
    
    		strHex += buffer;
    	}
    	return strHex;
    }
    
    typedef	unsigned __int64 ULONGLONG;
    inline ULONGLONG GetCycleCount()
    {
    	__asm RDTSC
    }
    
    //#include <windows.h>
    void CLargeInt::CreateRandom(CLargeInt &n, T_DWORD bitcount)
    {
    	n._len = (bitcount + 31) / 32;
    	for (T_DWORD i = 0; i <	n._len;	i++)
    	{
    		T_DWORD	byte[4];
    
    		for (int b = 0;	b < 4; b++)
    		{
    			for (int j = 0;	j < 4;	j++)
    			{
    				//Sleep(0);
    				j = j + 1 - 1;
    			}
    			byte[b]	= GetCycleCount() & 0xFF;
    		}
    
    		n._data[i] = (byte[0] << 24)
    			| (byte[1] << 16)
    			| (byte[2] << 8)
    			| (byte[3] << 0);
    	}
    	n._data[0] |= 1;
    	n._data[n._len - 1] |= 0x80000000;
    	if ((bitcount &	31))
    	{
    		n._data[n._len - 1] >>=	32 - (bitcount & 31);
    	}	
    }
    
    bool CLargeInt::Coprime(const CLargeInt	&source,const CLargeInt	&target)
    {
    	CLargeInt m(source),n(target),temp;
    	while(true)
    	{
    		Div(m,n,temp,m);
    		if( m._len == 1	&& m._data[0] == 1 ) return true;
    		else if( m._len	== 1 &&	m._data[0] == 0	) return false;
    
    		Div(n,m,temp,n);
    		if( n._len == 1	&& n._data[0] == 1 ) return true;
    		else if( n._len	== 1 &&	n._data[0] == 0	) return false;
    	}
    
    }


    3.RSA工具


    RSA工具(来源于网络),此工具用着比較顺手。软件下载地址:http://download.csdn.net/detail/yxstars/7734567





    文/闫鑫原创   转载请注明出处http://blog.csdn.net/yxstars/article/details/38443937


  • 相关阅读:
    使用JS获取上一页的url地址
    禁止弹窗中蒙层底部页面跟随滚动的几种方法汇总
    一个Option请求引发的深度解析
    html li标签前面添加图标三种方法
    CSS3
    计算x
    计算圆周率
    最长公共子序列
    小字辈(左子右兄加强版)
    Excel列名和列序号转换
  • 原文地址:https://www.cnblogs.com/hrhguanli/p/4039737.html
Copyright © 2011-2022 走看看