在一些老论坛、老程序的数据库里,密码字段只有 16 个字符,而不是常见的 32 个。这被称作「半 MD5」或「16 位 MD5」。同一时期流行的另一个做法是给密码加一段盐再哈希。这两种做法经常被放在一起讨论,但它们的性质其实完全不同:一个是纯粹的截断,另一个至少增加了攻击成本。
半 MD5 只是把结果切短
MD5 的输出是 128 位,用十六进制表示就是 32 个字符。所谓「半 MD5」,就是取这 32 个字符中间的第 9 到第 24 位,得到 16 个字符。
它没有引入任何新的计算,只是显示的时候少写了一半。这个做法在早期有它的现实原因:数据库字段短、论坛模板把 MD5 值当作用户标识、界面空间有限。但从安全角度看,截断只会让事情变糟。
原因在于碰撞概率。摘要越短,能表达的状态越少,找到两个不同输入产生相同摘要就越容易。完整的 MD5 已经被实际攻破,截断到一半之后,碰撞更容易构造。所以半 MD5 不能作为安全措施,它唯一合理的用途是做展示用的短标识,例如旧系统里给用户生成一个短一点的编号。
如果只是需要短标识,更合适的做法是用专门的短标识方案,而不是截断一个安全摘要——后者会让人误以为它还有摘要的保护能力。
加盐挡的是复用,不是计算
加盐的思路是:给每个用户生成一段随机数据(盐),把它和密码拼在一起再哈希,并把盐和哈希一起存储。
它的核心作用是让「一次计算」不能复用到「多个用户」。在没有盐的年代,攻击者算出一张彩虹表,可以拿去比对任何数据库;有了盐,每个用户的哈希都不同,彩虹表失效,攻击者必须针对每个用户单独尝试。
这就挡住了撞库和批量破解,价值很大。但要清楚它挡不住什么:它不改变单次哈希的代价。针对某一个特定账号反复尝试各种口令组合,成本仍然和以前一样低。MD5 每秒能算数百亿次,这个速度不会因为加了盐而下降。
所以「加盐能救 MD5 吗」的答案是:能挡住彩虹表,挡不住暴力破解。要抵抗后者,必须换算法,而不是加参数。
真正需要的是代价可调
慢哈希与通用哈希的分水岭,就在「代价能不能调」这一条上。
Bcrypt 用一个 cost 因子控制迭代轮数,每加 1 计算量翻倍。Argon2 除了迭代次数,还能要求占用指定大小的内存,让显卡的并行优势失去意义。PBKDF2 用迭代次数来控制,数值可以设到几十万次。
这些参数的共同特点是:可以随着硬件进步往上调。十年前设的 cost 值今天可能不够用,但至少有一个旋钮可以拧;而 MD5 没有任何旋钮,它的速度由算法决定,永远那么快。
这解释了为什么「输出更长」并不能解决口令安全问题。把 MD5 换成 SHA-512,输出从 32 个字符变成 128 个字符,看起来安全了很多,但攻击者的尝试速度几乎没有下降。长度对抗的是碰撞,不是穷举。
实践上要注意的几点
第一,慢哈希的输出格式通常自带盐和参数。Bcrypt 的字符串里包含了版本、cost、盐和摘要,Argon2 的编码串里包含了算法、版本、内存、迭代、并行度和盐。这意味着不需要单独维护盐字段,也不会出现「忘了加盐」的实现疏漏。
第二,存储字段要留足长度。Bcrypt 是 60 个字符,Argon2 的编码串通常更长,用 char(32) 或 varchar(40) 去存会被截断,导致验证永远失败。
第三,迁移时不要一次性强制所有人改密码。在登录流程里顺带升级是成本最低的方式,用户无感,系统压力也小。
第四,加盐要用密码学安全的随机源。用时间戳、用户 ID 或者 rand() 生成的盐,可预测性高,等于没加。
检查清单
- 半 MD5 只是截断摘要,会降低碰撞难度,不能当安全措施
- 加盐能挡住彩虹表和批量撞库,但挡不住针对单个账号的暴力尝试
- 抵抗暴力破解的关键是代价可调,Bcrypt、Argon2、PBKDF2 都提供这个能力
- 加长输出(如改用 SHA-512)不能替代慢哈希,长度对抗的是碰撞而非穷举
- 慢哈希的输出自带盐与参数,不需要额外维护字段
- 迁移前确认存储字段长度足够,并保证随机盐来自密码学安全随机源