浅谈密码存储简史
序言
在生活中,密码几乎无处不在。小到你的洛谷账号,大到你家里的银行卡,都离不开密码的保护。而信息技术发展初期,密码还只是小巧玲珑的几位数字或是图案。如今的密码却复杂得无法想象。
很多人觉得,这样增加了记忆的难度。但在信息科技工作者的眼中,如何保护好你的密码,也是一门不小的学问。在这篇文章中,你将看到密码存储的简史,看见人们同邪恶黑客斗争的足迹,体会信息科技的迅猛发展。
1. 明文存储
设想一下,如果你开了一个网站,有很多人前来注册。那么,你就需要把他们的密码存储到数据库里。
如果一个用户的密码是
像这样,将密码原封不动直接存入数据库的方式,叫做明文存储。
很显然,这种存储方式存在很大的安全隐患。网站的工作人员,以及某些邪恶黑客等不法分子,将直接知道这个账号的密码,从而利用其实施不法行径。
设想一个情境:
假如你的各种账号,包括 Youtube、INS、CSDN 等,密码全都是
但这还不够。某些不法分子还会用这个密码尝试登录其他平台,企图窃取你的所有信息。这样试图登陆其他平台窃取信息的攻击方式叫撞库攻击。也就是说,你的所有小秘密将一览无余。更可怕的是,不法分子还可以尝试登录你的银行账号,并将你的钱财洗劫一空。
总之,不论是明文存储,还是一套密码走天下,安全隐患都很巨大,势必将遭到 OIer 们的一致抵制。抵制明文存储,从你我做起。
2. 对称性加密
既然明文存储不可行,那么我们就要想办法给密码加密。早期一些网站加密的方式是对称性加密。为了引入这个主题,我们先从凯撒说起。
这位凯撒大帝将文件内容中的每一个字母向前(或向后)移动固定位数。如
也就是说,只要知道这个密钥,我们就能反推出原来的密码。同理,我们如果有一个密码字符串
// 以下 s 为密码字符串,n 为密钥
string caesar(string s, int n) { // 加密函数
string res;
for (int i = 0; i < s.length(); i++)
res += s[i] + n; // 前移/后移 n 个 ASCII 码位
return res;
}
string decode(string s, int n) { // 解密函数
string res;
for (int i = 0; i < s.length(); i++)
res += s[i] - n; // 通过密钥反推原来密码
return res;
}
/*
在上面的代码中,caeser(kasumi714, 1) 将返回 lbtvnj825,
而 decode(lbtvnj825, 1) 将解密回原来的 kasumi714。
*/
如果这个用户输入了密码,那么网站将把存储在里面的密文,通过密钥反推出明文,再与用户输入的内容比较。
像这样,通过一个密钥,将密码加密,而又用这个密钥将密文解密回明文的加密方式,叫做对称性加密。
通过这种方式衍生出的加密算法,有 DES、3DES(三重 DES)、AES 等。
但是,这样的加密方式仍然存在着缺陷。
加密、解密的过程中,密钥的维护可谓是重中之重。如果密钥被泄露出去,那这样就同明文存储没什么区别了。因此密钥通常由网站请求取得,且密钥也会被同步加密。
而且,你不可能让所有人公用一个密钥。这同样会造成惨重的密码泄露事件。于是你决定,每人生成一个随机密钥。但这样又大大增加了维护许多不同密钥的成本。
人类,是时候需要更好的加密方式了。
3. 非对称性加密
对称性加密的密钥,在加密解密过程中是恒定不变的,如果加密过程与解密过程的密钥不一样,那么这样的加密算法就是非对称加密。
其中,加密过程使用的密钥是公钥,任何人都能用公钥将密码加密。而解密过程使用的密钥是私钥。只有使用私钥才能将密码解密。
举一个例子:
假定你要加密一段文字,先随机找出两个素数
假定
这个用户将
第一步:设密码
第二步:设密文数组中第
需要注意的是,这一特性只有在
以该密码为例,进行第一步之后,密码串变为:
这时再进行解密,就能变回原来的密码。
可以用代码这样实现:
ull ksm(ull a, ull b, ull m) { // 使用快速幂函数
a %= m;
ull res = 1;
while (b > 0) {
if (b&1) res = res * a % m;
a = a * a % m;
b >>= 1;
}
return res;
}
vector <ull> en; // 密文库
vector <ull> de; // 解密后的明文库
const ull pub = 59, prv = 179, mod = 187;
// 这里公钥 = 59,私钥 = 179,公共模数 = 187
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
string s;
cin >> s;
for (int i = 0; i < s.length(); i++)
en.push_back(ksm((ull)s[i], pub, mod)); // encrypt 加密
for (int i = 0; i < en.size(); i++)
de.push_back(ksm(en[i], prv, mod)); // decrypt 解密
for (int i = 0; i < de.size(); i++)
cout << (char)de[i] << ",";
return 0;
}
由此衍生出的算法有 RSA、ECC 等。在数字签名等领域也有着广泛应用。但是,RSA 在实际应用时,
然而,非对称性加密也存在着致命弱点。
首先,非对称性加密仍然要加强对私钥的维护。其一,私钥不能由公钥推导出来。也就是说,私钥与公钥之间没有关联性。其二,如果私钥被泄露,那么密码泄露惨案仍会发生。
随着超级计算机技术的发展以及算力的不断提升,一些大数得以被分解素因数。如果私钥被超级计算机破解,那么,这将对 RSA 等算法造成毁灭性的打击。
有没有一种算法,能彻底抛弃密钥的束缚呢?
4. 哈希算法加密
4.1 引入哈希
上文提到的加密算法经历了加密和解密两个过程。但是,解密似乎是多余的存在。如果一个密码字符串经过许多次处理,变成了谁都不认识的神秘字符串,且这种字符串是基本唯一的。那就不需要再解密了。
于是,哈希算法加密诞生了。它通过巧妙的算法,将明文变为一串神秘的字符。并且这样的加密过程单向不可逆。你几乎不能再从这串神秘字符反推回原来的明文。
对此,我们首先要复习一下字符串哈希。
下面的情景出自洛谷 P3370 【模板】字符串哈希。
假设你有一个字符串集合
为了简便实现字符串查重,我们需要把字符串变成数字。常用的哈希方式是进制哈希,它通过一个进制数
这里的模数
通过哈希值,我们就能类比去重数字的方法间接去重字符串。它可以通过代码这样实现:
const ull b = 13331; // 这里模数比较大,防止哈希冲突
ull f(string s) { // 计算哈希值
ull sum = 0;
for (int i = 0; i < s.size(); i++)
sum = sum * b + s[i]; // 这里不模 2^64 的原因是此处 ull 会自然溢出,达到取模的效果
return sum;
}
ull a[10001];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
int n, cnt=1; cin >> n;
string s;
for (int i = 1; i <= n; i++) {
cin >> s;
a[i] = f(s); // 此处计算哈希函数,将字符串变为数字
}
sort(a+1,a+n+1); // 排序,之后如果某一数字与它前面的一个不相等,则cnt加1
for (int i = 1; i <= n-1; i++) {
if (a[i] != a[i+1])
cnt++;
}
cout << cnt;
return 0;
}
这便是哈希算法的简单应用。
由此衍生出的哈希加密算法,有 MD5、SHA-256、Bcrypt 等。
但两个不同字符串的哈希值可能相同,这就发生了哈希冲突。哈希冲突无法避免,但可以防御。防御哈希冲突的方法有很多种。这里不再赘述。
密码哈希为了防御冲突,进制数
0x63e24625127e67c69f2d78224533fbc852df213f6f139d6d9cb0007c7a2ff6ee
这时就可以把这个值存进数据库中。当用户输入密码时,将用户输入的密码哈希加密,再用数据库中的哈希值比较即可。
而且这样复杂的字符串连黑客都看不懂,即使知道算法是什么,也没有能力反推出原始密码了。这便是哈希加密算法的抗原像性。同时,当你给出一个字符串,以及它经过算法处理后得到的哈希值,你很难推出另一种输入,使得这种输入得到的哈希值与给定字符串相同。这是哈希加密算法的抗次原像性。
另一方面,由于哈希加密算法具有很强的抗碰撞性,使得你很难找出一对哈希值一样的字符串。
但是,黑客们偷密码的方式多种多样,既然你用哈希算法加密,我就用你的哈希算法打一个大表,用这个表去猜密码。这样攻击的方式叫做彩虹表攻击。彩虹表中设置的密码。首先就是简单的
同时,一些哈希算法有被破解的风险。当黑客在
4.2 哈希算法的强化
为了强化哈希算法,我们通常在原密码后面加一些“盐”。这个“盐”指的是随机字符串。如果把
0x572fffb7ad5904ca7a780d56d15f1235d1514dcbb0ff41e1e81e1990b9c85205
这时,我们再把这个值和加进去的“盐”一同存进数据库中。当用户输入密码时,将“盐”加在用户输入的密码后面,再重复 4.1 中的操作即可。
同时,也可以在哈希函数中加一个成本参数 cost,延长哈希加密的时间。对我们的影响是:原先等待
5. 密码安全与防范
密码安全仍然是人类面对的重要课题之一。一方面,人类正在不断加强加密算法;另一方面,黑客也在为偷取密码不择手段。黑客除了偷密码,还有更多手段。比如:
- 买通数据库管理人员骗取密码;
- 在客户端加装按键侦测器骗取密码;
- 使用程序暴力枚举破解密码;
- 使用钓鱼网站骗走密码;
- 通过爬虫爬走密码;
- 站在你身后偷窥密码(不要认为这很荒诞);
- ……
所以,为了保障你的密码足够安全,请谨记:
作为客户端,
- 请不要使用结构简单的字符串(如简单数字、字母、你的生日等)作为你登录网站的密码。
- 请不要在多个网站使用同一套密码。
- 尽量不要将密码直接存储在浏览器上,特别注意不要存储在其他电脑上。
- 尽量凭你的记忆力输入,不要在公共场合翻看密码本。
- 尽量不要使用网吧里的电脑输入密码,因为它们可能加装键盘侦测器。
- 尽量使用第三方账号登录网站。
作为服务端,
- 请不要使用安全隐患较大的存储方式(如明文存储、对称性加密、MD5、SHA-1 等)。
- 请尽量开发二次验证方式(如手机验证码、真人检测等)。
- 请尽量开发第三方账号登录方式。
- 请加强网站的防御攻击功能。
如果你担心你的密码遭到了泄露,你可以前往 Have I been pwned? 网站,输入你的电子邮箱进行验证。同时,该网站的“Who's been pwned”板块中,收录了许多密码泄露事件,并作了简要介绍。
例如:
-
2012 年 7 月,雅虎的在线发布服务“Voices”因 SQL 注入攻击而受到损害。此次泄露导致明文存储的近 50 万个用户名和密码被泄露。此次泄露表明,在受感染的账号中,同样拥有索尼泄露账号的人中有 59% 的人在这两种服务中重复使用了他们的密码。
-
2016 年底,优酷发生了数据泄露事件,9200 万个账号的 MD5 密码哈希值被泄露。
结语
密码学是一个古老的技术科学,在古代就被应用起来。而今,随着计算机技术的发展,密码学在计算机领域中的地位不断提高,在生活中的应用也更加广泛。
当你看完密码存储发展的历程之后,你将会想到,曾经有许多信息学工作者继往开来,为你日常生活中密码的安全性增添一份保障。同时,你也要知晓,保护好密码也是我们的共同责任。
未来,密码学将继续努力,用他自己的方式同黑客作斗争,为计算机事业的发展保驾护航。