密码学基础概念之 单向散列函数
我们常说,铁打的营盘流水的兵。在密码学里,最基础的概念就像是铁打的营盘,具有长久的生命力;而密码学算法就像是流水的兵,隔一阵儿就会换一茬。 所以掌握密码学的基础概念及背后原理尤为重要。
单向函数与散列函数
在讲单向散列函数之前,我们先分别介绍一下单向和散列函数:
单向函数
单向函数(One-way Function)是正向计算容易,逆向运算困难的函数。也就是说,给
定你一个输入,你很容易计算出输出;但是给定你一个输出,你却很难计算出输入是什
么。
比如把盘子打碎是一件很简单的事情,但是把这些碎片再拼接成一个完整的盘子,就是一件非常困难的事情。 细心的你或许会发现: 虽然把盘子碎片再拼接起来非常困难,但是仅仅就是非常困难而已,无论 是手工还是计算机辅助,碎盘子还是可以拼接起来的。
单向函数就是这样的一个盘子。虽然我们强调,单向函数只能正向计算,不能逆向运算。 但其实,这只是一个美好的愿望。事实上,单向函数具有不确定性,当每个单列函数被发明出来时,人们找不到它的破解方法。可是被破解的时候,人们又发现原来是有办法去逆向运算的。
今天还是安全的算法,明天就有可能被破解。所以这点要引起我们的警惕,比如说,一个应用程序至少要支持两种单向函数,当PlanA有问题时,PlanB可以进行兜底。
实用单向函数的特征
一个更实用的单向函数,正向计算会更容易,容易程度就是这个函数的计算性能;
一个更实用的单向函数,逆向运算会更困难,困难程度就是这个函数的破解强度。
散列函数
散列函数(Hash Function)是一个可以把任意大小的数据,转行成固定长度的数据的函
数。比如说,无论输入数据是一个字节,或者一万个字节,输出数据都是 16 个字节。
我们把转换后的数据,叫做散列值。因为散列函数经常被人们直译为哈希函数,所以我们
也可以称散列值为哈希值。通常的,对于给定的输入数据和散列函数,散列值是确定不变
的。
散列值碰撞
既然输入数据的大小没有限制,而输出结果的数据长度固定,
那么便会存在散列值相同的两个或者多个数据,这便是散列值碰撞,又叫做hash碰撞。
散列值碰撞带来的问题
比如,Java 语言里的 hashCode() 方法,或者数据结构和算法里的哈希值,就是一个散列函数的运用。
如果 hashCode() 的实现出现散列值碰撞,就会影响应用程序的性能,比如 HashMap 的
检索时间会显著加长。再比如说,如果我们使用 hashCode 作为键值或者索引,散列值碰
撞会导致检索错误,从而带来数据安全问题。
如何降低散列值碰撞的可能性
由于输入的数据大小没有限制,输出的长度固定,所以理论上我们是无法避免hash碰撞的,所以我们只能尽可能降低碰撞发生的可能性。
1. 增加数据输出的长度
我们可以让输出数据变长, 散列值越长,存在相同散列值的概率就越小,发生碰撞的可能性就越
小。比如16位的散列值变为32位的。
缺点:
散列值越长,通常也就意味着计算越困难,计算性能越差。当初我们要使用固定长度的散列值的原因,其实就是为了减少计算本身的性能损耗,从而获得性能优化。
所以,散列值也不是越长越好。散列值的长度选择,应该是权衡性能后的结果。比如 Java 语言里,hashCode() 的 返回值是 32 位的整数,也就意味着散列值的长度是 32 位。由于 hashCode() 的返回值主 要是用来检索,32 位的整数已经足够大了,所以这是一个合适的选择。
2. 散列值尽可能均匀分布
一个好的散列函数,它的散列值应该是均匀分布的。也就是说,每一个散列值出现的概率都是一样的。
碰撞攻击
如果不这样的话,一部分散列值出现的概率就会较高,另一部分散列值出现的概率会较低,别人就更容易构造出两个或者多个数据,使得它们具有相同的散列值。这种行为,叫做碰撞攻击。
单向散列函数
单向散列函数既是一个单向函数,也是一个散列函数。它有如下三个重要的特征;
- 单向散列函数正向计算容易,逆向运算困难;
- 单向散列函数运算结果均匀分布,构造碰撞困难;
- 对于相同的单向散列函数, 给定数据的散列值是确定的,长度是固定的。
大部分的 hashCode() 方法的实现,都满足不了逆向运算困难的要求,所以它们是不能算作单向散列函数的。比如说,按照 Java 的 hashCode() 方法的实现,32 位整数的哈希值是这个整数本身,所以逆向运算一点难度都没有,当然不能算作单向散列函数。
单向散列函数的雪崩效应
雪崩效应(Avalanche Effect)是密码学算法一个常见的特点,指的是输入数据的微小变
换,就会导致输出数据的巨大变化。严格雪崩效应是雪崩效应的一个形式化指标,我们也
常用来衡量均匀分布。严格雪崩效应指的是,如果输入数据的一位反转,输出数据的每一
位都有 50% 的概率会发生变化。
而一个适用于密码学的单向散列函数,就要具有雪崩效应的特点,也就是说,如果一个单向 散列函数具有雪崩效应,那么对于给定的数据,构造出一个新的、具有相同散列值的数据 是困难的。
举个栗子:
SHA-1算法
虽然world和volrld只有一个字符的变动,但是经过SHA-1算法加密后的散列值却有着很大的区别:
SHA-1("Hello, world!):
10010100 00111010 01110000 00101101 00000110 11110011 01000101 10011001 101011
SHA-1("Hello, vorld!):
11001011 11111111 11111011 10010011 01010111 11000010 10001101 01011000 001000
数据的完整性
完整性的核心是数据未经授权,不得更改。
直接解读 便是无论如何,数据都没有办法改动,一般情况下,很难有满足的场景。
曲线解读 便是如果数据有变动,能够被检测出来,我们就不采纳被篡改的数据。使用单向散列函数,就可以通过检查数据是否有变动,来解决数据完整性问题。
单向散列函数解决数据完整性的基本思路
在单向散列函数里,一段数据,无论它是少了一个字,多了一个字,或者修改了一个字,原始数据和修改后的数据的散列值都可能相差巨大。
而且,由于逆向运算困难,虽然存在具有相同散列值的两个或者多个数据,但是对于一个好的单向散列函数来说,刻意寻找这样的数据是困难的。如果困难程度足够大,我们就有
足够信心认为,如果散列值没有变化,它对应的输入数据也没有变化。
所以,单向函数和散列函数的组合,单向散列函数,就可以帮助我们解决完整性问题。 假如我们收到了一段数据,我们就可以重新计算这段数据的散列值。如果我们还可以获得 数据发送者计算的散列值,我们就可以对比新计算的散列值和接收到的散列值。如果两个 散列值是相同的,我们就可以认为这段数据是完整的;否则,这段数据就是被篡改过的。
比如现在当我们下载一些中间件时,官网都会提供SHA的校验值,来让我们校验数据的完整性。
在这个过程中我们又会面临两个问题:
- 我们该选择什么样的散列函数,它的破解难度才能足够大?这样,我们才 有足够的信心根据散列值判断数据的完整性。
- 我们怎么能够安全地获得数据发送者计算的散列值?如果我们接收到的是 被修改过的数据和修改过的散列值,我们是没有办法判断数据是不是完整的。
如何解决这两个问题,我将会在后续更新文章中进行介绍。