数据加密 ---- SHA 加密

版权声明:本文为博主原创文章,转载请务必注明作者与原文链接。 https://blog.csdn.net/jingerppp/article/details/84072020

1. 前言

前一篇博文介绍了 MD5算法 的形成和算法使用,MD5算法 是一个不可逆的加密算法,将数据以512bits 位单位进行散列组合最终生成128bits 的32位16进制数。1996年后被证实存在弱点,可以被加以破解,对于需要高度安全性的数据,专家一般建议改用其他算法。2004年,证实MD5算法无法防止碰撞(collision),因此不适用于安全性认证,如SSL公开密钥认证或是数字签名等用途。2009年,中国科学院的谢涛和冯登国仅用了2^{20.96} 的碰撞算法复杂度,破解了MD5的碰撞抵抗,该攻击在普通计算机上运行只需要数秒钟。

本博文将介绍另外一种单向加密的算法SHA,这是比MD5 算法更复杂的散列运算算法。

其他算法可以看:数据加密 ---- 总篇

2. 简介

SHA (英文名:Secure Hash Algorithm,中文名:安全散列算法),是一种密码散列函数,美国国家安全局设计。其家族还是很庞大的,有SHA-0、SHA-1、SHA-2、SHA-3等4类型算法。

其中,SHA-2 包括了SHA-224、SHA-256、SHA-384、SHA-512、SHA-512/224、SHA-512/256等算法。

SHA-3 包括了SHA3-224 、SHA3-256 、SHA3-384 、SHA3-512、SHAKE128 、SHAKE256等算法。

3. SHA-0 和SHA-1

最初载明的算法于1993年发布,称做安全散列标准(Secure Hash Standard),FIPS PUB 180。这个版本现在常被称为SHA-0。它在发布之后很快就被NSA撤回,并且由1995年发布的修订版本FIPS PUB 180-1(通常称为SHA-1)取代。SHA-1和SHA-0的算法只在压缩函数的消息转换部分差了一个比特的循环位移。根据NSA的说法,它修正了一个在原始算法中会降低散列安全性的弱点。然而NSA并没有提供任何进一步的解释或证明该弱点已被修正。而后SHA-0和SHA-1的弱点相继被攻破,SHA-1似乎是显得比SHA-0有抵抗性,这多少证实了NSA当初修正算法以增进安全性的声明。

SHA-0 和 SHA-1可将一个最大264比特的消息,转换成一串160位的消息摘要;其设计原理相似于MIT教授Ronald L. Rivest所设计的密码学散列算法MD4和MD5。

 

3.1 SHA-0的破解

在CRYPTO 98上,两位法国研究者提出一种对SHA-0的攻击方式:在2^{61}的计算复杂度之内,就可以发现一次碰撞(即两个不同的消息对应到相同的消息摘要);这个数字小于生日攻击法所需的280,也就是说,存在一种算法,使其安全性不到一个理想的散列函数抵抗攻击所应具备的计算复杂度。

2004年时,Biham和Chen也发现了SHA-0的近似碰撞,也就是两个消息可以散列出几乎相同的数值;其中162比特中有142比特相同。他们也发现了SHA-0的完整碰撞(相对于近似碰撞),将本来需要80次方的复杂度降低到62次方。

2004年8月12日,Joux, Carribault, Lemuet和Jalby宣布找到SHA-0算法的完整碰撞的方法,这是归纳Chabaud和Joux的攻击所完成的结果。发现一个完整碰撞只需要2^{51}的计算复杂度。他们使用的是一台有256颗Itanium2处理器的超级计算机,约耗80,000 CPU工时。

2004年8月17日,在CRYPTO 2004的Rump会议上,王小云,冯登国(Feng)、来学嘉(Lai),和于红波(Yu)宣布了攻击MD5、SHA-0和其他散列函数的初步结果。他们攻击SHA-0的计算复杂度是2^{40},这意谓的他们的攻击成果比Joux还有其他人所做的更好。

2005年二月,王小云和殷益群、于红波再度发表了对SHA-0破密的算法,可在2^{39}的计算复杂度内就找到碰撞。

 

3.2 SHA-1的破解

