密钥衍生函数

密钥衍生函数(Key derivation function,KDF)通过接收一个密文,和一个可选的 salt 参数,生成另外的密文,也就是 key。

KDF 目前经常用户密码的存储方案。KDF 与普通的哈希加盐的密码存储方案相比,区别在于 KDF 通常会有一个很多次的迭代过程,尽可能的增加计算的时间消耗,以抵挡暴力破解。目前常用的 KDF 包括 PBKDF2、bcrypt、scrypt。优先选用 scrypt 和 bcrypt,如果没有这两个,PBKDF2 也是可以的。

bcrypt

目前业界最常用的是 bcrypt。常用模式如下:

1
2
3
4
>>> import bcrypt
>>> password = b"super secret password"
>>> bcrypt.hashpw(password, bcrypt.gensalt(10))
'$2b$10$jN0BGbH1W/50V1mW/cLWeewfmp3d0zbnenrlDgii2M6nnFKfM5USC'

生成的 hash 串,开头的 $2b$ 是标准前缀,还有 $2a$$2y$ 等,起到类似于版本号的作用。目前的默认前缀是 $2b$。紧接着的 10 表示 2^10 次循环迭代。接着的 jN0BGbH1W/50V1mW/cLWee 128 bit,经过 base64 编码成 22 个字符,代表的是随机的 salt。随后的 wfmp3d0zbnenrlDgii2M6nnFKfM5USC 184 bit,base64 编码成 31 个字符,是密码的哈希。

之所以不使用常见的 MD5、SHA1、SHA256 之类的 hash 算法是因为常用的 hash 无法对抗暴力破解。在我的笔记本上,一个如上文的代码,当指数是 10 的时候,计算一次 bcrypt 大约花费 80 毫秒,指数是 12 的时候,花费 290 毫秒,指数是 14 的时候,花费 1.2 秒。与此相应的是,一次 md5 计算只花费了 30 微秒。

签名算法

签名算法(Signature)是基于非对称加密的一种消息验证机制。一个数字签名用于证明其消息的可靠性真实性。一个合法的数字签名,可以让接收者有理由相信,这条消息是由一个已知的发送者创建的,并且这个发送者不能否认曾经发送这个消息,并且这个消息在传输过程中没有被篡改过。

常见的应用场景如软件包的签名。软件发布者在发布软件的同时会签署一个数字签名,这样使用软件的人就可以确认这个软件的真实性、可靠性。

签名算法的工作方式十分简单,对于一段需要签名的消息,首先使用某种数字摘要算法,即哈希算法算出摘要,然后使用非对称加密算法的密钥对消息的摘要进行加密。这个加密后的数据就是这段消息的数字签名。当接收方接收到消息之后,首先同样经过哈希算出摘要,然后使用数字签名的非对称加密算法的公钥,解开签名,然后比对两个哈希,即可得知是否是合法的。

实践

常用的签名算法有基于 RSA 系列的 RSA-PCKS#1 v1.5 和 RSA-PSS,和美国联邦标准的 DSA 及其衍生算法 ECDSA。其中 RSA-PCKS#1 v1.5 和 DSA 都比较老旧,不推荐使用。最推荐使用椭圆曲线数学体系的 ECDSA,其次是 RSA-PSS。

数字证书

数字证书是签名算法和非对称加密的一种应用。

在一个通信过程中,为了证明自己的身份,最好的方式是提供自己的数字证书来证明。但是仅仅传输一个证书,还不足以证明,因为在传输过程中,这个证书也可能被中间人篡改、替换。所以需要引入一个第三方的权威机构 CA,来证明自己的身份,就如同现实中的公证处一般。

具体操作过程就是自己将一些证明信息和公钥发送给 CA,CA 经过一定的审核之后,计算出这段信息的数字签名。也就是通过特定的 Hash 算法算出摘要,然后用 CA 的私钥进行加密。这就是一个数字签名。服务端只需要把这个数字签名和自己的证书一起发给客户端。客户端接收到之后,拿到 CA 的公钥,解出信息的摘要,然后自己用相同的 Hash 算法计算摘要,比对两个摘要,如果相同,则说明验证通过。

更加详细的讲解可以参看这篇文章

信息认证码

信息认证码(message authentication code,MAC),是一段比较短的比特数据,用来检查一段信息、消息的可靠性和完整性,也被称为 tag。一个 MAC 算法接收一段任意长度的消息和一个固定长度的密钥 key,产生 tag 标记。一个 MAC 算法也有一个对应的校验算法,通过接收消息、key 和 tag,告诉是否合法。

如果只是需要确定一段特定消息的可靠性和完整性,可以使用签名算法,这个在后面会介绍。目前可以简单的认为签名算法通常用在非对称加密中,这里的 MAC 通常用于对称加密。

