好久没有写博客了,但是NeoAtlantis应用科学和神秘学实验室的负责人这一年来还是在忙碌着。过去几个月主要的工作重点在公司的一个内部项目上,因此虽然有所积累但是一直没有时间写下来。

今天想写的是一个计划中的新的口令管理体系。其实思路已经有很多人提出来过,就是基于HMAC函数生成口令。

在继续下文之前,我们要区分「口令」和「密码」两个术语。口令对应password或者passpharse,表示一个用于登录网站或者服务的字符串,可以但不一定用于加密信息,其主要作用是认证用户的身份。 「密码」单指密码学,即cryptography。因此,我们说口令管理而不是密码管理,避免误会成管理加密算法之类的意思。

1. 试图解决的问题

要更换这样的口令管理体系,是为了解决如下一些问题。

1.1 口令明文泄漏的信息

口令如果是用户自己决定的,一般会有一些含义,比如用户的身份或者姓名。如果口令被不靠谱的网站有意或者无意地以明文形式记录下来,然后这个网站在未来的某一天泄漏出这些数据,就会造成很大的风险。

被这个问题坑掉的人包括著名的互联网博客作者「编程随想」。

根据一些流传的说法,编程随想因为早年使用的口令包含他自己的姓名缩写,被人仔细调查后发现身份。

此外,如果在不同的网站上使用同样的口令,那么这些网站只要有一个记录了这个口令的明文,或者简单的口令散列(MD5/SHA1等),用户上过的所有的网站就会成片暴露。

1.2 口令随机性不够

口令要具备足够多的「熵」才能算是安全的。个人用户拍拍脑袋想出来的口令一般很难具有足够多的随机性。因此有经验的黑客使用的软件,一般会有比暴力破解更加有效的猜解方式。

1.3 口令管理器的弊端:单点故障

口令管理器虽然可以解决上述(1)和(2)的问题(1的问题还需要用户具有足够的自制力,比如为了每个新的账户生成一个口令),但是它也有一个明显的弊端:单点故障。

一旦口令管理器的设计不靠谱,或者用户因为什么原因泄漏了主口令,都会导致所有的口令全部泄漏。

有些自建的口令管理器可以使用两步认证,但是两步认证不能提供额外的加密,上面的风险仍然存在(比如从服务器端泄漏出去)。

2. 本实验室需求的口令管理体系

关于口令管理,我们提出的需求是:

  1. 不同网站上用的口令尽量不一致,每个网站都使用不同的口令。
  2. 即使一个网站的口令泄漏,也尽量不会危及其他网站上的口令。
  3. 有一套备份机制,可以离线存储用于恢复整个口令树的数据;同时还要兼顾此类数据免受偷窃或者物理暴力攻击的性能。
  4. 在索取口令之前,需要认证用户的身份,基于多个因素:知道、持有和本人。
  5. 口令管理器存在一个应急机制,可以在用户需求的情况下自毁,要求用户使用更加复杂的离线备份机制恢复整个体系。

为了完成以上需求,我们需要使用算法和技术手段作出改进。这包括多个方面:

第一,我们要使用HMAC算法,根据随机的熵,生成准随机的口令。这样,即使用户携带的口令种子(salt)丢失,也不一定等于口令泄漏。

第二,我们要使用技术手段,加强对用户身份的验证。口令管理不能仅仅是一个书面上的算法设计,它应当是一个实时运作的系统,把控风险,在发现非授权的用户试图访问密码时作出及时的反应。 具体而言,这一点还包括如下方面:

  • 抵御非授权用户通过普通信道(即和授权用户一样的方式)试图访问的风险
  • 抵御物理攻击,例如试图拆解电脑获得密钥
  • 响应用户的报警,具有自毁功能

2.1 口令生成机制的讨论

HMAC是一类单向函数,即只能从输入得到输出结果而不能反过来推算。它需要一个密钥和一个输入,对于给定的密钥和输入组合,它能生成一个唯一的结果。

中间密钥

在这个体系中,最根本的熵来自一个较长的随机密钥。这个随机的根密钥,平时不存在于互联网上,也不需要输入到系统中。它的作用是:加上第二个随机参数(我们称之为salt),得出无数多个中间密钥。

salt是一段可以公开存储的值,不需要随机。它可以是一个日期,一个数字,一段文字,用来区分不同的中间密钥。 中间密钥=导出算法(根口令, salt),而导出算法是一个单向函数,仅仅知道salt无法得知中间密钥,知道一个中间密钥和他对应的salt,也不能得出根口令或者其他的中间密钥。

根口令平时存储在安全的地方(比如银行),并且以纸质形式打印出加密的结果保存,或者也可以保存在需要生物认证(比如指纹)的U盘上,增加一些安全性。

中间密钥的存储

中间密钥是平时会存在于互联网上,或者随着用户走动的密钥。当然不是公开或者明文存储,而是放在一些密钥托管的地方。目前可能的方案有:

  1. 存储在密码管理器里,然后在一个固定域名上由密码管理器负责填入;
  2. 使用BasicCard这样的智能卡,编写程序,将密钥输入进去,然后只能根据密钥计算HMAC;
  3. 放入亚马逊KMS。但是KMS不支持HMAC密钥导入,所以只能用KMS加密保存,并在需要的时候暂时导入到内存里;
  4. 自制硬件,比如ATECC608B,支持导入密钥并计算HMAC;
  5. 利用YubiKey等支持HMAC Challenge-Response协议的硬件完成。

方案1是可以过渡的技术,保留了中间密钥导出口令的特点,可以作为概念验证使用。

