1算法原理及其编程实现

导读

并能保证任何两组不同的消息产生的消息摘要是不同的。虽然SHA1于早年间也传出了破解之道,但作为SHA家族的第一代算法,对我们仍然很具有学习价值和指导意义。

SHA-1算法的详细内容可以参考官方的RFC:

Redis的sha1.c文件实现了这一算法,但该文件源码实际上是出自Valgrind项目的/tests/sha1_test.c文件(可以看出开源的强大之处:取之于民,用之于民)。它包含四个函数:

SHA1算法流程概述

sha-1算法大致分为5步:

附加填充位、长度理论基础

给消息附加填充位使其模512与448同余(M%512 == 448)。即使满足了条件也要填充512位(比特)。填充过程是这样的:先补一位1,后面一律补0,直至满足条件。因此至少填充1位,最多填充512位。

因为我们存储的时候是以字节为单位存储的,所以我们的消息的长度(单位:位)一定是8的倍数。而我们填充的时候也一定是8位,8位的来填充。也即不可能只填充一个二进制位,至少是8个二进制位(一个字节)。因此最少填充1个字节,最多填充64个字节(64*8=512)。

在附加填充位完成之后,还要附加长度,即附加64位数据来存储原始消息的长度。因为在附加填充位完成之后,消息长度(单位:位)是512与448同余,而此时再附加64位之后,消息长度就变成了512的整数倍。

最后我们开始计算消息摘要的时候,就是每512位为一组开始计算的。

SHA_CTX结构

SHA_CTX 结构在头文件sha1.h中定义:

typedef struct {u_int32_t state[5];u_int32_t count[2];unsigned char buffer[64];} SHA1_CTX; 它有三个成员,其含义如下:

成员类型说明

bufferunsigned char[64]512(64×8)比特(位)的消息块(由原始消息经处理得出)

stateu_int32_t[5]160(5×32)比特的消息摘要(即SHA-1算法要得出的)

countu_int32_t[2]储存消息的长度(单位:比特)

SHA1Final

SHA1Final()是整个算法的入口与出口。该函数通过调用该文件内其他函数完成了SHA-1算法的整个流程。它的声明如下:

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次方。

finalcount是char数组,context->count是整型数组。这段语句的意思就是将整型数据分拆成单个字节存储。finalcount存储的结果可以理解为是一个大端序的的超大整型。举例比如:context->count[0]存储的是0x11223344,context->count[1]存储的是0x55667788。那么最后finalcount[0]~finalcount[7]存储的依次是:0x88、0x77、0x66、0x55、0x44、0x33、0x22、0x11

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 //504111111111 //511 可以看出它们的不同之处就是最后三位。504后三位全0,511后三位全1。context->count中存储的是消息的长度,它的单位是:位。前面我们提到了我们的数据是以字节来存储的,所以context->count[ ]中的数据肯定是8个倍数,所以后三位肯定是000。所以不管是000&000,还是000&111其结果都是0。

————————————————————————————————————————————————————–

虽然 &504和 &511在这里效果相同,但是&504可读性很差。而之所以会出现可读性这么差的代码,我的猜想是效率。下面是我的猜想,未验证。假设一个数A,当A和000…(全0)进行&操作的时候的时候,其结果必然是0(编译器可能直接判断为0,而不去理会A的值)。而当A和111…(全为1)的数进行&操作的时候,其结果是A的值,所以要进行一下copy,将A返回;又或者编译器是逐位判断的,所以A每一位和1进行&的时候,编译器都要去查看一下A对应位上的值,而与0进行&的时候,则直接设置结果为0.当然了,这只是我的猜想,正确与否请各位指正。

————————————————————————————————————————————————————–

谁说的,人非要快乐不可,好像快乐由得人选择。

1算法原理及其编程实现

相关文章:

你感兴趣的文章:

标签云: