第三代树洞中,我们采用了全新的加密体系。
我们将通过用户设定的密码来加密用户的个人信息,通过非对称密钥及Shamir密钥分发算法,实现了如下关键特性:
- 服务器如果发生数据泄漏,将不会泄露发帖者个人信息。也就是说,即便网络黑客通过攻击手段获取了服务器中的全部数据,也无法将用户的发帖与邮箱对应。
- 从密码学上要求一定数量的"密钥保管员"同意才可以解密用户个人信息。
- 用户的密码明文不上传服务器,不会离开用户的设备。
本文档中,首先介绍了一些基础的密码学知识,然后详述了第三代树洞的加密体系,最后讨论了一些实现细节。
在对称加密算法中,加密和解密使用的是同一把钥匙,即:使用相同的密码进行加密和解密;
加密过程如下:
加密:原文 + 密匙 = 密文
解密:密文 - 密匙 = 原文
一个最经典的对称加密算法叫凯撒算法,据说是凯撒发明并用来传递军令的。移位密码非常简单,就是对明文做相同的偏移并取模,即E(x)=x+b(mod m)。
选择密钥k,0<k<26
k=23
E(x)=x+23(mod 26)
Plain: ABCDEFGHIJKLMNOPQRSTUVWXYZ
Cipher: XYZABCDEFGHIJKLMNOPQRSTUVW
Plaintext: THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG
Ciphertext: QEB NRFZH YOLTK CLU GRJMP LSBO QEB IXWV ALD
由于移位密码过于简单,现实中很少使用。
现代对称加密通常选择更复杂的AES算法进行加密,更详细的科普可见https://www.jianshu.com/p/3840b344b27c。
明白了对称加密后,我们来了解一下什么是非对称加密。我们知道,对称加密是使用的同一把密匙进行加密和解密。那么,非对称加密自然是使用不同的密钥进行加密和解密啦。
非对称加密有两个钥匙,及公钥(Public Key)和私钥(Private Key)。公钥和私钥是成对的存在,如果对原文使用公钥加密,则只能使用对应的私钥才能解密;因为加密和解密使用的不是同一把密钥,所以这种算法称之为非对称加密算法。
通过私钥经过一系列算法是可以推导出公钥的,也就是说,公钥是基于私钥而存在的。但是无法通过公钥反向推倒出私钥,这个过程的单向的。
根据上图,我们可以看到,对于原文,通过接收方的公钥进行加密,发送给接收方,接收方拿到密文后,通过自己的私钥可以解密,获取原文信息。在这个过程中,即使接收方公钥泄漏,也不会导致消息泄漏,因为密文只能通过接收方的私钥才能打开。所以,信息安全过程中,接收方只需要保管好自己的私钥不泄露即可。
非对称加密的实现方法都有比较复杂的数学。最著名的非对称加密算法就是RSA了,更详细的科普可见Wikipedia。
单向加密又称为不可逆加密算法,其密文是由加密散列函数(Hash function)生成的。相同的明文产生相同的密文,不同的明文(以几乎100%的概率)产生不同的密文。没有算法直接从密文就能获取明文,只能通过暴力破解(也就是一个个试)的方式。著名的散列函数有MD5, SHA等。
在老版本的树洞加密方式中,我们直接对用户的注册邮箱进行单向加密后就存储了。由于用户的邮箱明文有一定的规律,有些时候只需要尝试较少次数(百万量级)就可以破解出邮箱明文,这个问题在老版本树洞中广为诟病。
在新版树洞加密体系中,我们采取了完全不同的加密策略,解决了暴力破解的问题,详见下文。
从管理学上,如果一个可以解密全部数据的核心密钥掌握在一个人手中,会是非常危险的事情。这就是Shamir密钥分享(Shamir's Secret Sharing,SSS)解决的问题。顺便一提,Shamir就是RSA中的那个S。
Shamir算法完成的事情可以用一个例子来讲述:
自从阿里巴巴和四十大盗的故事流传出来后,后世的强盗们考虑加强藏宝洞的安保措施,他们找到了一家安保公司,提出这样一个要求:n个强盗每人都有一把钥匙,任意k个强盗聚在一起都可以打开藏宝洞的锁,k-1或更少的钥匙都无法开启。
这个要求其实很巧妙,n个人最坏情况即使丢失了n-k把钥匙,依旧能够打开锁;而少于k个人聚在一起或者密钥被窃取,也不会造成财产损失。它既能提供冗余性,也能防共谋。
Shamir算法巧妙地构造了一个k-1次随机多项式,将核心密钥编码到多项式的常数项,在这个k-1次曲面上随机选择n个点分发给n个参与者即完成了密钥分发的过程。更详细的科普可见Wikipedia。
第三代树洞的加密体系用到了全部以上三种算法。具体的做法如下:
- 设密钥保管员数量是n个,每个密钥保管员会在自己的永久离线的保密设备中生成1个非对称密钥对,命名为
private_key_1, public_key_1, private_key_2, ..., private_key_n, public_key_n
,然后将public_key_1, ..., public_key_n
保存到服务器上。 - 用户的客户端会向服务器上传用户的邮箱(email)、密码哈希值(password_hash,等于sha256(sha256(password))),也就是说用户的密码明文从来不会离开用户的设备。
- 服务器上有关用户邮箱的部分只存储
email_encrypted = AESEncrypt(email, password_hash)
。- 这样用户登录时,后端可以通过对比
email_encrypted
和服务器中的数据是否相同,以验证用户的密码。 - 如果需要变更密码,对比
email_encrypted
然后改为新的email_encrypted
即可,然后执行步骤4-6。
- 这样用户登录时,后端可以通过对比
- 在验证完用户邮箱后,服务器的内存和数据库中会删除所有的用户邮箱明文。
- 对于每个用户的
password_hash
,我们使用Shamir算法生成n个密钥切片key_share_1, ..., key_share_n
,大于等于k个切片才可以还原出password_hash
。生成后,服务器的内存中就删除password_hash
的全部存在。 - 在服务器上我们通过非对称加密得到
encrypted_key_share_i = RSAEncrypt(key_share_i, public_key_1), i = 1, 2, ..., n
,然后服务器的内存中就删除key_share_i, i = 1, 2, ..., n
的全部存在。服务器中会将encrypted_key_share_i, i = 1, 2, ..., n
存储到数据库中。
总结来说,对于每一个用户,与用户注册邮箱相关的内容,服务器只存储 email_encrypted
, encrypted_key_share_i, i = 1, 2, ..., n
。
我们需要单独维护一列已注册邮箱的哈希值列表,这个列表中只记录邮箱哈希值,不与用户信息和用户发帖关联。
由于服务器上的用户邮箱被用户密码加密了起来,从邮箱解密对应的用户需要k个密钥保管员交出私钥遍历全部数据库,这会让用户找回密码变得困难。
一个可行的解决方案是,对每一个用户生成一个随机字符串forget_pw_nonce
,在用户注册成功后发送到用户邮箱,并提醒用户不要删除。这会引入新的安全风险,即: 如果用户的邮件系统和服务器中的数据同时被泄露则会导致发帖与用户邮箱的关联。这是安全性与便捷性的一个取舍。
- 对于一个特定用户,从数据库中读取出
encrypted_key_share_i, i = 1, 2, ..., n
,email_encrypted
。此后的步骤都可以在线下离线完成。 - n个密钥保管员中的k个如果同意,就从
encrypted_key_share_i
中解密出key_share_i
- 从k个
key_share_i
中通过Shamir算法解密可以得到password_hash
- 由于
email_encrypted = AESEncrypt(email, password_hash)
,解密就可以计算出email = AESDecrypt(email_encrypted, password_hash)
- 这样就获得了
email
因为从encrypted_key_share_i
中解密出key_share_i
的过程需要private_key_i
,而private_key_i
只存在于密钥保管员的永久离线设备上。