密码学2 单向散列函数

106 阅读8分钟

密码学基础概念之 单向散列函数

我们常说,铁打的营盘流水的兵。在密码学里,最基础的概念就像是铁打的营盘,具有长久的生命力;而密码学算法就像是流水的兵,隔一阵儿就会换一茬。 所以掌握密码学的基础概念及背后原理尤为重要。

单向函数与散列函数

在讲单向散列函数之前,我们先分别介绍一下单向和散列函数:

单向函数
单向函数(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. 散列值尽可能均匀分布
一个好的散列函数,它的散列值应该是均匀分布的。也就是说,每一个散列值出现的概率都是一样的

碰撞攻击
如果不这样的话,一部分散列值出现的概率就会较高,另一部分散列值出现的概率会较低,别人就更容易构造出两个或者多个数据,使得它们具有相同的散列值。这种行为,叫做碰撞攻击。

单向散列函数

单向散列函数既是一个单向函数,也是一个散列函数。它有如下三个重要的特征;

  1. 单向散列函数正向计算容易,逆向运算困难;
  2. 单向散列函数运算结果均匀分布,构造碰撞困难;
  3. 对于相同的单向散列函数, 给定数据的散列值是确定的,长度是固定的。

大部分的 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的校验值,来让我们校验数据的完整性。

在这个过程中我们又会面临两个问题:

  1. 我们该选择什么样的散列函数,它的破解难度才能足够大?这样,我们才 有足够的信心根据散列值判断数据的完整性。
  2. 我们怎么能够安全地获得数据发送者计算的散列值?如果我们接收到的是 被修改过的数据和修改过的散列值,我们是没有办法判断数据是不是完整的。

如何解决这两个问题,我将会在后续更新文章中进行介绍。