哈希算法:密码安全的基石,不只是“加密”
在上一篇你可能在用错密码:服务端密码安全的真相与陷阱我们聊了密码安全的重要性,以及开发者常犯的几个错误。很多掘友可能会觉得,不就是把密码“加密”一下嘛,用个哈希算法不就得了(我最初也是这么简单的做)?
但实际上,仔细想想,会发现 哈希和加密是两个完全不同的概念,这往往是很多开发者对密码安全理解的第一个误区。
哈希与加密:有何不同?
我们先来明确一下这两个核心概念:
- 加密:是一个可逆的过程。你用一个密钥把数据加密,就能用对应的密钥把数据解密还原成原始数据。例如,计科同学通常最初接触编程都会写一道入门算法题目:凯撒密码,大致就是字符的移动,这其中的密钥就是移动长度,那么反过来被加密后的密码,也可以通过这个移动长度逆向移动,从而获取原密码。
- 哈希:是一个单向的、不可逆的过程。你把任意长度的数据输入哈希函数,它会生成一个固定长度的哈希值。这个过程是不可逆的,理论上无法从哈希值反推出原始数据。
更通俗的讲:
- 加密 就像你把一份文件锁进保险箱,只要有钥匙,随时可以取出来。
- 哈希 把一张A4纸粉碎成纸屑,它变成了一堆碎纸屑,你无论如何也不能从这些纸屑还原出文件本身。
所以,当我们将用户密码进行哈希处理后,数据库中存储的只是一个哈希值,而不是原始密码。这样即使数据库被攻破,攻击者也无法直接获取用户密码。
为什么密码要用哈希,而不是加密?
答案很简单:为了安全!
如果你的服务端存储的是用户密码的加密版本,那么:
- 加密密钥的管理是个难题:你把密码加密了,那加密的密钥存在哪里?如果密钥和加密后的密码都存储在同一个系统甚至同一个数据库里,一旦系统被攻破,密钥泄露,所有加密密码都会被解密。这就像你把保险箱和钥匙一起放在一个房间里。
而使用哈希算法存储密码,我们只需要在用户登录时,将用户输入的密码同样进行哈希处理,然后比较其哈希值是否与数据库中存储的一致。如果一致,则验证通过。这个过程中,原始密码从未以明文形式出现在服务端内存中(除了用户输入瞬间),也从未存储在数据库中。
强哈希算法的关键特性
不是所有哈希算法都适合用于密码存储。一个安全的密码哈希算法通常具备以下特性:
-
不可逆 :这是最基本的要求,无法从哈希值反推原始密码。
-
抗碰撞 :很难找到两个不同的输入产生相同的哈希值。
-
雪崩效应 :输入哪怕只改变一点点(比如一个空格),输出的哈希值也会发生巨大变化。这使得通过猜测或微调输入来推断密码变得极其困难。
-
慢计算 :这一点对于密码哈希尤为重要。传统的哈希算法如 MD5、SHA-256 都设计为快速计算。但对于密码哈希,我们恰恰需要它“慢”下来,以此来增加暴力破解和彩虹表攻击的成本。这就是为什么会引入“加盐”和“迭代次数”的概念。
为什么 MD5、SHA-256 不再安全?
前面提到的 MD5、SHA-256 之所以不安全,主要问题就在于它们的计算速度太快:
- 速度快:攻击者可以在短时间内进行数以亿计的哈希计算,配合彩虹表(预先计算好的哈希值-明文对应表)或暴力破解,很快就能猜出密码。
因此,在服务端密码存储上,MD5 和 SHA-256 已经被证明是不安全的。
下一步:选择更安全的哈希算法
既然 MD5、SHA-256 已经过时,那我们应该选择哪些哈希算法来保护密码呢?
现代密码哈希算法通常会故意设计得耗时,并且支持加盐和迭代。例如:
- Bcrypt:一种专门为密码存储设计的哈希算法,内置了加盐和迭代功能,且可以调节工作因子(成本因子)来控制计算耗时。
- Scrypt:与 Bcrypt 类似,但它在内存消耗方面也做了优化,可以抵抗定制硬件攻击。
下一篇,我会尝试解释哈希算法过程,以及最核心的实践——加盐(Salt) 的重要性,大概会从MD5讲起,然后过渡讲到Bcrypt。