很久以前使用Django写过一个用户系统(一半为了练手,一半为了完成大创项目),其中包含了密码的设置、验证与重置功能。近期正好看到和密码相关的视频,在此记录一下我当时查阅资料后,能想出来的最好的密码验证方式。

目标/原则

  • 后端不存储明文密码
  • 不传输明文密码
  • 不传输不带盐的密码摘要
  • 能应对一定程度的流量嗅探攻击

哈希函数和“盐”的利用

哈希函数

首先,记明文密码为password哈希函数hash(data, ...),哈希函数接收一至多个参数,将所有参数按序拼接后映射为n位二进制整数(在我的实现中使用了sha512算法);哈希过程不可逆,不可反推,不同的输入可能对应相同的输出,但可能性极小,可以忽略不计。即:已知hash(a)的结果,不可推导出a的值;已知hash(a) == hash(b)为真,而a != b也为真,该事件发生的可能性极小,因此可以近似地认为若hash(a) == hash(b)为真,则a == b也为真,下文不再考虑上述可能性极小的事件。常见的哈希算法有sha256sha512等。

带静态盐的哈希

由于不可传输用户的明文密码,我们只能使用哈希函数将明文密码映射为一个固定长度的整数。然而,由于哈希算法都是公开的,如果使用hash(password)作为密码凭据,则存在如下风险:在后端数据泄露后,攻击者使用hash(password)作为密码凭证,进行“撞库”,而如果一些用户在其它网站也使用了相同的用户名和密码,且这些网站也使用hash(password)作为密码凭据,则这些用户的账号将受到威胁。此外,一些用户可能使用弱密码或常见的密码,攻击者在非法获取网站的数据库后,可以通过“彩虹表”猜测用户的明文密码,即:通过事先计算一些可能的密码(通常是弱密码)的哈希值(即哈希函数的输出),建立一个从哈希值到密码的映射,从而在非法获取hash(password)后能反查出明文密码。

因此,此系统使用随机的“盐”(salt)与明文密码混合后进行哈希,得到的结果作为密码凭证,每次生成新密码凭据,都会生成一个新的随机的盐。为了与将要出现的“动态盐”(dynamicSalt)区分,这里称直接与明文密码混合的“盐”为“静态盐”(staticSalt),因此我们有hash(password, staticSalt)staticSalt组成的元组作为密码凭据。显然,不同的password对应的hash(password, staticSalt)也不同,实际上,即使password相同,只要其对应的staticSalt不同,hash(password, staticSalt)也将不同,因而元组(hash(password, staticSalt), staticSalt)可以代表密码明文password作为凭据。将静态盐作为凭据的一部分是为了之后能够验证密码,而只知道staticSalthash(password, staticSalt)是不能倒推出密码明文password的。使用了静态盐,就算数据库泄露,由于不同的站点使用的静态盐不一致,即使用户在不同站点间使用相同的密码,攻击者也无法复用泄露的凭据,或进行“撞库”。

带动态盐的哈希

在验证密码时,假设客户端直接向服务器传输hash(password, staticSalt)以验证密码,在这种情况下,服务器直接对比数据库中的凭据与客户端传输过来的凭据是否完全一致即认为验证通过。实际上,客户端登录时的流量可能被攻击者截获。显然,若hash(password, staticSalt)(以及用户名username)被攻击者截获,则攻击者只要向服务器发送hash(password, staticSalt)和用户名username即可骗取服务器的信任,而攻击者自始自终无需知晓密码明文password,这是一种重放攻击。因而仅仅使用(hash(password, staticSalt), staticSalt)作为凭据是不够的。为了解决这个问题,我们引入动态盐dynamicSalt,每次登录时服务器都随机生成一个动态盐传输给客户端,客户端登录时不直接传输hash(password, staticSalt),而传输hash(hash(password, staticSalt), dynamicSalt),服务器收到后先对原本的凭据做同样的运算后再对比即可验证密码正误。在引入动态盐后,用户每次登录向服务器传输的凭据都不一样,即使流量被截获,攻击者也无法重放旧的凭据欺骗服务器:假设用户某次登录所使用的动态盐为salt1,而其登录时的所有流量均被攻击者截获,即攻击者知晓了usernamehash(hash(password, staticSalt), salt1)staticSaltsalt1;攻击者在试图模拟该用户登录时,服务器发送给他的动态盐为salt2salt2异与salt1),此时攻击者必须发送给服务器hash(hash(password, staticSalt), salt2)才能骗过服务器,而此时攻击者无法根据其获取的信息推出passwordhash(password, staticSalt)中的任何一个从而重新计算凭据,因此他无法完成攻击。