鉴于SHA-0的破密成果,专家们建议那些计划利用SHA-1实现密码系统的人们也应重新考虑。在2004年CRYPTO会议结果公布之后,NIST即宣布他们将逐渐减少使用SHA-1,改以SHA-2取而代之。

2005年,Rijmen和Oswald发表了对SHA-1较弱版本(53次的加密循环而非80次)的攻击:在2^{80} 的计算复杂度之内找到碰撞。

2005年二月,王小云、殷益群及于红波发表了对完整版SHA-1的攻击,只需少于2^{69}的计算复杂度,就能找到一组碰撞。(利用生日攻击法找到碰撞需要2^{80} 的计算复杂度。)

这篇论文的作者们写道;“我们的破密分析是以对付SHA-0的差分攻击、近似碰撞、多区块碰撞技术、以及从MD5算法中查找碰撞的消息更改技术为基础。没有这些强力的分析工具,SHA-1就无法破解。”此外,作者还展示了一次对58次加密循环SHA-1的破密,在2^{33} 个单位操作内就找到一组碰撞。完整攻击方法的论文发表在2005年八月的CRYPTO会议中。

殷益群在一次面谈中如此陈述:“大致上来说,我们找到了两个弱点:其一是前置处理不够复杂;其二是前20个循环中的某些数学运算会造成不可预期的安全性问题。”

2005年8月17日的CRYPTO会议尾声中王小云、姚期智、姚储枫再度发表更有效率的SHA-1攻击法,能在2^{63} 个计算复杂度内找到碰撞。

2006年的CRYPTO会议上,Christian Rechberger和Christophe De Cannière宣布他们能在容许攻击者决定部分原消息的条件之下,找到SHA-1的一个碰撞。

在密码学的学术理论中,任何攻击方式,其计算复杂度若少于暴力搜索法所需要的计算复杂度,就能被视为针对该密码系统的一种破密法;但这并不表示该破密法已经可以进入实际应用的阶段。

就应用层面的考量而言,一种新的破密法出现,暗示着将来可能会出现更有效率、足以实用的改良版本。虽然这些实用的破密法版本根本还没诞生,但确有必要发展更强的散列算法来取代旧的算法。在“碰撞”攻击法之外,另有一种反译攻击法(Pre-image attack),就是由散列出的字符串反推原本的消息;反译攻击的严重性更在碰撞攻击之上,但也更困难。在许多会应用到密码散列的情境(如用户密码的存放、文件的数字签名等)中,碰撞攻击的影响并不是很大。举例来说,一个攻击者可能不会只想要伪造一份一模一样的文件,而会想改造原来的文件,再附上合法的签名,来愚弄持有公钥的验证者。另一方面,如果可以从密文中反推未加密前的用户密码,攻击者就能利用得到的密码登录其他用户的账户,而这种事在密码系统中是不能被允许的。但若存在反译攻击,只要能得到指定用户密码散列过后的字符串(通常存在影档中,而且可能不会透露原密码信息),就有可能得到该用户的密码。

2017年2月23日,Google公司公告宣称他们与CWI Amsterdam合作共同创建了两个有着相同的SHA-1值但内容不同的PDF文件,这代表SHA-1算法已被正式攻破。

3.3 SHA-1 算法

算法的步骤大概跟MD5算法 差不多,运算上更复杂一些。

1、扩充数据

将数据扩充到512 bits(64 bytes)的倍数,这些n 段512bits(64字节)的数据会作为原始信息进行处理。

这一步的处理同MD5算法

2、循环运算每一段512bits(64 字节)的数据(MainLoop)

MD5 算法 的区别主要在这里。

  • 将512bits数据(16*4字节)扩展为80*4字节

下面是伪代码:

    Extend the sixteen 32-bit words into eighty 32-bit words:
    for i from 16 to 79
        w[i] := (w[i-3] xor w[i-8] xor w[i-14] xor w[i-16]) leftrotate 1

从第 17个开始进行扩展,用C 代码表示为:

#define shift(value, bits) (((value) << (bits)) | ((value) >> (32 - (bits))))


...

