设为首页 加入收藏

TOP

Redis源码中探秘SHA-1算法原理及其编程实现(一)
2015-11-21 01:34:57 来源: 作者: 【 】 浏览:1
Tags:Redis 源码 探秘 SHA-1 算法 原理 及其 编程 实现

导读

SHA-1算法是第一代“安全散列算法”的缩写,其本质就是一个Hash算法。SHA系列标准主要用于数字签名,生成消息摘要,曾被认为是MD5算法的后继者。如今SHA家族已经出现了5个算法。Redis使用的是SHA-1,它能将一个最大2^64比特的消息,转换成一串160位的消息摘要,并能保证任何两组不同的消息产生的消息摘要是不同的。虽然SHA1于早年间也传出了破解之道,但作为SHA家族的第一代算法,对我们仍然很具有学习价值和指导意义。

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

SHA1Init
SHA1Update
SHA1Transform
SHA1Final

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;
它有三个成员,其含义如下:
成员 类型 说明
buffer unsigned char[64] 512(64×8)比特(位)的消息块(由原始消息经处理得出)
state u_int32_t[5] 160(5×32)比特的消息摘要(即SHA-1算法要得出的)
count u_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    //504
111111111    //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 2 3 4 下一页 尾页 1/4/4
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
分享到: 
上一篇MyBtis注解方式注册异常 下一篇数据库-关系代数与关系运算

评论

帐  号: 密码: (新用户注册)
验 证 码:
表  情:
内  容: