本文基于Redis 2.8.19
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
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算法的生成的消息摘要。