String getSHA1() {
    ...
    ...


    for(unsigned int i=0;i<strlength/16;i++) //512bits 数据分段
    {
        unsigned int num[80];
        unsigned int j;
        for(j=0;j<16;j++)
            num[j]=strByte[i*16+j]; //得到512bits 数据

        for(j=16;j<80;j++)
            num[j]=shift((num[j-3]^num[j-8]^num[j-14]^num[j-16]), 1); //扩展到80个
        mainLoop(num); //进入主循环
    }

    ...
}
  • 进入主循环,进行80次(4组 * 20次)循环

MD5算法,也是将运算分为4 组:

F(X,Y,Z)=(X & Y) | ((~X) & Z) 

G(X,Y,Z)=X ⊕ Y ⊕ Z

H(X,Y,Z)=(X & Y) | (X & Z) | (Y & Z)

I(X,Y,Z)=X ⊕ Y ⊕ Z

其中,& 是与运算, | 是或运算,~ 是取反运算,⊕ 是异或运算

也就是说函数F 是前20次循环使用的,G函数是21 至 40次循环使用,H 函数是41 至60次循环使用,I 函数是最后20次循环使用。

具体的运算流程如下图所示:

A、B、C、D 、E分别是上一段512bits 处理后留下来的5个整数(第一次运算的时候这5个数为固定的常数)。

在对该512bits 数据运算前需要先把这5个整数临时存起来(后面会使用到)。

A'=A;

B'=B;

C'=C;

D'=D;

E'=E;

开始进入512bits 数据的运算。F 代表上面提到的 4 组运算函数,B、C、D三个数分别是4组运算函数的参数。 W_t 表示一个32 bits(4个字节) 的输入数据(512bits 数据其中的32bits), K_t 表示一个32bits 的常数(这个也是固定的)。

将上图运算总结成公式:
temp = shift(A, 5) + F() + E + K_t + W_t;
E = D;
D = C;
C = shift(B, 30);
B = A;
A = temp;

将每一组运算后得到最新的5个数A、B、C、D、E作为下一组运算的A、B、C、D、E,一直到4组运算(80次循环) 彻底结束。

  • 一段512bits 的80 次循环结束之后,需要为下一段512bits 的80 次循环准备新的A、B、C、D、E。

将上一段80 次循环后最终得到的A、B、C、D 、E(也就是上面一步得到的最后的5个数)与循环之前的保存下来的初始值A'、B'、C'、D'、E' 对应相加。

A=A' + A;

B=B' + B;

C=C' + C;

D=D' + D;

E=E' + E;

叠加运算结束,标志该段512bits数据处理完毕。

3、最后一段512bits 运算后得到最终的A、B、C、D、E,即为最终的160bits数

因为需要得到最后160bits(40 位16进制)的字符串,所以要将每个4字节的数转换成8位的16进制字符串。

3.4 散列举例

一般128位的SHA-1散列被表示为40位十六进制数字。以下是一个43位长的仅ASCII字母列的SHA-1散列:

SHA1("The quick brown fox jumps over the lazy dog")
= 2fd4e1c67a2d28fced849ee1bb76e7391b93eb12

即使在原文中作一个小变化(比如将dog 换位cog,用c取代d)其散列也会发生巨大的变化:

SHA1("The quick brown fox jumps over the lazy cog")
= de9f2c7fd25e1b3afad3e85a0bd17d9b100db4b3

空文的散列为:

SHA1("")
= da39a3ee5e6b4b0d3255bfef95601890afd80709

3.5 SHA-1 算法用C 代码实现

#include<iostream>
#include<string>
using namespace std;
#define shift(x, n) (((x) << (n)) | ((x) >> (32-(n))))//右移的时候,高位一定要补零,而不是补充符号位
#define F(x, y, z) (((x) & (y)) | ((~x) & (z)))
#define G(x, y, z) ((x) ^ (y) ^ (z))
#define H(x, y, z) (((x) & (y)) | ((x) & (z)) | ((y) & (z)))
#define I(x, y, z) G(x, y, z)

#define A 0x67452301
#define B 0xefcdab89
#define C 0x98badcfe
#define D 0x10325476
#define E 0xC3D2E1F0

//strBaye的长度
unsigned int strlength;
//A,B,C,D的临时变量
unsigned int atemp;
unsigned int btemp;
unsigned int ctemp;
unsigned int dtemp;
unsigned int etemp;

