标签:
SHA-1算法是第一代“安全散列算法”的缩写,其本质就是一个Hash算法。SHA系列标准主要用于数字签名,生成消息摘要,曾被认为是MD5算法的后继者。如今SHA家族已经出现了5个算法。Redis使用的是SHA-1,它能将一个最大2^64比特的消息,转换成一串160位的消息摘要,并能保证任何两组不同的消息产生的消息摘要是不同的。虽然SHA1于早年间也传出了破解之道,但作为SHA家族的第一代算法,对我们仍然很具有学习价值和指导意义。
SHA-1算法的详细内容可以参考官方的RFC:http://www.ietf.org/rfc/rfc3174.txt
Redis的sha1.c文件实现了这一算法,但该文件源码实际上是出自Valgrind项目的/tests/sha1_test.c文件(可以看出开源的强大之处:取之于民,用之于民)。它包含四个函数:
typedef struct { u_int32_t state[5]; u_int32_t count[2]; unsigned char buffer[64]; } SHA1_CTX;它有三个成员,其含义如下:
成员 | 类型 | 说明 |
---|---|---|
buffer | unsigned char[64] | 512(64×8)比特(位)的消息块(由原始消息经处理得出) |
state | u_int32_t[5] | 160(5×32)比特的消息摘要(即SHA-1算法要得出的) |
count | u_int32_t[2] | 储存消息的长度(单位:比特) |
void SHA1Final(unsigned char digest[20], SHA1_CTX* context);
unsigned i; unsigned char finalcount[8]; unsigned char c;后面是个条件测试宏,因为是 #if 0,所以我们只关注它 #else 的部分:
for (i = 0; i < 8; i++) { finalcount[i] = (unsigned char)((context->count[(i >= 4 ? 0 : 1)] >> ((3-(i & 3)) * 8) ) & 255); /* Endian independent */ }首先我们注意到了有一句注释 Endian independent,直译是端独立,即该语句的结果与机器是大端还是小端无关。相信很多人在了解了大小端以后,在这里都会陷入迷惘。相反如果你不了解大小端的话,这条语句理解起来反而轻松。我们需要理解的是比如:unsigned int a = 0x12345678; unsigned int b = (a>>24)&255。无论机器是大端还是小端,b的值都是0x12(0x00000012)。大小端对于移位操作的结果并无影响,a>>24 的语义一定是a 除以 2的24次方。
c = 0200; SHA1Update(context, &c, 1);c的二进制表示为10 000 000。因为前面我讲解了,填充的时候是以字节为单位的,最少1个字节,最多64个字节。并且第一位要填充1,后面都填充0。所以拿到一个消息我们首先要给他填充一个字节的10 000 000.SHA1Update() 函数就是完成的数据填充(附加)操作,该函数具体细节容后再禀。这里我们先关注整体结构。
while ((context->count[0] & 504) != 448) { c = 0000; SHA1Update(context, &c, 1); }这段代码很容易看出它的功能就是:循环测试数据模512是否与448同余。不满足条件就填充全一个字节0。细心的你也许会发现这里的条件是不是写错了:
while ((context->count[0] & 504) != 448) //你觉得应该是 while ((context->count[0] & 511) != 448)理论上来说,你的想法确实不错。不过源码也没问题,我们可以用bc来看一下这两个数的二进制表达:
111111000 //504 111111111 //511可以看出它们的不同之处就是最后三位。504后三位全0,511后三位全1。context->count中存储的是消息的长度,它的单位是:位。前面我们提到了我们的数据是以字节来存储的,所以context->count[ ]中的数据肯定是8个倍数,所以后三位肯定是000。所以不管是000&000,还是000&111其结果都是0。
SHA1Update(context, finalcount, 8); /* Should cause a SHA1Transform() */很明显,这一句完成的就是附加长度了。根据注释可以看出,这将触发SHA1Transform()函数的调用,该函数的功能就是进行运算,得出160位的消息摘要(message digest)并储存在context-state[ ]中,它是整个SHA-1算法的核心。其实现细节请看下文的:计算消息摘要一节。
for (i = 0; i < 20; i++) { digest[i] = (unsigned char) ((context->state[i>>2] >> ((3-(i & 3)) * 8) ) & 255); }最后的这步转换将消息摘要转换成单字节序列。用代码来解释就是:将context-state[5]中储存的20个字节(5×4字节)的消息摘要取出,将其存储在20个单字节的数组digest中。并且按大端序存储(与之前分析context->count[ ]到finalcount[ ]转换的思路相同)。SHA-1算法最后要得出的就是这160位(20字节)的数据。
void SHA1Update(SHA1_CTX* context, const unsigned char* data, u_int32_t len);data就是我们要附加的数据。len是data的长度(单位:字节)
j = context->count[0]; if ((context->count[0] += len << 3) < j) context->count[1]++; context->count[1] += (len>>29);context->count[ ]存储的是消息的长度,超出context->count[0]的存储范围的部分存储在context->count[1]中。len<<3就是len*8的意思,因为len的单位是字节,而context->count[ ]存储的长度的单位是位,所以要乘以8。 if ((context->count[0] += len << 3) < j) 的意思就是说如果加上len*8个位,context->count[0]溢出了,那么就要:context->count[1]++;进位。
j = (j >> 3) & 63;j>>3获得的就是字节数,j = (j >> 3) & 63得到的就是低6位的值,也就是代表64个字节(512位)长度的消息。,因为我们每次进行计算都是处理512位的消息数据。
if ((j + len) > 63) { memcpy(&context->buffer[j], data, (i = 64-j)); SHA1Transform(context->state, context->buffer); for ( ; i + 63 < len; i += 64) { SHA1Transform(context->state, &data[i]); } j = 0; } else i = 0; memcpy(&context->buffer[j], &data[i], len - i);这段代码大致的含义就是:如果j+len的长度大于63个字节,就分开处理,每64个字节处理一次,然后再处理后面的64个字节,重复这个过程;否则就直接将数据附加到buffer末尾。逐句分析一下:
memcpy(&context->buffer[j], data, (i = 64-j)); SHA1Transform(context->state, context->buffer);
for ( ; i + 63 < len; i += 64) { SHA1Transform(context->state, &data[i]); } j = 0;然后开始循环,每64个字节处理一次。这里可能有朋友会好奇每次i递增的步长都是64,那么为什么比较的时候是 i + 63 < len;而不是 i + 64 < len;呢?其原因很简单——因为下标是从0计数的。这些细节大家简单琢磨一下就OK啦。
else i = 0; memcpy(&context->buffer[j], &data[i], len - i);如果前面的if不成立,那么也就是说原始数据context->buffer加上新的数据data的长度还不足以凑成64个字节,所以直接附加上data就行了。相当于:memcpy(&context->buffer[j], &data[i], 0);
H0 = 0x67452301 H1 = 0xEFCDAB89 H2 = 0x98BADCFE H3 = 0x10325476 H4 = 0xC3D2E1F0在开始计算消息摘要之前,要先初始化这5个字的缓冲区,也就是按照上面的数值赋值。这步操作体现在sha1.c文件的SHA1Init()函数中。
void SHA1Init(SHA1_CTX* context) { /* SHA1 initialization constants */ context->state[0] = 0x67452301; context->state[1] = 0xEFCDAB89; context->state[2] = 0x98BADCFE; context->state[3] = 0x10325476; context->state[4] = 0xC3D2E1F0; context->count[0] = context->count[1] = 0; }
f(t;B,C,D) = (B AND C) OR ((NOT B) AND D) ( 0 <= t <= 19) f(t;B,C,D) = B XOR C XOR D (20 <= t <= 39) f(t;B,C,D) = (B AND C) OR (B AND D) OR (C AND D) (40 <= t <= 59) f(t;B,C,D) = B XOR C XOR D (60 <= t <= 79).每一轮还会用到一个附加常数K(t):
K(t) = 0x5A827999 ( 0 <= t <= 19) K(t) = 0x6ED9EBA1 (20 <= t <= 39) K(t) = 0x8F1BBCDC (40 <= t <= 59) K(t) = 0xCA62C1D6 (60 <= t <= 79)
1. M(t) = W(t) (0<= t<= 15) 2. W(t) = S^1(W(t-3) XOR W(t-8) XOR W(t-14) XOR W(t-16)) (16<= t <= 79,S^1()表示循环左移1位) 3. A = H0, B = H1, C = H2, D = H3, E = H4. 4. 对于(0<= t <= 79)开始执行80轮变换 TEMP = S^5(A) + f(t;B,C,D) + E + W(t) + K(t); E = D; D = C; C = S^30(B); B = A; A = TEMP; 5. H0 = H0 + A, H1 = H1 + B, H2 = H2 + C, H3 = H3 + D, H4 = H4 + E.上面的数学表达式改编自RFC文档,在经过80轮运算之后的H0~H4就是SHA-1算法要生成的160比特(位)的消息摘要。里面使用了ABCDE这5个符号,Redis源码中使用的是v表示符号A;w、x、y代表上文中的B、C、D,z表示上文中的TEMP。在5步之中的前面两步中,进行了消息块M(i)到W(i)的转换,这样做的目的是将16个字(32位)的消息块(M)转换成80个字的字块(W)。
#define rol(value, bits) (((value) << (bits)) | ((value) >> (32 - (bits))))
typedef union { unsigned char c[64]; u_int32_t l[16]; } CHAR64LONG16; #ifdef SHA1HANDSOFF CHAR64LONG16 block[1]; /* use array to appear as a pointer */
W(i) = block-l[i&15] // 16<= i <= 79
#if BYTE_ORDER == LITTLE_ENDIAN #define blk0(i) (block->l[i] = (rol(block->l[i],24)&0xFF00FF00) |(rol(block->l[i],8)&0x00FF00FF)) #elif BYTE_ORDER == BIG_ENDIAN #define blk0(i) block->l[i]blk0的功能实际是就是进行字节序的转换。如果是小端序就将block->l[i] 转换为大端序(上面代码中的第2行),如果是大端序就不操作,直接等价于block->l[i]。实际上在调用blk0(i)的时候,它参数i的取值范围是0~15。
#define blk(i) (block->l[i&15] = rol(block->l[(i+13)&15]^block->l[(i+8)&15] ^block->l[(i+2)&15]^block->l[i&15],1))实际上在调用blk(i)的时候,它的参数i的取值范围是16~79。 实际上它实现的功能我们在下面会用到,它实际计算的表达式是:
用符号W(i)来表示block-l[i] W(i) = S^1( W(i-3) XOR W(i-8) XOR W(i-14) XOR W(i-16) ) //S^1()表示循环左移 因为观察上面表达式可知,我们只需要16个字的存储空间来保存就可以了。所以在实现上等价与: W(i%16) = S^1( W((i-3)%16) XOR W((i-8)%16) XOR W((i-14)%16) XOR W((i-16)%16) )
因为a&15等价与a%16 则源码blk(i)完成的操作等价于: W(i%16) = S^1( W((i+13)%16) XOR W((i+8)%16) XOR W((i+2)%16) XOR W((i%16)) ) 设m+n=16 (i+n)%16 = (i+16-m)%16 = ((i-m)%16 + 16%16)%16 = (i-m)%16 当n=13,8,2,0时,m等于3,8,14,16 所以: W(i%16) = S^1( W((i+13)%16) XOR W((i+8)%16) XOR W((i+2)%16) XOR W((i%16)) ) W(i%16) = S^1( W((i-3)%16) XOR W((i-8)%16) XOR W((i-14)%16) XOR W((i-16)%16) ) 等价
/* (R0+R1), R2, R3, R4 are the different operations used in SHA1 */ #define R0(v,w,x,y,z,i) z+=((w&(x^y))^y)+blk0(i)+0x5A827999+rol(v,5);w=rol(w,30); #define R1(v,w,x,y,z,i) z+=((w&(x^y))^y)+blk(i)+0x5A827999+rol(v,5);w=rol(w,30); #define R2(v,w,x,y,z,i) z+=(w^x^y)+blk(i)+0x6ED9EBA1+rol(v,5);w=rol(w,30); #define R3(v,w,x,y,z,i) z+=(((w|x)&y)|(w&x))+blk(i)+0x8F1BBCDC+rol(v,5);w=rol(w,30); #define R4(v,w,x,y,z,i) z+=(w^x^y)+blk(i)+0xCA62C1D6+rol(v,5);w=rol(w,30);这段代码中的R2、R3、R4三个宏函数,所完成的操作就是 t(t表示轮数,共计80轮运算) 在范围 [20,39]、[40,59]、[60,79]的时候的运算。对应RFC文档中:求解TEMP、给A~E重新赋值。但是我们可以看到当 t 的范围在[0,20]的时候使用了R0和R1这两个宏函数来表示,它们的差别之处在于R0里面计算 z(即TEMP)值的时候,加上的是blk0(i),而R1中加的是blk(i) (和R2、R3、R4一样,加的是blk(i))。造成这个差别的原因是在前面提到了5步运算的前两步中:当 t 取值[0,15]的时候W(t)直接等于M(t),而 t>15以后(这里是t取值[16,19])Wt则需要进行转换才能得到,即
W(t) = S^1(W(t-3) XOR W(t-8) XOR W(t-14) XOR W(t-16))
#include <stdio.h> int main(){ for(int w=0;w<2;w++){ for(int x=0;x<2;x++){ for(int y=0;y<2;y++){ printf("-------------\n"); //分割线,使更容易比较阅读 printf("%d %d %d:%d\n",w,x,y,(w&(x^y))^y); printf("%d %d %d:%d\n",w,x,y,(w&x)|(~w&y)); } } } }谈了这么多,是时候引入sha1.c文件的核心函数了——SHA1Transform()
void SHA1Transform(u_int32_t state[5], const unsigned char buffer[64])
memcpy(block, buffer, 64);将参数buffer里面的字节复制到block中。
a = state[0]; b = state[1]; c = state[2]; d = state[3]; e = state[4];实际上完成的就是RFC文档中的H0~H4赋值给ABCDE的操作。接下来就是80轮运算的代码。每20轮为一组,共分四组。
R0(a,b,c,d,e, 0); R0(e,a,b,c,d, 1); R0(d,e,a,b,c, 2); R0(c,d,e,a,b, 3); R0(b,c,d,e,a, 4); R0(a,b,c,d,e, 5); R0(e,a,b,c,d, 6); R0(d,e,a,b,c, 7); R0(c,d,e,a,b, 8); R0(b,c,d,e,a, 9); R0(a,b,c,d,e,10); R0(e,a,b,c,d,11); R0(d,e,a,b,c,12); R0(c,d,e,a,b,13); R0(b,c,d,e,a,14); R0(a,b,c,d,e,15); R1(e,a,b,c,d,16); R1(d,e,a,b,c,17); R1(c,d,e,a,b,18); R1(b,c,d,e,a,19); ...第一组比较特殊,使用了R0和R1两个宏函数,其原因前面已经介绍了。因为第0~15轮运算和16~79轮运算的时候消息块M(i)和字块W(i)的转换是不一样的。后面的20~39轮,40~59轮,60~79轮就是依次使用的R2,R3,R4来运算了,比较好理解,就不表了。接着:
state[0] += a; state[1] += b; state[2] += c; state[3] += d; state[4] += e; /* Wipe variables */ a = b = c = d = e = 0;完成的就是更新缓冲区H0~H4的内容。然后把a~e清空为0(这一步我感觉意义不到,本身就是栈存储的5个变量,函数结束后就释放了啊)。最后state[0]~state[4]中存储的就是SHA-1算法的生成的消息摘要。
标签:
原文地址:http://blog.csdn.net/guodongxiaren/article/details/44926823