哈希算法将任意长度的二进制值映射为固定长度的较小二进制值,这个小的二进制值称为哈希值。哈希值是一段数据唯一且极其紧凑的数值表示形式。如果散列一段明文而且哪怕只更改该段落的一个字母,随后的哈希都将产生不同的值。要找到散列为同一个值的两个不同的输入,在计算上是不可能的,所以数据的哈希值可以检验数据的完整性。
链表查找的时间效率为O(N),二分法为log2N,B+ Tree为log2N,但Hash链表查找的时间效率为O(1)。
设计高效算法往往需要使用Hash链表,常数级的查找速度是任何别的算法无法比拟的,Hash链表的构造和冲突的不同实现方法对效率当然有一定的影响,然而Hash函数是Hash链表最核心的部分,下面是几款经典软件中使用到的字符串Hash函数实现,通过阅读这些代码,我们可以在Hash算法的执行效率、离散性、空间利用率等方面有比较深刻的了解。
下面分别介绍几个经典软件中出现的字符串Hash函数。
●PHP中出现的字符串Hash函数
static unsigned long hashpjw(char *arKey, unsigned int nKeyLength)
{
unsigned long h = 0, g;
char *arEnd=arKey+nKeyLength;
while (arKey < arEnd) {
h = (h << 4) + *arKey++;
if ((g = (h & 0xF0000000))) {
h = h ^ (g >> 24);
h = h ^ g;
}
}
return h;
}
●OpenSSL中出现的字符串Hash函数
unsigned long lh_strhash(char *str)
{
int i,l;
unsigned long ret=0;
unsigned short *s;
if (str == NULL) return(0);
l=(strlen(str)+1)/2;
s=(unsigned short *)str;
for (i=0; i
ret^=(s[i]<<(i&0×0f));
return(ret);
}
/* The following hash seems to work very well on normal text strings
* no collisions on /usr/dict/words and it distributes on %2^n quite
* well, not as good as MD5, but still good.
*/
unsigned long lh_strhash(const char *c)
{
unsigned long ret=0;
long n;
unsigned long v;
int r;
if ((c == NULL) || (*c == ‘/0′))
return(ret);
/*
unsigned char b[16];
MD5(c,strlen(c),b);
return(b[0]|(b[1]<<8)|(b[2]<<16)|(b[3]<<24));
*/
n=0×100;
while (*c)
{
v=n|(*c);
n+=0×100;
r= (int)((v>>2)^v)&0×0f;
ret=(ret(32-r));
ret&=0xFFFFFFFFL;
ret^=v*v;
c++;
}
return((ret>>16)^ret);
}
●MySql中出现的字符串Hash函数
#ifndef NEW_HASH_FUNCTION
/* Calc hashvalue for a key */
static uint calc_hashnr(const byte *key,uint length)
{
register uint nr=1, nr2=4;
while (length–)
{
nr^= (((nr & 63)+nr2)*((uint) (uchar) *key++))+ (nr << 8);
nr2+=3;
}
return((uint) nr);
}
/* Calc hashvalue for a key, case indepenently */
static uint calc_hashnr_caseup(const byte *key,uint length)
{
register uint nr=1, nr2=4;
while (length–)
{
nr^= (((nr & 63)+nr2)*((uint) (uchar) toupper(*key++)))+ (nr << 8);
nr2+=3;
}
return((uint) nr);
}
#else
/*
* Fowler/Noll/Vo hash
*
* The basis of the hash algorithm was taken from an idea sent by email to the
* IEEE Posix P1003.2 mailing list from Phong Vo (kpv@research.att.com) and
* Glenn Fowler (gsf@research.att.com). Landon Curt Noll (chongo@toad.com)
* later improved on their algorithm.
*
* The magic is in the interesting relationship between the special prime
* 16777619 (2^24 + 403) and 2^32 and 2^8.
*
* This hash produces the fewest collisions of any function that we’ve seen so
* far, and works well on both numbers and strings.
*/
uint calc_hashnr(const byte *key, uint len)
{
const byte *end=key+len;
uint hash;
for (hash = 0; key < end; key++)
{
hash *= 16777619;
hash ^= (uint) *(uchar*) key;
}
return (hash);
}
uint calc_hashnr_caseup(const byte *key, uint len)
{
const byte *end=key+len;
uint hash;
for (hash = 0; key < end; key++)
{
hash *= 16777619;
hash ^= (uint) (uchar) toupper(*key);
}
return (hash);
}
#endif
Mysql中对字符串Hash函数还区分了大小写
●另一个经典字符串Hash函数
unsigned int hash(char *str)
{
register unsigned int h;
register unsigned char *p;
for(h=0, p = (unsigned char *)str; *p ; p++)
h = 31 * h + *p;
return h;
}
Hash 算法及其应用
---------------
什么是 Hash
Hash 的重要特性
Hash 函数的实现
主要的 Hash 算法
Hash 算法的安全问题
Hash 算法的应用
结 论
---------------
Hash,一般翻译做“散列”,也有直接音译为"哈希"的,就是把任意长度的输入(又叫做
预映射, pre-image),通过散列算法,变换成固定长度的输出,该输出就是散列值。这
种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能
会散列成相同的输出,而不可能从散列值来唯一的确定输入值。
数学表述为:h = H(M) ,其中H( )--单向散列函数,M--任意长度明文,h--固定长度散列
值。
在信息安全领域中应用的Hash算法,还需要满足其他关键特性:
第一当然是单向性(one-way),从预映射,能够简单迅速的得到散列值,而在计算上不可能
构造一个预映射,使其散列结果等于某个特定的散列值,即构造相应的M=H-1(h)不可行。
这样,散列值就能在统计上唯一的表征输入值,因此,密码学上的 Hash 又被称为"消息摘
要(message digest)",就是要求能方便的将"消息"进行"摘要",但在"摘要"中无法得到比
"摘要"本身更多的关于"消息"的信息。
第二是抗冲突性(collision-resistant),即在统计上无法产生2个散列值相同的预映射。
给定M,计算上无法找到M',满足H(M)=H(M') ,此谓弱抗冲突性;计算上也难以寻找一对
任意的M和M',使满足H(M)=H(M') ,此谓强抗冲突性。要求"强抗冲突性"主要是为了防范
所谓"生日攻击(birthday attack)",在一个10人的团体中,你能找到和你生日相同的人的
概率是2.4%,而在同一团体中,有2人生日相同的概率是11.7%。类似的,当预映射的空间
很大的情况下,算法必须有足够的强度来保证不能轻易找到"相同生日"的人。
第三是映射分布均匀性和差分分布均匀性,散列结果中,为 0 的 bit 和为 1 的 bit ,
其总数应该大致相等;输入中一个 bit 的变化,散列结果中将有一半以上的 bit 改变,
这又叫做"雪崩效应(avalanche effect)";要实现使散列结果中出现 1bit 的变化,则输
入中至少有一半以上的 bit 必须发生变化。其实质是必须使输入中每一个 bit 的信息,
尽量均匀的反映到输出的每一个 bit 上去;输出中的每一个 bit,都是输入中尽可能多
bit 的信息一起作用的结果。
Damgard 和 Merkle 定义了所谓“压缩函数(compression function)”,就是将一个固定
长度输入,变换成较短的固定长度的输出,这对密码学实践上 Hash 函数的设计产生了很
大的影响。Hash函数就是被设计为基于通过特定压缩函数的不断重复“压缩”输入的分组
和前一次压缩处理的结果的过程,直到整个消息都被压缩完毕,最后的输出作为整个消息
的散列值。尽管还缺乏严格的证明,但绝大多数业界的研究者都同意,如果压缩函数是安
全的,那么以上述形式散列任意长度的消息也将是安全的。这就是所谓 Damgard/Merkle
结构:
在下图中,任意长度的消息被分拆成符合压缩函数输入要求的分组,最后一个分组可能需
要在末尾添上特定的填充字节,这些分组将被顺序处理,除了第一个消息分组将与散列初
始化值一起作为压缩函数的输入外,当前分组将和前一个分组的压缩函数输出一起被作为
这一次压缩的输入,而其输出又将被作为下一个分组压缩函数输入的一部分,直到最后一
个压缩函数的输出,将被作为整个消息散列的结果。
MD5 和 SHA1 可以说是目前应用最广泛的Hash算法,而它们都是以 MD4 为基础设计的。
1) MD4
MD4(RFC 1320)是 MIT 的 Ronald L. Rivest 在 1990 年设计的,MD 是 Message Digest
的缩写。它适用在32位字长的处理器上用高速软件实现--它是基于 32 位操作数的位操作
来实现的。它的安全性不像RSA那样基于数学假设,尽管 Den Boer、Bosselaers 和 Dobb
ertin 很快就用分析和差分成功的攻击了它3轮变换中的 2 轮,证明了它并不像期望的那
样安全,但它的整个算法并没有真正被破解过,Rivest 也很快进行了改进。
下面是一些MD4散列结果的例子:
MD4 ("") = 31d6cfe0d16ae931b73c59d7e0c089c0
MD4 ("a") = bde52cb31de33e46245e05fbdbd6fb24
MD4 ("abc") = a448017aaf21d8525fc10ae87aa6729d
MD4 ("message digest") = d9130a8164549fe818874806e1c7014b
MD4 ("abcdefghijklmnopqrstuvwxyz") = d79e1c308aa5bbcdeea8ed63df412da9
MD4 ("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789") = 043f8
582f241db351ce627e153e7f0e4
MD4 ("123456789012345678901234567890123456789012345678901234567890123456789012
34567890") = e33b4ddc9c38f2199c3e7b164fcc0536
2) MD5
MD5(RFC 1321)是 Rivest 于1991年对MD4的改进版本。它对输入仍以512位分组,其输出是
4个32位字的级联,与 MD4 相同。它较MD4所做的改进是:
1) 加入了第四轮
2) 每一步都有唯一的加法常数;
3) 第二轮中的G函数从((X ∧ Y) ∨ (X ∧ Z) ∨ (Y ∧ Z)) 变为 ((X ∧ Z) ∨ (Y ∧
~Z))以减小其对称性;
4) 每一步都加入了前一步的结果,以加快"雪崩效应";
5) 改变了第2轮和第3轮中访问输入子分组的顺序,减小了形式的相似程度;
6) 近似优化了每轮的循环左移位移量,以期加快"雪崩效应",各轮的循环左移都不同。
尽管MD5比MD4来得复杂,并且速度较之要慢一点,但更安全,在抗分析和抗差分方面表现
更好。
消息首先被拆成若干个512位的分组,其中最后512位一个分组是“消息尾+填充字节(100…
0)+64 位消息长度”,以确保对于不同长度的消息,该分组不相同。64位消息长度的限制
导致了MD5安全的输入长度必须小于264bit,因为大于64位的长度信息将被忽略。而4个32
位寄存器字初始化为A=0x01234567,B=0x89abcdef,C=0xfedcba98,D=0x76543210,它们
将始终参与运算并形成最终的散列结果。
接着各个512位消息分组以16个32位字的形式进入算法的主循环,512位消息分组的个数据
决定了循环的次数。主循环有4轮,每轮分别用到了非线性函数
F(X, Y, Z) = (X ∧ Y) ∨ (~X ∧ Z)
G(X, Y, Z) = (X ∧ Z) ∨ (Y ∧ ~Z)
H(X, Y, Z) =X ⊕ Y ⊕ Z
I(X, Y, Z) = X ⊕ (Y ∨ ~Z)
这4轮变换是对进入主循环的512位消息分组的16个32位字分别进行如下操作:将A、B、C、
D的副本a、b、c、d中的3个经F、G、H、I运算后的结果与第4个相加,再加上32位字和一个
32位字的加法常数,并将所得之值循环左移若干位,最后将所得结果加上a、b、c、d之一
,并回送至ABCD,由此完成一次循环。
所用的加法常数由这样一张表T[i]来定义,其中i为1…64,T[i]是i的正弦绝对值之42949
67296次方的整数部分,这样做是为了通过正弦函数和幂函数来进一步消除变换中的线性性
。
当所有512位分组都运算完毕后,ABCD的级联将被输出为MD5散列的结果。下面是一些MD5散
列结果的例子:
MD5 ("") = d41d8cd98f00b204e9800998ecf8427e
MD5 ("a") = 0cc175b9c0f1b6a831c399e269772661
MD5 ("abc") = 900150983cd24fb0d6963f7d28e17f72
MD5 ("message digest") = f96b697d7cb7938d525a2f31aaf161d0
MD5 ("abcdefghijklmnopqrstuvwxyz") = c3fcd3d76192e4007dfb496cca67e13b
MD5 ("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789") = d174a
b98d277d9f5a5611c2c9f419d9f
MD5 ("123456789012345678901234567890123456789012345678901234567890123456789012
34567890") = 57edf4a22be3c955ac49da2e2107b67a
参考相应RFC文档可以得到MD4、MD5算法的详细描述和算法的C源代码。
Hash,一般翻译做“散列”,也有直接音译为"哈希"的,就是把任意长度的输入(又叫做预映射, pre-image),通过散列算法,变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列 ...
Hash,一般翻译做“散列”,也有直接音译为"哈希"的,就是把任意长度的输入(又叫做预映射, pre-image),通过散列算法,变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,而不可能从散列值来唯一的确定输入值。
HASH算法的一般用途:
HASH算法一般用于数据的校验.
HASH算法原理:读者可以直接把HASH算法当成一个映射f : A B . 输入为A, 长度不固定. 输出为B, B的长度是定好的. HASH算法也只是一类算法的统称, HASH算法有很多种. 我们也可以规定自己的HASH算法. 比如如下:
规定:
A: 字节序列, 长度不定.
B: 一个字节.
f : B = ∑A[i] % 256 . (I = 0,1,2, … length-1)
以上就是一个非常简单的HASH算法. 它将输入字节序列的各个字节累加然后对256取模得到B字节.
我们用C语言来表达这个算法可以如下:
unsigned char get_hash(unsigned char * A, int A_len)
{
unsigned char B = 0;
int i = 0;
for(;i<A_len;++i)
{
B += A[i];
B %= 256;
}
return B;
}
有的朋友会说, 咦, 你这个C实现的算法和你说的怎么不一样, 你说的f明明是先全部累加再对256取模的, 你这里怎么每次都取了一次摸了? 其实是这样的, 我们这里这样做的目的是防止当先全部累加时unsigned char 类型的B发生溢出现象, 所以这里每次都模了一次, 而这样对算法也没有影响的.
因为根据数论知识, (A+B) % C = A%C + B%C .
我们再来看另外一个例子:
A: 一串字符串
B: 四个字节的DWORD值.
f: 所有字符累乘然后模以0xffffffff
C语言算法如下:
DWORD get_hash(char *str)
{
DWORD B = 0;
int i = 0;
for(;i<strlen(str);++i)
{
B *= (DWORD)str[i];
B %= 0xffffffff;
}
return B;
}
相信大家都能很轻松的看懂上面的算法, 也能完全理解HASH的原理及思想.
那么这一节的介绍就到这里, 接下来的几节我将会带领大家一起体验HASH算法在实战中的作用.
1、概念
Hash,一般翻译做“散列”,也有直接音译为"哈希"的,就是把任意长度的输入(又叫做预映射, pre-image),通过散列算法,变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,而不可能从散列值来唯一的确定输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。总之,hash是指用一小段数据来标识容量很大的一段数据,以验证它的完整性。
2、著名的hash算法(辅助文件quake3.md4.pdf)
数学表述为:h = H(M) ,其中H( )--单向散列函数,M--任意长度明文,h--固定长度散列值。
在信息安全领域中应用的Hash算法,还需要满足其他关键特性:
第一当然是单向性(one-way),从预映射,能够简单迅速的得到散列值,而在计算上不可能构造一个预映射,使其散列结果等于某个特定的散列值,即构造相应的M=H-1(h)不可行。这样,散列值就能在统计上唯一的表征输入值,因此,密码学上的 Hash 又被称为"消息摘要(message digest)",就是要求能方便的将"消息"进行"摘要",但在"摘要"中无法得到比"摘要"本身更多的关于"消息"的信息。
第二是抗冲突性(colli[font=新宋体]sion-resistant),即在统计上无法产生2个散列值相同的预映射。给定M,计算上无法找到 M',满足H(M)=H(M') ,此谓弱抗冲突性;计算上也难以寻找一对任意的M和M',使满足H(M)=H(M') ,此谓强抗冲突性。要求"强抗冲突性"主要是为了防范所谓"生日攻击(birthday attack)",在一个10人的团体中,你能找到和你生日相同的人的概率是2.4%,而在同一团体中,有2人生日相同的概率是11.7%。类似的,当预映射的空间很大的情况下,算法必须有足够的强度来保证不能轻易找到"相同生日"的人。
第三是映射分布均匀性和差分分布均匀性,散列结果中,为 0 的 bit 和为 1 的 bit ,其总数应该大致相等;输入中一个 bit 的变化,散列结果中将有一半以上的 bit 改变,这又叫做"雪崩效应(avalanche effect)";要实现使散列结果中出现 1bit 的变化,则输入中至少有一半以上的 bit 必须发生变化。其实质是必须使输入中每一个 bit 的信息,尽量均匀的反映到输出的每一个 bit 上去;输出中的每一个 bit,都是输入中尽可能多 bit 的信息一起作用的结果。
Damgard 和 Merkle 定义了所谓“压缩函数(compression function)”,就是将一个固定长度输入,变换成较短的固定长度的输出,这对密码学实践上 Hash 函数的设计产生了很大的影响。Hash函数就是被设计为基于通过特定压缩函数的不断重复“压缩”输入的分组和前一次压缩处理的结果的过程,直到整个消息都被压缩完毕,最后的输出作为整个消息的散列值。尽管还缺乏严格的证明,但绝大多数业界的研究者都同意,如果压缩函数是安全的,那么以上述形式散列任意长度的消息也将是安全的。这就是所谓 Damgard/Merkle 结构:
任意长度的消息被分拆成符合压缩函数输入要求的分组,最后一个分组可能需要在末尾添上特定的填充字节,这些分组将被顺序处理,除了第一个消息分组将与散列初始化值一起作为压缩函数的输入外,当前分组将和前一个分组的压缩函数输出一起被作为这一次压缩的输入,而其输出又将被作为下一个分组压缩函数输入的一部分,直到最后一个压缩函数的输出,将被作为整个消息散列的结果。
MD5 和 SHA1 可以说是目前应用最广泛的Hash算法,而它们都是以 MD4 为基础设计的。
1)MD4
MD(RFC 1320)是 MIT 的 Ronald L. Rivest 在 1990 年设计的,MD 是 Message Digest 的缩写。它适用在32位字长的处理器上用高速软件实现--它是基于 32 位操作数的位操作来实现的。它的安全性不像RSA那样基于数学假设,尽管 Den Boer、Bosselaers 和 Dobbertin 很快就用分析和差分成功的攻击了它3轮变换中的 2 轮,证明了它并不像期望的那样安全,但它的整个算法并没有真正被破解过,Rivest 也很快进行了改进。下面是一些MD4散列结果的例子:
MD4 ("") = 31d6cfe0d16ae931b73c59d7e0c089c0
MD4 ("a") = bde52cb31de33e46245e05fbdbd6fb24
MD4 ("abc") = a448017aaf21d8525fc10ae87aa6729d
MD4 ("message digest") = d9130a8164549fe818874806e1c7014b
MD4 ("abcdefghijklmnopqrstuvwxyz") = d79e1c308aa5bbcdeea8ed63df412da9
MD4("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789")=043f8582f241db351ce627e153e7f0e4
MD4 ("12345678901234567890123456789012345678901234567890123456789012345678901234567890") = e33b4ddc9c38f2199c3e7b164fcc0536
2) MD5
MD5(RFC 1321)是 Rivest 于1991年对MD4的改进版本。它对输入仍以512位分组,其输出是4个32位字的级联,与 MD4 相同。它较MD4所做的改进是:
1) 加入了第四轮
2) 每一步都有唯一的加法常数;
3) 第二轮中的G函数从((X ∧ Y) ∨ (X ∧ Z) ∨ (Y ∧ Z)) 变为 ((X ∧ Z) ∨ (Y ∧ ~Z))以减小其对称性;
4) 每一步都加入了前一步的结果,以加快"雪崩效应";
5) 改变了第2轮和第3轮中访问输入子分组的顺序,减小了形式的相似程度;
6) 近似优化了每轮的循环左移位移量,以期加快"雪崩效应",各轮的循环左移都不同。
尽管MD5比MD4来得复杂,并且速度较之要慢一点,但更安全,在抗分析和抗差分方面表现更好。
消息首先被拆成若干个512位的分组,其中最后512位一个分组是“消息尾+填充字节(100…0)+64 位消息长度”,以确保对于不同长度的消息,该分组不相同。64位消息长度的限制导致了MD5安全的输入长度必须小于264bit,因为大于64位的长度信息将被忽略。而4个32位寄存器字初始化为A=0x01234567,B=0x89abcdef,C=0xfedcba98,D=0x76543210,它们将始终参与运算并形成最终的散列结果。
接着各个512位消息分组以16个32位字的形式进入算法的主循环,512位消息分组的个数据决定了循环的次数。主循环有4轮,每轮分别用到了非线性函数
F(X, Y, Z) = (X ∧ Y) ∨ (~X ∧ Z)
G(X, Y, Z) = (X ∧ Z) ∨ (Y ∧ ~Z)
H(X, Y, Z) =X ⊕ Y ⊕ Z
I(X, Y, Z) = X ⊕ (Y ∨ ~Z)
其中,∧代表按位与,∨代表按位或,~代表按位取反,⊕代表按位异或。
这4轮变换是对进入主循环的512位消息分组的16个32位字分别进行如下操作:将A、B、C、D的副本a、b、c、d中的3个经F、G、H、I运算后的结果与第4个相加,再加上32位字和一个32位字的加法常数,并将所得之值循环左移若干位,最后将所得结果加上a、b、c、d之一,并回送至ABCD,由此完成一次循环。所用的加法常数由这样一张表T来定义,其中i为1…64,T是i的正弦绝对值之4294967296次方的整数部分,这样做是为了通过正弦函数和幂函数来进一步消除变换中的线性。当所有512位分组都运算完毕后,ABCD的级联将被输出为MD5散列的结果。下面是一些MD5散列结果的例子:
MD5 ("") = d41d8cd98f00b204e9800998ecf8427e
MD5 ("a") = 0cc175b9c0f1b6a831c399e269772661
MD5 ("abc") = 900150983cd24fb0d6963f7d28e17f72
MD5 ("message digest") = f96b697d7cb7938d525a2f31aaf161d0
MD5 ("abcdefghijklmnopqrstuvwxyz") = c3fcd3d76192e4007dfb496cca67e13b
MD5("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789")=d174ab98d277d9f5a5611c2c9f419d9f
MD5 ("12345678901234567890123456789012345678901234567890123456789012345678901234567890") = 57edf4a22be3c955ac49da2e2107b67a
参考相应RFC文档可以得到MD4、MD5算法的详细描述和算法的C源代码。
3) SHA1 及其他
SHA1是由NIST NSA设计为同DSA一起使用的,访问[url]http://www.itl.nist.gov/fipspubs[/url]可以得到它的详细规范--[/url]"FIPS PUB 180-1 SECURE HASH STANDARD"。它对长度小于264的输入,产生长度为160bit的散列值,因此抗穷举(brute-force)性更好。SHA-1 设计时基于和MD4相同原理,并且模仿了该算法。因为它将产生160bit的散列值,因此它有5个参与运算的32位寄存器字,消息分组和填充方式与MD5 相同,主循环也同样是4轮,但每轮进行20次操作,非线性运算、移位和加法运算也与MD5类似,但非线性函数、加法常数和循环左移操作的设计有一些区别,可以参考上面提到的规范来了解这些细节。下面是一些SHA1散列结果的例子:
SHA1 ("abc") = a9993e36 4706816a ba3e2571 7850c26c 9cd0d89d
SHA1 ("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq") = 84983e44 1c3bd26e baae4aa1 f95129e5 e54670f1
其他一些知名的Hash算法还有MD2、N-Hash、RIPE-MD、HAVAL等等。上面提到的这些都属于"纯"Hash算法。还有另2类Hash算法,一类就是基于对称分组算法的单向散列算法,典型的例子是基于DES的所谓Davies-Meyer算法,另外还有经IDEA改进的Davies- Meyer算法,它们两者目前都被认为是安全的算法。另一类是基于模运算/离散对数的,也就是基于公开密钥算法的,但因为其运算开销太大,而缺乏很好的应用前景。
没有通过分析和差分攻击考验的算法,大多都已经夭折在实验室里了,因此,如果目前流行的Hash算法能完全符合密码学意义上的单向性和抗冲突性,就保证了只有穷举,才是破坏Hash运算安全特性的唯一方法。为了对抗弱抗冲突性,我们可能要穷举个数和散列值空间长度一样大的输入,即尝试2^128或 2^160个不同的输入,目前一台高档个人电脑可能需要10^25年才能完成这一艰巨的工作,即使是最高端的并行系统,这也不是在几千年里的干得完的事。而因为"生日攻击"有效的降低了需要穷举的空间,将其降低为大约1.2*2^64或1.2*2^80,所以,强抗冲突性是决定Hash算法安全性的关键。
在NIST新的 Advanced Encryption Standard (AES)中,使用了长度为128、192、256bit 的密钥,因此相应的设计了 SHA256、SHA384、SHA512,它们将提供更好的安全性。
Hash算法在信息安全方面的应用主要体现在以下的3个方面:
1) 文件校验
我们比较熟悉的校验算法有奇偶校验和CRC校验,这2种校验并没有抗数据篡改的能力,它们一定程度上能检测并纠正数据传输中的信道误码,但却不能防止对数据的恶意破坏。
MD5 Hash算法的"数字指纹"特性,使它成为目前应用最广泛的一种文件完整性校验和(Checksum)算法,不少Unix系统有提供计算md5 checksum的命令。它常被用在下面的2种情况下:
第一是文件传送后的校验,将得到的目标文件计算 md5 checksum,与源文件的md5 checksum 比对,由两者 md5 checksum 的一致性,可以从统计上保证2个文件的每一个码元也是完全相同的。这可以检验文件传输过程中是否出现错误,更重要的是可以保证文件在传输过程中未被恶意篡改。一个很典型的应用是ftp服务,用户可以用来保证多次断点续传,特别是从镜像站点下载的文件的正确性。更出色的解决方法是所谓的代码签名,文件的提供者在提供文件的同时,提供对文件Hash值用自己的代码签名密钥进行数字签名的值,及自己的代码签名证书。文件的接受者不仅能验证文件的完整性,还可以依据自己对证书签发者和证书拥有者的信任程度,决定是否接受该文件。浏览器在下载运行插件和java小程序时,使用的就是这样的模式。
第二是用作保存二进制文件系统的数字指纹,以便检测文件系统是否未经允许的被修改。不少系统管理/系统安全软件都提供这一文件系统完整性评估的功能,在系统初始安装完毕后,建立对文件系统的基础校验和数据库,因为散列校验和的长度很小,它们可以方便的被存放在容量很小的存储介质上。此后,可以定期或根据需要,再次计算文件系统的校验和,一旦发现与原来保存的值有不匹配,说明该文件已经被非法修改,或者是被病毒感染,或者被木马程序替代。TripWire就提供了一个此类应用的典型例子。更完美的方法是使用"MAC"。"MAC" 是一个与Hash密切相关的名词,即信息鉴权码(Message Authority Code)。它是与密钥相关的Hash值,必须拥有该密钥才能检验该Hash值。文件系统的数字指纹也许会被保存在不可信任的介质上,只对拥有该密钥者提供可鉴别性。并且在文件的数字指纹有可能需要被修改的情况下,只有密钥的拥有者可以计算出新的散列值,而企图破坏文件完整性者却不能得逞。
2) 数字签名
Hash 算法也是现代密码体系中的一个重要组成部分。由于非对称算法的运算速度较慢,所以在数字签名协议中,单向散列函数扮演了一个重要的角色。在这种签名协议中,双方必须事先协商好双方都支持的Hash函数和签名算法。签名方先对该数据文件进行计算其散列值,然后再对很短的散列值结果--如Md5是16个字节,SHA1是20字节,用非对称算法进行数字签名操作。对方在验证签名时,也是先对该数据文件进行计算其散列值,然后再用非对称算法验证数字签名。对 Hash 值,又称"数字摘要"进行数字签名,在统计上可以认为与对文件本身进行数字签名是等效的。而且这样的协议还有其他的优点:
首先,数据文件本身可以同它的散列值分开保存,签名验证也可以脱离数据文件本身的存在而进行。
再者,有些情况下签名密钥可能与解密密钥是同一个,也就是说,如果对一个数据文件签名,与对其进行非对称的解密操作是相同的操作,这是相当危险的,恶意的破坏者可能将一个试图骗你将其解密的文件,充当一个要求你签名的文件发送给你。因此,在对任何数据文件进行数字签名时,只有对其Hash值进行签名才是安全的。
3) 鉴权协议
如下的鉴权协议又被称作挑战--认证模式:在传输信道是可被侦听,但不可被篡改的情况下,这是一种简单而安全的方法。需要鉴权的一方,向将被鉴权的一方发送随机串(“挑战”),被鉴权方将该随机串和自己的鉴权口令字一起进行 Hash 运算后,返还鉴权方,鉴权方将收到的Hash值与在己端用该随机串和对方的鉴权口令字进行 Hash 运算的结果相比较(“认证”),如相同,则可在统计上认为对方拥有该口令字,即通过鉴权。
3、Hash表—数据结构
哈希表(Hash Table)的应用近两年才在NOI中出现,是一种高效的数据结构。哈希表最大的优点,就是把数据的存储和查找消耗的时间大大降低,几乎可以看成是常数时间;而代价仅仅是消耗比较多的内存。然而在当前可利用内存越来越多的情况下,用空间换时间的做法是值得的。另外,编码比较容易也是它的特点之一。哈希表又叫做散列表,分为"开散列" 和"闭散列"。
3.1 基础操作
3.1.1 基本原理
我们使用一个下标范围比较大的数组来存储元素。可以设计一个函数(哈希函数,也叫做散列函数),使得每个元素的关键字都与一个函数值(即数组下标)相对应,于是用这个数组单元来存储这个元素;也可以简单的理解为,按照关键字为每一个元素"分类",然后将这个元素存储在相应"类"所对应的地方。但是,不能够保证每个元素的关键字与函数值是一一对应的,因此极有可能出现对于不同的元素,却计算出了相同的函数值,这样就产生了"冲突",换句话说,就是把不同的元素分在了相同的"类"之中。后面我们将看到一种解决"冲突"的简便做法。总的来说,"直接定址"与"解决冲突"是哈希表的两大特点。
我们可以举一个哈希表的最简单的例子。假设要建立一张全国30个地区的各民族人口统计表,每个地区为一个记录,记录的各数据项为:
编号
地区名
总人口
汉族
回族
…
显然,可以用一个一维数组C(1:30)来存放这张表,其中C[i]是编号为i的地区的人口情况。编号i便为记录的关键字,由它唯一确定记录的存储位置C [i]。例如:假设北京市的编号为1,则若要查看北京市的各民族人口,只要取出C[1]的记录即可。假如把这个数组看成是哈希表,则哈希函数f(key) =key。然则,很多情况下的哈希函数并不如此简单。可仍以此为例,为了查看方便应以地区名作为关键字。假设地区名以汉语拼音的字符表示,则不能简单地取哈希函数f(key)=key,而是首先要将它们转化为数字,有时还要作些简单地处理。例如:BEIJING的哈希函数值为字母“B”在字母表中的序号等于02;或(2)先求关键字的第一个和最后一个字母在字母表中的序号之和,然后判别这个和值,若比30(表长)大,则减去30。例如:TIANJIN的首尾两个字母“T”和“N”的序号之和为34,故取04为它的哈希函数值;或(3)先求每个汉字的第一个拼音字母的ASCII码(和英文字母相同)之和的八进制形式,然后将这八进制数看成是十进制再除以30取余数,若余数为零则加上30而为哈希函数值。例如:HENAN的头两个拼音字母为“H”和“N”,它们的ASCII码之和为(226)8,以(226)10除以(30)10得余数为16,则16为HENAN的哈希函数值,即记录在数组中的下标值。上述人口统计表中部分关键字在这三种不同的哈希函数值如下表所列:
Key
BEIJING
TIANJIN
HEBEI
SHANXI
SHANGHAI
SHANDONG
HENAN
SICHUAN
f1(key)
02
20
08
19
19
19
08
19
f2(key)
09
04
17
28
28
26
22
03
f3(key)
04
26
02
13
23
17
16
16
从这个例子可见:
(1)哈希函数是一个映象,因此哈希函数的设定很灵活(Hash哈希的原意为杂凑),只要使得任何关键字由此所得的哈希函数值落在表长允许范围之内即可;
(2)对不同的关键字可能得到同一哈希地址,即key1!=key2,而f(key1)=f(key2),这种现象称冲突(collision)。具有相同函数值的关键字对该哈希函数来说称做同义词(synonym)。例如:关键字HEBEI和HENAN不等,但f1(HEBEI)= f1(HENAN),又如: f2(SHANXI)= f2(SHANGHAI);f3(HENAN)= f3(SICHUAN)。这种现象给建表造成困难,如在第一种哈希函数的情况下,因为山西、上海、山东和四川这四个记录的哈希地址均为19,而C[19] 只能存放一个记录,那么其它三个记录存放在表中什么位置呢?并且,从上表三个不同的哈希函数的情况可以看出,哈希函数选得合适可以减少这种冲突现象。特别是在这个例子中。中可能有30个记录,可以仔细分析这30个关键字的特性,选择一个恰当的哈希函数来避免冲突的发生。
然而,在一般情况下,冲突只能尽可能地少,而不能完全避免。因为,哈希函数是从关键字集合到地址集合的映象。通常,关键字集合比较大,它的元素包括所有可能的关键字,而地址集合的元素仅为哈希表中的地址值。假设表长为n,则地址为0到n-1。例如,在C语言的编译程序中可对源程序中的标识符建立一张哈希表。在设定哈希函数时考虑的关键字集合应包含所有可能产生的关键字;假设标识符定义为以字母为首的8位字母或数字,则关键字(标识符)的集合大小为 C152* C762*7!=1.09388×1012,而在一个源程序中出现的标识符是有限的,设表长为1000足矣。地址集合中的元素为0到999。因此,在一般情况下,哈希函数是一个压缩映象,这就不可避免产生冲突。因此,在建造哈希表时不仅要设定一个“好”的哈希函数,而且要设定一种处理冲突的方法。
综上所述,可如下描述哈希表:根据设定的哈希函数H(key)和处理冲突的方法将一组关键字映象到一个有限的连续的地址集(区间)上,并以关键字在地址值中的“象”作为记录在表中的存储位置,这种表便称为哈希表,这一映象过程称为哈希造表或散列,所得存储位置称哈希地址或散列地址。
3.1.2 哈希函数的构造方法
哈希函数的构造方法有很多,在这里只介绍几种比较简单的,以便大家开拓思路,希望能起到抛砖引玉的作用。
1、直接定址法:
取关键字或关键字的某个线性函数值为哈希地址。即:
H(key)=key或H(key)=a*key+b
其中a和b为常数(这种哈希函数叫做自身函数)。
例如:有一个从1岁到100岁的人口数字统计表,其中,年龄作为关键字,哈希函数取关键字自身。如下表所示:
地址
01
02
03
…
25
26
…
100
年龄
1
2
3
…
25
26
…
100
人数
3000
2000
5000
…
1050
…
…
…
…
这样,若要询问25岁的人有多少,则只要查表的第25项即可。
2、除余法:
选择一个适当的正整数 p ,令 h(k ) = k mod p
这里, p 如果选取的是比较大的素数,效果比较好。而且此法非常容易实现,因此是最常用的方法。
4、hash在BT中的作用
在bt的下载中,hash主要来验证文件的完整性,并且hash还可以作为不同文件判别的标志。下载者每得到一个块,需要算出下载块的hash验证码与. torrent文件中的对比,如果一样则说明块正确,不一样则需要重新下载这个块。这种规定是为了解决下载内容准确性的问题。
三、文件分段下载
1、文件分段下载的原理:
下载文件的过程等同于打开一个远程文件,分块读取,写入本地文件,关闭远程文件。分段下载的原理是这样的:首先取得远程文件的长度,然后移动文件指针比如100个字节,如果成功,表示支持断点续传,可以分段下载,否则不支持断点续传,只能连续下载。
如果支持断点续传,通过一个分段策略(比如文件长度小于10K就不分段等等)将文件平分为n段,然后创建n个线程,每个线程都打开远程文件,将文件指针移动到本线程负责的分段的首位置,开始读取文件,直至本分段内容读取完成。如果此时其它线程工作还没有完成,可以选择其中一个最长的分段再一分为二,接管后面那段继续工作。还应该有一个总管线程,负责监视当前每个下载线程已读取的字节数,如果超过一个规定的数值(比如100K),就将已读取的字节写入本地文件相应的位置,然后刷新各线程的工作状态,保证读取文件的同步。