Auth: Authentication: Password

 20th August 2020 at 2:19pm

在很久以前大家刚用密码来做认证时,密码是以明文存储在数据库中的。这样会导致一旦数据库泄漏,则所有用户的密码都被黑客获取。

Encrypted password

简单的解决办法可以是,把密码做加密,这样数据库中不再是明文密码。但是一旦黑客同时拿到数据库及加密方式、密钥等,那他一样可以获得密码原文。

Password hashing

对密码做哈希再存数据库可以解决这个问题。由于哈希出来的结果不可逆,数据库管理员或者黑客即使获得了数据,也无法逆向出密码原文。

但是这里存在一个问题,黑客可以预先计算出大量常见密码串的哈希值,然后拿数据库中哈希过的密码去查,就可以查出原来的密码。这种预先计算出来的数据被称为「彩虹表」。为了对抗这种方法,我们需要对密码「加盐」。

「加盐」即是随机生成一小段数据,将它加在密码上再做哈希:

「加盐」使得黑客破解密码的成本大大提高。不加盐时黑客只需要一张彩虹表就可以破解,但是一旦各数据库中加了不同的盐,那黑客需要预先计算好非常多的彩虹表才可以做破解。

一般一个多用户的系统中,对每个用户使用不同的盐是更好的实践,因为黑客破解时需要计算更多的彩虹表。但是注意这并不表示密码是绝对安全的。如果黑客要破解的密码价值足够高(比如破解美国总统的密码),那他们仍然可以运用大量算力将单个密码破解出来。

"Stretching" password

随着 CPU 速度越来越高,像 MD5 这种在设计之初计算得非常慢的哈希函数,在现代 CPU 上的计算速度已经非常快。同时 GPU 在做这类计算上速度又比 CPU 更快。因此黑客计算彩虹表的开销大幅下降。因此大家又想出了一个被称为「stretching」的方法。它的想法很简单,就是于其只做一次哈希,不如做多次哈希(将哈希出来的结果继续哈希)。这样黑客破解时的计算成本就变高了。哈希的次数被称为迭代次数(iteration)。Django 2.2 中默认对密码做 150000 次迭代。

同时一些哈希算法被设计成故意运行缓慢的:

  • PBKDF2:特点是过程中会生成一个 key,这个 key 是通过伪随机函数、密码原文和盐计算出来的,而且计算次数可以指定的。Django 默认使用这个算法,并且 RFC 8018 (2017) 及 NIST 推荐使用它
  • Bcrypt:OpenBSD、SUSE Linux 的默认哈希函数。特点是你可以指定「复杂度」来影响算法的计算速度
  • Scrypt:特点是难被并行运行,使得 GPU 农场无用武之地
  • Argon2:也是一个评价较高的算法

Strong passwords are important

虽然开发者在保存密码上做足了功课,似乎你的密码已经坚不可破。但是设置一个强密码还是很必须的。密码的位数越多、其中涉及的特殊字符组合越复杂,黑客破解时的计算成本也呈指数上升。

Server-relief

对于像 Google、Facebook 这样的大公司,经常会有大量用户同时登录,这对服务器 CPU 和内存的消耗是非常大的。Server-relief 即是指,将一些计算放在客户端去做,再将结果发给服务端做最终校验。

Django auth implementation

使用默认配置时,Django 会采用 PBKDF2 算法及 SHA256 摘要算法,迭代 150000 次(Django 2.2)来生成密码:

  1. 生成一个密码学安全的随机字符串作为盐
  2. 使用 PBKDF2 算法,将密码原文、盐及迭代次数(默认 150000 次)作为参数,并以 SHA256 为摘要算法,生成一串 hash 值
  3. "%s$%d$%s$%s" % (self.algorithm, iterations, salt, hash) 形式将密码保存至数据库(默认 auth_user 表)

代码入口在 django.contrib.auth.base_user.AbstractBaseUser.set_password

Libraries

Resources