前面介绍了如何使用 Tink 实现最基本的对称加密算法来完成加解密任务,虽然我们看到,Tink 的使用非常简单,但好像还少了点什么?
来回顾一下使用 Tink 的时候,调用的加解密接口:
- 加密:
Encrypt(plaintext, additionalData []byte) ([]byte, error)
,使用明文和额外认证数据可以得到密文; - 解密:
Decrypt(ciphertext, additionalData []byte) ([]byte, error)
,使用密文和额外认证数据可以得到明文;
少得是什么呢?对,少的是 Key(密钥)。作为一种对称加密技术,怎么会没有密钥呢。而对于现代化的加密系统来说,算法一般都是公开的,而弱点往往会出现在密钥的选择和使用上。
Tink 的解决方案是,处理加解密任务的密钥使用密码学安全的随机函数生成,并保存在 Keyset(键集)中,但对 Keyset 的保管就需要你来设计了,一般采用 KMS(密钥管理系统)来处理。
那这篇文章的内容,就是介绍如何保存 Keyset。但这里不是采用 KMS,而是直接采用 AEAD 来加密 Keyset,这样的场景主要用在单机或没有 KMS 的环境下,比如安卓手机上使用。
Tink 中的重要组件
现在来回顾一下前面介绍的 Tink 的执行流程,大致如下所示:
通过 KeyTemplate 生成 KeysetHandle,然后通过 KeysetHandle 创建 primitive,最后通过 primitive 完成加解密行为。
其实这个中间还有两个重要的组件参与其中,一个是 KeysetManager,另一个是 PrimitiveSet。
它们的分工各不相同,主要功能如下:
- KeysetHandle 前面介绍过,它主要完成对 Keyset 的操作,可以用它来执行密钥的打印、保存和读取操作;
- KeysetManager 用来管理 Keyset,主要针对其中的密钥进行管理,比如新增、轮换、失效、删除等;
- PrimitiveSet 这个主要是通过 Keyset 中所有可以使用的密钥来生成一个集合,为什么是所有可用的密钥呢?这个是与密钥管理有关的,后面会讲。
提示:加密操作使用 Keyset 中的 primary_key,解密操作使用 Keyset 中所有可用(Enable)的密钥。
找到保存 Keyset 的方法
通过前面使用 Tink 的示例,就是采用了两段代码的,一段用来加密的代码,一段用来解密的代码,它们需要分别运行。当使用加密代码来加密数据后,当我们在解密数据的时候就出现了问题,无法还原以前加密的数据了。
这是因为在代码运行的过程中,当前生成的 Keyset 存在于内存之中,会随着程序的停止而被系统自动的回收。Keyset 的回收当然会导致其中的 Key 也被回收,没有了 Key,也就无法还原加密过后的内容了。
所以,当我们加密代码去加密一段消息,并保存在硬盘上之后,过段时间运行解密代码解密密文。这个时候,由于没有了 Key,当然也就无法还原该加密后的消息。
解决方法就是,需要保存当前加密过程中使用的 Keyset,以便在程序再次运行的时候,可以载入加密时使用的 Keyset,就能够正常的解密了。
Tink 的设计为了安全,所有使用到的 Key 都是采用密码学安全的随机数生成器随机生成的,而且也不允许你访问生成的 Key。你只能使用 Key,而不能查看 Key。
那不能查看,又如何保存 Key 呢?很显然,该功能就是通过以前介绍过的 KeysetHandle 来实现的,现在,我们来查看一下 KeysetHandle 提供的方法有哪些:
func NewHandle(kt *tinkpb.KeyTemplate) (*Handle, error)
func NewHandleWithNoSecrets(ks *tinkpb.Keyset) (*Handle, error)
func Read(reader Reader, masterKey tink.AEAD) (*Handle, error)
func ReadWithNoSecrets(reader Reader) (*Handle, error)
func (h *Handle) Public() (*Handle, error)
func (h *Handle) String() string
func (h *Handle) Write(writer Writer, masterKey tink.AEAD) error
func (h *Handle) WriteWithNoSecrets(w Writer) error
func (h *Handle) Primitives() (*primitiveset.PrimitiveSet, error)
func (h *Handle) PrimitivesWithKeyManager(km registry.KeyManager) (*primitiveset.PrimitiveSet, error)
func (h *Handle) hasSecrets() bool
注意到上面的 Write 方法,也就是 func (h *Handle) Write(writer Writer, masterKey tink.AEAD) error
,它看起来就可以用来保存当前的 Keyset,实际上它确实可以做到这一点。
而下一次,我们需要读取出保存的 Keyset,可以通过 Read 方法,也就是利用 func Read(reader Reader, masterKey tink.AEAD) (*Handle, error)
这个方法。
看了这两个方法的签名后,现在我们有三个未知的内容需要了解一下了,其实它们是三个接口:
- keyset.Writer
- keyset.Reader
- tink.AEAD
keyset.Writer 和 keyset.Reader
在当前的目录下(go/keyset),就可以发现 write.go 方法,它定义了 Writer 接口,里面有两个方法:
// Writer knows how to write a Keyset or an EncryptedKeyset to some source.
type Writer interface {
// Write keyset to some storage system.
Write(Keyset *tinkpb.Keyset) error
// Write EncryptedKeyset to some storage system.
WriteEncrypted(keyset *tinkpb.EncryptedKeyset) error
}
同样,在当前目录下 reader.go 文件中,同样也有 Reader 接口的定义:
// Reader knows how to read a Keyset or an EncryptedKeyset from some source.
// In order to turn a Reader into a KeysetHandle for use, callers must use
// insecure.KeysetHandle or by keyset.Read (with encryption).
type Reader interface {
// Read returns a (cleartext) Keyset object from the underlying source.
Read() (*tinkpb.Keyset, error)
// ReadEncrypted returns an EncryptedKeyset object from the underlying source.
ReadEncrypted() (*tinkpb.EncryptedKeyset, error)
}
注意:为了安全,Tink 都是使用其中的带有 Encrypted 的方法来保存或读取 Keyset 的,如 WriteEncrypted 和 ReadEncrypted。
在当前的目录下,可以找到这两个接口的实现类了(Go 语言使用的是 struct 来实现类的功能的)。
- binary_io.go 文件中的 BinaryReader 和 BinaryWriter,它的作用是以二进制的方式读取和写入;
- json_go 文件中的 JSONReader 和 JSONWriter,它的作用是以JSON 的方式读取和写入;
- mem_io.go 文中的 MemReaderWriter,它的作用是写入到内存或从内存中读取;
tink.AEAD
这个 tink.AEAD 接口也就是我们前面介绍过的关联数据的认证加密,接口前面也介绍过了,就是下面这样:
type AEAD interface {
Encrypt(plaintext, additionalData []byte) ([]byte, error)
Decrypt(ciphertext, additionalData []byte) ([]byte, error)
}
实现类有很多,最常用的当然就是 AESGCM 这个类了,它在 go/aead/subtle 目录下的 aes_gcm.go 文件中,主要的方法如下:
func NewAESGCM(key []byte) (*AESGCM, error)
func (a *AESGCM) Encrypt(pt, aad []byte) ([]byte, error)
func (a *AESGCM) Decrypt(ct, aad []byte) ([]byte, error)
创建它只需要一个 Key,然后就可以调用它的加解密方法了。
Keyset 的保存
上面找到了保存 Keyset 所需要的内容,现在就可以利用上面学到的知识来保存 KeysetHandle 中的 Keyset 了。
保存的 Keyset 是以加密的方式,而且采用的是 AEAD 的方式来进行的,我们挑选的是最常用的实现 AESGCM。这时你只需要指定 Key 就要以了,但要注意的是 Key 的长度,一般最常使用的两种 Key 长度是:
- 16 字节(128 位),也就是采用 AES 128 来加密数据;
- 32 字节(256 位),也就是采用 AES 256 来加密数据;
下面使用 16 字节来作演示,你可以自已试下 32 字节的长度的密钥:
package main
import (
"fmt"
"log"
"os"
"github.com/google/tink/go/aead"
"github.com/google/tink/go/aead/subtle"
"github.com/google/tink/go/keyset"
)
func main() {
// 通过密钥生成 AEAD
masterKey := []byte("1234567890123456")
masterAEAD, _ := subtle.NewAESGCM(masterKey)
// 打开待写入的文件
masterFile, _ := os.OpenFile("tmp.txt", os.O_CREATE|os.O_WRONLY, 0644)
writer := keyset.NewJSONWriter(masterFile)
// 生成一个新的 Keyset
kh, err := keyset.NewHandle(aead.AES128GCMKeyTemplate())
if err != nil {
log.Fatal(err)
}
// 打印并保存 Keyset,Keyset 以加密的 JSON 格式保存
fmt.Println(kh.String())
kh.Write(writer, masterAEAD)
}
注意:生成 AEAD 的密钥千万不能这样操作,这会变成整个加密系统的弱点,这个密钥要保存在密钥管理系统或其它安全的地方。密钥一般采用随机生成,而不要自行指定。
在这里,我们打印出来了 Keyset 的信息,大至为以下的形式。为什么是大致呢?因为 key_id 是随机生成的。其中的 primary_key_id 是当前用于加密使用的 key_id,前面提到过,这个概念与密钥管理有关。
primary_key_id:4210723985 key_info:<type_url:"type.googleapis.com/google.crypto.tink.AesGcmKey" status:ENABLED key_id:4210723985 output_prefix_type:TINK >
注意:我们这里使用到的 masterKey 和 Keyset 中的密钥是没有关系的,masterKey 只用于加密 Keyset,当然包括其中的密钥。而 Keyset 中的密钥主要用于处理加解密任务。
加密后的 Keyset 保存到了 tmp.txt 文件中,内容是以 JSON 的形式保存的,同下面的形式:
{"encryptedKeyset":"/4W/8TuCaA35aterZj4T1SlumR8ksk0SQdTWQeIxg0spKg7LSpEIZXi+N9ljl889qhGyg/mTeQqWcwub0WbmKs5ACmuFVoUMxG8BVe9sfXlP6Q5rvoWu3aEFnR1KDr4Ep73FBXD+kT2PVHk6O/btB+6n92WFZg==","keysetInfo":{"primaryKeyId":211765598,"keyInfo":[{"typeUrl":"type.googleapis.com/google.crypto.tink.AesGcmKey","status":"ENABLED","keyId":211765598,"outputPrefixType":"TINK"}]}}
注意:加密后的 Keyset 文件也需要妥善保管,这里随意保存成 tmp.txt 只做演示,你需要把它保存到相对安全的位置,因为它关乎到加密信息的正常解密。
Keyset 的读取
当你需要再次的使用 Keyset 来完成其它的加密或解密任务时,就需要从保存的 tmp.txt 文件中读取并解密出 Keyset。下面是一个简单的演示,展现了如何从文件中获取并解密 Keyset 文件。
这里需要两个参数,一个是保存时使用到的密钥,一个是 Keyset 文件(上次保存的是 tmp.txt)。
package main
import (
"fmt"
"os"
"github.com/google/tink/go/aead/subtle"
"github.com/google/tink/go/keyset"
)
func main() {
// 通过密钥生成 AEAD
masterKey := []byte("1234567890123456")
masterAEAD, _ := subtle.NewAESGCM(masterKey)
// 使用 JSON 读取器从文件中读取加密的 Keyset
masterFile, _ := os.Open("tmp.txt")
reader := keyset.NewJSONReader(masterFile)
// 生成 Keyset
kh, err := keyset.Read(reader, masterAEAD)
if err != nil {
panic(err)
}
// 打印出 Keyset 的内容,现在就可以正常的使用该 Keyset 了
fmt.Println(kh.String())
}
代码运行时,输出的 Keyset 内容大致就是下面内容:
primary_key_id:4210723985 key_info:<type_url:"type.googleapis.com/google.crypto.tink.AesGcmKey" status:ENABLED key_id:4210723985 output_prefix_type:TINK >
我们发现,上面的 Keyset 在保存时,输出的 primary_key_id 和当前 Keyset 中唯一的密钥 key_id 都是 4210723985 这个数字,并和这次从文件中读取的 primary_key_id 和 Keyset 中的密钥 key_id 一致。
这至少可以证明了 tmp.txt 文件中的内容就是前面保存的 Keyset ,因为它和当时我们保存时打印出来的 primary_key_id 和 key_id 一样。key_id 和 Key 都是 Tink 采用密码学安全的随机生成器产生的。
key_id 是用来解密密文的时候使用的,密文会包含 key_id 的值。而 Key 则是解密密文所使用到的密钥。
总结
这里,我们提到了一个基本的 Tink 设计理念,就是 Keyset 中保存的 Key 是受保护的,无法输出查看,也无法以明文方式导出。
如何我们想要保存 Keyset,就需要通过使用 AEAD 的方式来保存,也就是会加密它后才保存。这样,保存之后的 Keyset 以密文的形式存放,多了一层安全性。
保存的 Keyset 可以使用 JSON 格式,也可以使用二进制格式,这个要根据需求来决定。上面演示的是 JSON 格式,你可以自行的体验一下二进制格式。
最后,也是最重要的,上面演示中的 masterKey 是自行指定的,是非常不安全的,后面会讲解通过使用密钥管理系统来保存 Keyset,这才是正确的做法。
始终记住,这篇文章的场景只适合在无法使用密钥管理系统的情况下采用。