const unsigned int k[]={0x5A827999,0x6ED9EBA1,0x8F1BBCDC,0xCA62C1D6};

const char str16[]="0123456789abcdef";
void mainLoop(unsigned int M[])
{
    unsigned int f,g;
    unsigned int a=atemp;
    unsigned int b=btemp;
    unsigned int c=ctemp;
    unsigned int d=dtemp;
	unsigned int e=etemp;
    for (unsigned int i = 0; i < 80; i++)
    {
        if(i<20){
            f=F(b,c,d);
        }else if (i<40){
            f=G(b,c,d);
        }else if(i<60){
            f=H(b,c,d);
        }else{
            f=I(b,c,d);
        }
        g=k[i/20];

        unsigned int tmp=shift(a, 5)+f+e+g+M[i];
        e=d;
        d=c;
        c=shift(b, 50);
        b=a;
        a=tmp;
    }
    atemp=a+atemp;
    btemp=b+btemp;
    ctemp=c+ctemp;
    dtemp=d+dtemp;
    etemp=e+etemp;
}
/*
*填充函数
*处理后应满足bits≡448(mod512),字节就是bytes≡56(mode64)
*填充方式为先加一个1,其它位补零
*最后加上64位的原来长度
*/
unsigned int* add(string str)
{
    unsigned int num=((str.length()+8)/64)+1;//以512位,64个字节为一组
    unsigned int *strByte=new unsigned int[num*16];    //64/4=16,所以有16个整数
    strlength=num*16;
    for (unsigned int i = 0; i < num*16; i++)
        strByte[i]=0;
    for (unsigned int i=0; i <str.length(); i++)
    {
        strByte[i>>2]|=(str[i])<<((i%4)*8);//一个整数存储四个字节,i>>2表示i/4 一个unsigned int对应4个字节,保存4个字符信息
    }
    strByte[str.length()>>2]|=0x80<<(((str.length()%4))*8);//尾部添加1 一个unsigned int保存4个字符信息,所以用128左移
    /*
    *添加原长度,长度指位的长度,所以要乘8,然后是小端序,所以放在倒数第二个,这里长度只用了32位
    */
    strByte[num*16-2]=str.length()*8;
    return strByte;
}
string changeHex(int a)
{
    int b;
    string str1;
    string str="";
    for(int i=0;i<4;i++)
    {
        str1="";
        b=((a>>i*8)%(1<<8))&0xff;   //逆序处理每个字节
        for (int j = 0; j < 2; j++)
        {
            str1.insert(0,1,str16[b%16]);
            b=b/16;
        }
        str+=str1;
    }
    return str;
}
string getSHA1(string source)
{
    atemp=A;    //初始化
    btemp=B;
    ctemp=C;
    dtemp=D;
	etemp=E;
    unsigned int *strByte=add(source);
    for(unsigned int i=0;i<strlength/16;i++) //512bits 数据分段
    {
        unsigned int num[80];
        unsigned int j;
        for(j=0;j<16;j++)
            num[j]=strByte[i*16+j]; //得到512bits 数据

        for(j=16;j<80;j++)
            num[j]=shift((num[j-3]^num[j-8]^num[j-14]^num[j-16]), 1); //扩展到80个
        mainLoop(num); //进入主循环
    }
    return changeHex(atemp).append(changeHex(btemp)).append(changeHex(ctemp))
                   .append(changeHex(dtemp)).append(changeHex(etemp));
}
unsigned int main()
{
    string ss;
//    cin>>ss;
    string s=getSHA1("abc");
    cout<<s;
    return 0;
}

android 中也提供了source code:external/boringssl/src/crypto/fipsmodule/sha/sha1.c

比较后当然 source code 比本人写的好,通过16个变量解决扩展到80个数,省下64 * 4 = 256 bytes。

关于SHA-2算法 在 下一篇 博文中继续分析

其他算法可以看:数据加密 ---- 总篇

附:

android 实例代码:https://blog.csdn.net/shift_wwx/article/details/84100407

猜你喜欢

转载自blog.csdn.net/jingerppp/article/details/84072020