综上所述,带盐哈希能够代替密码明文本身作为验证密码的凭据;为了防止数据库泄露后的“撞库”和“彩虹表”攻击,需要引入静态盐;为了防止重放攻击,需要引入动态盐,做到一次一密。

功能实现

此系统包含基本的注册时的设置密码,登录时的验证密码,更改密码时的重置密码功能。涉及的数据包含了上面提到的动态盐(dynamicSalt)、静态盐(staticSalt)、密码明文(password),以及哈希函数hash(data, ...)),还包括用户名(username)。下图为系统的数据流图:

数据流图

设置密码

在新用户注册时,应为用户设置新密码,用户需告知服务器代表其密码的凭据,在之后的校验中,服务器根据代表密码的凭据验证用户的密码。其具体流程如下:

  1. 客户端向服务器请求静态盐staticSalt,服务器生成一个随机的静态盐返回给客户端;
  2. 用户输入密码明文password,客户端使用预定的哈希函数hash计算hash(password, staticSalt)并将计算结果,共用户名username发送给服务器。至此,服务器已拥有完整的密码凭据(hash(password, staticSalt), staticSalt),服务器保存username(hash(password, staticSalt), staticSalt)至数据以备后续验证。

下图为设置密码的时序图:

时序图

验证密码

在用户登录时,或试图重置密码时,需要验证其当前的密码。其具体流程如下:

  1. 客户端发送用户名username到服务器,请求登录(验证密码);
  2. 服务器根据username查询得到该账号对应的密码凭据(hash(password, staticSalt), staticSalt),同时生成一个随机的动态盐dynamicSalt,与凭据中的staticSalt一并发送给客户端;
  3. 用户输入当前密码明文password,客户端使用预定的哈希函数hash计算hash(hash(password, staticSalt), dynamicSalt)并将计算结果发送给服务器;
  4. 服务器进行同样的运算,将结果与客户端发送的结果对比,完全一致则通过验证,否则报告密码不一致。

下图为验证密码的时序图:

时序图

重置密码

重置密码需要在验证当前密码后,然后设置新的密码。具体流程等价于上述“验证密码”和“设置密码”的组合,这里不再赘述。

下图为重置密码的时序图:

时序图

此方法没有克服的缺陷

此方法最明显的缺陷在于当用户设置密码时,需要向服务器发送hash(password, staticSalt),这是非常重要的凭据,一旦泄露,或者被截获,攻击者就可以计算出hash(hash(password, staticSalt), dynamicSalt),从而欺骗服务器。然而同样的缺陷在仅使用动态盐或静态盐,甚至明文存储密码的方案中同样存在。对于嗅探攻击,此方法只会在设置密码阶段受到攻击;对于数据库泄露,不管使用何种密码存储、验证方案,都需要通知用户立刻修改该站点上的密码,包括其它与泄露的站点共用相同密码的站点上的密码。为了弥补此方法的缺陷,可以利用HTTPS加大攻击难度,增大攻击成本(但HTTPS不是万能的,它仍可能受到中间人攻击)。

声明

上述方法只是本人参考了网络上的资料,加以本人最大之所知及所学总结出来的,如有疏漏、谬误、不合理之处,还请提出。

时序图和数据流图可能并不规范和清晰,但暂时无法改善。



# password verification   # identification   # 密码验证   # 认证   # 密码传输