对称加密算法和 AEAD
对称加密
对称加密算法就是加密和解密都使用相同的密钥的算法。它的加解密流程图采用的是下面的形式来完成的:
明文 --> 密钥 --> 密文
解密过程就是:
密文 --> 密钥 --> 明文
当明文完成加密过程之后就会形成密文。根据使用场景的不同,密文可以发送给其它处理对象,或者是保存到存储介质中,当需要使用的时候,通过解密步骤就可以得到明文。
我们所熟悉的 DES、3DES、AES 都是对称加密算法,相信稍微有点计算机方面知识和经验的同学都知道在选择对称加密算法的时候,首选就是 AES 算法。一是 AES 是所有程序开发语言中都会包含在标准库中的算法,同时,也是这些算法中推出时间靠后的密码算法了。
DES 出现在 1974 年,AES 的出现在 2001 年。
如果没有什么特殊情况的话,在软件领域,往往推出时间越靠后就代表越安全,比如软件的版本,后推出的版本就有可能会解决前期版本中的安全性问题。在密码算法也大致如此。但这并不是绝对的,不过 AES 确实目前还是安全的、也是首选的对称密码算法。
但选择了 AES 作为加解密算法之后,你还将面临一个难题,如何在实际的生产环境中安全的运用它。作为对称加密算法,它属于块(block)密码,所以你还要考虑以下的问题:
- 加密模式怎么选?
- 加密模式使用的 IV 或 nonce 怎么选?
当然这些最基本的内容我在这里不展开了,以后再补写点基础知识。这就是 AES 一次只能处理 128 比特即 16 字节的数据,而需要处理信息有 1024 字节这么大,怎么处理?
然后需要考虑的问题是对密文进行认证。也就是对明文使用完对称加密算法后会得到密文,在你不确定生成的密文最终处于什么样的保护情况,对密文进行认证是一个非常值得考虑的做法。
什么是保护情况呢?举个例子,比方说你有套别墅、有辆几百万的保时捷,每天回家后将车停在自家别墅的车库里,锁不锁车无所谓,锁匙放不放车上也无所谓,因为这个时候环境是安全的。而你开车上班,把车停在了办公楼下的地下车库,这时安全的做法就是锁车拿走锁匙。
所以,我们不能只用到对称加密算法就行了,还需要用到签名算法,对密文添加完整性和认证的检测能力,这就是 Authenticated Encryption,简称 AE。
Authenticated Encryption
那这个对密文签名需要怎么做?虽然大至有几种方法,但最常用的作法就是对明文加密完成之后的密文运用 MAC (消息认证码)算法进行签名,然后将密文+签名返回。
后面进一步的发展,我们可以添加一些明文的信息到密文中去,这些明文的信息不需要加密,但也需要保证它们的完整性和认证能力,这就是 Authenticated Encryption with Associated Data,简称 AEAD(关联数据的认证加密)。
上面的这些知识又需要学习大量的密码学相关理论了,但有了 Tink,上面这些问题你就不用考虑了,Tink 能帮你搞定。
Tink 的 AEAD primitives 就是为这个目标而诞生的,让你不用和这些低层的加密算法 API 打交道也能很好的处理密码学相关的问题。
Tink 将对称加密算法包装成了 AEAD primitives,是一种对称加密的标准实践,也是目前安全的实践。
AEAD 实现了对密文的机密性、完整性和认证的功能,能有效的防止针对密文的攻击。比如说选择密文攻击,攻击者通过修改密文后,提交给加密算法,来观察加密算法的反应,从而通过精心构建的密文破解密钥。
你可以这样想 AE:
- DES、3DES、AES 提供了加密功能;
- MAC 实现了完整性和认证的功能。
而 AEAD 则是在上面 AE 的基础上,让你可以加入一些额外的认证数据,它是 AE 的另一种用法。
我们来看一下具体使用 Tink AEAD primitives 的方法。
AEAD 官方示例
准备及运行
目前官方给出的 AEAD 示例代码如下,创建一个 main.go 的文件,然后运行看看结果:
package main
import (
"fmt"
"log"
"github.com/google/tink/go/aead"
"github.com/google/tink/go/keyset"
)
func main() {
kh, err := keyset.NewHandle(aead.AES256GCMKeyTemplate())
if err != nil {
log.Fatal(err)
}
a, err := aead.New(kh)
if err != nil {
log.Fatal(err)
}
ct, err := a.Encrypt([]byte("this data needs to be encrypted"), []byte("associated data"))
if err != nil {
log.Fatal(err)
}
pt, err := a.Decrypt(ct, []byte("associated data"))
if err != nil {
log.Fatal(err)
}
fmt.Printf("Cipher text: %s\nPlain text: %s\n", ct, pt)
}
运行 main.go 之后,会输出以下的内容,你的密文确定不长成这个样子的:
$ go run main.go
Cipher text: ╔E���᎔� c�1�L:�>�Yz��G ��M���ǖP���╗�<V.ʱL�\�D"�pC$e`٪5�D
Plain text: this data needs to be encrypted
一般情况下我不太喜欢将密文直接输出进为字符串的形式,因为里面包含有太多的不可见字符,打印出来格式千奇百怪。
所以,我喜欢将包含不可见字符的密文输出为 HEX(十六进制串)或 Base64,所以,这里可以将最后一行语句改成:
fmt.Printf("Cipher text: %x\nPlain text: %s\n", ct, pt)
修改之后,再次运行下 main.go 程序:
$ go run main.go
Cipher text: 0187574a607e5a6e321cd16ea125546ef145d0f9e250a7ae4ce5ef259a715a7025cacdeb8e004c1bb98b2db96de439ed9023e8c5636b05f462dd298c597789c7
Plain text: this data needs to be encrypted
总算是没有看到 '�' 这样的不可见字符了,这样看起来就舒服多了。
示例执行流程
从上面的示例代码中,也就是通过该加解密过程来分析,在代码的处理过程中,一共包含的几个步骤,分别用创建的对象来分析,结果如下:
创建 KeyTemplate;
语句aead.AES256GCMKeyTemplate()
创建了一个密钥模板,该模板是定义如何生成密钥,包含加解密算法、密钥长度、IV 长度等值。比如这里定义了一个基于 AES 的 32 位算法,采用 GCM 加密模式生成密钥;通过 KeyTemplate 创建 KeysetHandle;
根据 KeyTemplate 创建的密钥(Key)存放在键集(Keyset,也可称为密钥集)中,KeysetHandle 就是操作密钥集的,它用来执行对密钥的打印、保存和读取操作,但它会对敏感的密钥信息进行保护,比方说你不能直接打印密钥的具体内容,但可以看到密钥的元信息(ID号、状态、密钥前缀)。通过 KeysetHandle 创建 AEAD primitive;
现在,就可以通过键集(Keyset)中保存的密钥来创建 AEAD 加密使用的 primitive,它将提供加密(Encrypt)和解密(Decrypt)方法用于后继的加解密操作。使用 AEAD primitive 执行加密或解密操作。
加密和解密接口
上面我们使用 AEAD primitive 的加密(Encrypt)和解密(Decrypt)方法,它们的接口定义如下:
type AEAD interface {
// Encrypt encrypts plaintext with additionalData as additional
// authenticated data. The resulting ciphertext allows for checking
// authenticity and integrity of additional data additionalData,
// but there are no guarantees wrt. secrecy of that data.
Encrypt(plaintext, additionalData []byte) ([]byte, error)
// Decrypt decrypts ciphertext with {@code additionalData} as additional
// authenticated data. The decryption verifies the authenticity and integrity
// of the additional data, but there are no guarantees wrt. secrecy of that data.
Decrypt(ciphertext, additionalData []byte) ([]byte, error)
}
-
Encrypt
Encrypt(plaintext, additionalData []byte) ([]byte, error)
- 输入:明文(plaintext)和额外的验证数据(additionalData);
- 返回:密文(ciphertext),或者错误信息;
-
Decrypt
Decrypt(ciphertext, additionalData []byte) ([]byte, error)
- 输入:密文(ciphertext)和额外的验证数据(additionalData);
- 返回:明文(plaintext),或者错误信息;
所有的处理操作都是以 []byte 的形式来处理的,而现实世界中一般都是字符串,但在 Go 语言中,字符串转换成 []byte 是相当的简单,只需要 []byte("这是字符串")
就可以了。
你会发现,这里有一个重要的内容没有看到,就是加解密使用的密钥。这可以是个对密码体系来说非常关键部分,怎么就没有出现了?难道不需要使用么?
这就是 Tink 设计该加密库的一个要点,就是因为密钥相当重要,所以必须通过密钥系统来管理。后面文章中会提到如何使用密钥管理系统来进行密钥管理的内容。
额外认证数据是什么?
额外认证数据 (additional authenticated data, AAD),是在使用 AEAD primitive 时,加密和解密中需要传入的。按照定义它是可以被用来提供一些包括版本或是以密文相关内容的信息,加密算法不会加密这类的数据,但会对该数据进行谁。
现在,我们来看看额外谁数据的特点。
额外认证数据是否被加密?
额外认证数据不是说以明文的形式存在于密文之中的么,那这些数据是插入到密文的前面?还是后面?或者说是中间?那现在采用一段代码来观察一下效果。以传入大量的小写字母 “a”,它的十六进制数是 0x61,来看看密文的变化,有没有这么多的 0x61 出现。
package main
import (
"fmt"
"github.com/google/tink/go/aead"
"github.com/google/tink/go/keyset"
)
func main() {
kh, _ := keyset.NewHandle(aead.AES256GCMKeyTemplate())
a, _ := aead.New(kh)
ct, _ := a.Encrypt([]byte("this data needs to be encrypted"), []byte("aaaaaaaaaaaaaaaa"))
pt, _ := a.Decrypt(ct, []byte("aaaaaaaaaaaaaaaa"))
fmt.Printf("Cipher text: %x\nPlain text: %s\n", ct, pt)
}
输出为:
Cipher text: 0177d8f90b3c1ebab79afcb179e564762f71dcf65f98b022cce5c55099deda197dac1d33cd15d6b858050b2a9e153f8cb2997d28c87e79076c3d62761b211ad1
Plain text: this data needs to be encrypted
通过输出的结果可以发现,密文中并没有那么多的 0x61 出现,那是不是这些数据被加密呢?
其实这些额外认证数据并没有被加密,它们只是和密文做了当应的运算,主要是和密文进行异或运算。
额外认证数据不一致能解密么?
如果输入了错误的额外认证数据,密文是否能正常的解密?我们使用代码测试下:
package main
import (
"fmt"
"log"
"github.com/google/tink/go/aead"
"github.com/google/tink/go/keyset"
)
func main() {
kh, _ := keyset.NewHandle(aead.AES256GCMKeyTemplate())
a, _ := aead.New(kh)
ct, _ := a.Encrypt([]byte("this data needs to be encrypted"), []byte("associated data"))
pt, err := a.Decrypt(ct, []byte("changed associated data"))
if err != nil {
log.Fatal(err)
}
fmt.Printf("Cipher text: %x\nPlain text: %s\n", ct, pt)
}
由于提供了不一样的额外认证数据,运行该代码之后,会得到信息:
aead_factory: decryption failed
现在的结论就是加密时,如果使用和加密不一样的额外认证数据是无法将密文还原成明文的。
关于安全性的问题
虽然了解到上面的两点内容,包括不知道额外认证数据不能解密密文,但最终我们需要知道的是,AEAD 的安全性还是会体现在密钥的安全性上,如果密钥被他人获取,你是无法通过额外认证数据获得任何安全上的保障的。
在对称加密体系中,安全保障是通过密钥来提供的,这点是需要牢记的。
你能正常的解密么?
通过上面的示例和对该示例主要步骤的分解,让你可以掌握基础的 Tink 在对称加密方面的使用。
但是这还远远不够。你可以运行下面的两段代码来体验下在使用过程中遇到的问题。也就是加解密分离,因为上面那段示例代码只是示例使用,不可能用到任何环境,因为你不可能看到加密后马上又解密的。
一般来说,我们的应用场景可能是下面这样:
- 一个文件或字符串,通过加密算法加密后,保存到文件系统或数据库中,等到使用的时候再读出来解密后使用;
- 一段消息,通过加密算法加密后发送到指定的授受者,然后授受者使用事先约定好的密钥解密后查看;
为了展示起来清晰,我将加解密的过程分开到两段不同的代码中,这样你可以完全的了解代码所需要做的事情。
加密信息
首先,过程 Encrypt 接口对一段明文信息 “this data needs to be encrypted” 进行加密:
package main
import (
"fmt"
"log"
"github.com/google/tink/go/aead"
"github.com/google/tink/go/keyset"
)
func main() {
kh, err := keyset.NewHandle(aead.AES256GCMKeyTemplate())
if err != nil {
log.Fatal(err)
}
a, err := aead.New(kh)
if err != nil {
log.Fatal(err)
}
ct, err := a.Encrypt([]byte("this data needs to be encrypted"), []byte("associated data"))
if err != nil {
log.Fatal(err)
}
fmt.Printf("Cipher text: %x\n", ct)
}
运行后可以得到密文:
$ go run main.go
Cipher text: 01ec5977d49fd2565fa00dd241b1d8dcbd073b497cdf4d8454dcc15dbf5e9705831125e99ac523d4f787e5e7a2b7bb51248ea4459ef6db749b2cbd0b321dcd40
解密密文
当得到密文之后,可以通过使用 Decrypt 进行解密。由于得到的是二进制字符串,在 Go 中需要使用 hex.DecodeString 方法将其转换成 []byte 类型:
package main
import (
"encoding/hex"
"fmt"
"log"
"github.com/google/tink/go/aead"
"github.com/google/tink/go/keyset"
)
func main() {
kh, err := keyset.NewHandle(aead.AES256GCMKeyTemplate())
if err != nil {
log.Fatal(err)
}
a, err := aead.New(kh)
if err != nil {
log.Fatal(err)
}
pt := "01ec5977d49fd2565fa00dd241b1d8dcbd073b497cdf4d8454dcc15dbf5e9705831125e99ac523d4f787e5e7a2b7bb51248ea4459ef6db749b2cbd0b321dcd40"
ptByte, err := hex.DecodeString(pt)
if err != nil {
log.Fatal(err)
}
ct, err := a.Decrypt(ptByte, []byte("associated data"))
if err != nil {
log.Fatal(err)
}
fmt.Printf("Cipher text: %x\n", ct)
}
运行代码对上面的密文进行解密:
$ go run main.go
aead_factory: decryption failed
exit status 1
然后,我们发现解密失败。这是为什么呢?
其实这是因为解密使用的密钥和加密使用的密钥不一样,加密使用的密钥其实是随机生成的,而并没有保存下来,加密代码运行完成之后,密钥也就消失了。
后面会提到使用密钥管理系统来处理密钥问题。
重要的概念
- KeysetHandle:用来处理键集(Keyset)的对象;
- Keyset:键集,由密钥(Key)构成的一个集合;
- Key:密钥,在对称加密算法中,加密和解密密钥是一样的;
- 密钥管理系统(KMS):一个用来保存和管理密钥生命周期的系统;
- Primitives(原语):这里指定的是与密码学处理相关的接口;
总结
通过上面的 AEAD 加解密流程和注意事项等内容,我想你也应该能了解到 Tink 里加解密相关设计和实现的思路。这样的设计和实现的方式可以大大的减化你编写对称加密实现自身业务的难度。
在这里我们了解到使用 AEAD Primitives 来处理对称加解密的过程,并了解到了额外认证数据(add)使用的注意事项。
不过 Tink 不是万能的,它解决了这方面的问题,但密钥管理的问题还是留给你了,因为它毕竟只是一个密码库。Tink 提供了相应的 API 连接 KMS (密钥管理系统),但密钥管理的问题还是需要你去规划和实施的部分,以后会介绍。
现在,你可以自己尝试一下,如果加密时不使用额外认证数据,比如使用 nil 或 []byte("")
,得到密文后,能不能使用 nil 或 []byte("")
正解解密密文。