如果只需要检验一段消息的完整性,那么使用 CRC 之类的校验码或者 SHA 之类的哈希函数,都可以做到。但是为了保证可靠性,当有人攻击的时候,如果消息被篡改,那么攻击这可以重新计算校验码或者哈希。所以 MAC 类方法都需要有一个别人都不知道的 key。

MAC Encrypt

MAC 和 Encrypt 有三种结合使用的方式。

  1. Encrypt-then-MAC:$ C = E(K_C, P), t = MAC(K_M, C)$。先将原文加密,然后计算密文的 MAC。然后把 C 和 t 一块发送出去。
  2. MAC-then-Encrypt:$ t = MAC(K_M, P), C = E(K_C, P || t)$。先计算原文的 MAC,然后把原文和计算出来的 tag 和在一块,进行加密。最后只需要发送加密后的密文即可。
  3. Encrypt-and-MAC:$ C = E(K_C, P), t = MAC(K_M, P) $ 分别计算密文和 tag,分别把密文和 tag 发送出去。

总得说来,更推荐使用 Encrypt-then-MAC,优点在于收到 C 和 t 之后,可以先检验 t,如果不正确,就不用对 C 进行解密了。IPSec 就使用这个方式。MAC-then-Encrypt 也可以使用,但是收到密文之后,必须先全部解密出来之后,才能校验消息是否完整。TLS 早期使用这种方式。Encrypt-and-MAC 最差,通过 t 就可以暴露出原文的一些特征。同样的原文会得到同样的 t。SSH 使用这种方式。

HMAC

HMAC 是目前最常用,也是目前比较安全的一种 MAC 算法。他底层的哈希函数要求很低,哪怕是目前不是特别安全的 MD5 或者 SHA-0 都可以。HMAC 的工作示意如下图:

hmac-sha1

其中 i_pad 是 0x363636…3636,一个常量,o_pad 是 0x5c5c5c…5c5c,另一个常量。

Authenticated encrption modes

在通常的密码学应用中,保密性用加密实现,消息认证用 MAC 实现。这两种算法的配合方式,引发了很多安全漏洞,上述的 3 种方法 Encrypt-then-MAC、MAC-then-Encrypt 和 Encrypt-and-MAC ,后来发现,后两者都是有安全问题的,所以,2008年起,逐渐提出了用一个算法在内部同时实现 cipher+ MAC 的想法,称为 AEAD(Authenticated encryption with additional data)。 在AEAD这种概念里,cipher + MAC 被一个 AEAD 算法替换。

AES-GCM

目前网络上最常用的 AEAD 类算法就是 AES-GCM。互联网上大部分的 HTTPS 流量都依赖于这个算法。

与 AES-GCM 相似的还有个 Chacha20-Poly1305 算法。但是目前最常用的还是 AES-GCM,因为在 Intel 的 CPU 中有专门优化的硬件指令,所以 AES-GCM 依然比 Chacha20-Poly1305 快很多。但是在移动端,ARM 处理器上,Chacha20-Poly1305 就比 AES-GCM 快了。

Hash 函数

hash 函数就是将任意长度的一个字符串映射到一个固定长度字符串的过程,也叫摘要。

密码学中的 hash 函数可以用来构建安全的消息认证算法、签名算法和随机数生成算法。对于密码学 hash 算法,我们希望以下三个事情是很难做的:

  1. 修改一个消息却不改变哈希值;
  2. 对于一个给定的哈希值来构造一个消息;
  3. 找到两个具有相同哈希值的消息;

对于第一个性质,通常就是所谓的雪崩效应。哪怕仅仅修改了原消息的一个 bit,也会导致最后的哈希摘要产生巨大的变化。

第二个特性就是单向性。通过消息算出哈希非常容易,但是根据哈希找出原摘要十分的困难。

第三个特性就要求哈希函数耐碰撞。

常见的 Hash 函数包括 MD5、SHA-1、SHA-2 和 SHA-3。MD5 和 SHA-1 最常见,但是已经不是那么的安全了。推荐使用 SHA-2。

密码存储

通常业务是不会直接把密码存储到数据库中的。一旦数据库泄露,所有用户的密码就会泄露。通常都会把用户的密码进行 Hash 之后再存储到数据库中。校验用户密码的时候只要比对 Hash 值是否相同即可。

但是对于仅仅将密码进行 MD5 或者 SHA-1 的一次 Hash 之后存储也是不安全的。尽管攻击者不能根据哈希值算出原密码,但是攻击者可以构造一个叫做彩虹表的结构。彩虹表中存储了大量常用字符串和其 Hash 的映射关系。那么就可以根据 Hash 值反向查找到原密码了。

对于彩虹表,通常采用一种叫加盐的方式。对于用户的密码,进行 Hash 之前先将其和一个随机字符串结合之后再 Hash。如果盐使用不当的话,依然无法保证安全性。比如全局使用一个固定的盐,那么彩虹表只需要根据这个固定的盐再重新算一次就好了。盐一定要保证每个密码都是随机的。