如何生成一个足够安全又容易记住的密码?
本文提到的计算方法已由随机密码生成器 & 密码强度计算器实现。一些基本的密码学概念可以参考密码学概念科普(加密算法、数字签名、散列函数、HMAC)。
什么是密码强度?
密码强度是指破解一个密码需要尝试的次数,其本质是密码所包含信息熵的大小。所以通常不是使用十进制数字来表示,而是用这个数字以 2 为底的对数来表示,也可以理解为把这个数字转换成二进制的位数。
例如使用 6 位数字作为密码,暴力破解最多需要尝试 10 ^ 6
次。他的密码强度就是 log2(10 ^ 6) = 23.25
。
AES-128/192/256 的密钥长度分别是 128/192/256 位,因为破解 AES 算法除了把每种可能都尝试一遍之外没有更好的办法了,需要尝试的次数分别是 2 ^ 128
/2 ^ 192
/2 ^ 256
次,所以他们的密码强度就是 128/192/256 位。
因为密码强度是使用二进制位数来表示的,所以密码强度每增加一位,意味着二进制数增加了一位,其破解的工作量就要翻倍了。
破解一个密码并不需要尝试到最后一种可能,平均来说,应该尝试一半的密码就可以了。所以实际破解密码所需的平均工作量,是比密码强度少一位的。
密码强度就是破解难度吗?
银行卡密码只有 6 位数字,而网站的密码通常至少要有 8 位的字母、数字和符号。不用计算都知道前者密码强度一定比后者低很多,但不会有人觉得银行卡密码不安全。
这是因为密码强度只表示了要破解一个密码需要尝试的次数,还有一个重要的因素是每次尝试所需的时间。两者相乘才是预计破解一个密码所需的时间。
银行卡密码虽然只有 6 位数字,但通常每天输错三次就锁定了。假设银行没有别的风控手段,每天都可以试三次,那么 6 位数字的密码平均需要 10 ^ 6 / 2 / 3 / 365 = 456
年才试出来。等到这时候不知道银行还在不在,反正我是不在了。
所以可以看出,像这种在线系统,更多是在尝试时间上做文章,也就是系统本身的安全策略比密码强度更重要。大多数网站也是这样,可以进行控制的手段有很多,例如输错多少次就要锁定一段时间,甚至每次锁定的时间成指数变长;通过图形验证码防止机器暴力破解,来降低尝试的速度;结合短信验证码等其他验证手段等。
除了在线系统外,还有一些离线的数据。例如一个加密的压缩文件,或者 OpenPGP、SSH 等各种密钥。这些数据可以被人截取之后离线攻击,攻击者可不会每天试三个密码就停下来了。这时就需要通过一些算法来延缓攻击者尝试密码的速度了,这就轮到我们的 KDF(密钥派生函数) 登场了。
理论上我们也可以开发出一个算法,你输入一个密码,他要计算 8 个小时才能给出结果,同样可以达到一天只能试三个密码的效果,这样按照上面的计算,同样使用 6 为数字的密码就可以保护四百多年了。但这里存在几个问题:
第一是算法可不分人,攻击者试一个密码要 8 小时,你也一样,你能忍受输入一个密码睡一觉起来才能看到结果吗?
第二个问题是芯片的性能是不断增长的。银行可以保证一百年后也一天只让你试三个密码(如果那时候还有密码的话)。但根据摩尔定律,今天需要计算 8 小时的算法可能 18 个月后就只需要 4 个小时了。
第三个问题是,KDF 是有上限的。KDF 是基于你输入的密码派生出那个真正用来加密的密钥。如果它真的慢到了一定程度,那攻击者不如直接去尝试派生出来的密钥算了。
所以 KDF 只能把每次尝试密码延缓到一个合理的范围,剩下的就要靠密码强度了。
我们需要多强的密码?
没有永远安全的密码,随着新技术的诞生、算力的提升,无论多强的密码都会被破解。所以要想知道需要多强的密码,首先需要想一下你希望密码在能在多长时间内不被人破解?假设这个值是 y
年。
刚才提到破解一个密码所需的时间还和尝试一个密码所需的时间相关,这通常可以估算或者实际测试出来。例如 KeePass 可以使用 Argon2d 算法,根据你的设备设置参数,使尝试一个密码所需的时间大概为你所设置的时间。假设这个值是 t
秒。
根据这些信息,我们能算出来在这些年内一共可以尝试 (365 * 24 * 60 * 60) / t * y
个密码。根据密码强度的定义,所需的密码强度为:
1 | Math.log2((365 * 24 * 60 * 60) / t * y) + 1 |
在某些场景下,这个公式就够了。但很多时候并不是这样的,因为这个公式没有考虑:算力是不断提升的;攻击者可以离线破解。
随着算力的提升,每年能尝试的密码次数也将不断提升。假设算力每年提升 n
倍,那么每年能尝试的密码个数将是一个等比数列,在 y
年之内一共能尝试的密码次数就是一个等比数列求和问题了:(365 * 24 * 60 * 60) / t * (1 - n ^ y) / (1 - n)
。
算力通常都是用多少个月翻一番来表示,假设 m
个月翻一番,则 n ^ (m / 12) = 2
,所以 n = 2 ^ (12 / m)
。
在你的设备上需要 1 秒钟做到的事情,在攻击者的设备上可不是。攻击者的设备通常性能会更好,甚至可能是专用的硬件。我们可以用一个倍数来表示,假设攻击者设备的性能是我们的 x
倍,那么这些年能尝试的个数也将是 x
倍:(365 * 24 * 60 * 60) / t * (1 - n ^ y) / (1 - n) * x
。
这个倍数更多是一个成本的问题,也就是你认为攻击者愿意花多大代价来破解你的密码。可以这样假设,你认为攻击者愿意每年花一百万来破解你的密码,而你当前的设备价值一万元,那么可以简单的认为攻击者的算力就是你的一百倍。这样估算很粗暴,但对最终的结果不会产生太大的影响,或者你可以料敌从宽,直接把这个数字估计成一万倍。我觉得如果攻击者有一百万,与其花在破解我的密码上,不如去挖矿更好一点。或者可以试一下每年花一百万来问我,我可能一秒就可以告诉他。
注意这个成本是每年,因为我们的公式还考虑到了算力是每年提升的,而攻击者如果想利用算力每年提升的成果,就需要每年更新设备才行。
如果攻击者不能离线攻击的话,只用第一个公式就够了。因为不能离线攻击,意味着每次密码的验证都要在你的设备上发生,那么无论攻击者的设备有多强都用不上了。而且不能离线攻击,只要你不换设备,摩尔定律对你就不生效。
最终的公式大概是这样:
1 | n = 2 ** (12 / m) |
例如使用如下参数计算可以得到大概需要 63.29 位的密码强度。
1 | t = 0.1, y = 30, m = 18, x = 10000 |
如何生成一个这样的密码?
如果只使用 26 个字母作为密码,4 位的密码强度大概是 log2(26 ^ 4) = 18.80
位。那么如果我使用 mima
作为密码,他的强度是否有 18.80 位呢?
我们在最开始时就提到了,密码强度的本质是信息熵。4 位的密码之所以有 18.80
位的强度,是因为他有 26 ^ 4
种可能,攻击者平均需要做 26 ^ 4 / 2
次尝试。如果你使用的是汉字拼音作为密码,而攻击者也知道了这一点,他就只会使用汉字拼音来尝试,两个拼音总共有 413 ^ 2
种可能,所以这个密码的实际强度为 log2(413 ^ 2) = 17.38
。如果更进一步他知道你只使用了包含了两个字母的拼音,那么密码强度就只剩下 log2(79 ^ 2) = 12.61
了。
密码学有个原则叫柯克霍夫原则,是说你必须假设攻击者对你的系统完全了解,只是不知道你的密钥。所以你看我们常用的加解密算法全都是公开的、广为流传的、久经考验的,这根本就不是秘密,保密的只有密钥或者非对称加密的私钥。你不能说靠脑洞,攻击者肯定不知道我用的是拼音、双拼、五笔编码、古诗词、名字、生日、地名等等。攻击者可能就是会收集和你相关的信息,把它们做成字典来破解你的密码,这很容易做到。
所以计算一个密码的强度,不能单纯的看他所包含的字符数量。更合理的是应该看你用于组成密码的最小单元(我们把他叫做元素吧)的数量。
例如你使用 5 个汉字的拼音来组成密码,那么它的密码强度就是 log2(413 ^ 5) = 43.45
。无论这五个拼音包含的字符数是 10 个还是 30 个,它的强度都是 43.45 位。每个元素所能提供的密码强度为 log2(413) = 8.69
。
另外要保证这个密码的强度真的是 43.45,还有个因素很重要,就是你选取的元素必须是足够随机的。如果你不愿意记太长的密码,只选或者倾向于选两个字母的,那你的密码长度还是只有或略高于 log2(79 ^ 5) = 31.52
。
如果交给人主观去想,那么生成的密码肯定是不够随机的。所以有一种专门用于生成密码的方法叫 Diceware。大概步骤是提前准备好了一个包含了 6 ^ 5
个单词的列表,给每个单词编个号。然后扔五次骰子得到一个编号,去表里找到这个编号对应的单词。重复这个过程,找到多个单词组成一个密码。每个单词大概可以提供 log2(6 ^ 5) = 12.92
位的强度,六个单词大概是 77.52。
我们可以对比一下容易想到的可以用来生成密码的各种元素集合:
元素集合 | 包含元素个数 | 每个元素能提供的熵 | 每个元素平均包含的字符数 | 平均每个字符提供的熵 |
---|---|---|---|---|
Diceware 单词列表 | 7776 | 12.92 | 4.24 | 3.04 |
拼音 | 413 | 8.69 | 3.24 | 2.68 |
双拼 | 412 | 8.69 | 2 | 4.34 |
五笔 | 6763 | 12.72 | 4 | 3.18 |
五笔二级简码 | 589 | 9.20 | 2 | 4.60 |
小写字母 | 26 | 4.70 | 1 | 4.70 |
ASCII 可打印字符 | 95 | 6.57 | 1 | 6.57 |
可以看到集合中元素的个数越多,每个元素能提供的熵就越高。那么生成同等强度的密码所需的元素个数就越少,也就更好记。
例如 Diceware 中每个元素的熵是小写字母的 2.7 倍。也就是同样生成 60 位强度的密码,使用 Diceware 只需要 5 个单词,而使用小写字母则需要 13 个字母。显然 5 个单词比 13 个字母更容易记住。
但也不一定元素越多越好,因为密码强度每多一位,所需的元素的个数将会翻倍。想要提供 13 位的密码强度还只需要 8192 个元素,给每个元素安排一个长度适中又容易记忆的编码还有可能,Diceware 就接近这个数量。但 14 位的密码强度就需要 16384 个元素了,15 位则达到了 32768。给这么多元素每个都分配一个不重复又容易记忆的编码就很困难了,带来的熵增又不明显,性价比就很低了。很可能把元素都记住了,但忘了这个元素该怎么映射到键盘上的按键了。
还可以看到每个字符所能提供的熵越高,最终生成的密码所包含的字符数就越少,也就越容易输入。
例如五笔二级简码中每个字符提供的熵可以达到 4.60,已经是 Diceware 的 1.5 倍了。也就是用同样生成 92 位强度的密码,使用五笔二级简码只需要 20 个字符,而 Diceware 则需要 30 个字符。
但这同样是有上限的,如果你的元素中只用到了小写字母,那么每个字符能提供的上限也不会超过 26 种,也就是 4.7 位。五笔二级简码已经接近这个了,说明把键盘上两个按键的组合已经利用的很充分了。就算元素中用到了全部 ASCII 可打印字符,那么每个字符字符能提供的熵的极限也就是 6.57 位了。这样生成的密码可以更短,但不一定更高效,因为很多符号都需要用过 Shift 组合来输入,所以总的按键次数不一定会更少。
你也可以随意收集任意其他适合你的元素来作为生成密码的材料,或者在已有的集合的基础上做一些增删。在真正使用之前,最好计算下单位元素和单位字符所能提供的熵,评估一下这个方案的“性价比”。
例如你可以给每句古诗分配一个编码,但要注意这个编码不能重复,重复的编码应视为同一个。古诗的句子至少应该有上万个吧,假设有一万个。我们假设每个句子的拼音首字母都不重复,那么就可以用这个来作为编码,假设每句古诗有 5 个字,那么长度就是 5 个字符。这样每个元素的熵就是 log2(10000) = 13.29
,每个字符的熵是 13.29 / 5 = 2.66
,看起来好像还不如 Diceware。
结合这个表格,选一个适合自己的元素集合。然后根据前面计算出需要的密码强度,就可以知道需要选择多少个元素来组成密码了。接下来就是扔骰子或者使用随机数等方式,选出这么多个元素就可以了。
这些方案都已经在随机密码生成器&密码强度计算器中预置,可以直接使用。