方案2是本实验室已经开发的技术,但是安全性中等。而且不适合于智能设备,电脑上也需要安装额外的程序。但是优点是价格便宜,便于携带,不引人注目。智能卡可以每次插入后重新要求输入PIN,避免暴力破解的可能。

方案3安全性中等(需要信任亚马逊的计算资源),但是可以作为网页应用,用起来比较方便。

方案4可以做到比2稍好的安全性,但是开发技术难度大,需要一些时间。

方案5是本实验室还需要探索的技术,但是因为YubiKey等安全产品已经非常普及,应该是便于开发的。缺点是这些产品可能缺少防止暴力猜解的设计。 中间密钥的存储应当具备这一设计,类似银行用智能卡的PIN认证,只有三次机会。

口令的生成

我们可以根据中间密钥和一些参数,生成无限多的实际口令。我们可以选定一种确定性(deterministic)的算法,将这些参数编码为一个字符串序列。 这样,只要对这个字符串序列进行HMAC计算,并以中间密钥为输入,就可以得到一定长度的随机字节。

但是,口令还需要考虑如下有关口令格式的问题:

  1. 不同网站允许的口令长度不同。因此,需要记录用于某个网站的口令实际有多长。
  2. 不同网站允许的口令可以包含的字符串有区别,例如有些网站允许任意特殊字符,有些不允许#$之类的部分特殊字符。

因此我们需要一个方法,给定一定量的输入,能根据不同的口令格式配置,输出符合要求的口令。为此,我们还需要首先约定一个口令格式配置的表记法。

此外,我们需要确定用于生成口令所需要的参数。这些参数可能无须严格保密,但需要确定如下特性:

第一:这些参数的组合,对不同网站和网站上的不同用户是不同的。因此需要记录网站的用户名和域名。

第二:网站需要按照功能分类隔离,不同分类的网站使用不同的中间密钥。这在最后选用中间密钥生成口令时要作出区分。

第三:口令生成参数不能根据本文或者日后公布的代码举一反三得知。因为如果这样,一个人如果具备了访问安全模块的能力,可以根据公开的信息猜测出不同网站所用的生成参数。 为此,生成参数中需要包含一个相对固定的「代际口令」。这个代际口令,可以类比为普通用户在一段时间内常用于各个网站的通用密码。 由于这个代际口令从来不会被公开在互联网上,只要有足够的长度它基本上是安全的。

口令生成参数在送入安全模块进行HMAC计算之前,需要经过某种SHA算法首先变成散列。这样有几个好处:一是可以避免具体的口令生成参数在传输途中的泄漏,尤其是代际口令本身。 二是可以缩减真正的生成参数的长度,将其限定在可行的范围。一些硬件产品会限制可以用于HMAC计算的输入长度,比如YubiKey只支持64字节,智能卡如BasicCard的命令长度有大约254字节的限制。

2.2 标准化定义前述的规则

综上所述,为了记录一个口令可以通过哪些参数生成,用户可以将如下信息明文记录在某个地方备份:

  1. category - 网站分类代号(用于选择对应这一代号的中间密钥)
  2. domain - 网站域名(或者代称)
  3. username - 网站用户名
  4. hint - 代际口令的提示。这一提示只是告诉使用者应当输入哪个代际口令generation_password
  5. format - 口令格式表记。
2.2.1 口令请求URI

上面这些参数只有1-3是对于生成口令有实际意义的。所有的参数可以写作一种URI的格式,我们约定为:

pwdreq://<username>@<domain>/<category>?format=<format>#[hint]

为了便于记录,我们要求,除了hint之外,这个URI中的所有变量必须使用ASCII中的可打印字符记录。

2.2.2 口令格式表记

关于口令格式表记的约定,我们使用一个正则表达式表示:

([1-9]|[1-9][0-9])U?L?N?S?

这个表达式一开始要求给出口令长度,从1-99个字符都可以。口令包含的字符集由后面跟随的4个字母定义,他们分别代表:

  1. U - Uppercase,大写字母,即A-Z
  2. L - Lowercase,小写字母,即a-z
  3. N - Numbers,数字,即0-9
  4. S - Special,特殊字符,这里选择相对保守的字符集,仅包含 !@#$%^&

当这些字母都没有出现时,默认为L,即全部为小写字母。

2.2.3 从口令请求URI,得出口令导出参数的过程

口令导出参数password_derivation_parameter,是一个使用小写的HEX方式表示的,如下构建的字符串的SHA256散列结果。

password_derivation_parameter = SHA256(
    "\n".join([category, domain, username, generation_password])
).hex().lower()

这一代码中包含了网站分类代号、域名、用户名和代际口令四个因子,每行一个,使用换行符\n分隔。

password_derivation_parameter在之后的处理中要作为64字节长度的二进制数据输入,而不以其原本代表的二进制值作为数据。

2.2.4 从口令导出参数,得出口令的过程

根据category,我们要寻找到对应这个分类的中间密钥intermediate_key,其方式由具体的程序实现确定。 一般而言,intermediate_key=SHA256_HMAC(key=root_key, data=category),其具体存储方式待定,一般是位于一个硬件安全模块上。

我们在安全模块上,使用SHA256-HMAC处理口令导出参数,得到口令种子password_seed

password_seed = SHA256_HMAC(key=intermediate_key, data=password_derivation_parameter)

最后,我们从password_seed得出一个符合format(口令格式表记)要求的密码。为此,我们采用如下算法:

  1. 计算h=SHA256(password_seed)的值
  2. 将结果h按照Python的base85方式编码。
  3. 从上一步的结果中,提取被口令格式表记所规定的字符集包含的字符,作为口令的一部分。
  4. 如果积累的口令长度满足需求,即截断累积的结果到规定长度,作为输出。
  5. 否则,计算h=SHA256(h),然后进入第二步循环。