还记得 Tink 的第一个演示么?回头可以再看一下,在代码中有一行用来生成新的 KeysetHandle,如下:
kh, err := keyset.NewHandle(aead.AES256GCMKeyTemplate())
其中,我们提到了 aead.AES256GCMKeyTemplate()
创建了一个密钥模板,该模板是定义如何生成加解密数据所使用的密钥,这类模板中包含加解密算法、密钥长度、IV 长度等值。
这篇文章主要就用来分析还有哪一些常用的 KeyTemplate。
KeyTemplate
目前的 Tink 中,用于生成 KeyTemplate 的方法,按照加密算法和加密模式的不同,可以分为以下的三大类,它们被定义在 go/aead/aead_key_templates.go
文件中:
- 使用 AES 算法,并采用 GCM 模式的 KeyTemplate 有:
- AES128GCMKeyTemplate():使用 128 位密钥长度的基于 GCM 的 AES 算法;
- AES256GCMKeyTemplate():使用 256 位密钥长度的基于 GCM 的 AES 算法;
- 使用 AES 算法,并采用 CTR 模式的 KeyTemplate 有:
- AES128CTRHMACSHA256KeyTemplate():
- AES256CTRHMACSHA256KeyTemplate():
注意采用 CTR 模式需要对密文进行认证,所以需要使用 HMACSHA256 算法,而 GCM 模式自带 GMAC 功能对消息进行认证。
- 使用 ChaCha20 算法,并采用 Poly1305 消息认证码算法有:
- ChaCha20Poly1305KeyTemplate():
- XChaCha20Poly1305KeyTemplate():
上面所有的方法,返回的都是 *tinkpb.KeyTemplate 类型,它是一个通过 ProtoBuf 协议定义的结构体类型。
KeyTemplate 结构体
在 go/proto/tink_go_proto/tink.pd.go
文件中,定义了 KeyTemplate 结构体,熟悉 Go 的同学都知道,这种 .pd.go 后缀的文件,都是通过 .proto 文件自动生成的。
下面是 tink.pd.go 中的 KeyTemplate 结构体和它的定义文件 tink.proto 中定义的 message KeyTemplate
类型。
type KeyTemplate struct {
// Required.
TypeUrl string `protobuf:"bytes,1,opt,name=type_url,json=typeUrl,proto3" json:"type_url,omitempty"`
// Optional.
// If missing, it means the key type doesn't require a *KeyFormat proto.
Value []byte `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"`
// Optional.
// If missing, uses OutputPrefixType.TINK.
OutputPrefixType OutputPrefixType `protobuf:"varint,3,opt,name=output_prefix_type,json=outputPrefixType,proto3,enum=google.crypto.tink.OutputPrefixType" json:"output_prefix_type,omitempty"`
......
}
提示:OutputPrefixType 是 int32 的别名,它定义的是密文的输出前缀。
定义 KeyTemplate 结构体类型的文件 proto/tink.proto
中,有这个结构体的定义:
message KeyTemplate {
// Required.
string type_url = 1; // in format type.googleapis.com/packagename.messagename
// Optional.
// If missing, it means the key type doesn't require a *KeyFormat proto.
bytes value = 2; // contains specific serialized *KeyFormat proto
// Optional.
// If missing, uses OutputPrefixType.TINK.
OutputPrefixType output_prefix_type = 3;
}
根据生成的 tink.pd.go 文件中 KeyTemplate 的内容,该结构体内的主要字段用途为:
- TypeUrl: 代表使用的密码算法的类型,它以 URL 的形式显示;
- Value:不同类型的密码算法结构体序列化后的字节数组,最主要就参数就是密钥长度;
- OutputPrefixType:输出的前缀;
- TypeUrl
按照使用密码算法模板的不同,TypeUrl 为:
- AES GCM:"type.googleapis.com/google.crypto.tink.AesGcmKey"
- AES CTR:"type.googleapis.com/google.crypto.tink.AesCtrHmacAeadKey"
- ChaCha20Poly1305:"type.googleapis.com/google.crypto.tink.ChaCha20Poly1305Key"
- xChaCha20Poly1305:"type.googleapis.com/google.crypto.tink.XChaCha20Poly1305Key"
- OutputPrefixType
而 OutputPrefixType 一般则固定为 OutputPrefixType = 1
,一种 int32 的数据类型。这个前缀会在每一次输出密文的时候输出到最前面,所以我们可以发现,所有的密文都会以 01
开头的原因了。
- Vault
Value 中才会保存着生成与 Key 相关的信息,对于 AES GCM 来说,Vault中保存的是 AesGcmKeyFormat 序列化之后的 []byte 数组,它的定义如下:
type AesGcmKeyFormat struct {
KeySize uint32 `protobuf:"varint,2,opt,name=key_size,json=keySize,proto3" json:"key_size,omitempty"`
Version uint32 `protobuf:"varint,3,opt,name=version,proto3" json:"version,omitempty"`
......
}
其中,最重要的那个参数就是 KeySize,用来描述密钥的长度。
AES GMC 实现
上面我们简单介绍了,AES GCM 按照密钥的长度分为两种,一种是 AES 128,另一种是 AES 256。而采用 AES GCM 模式自带有密文签名的功能,所以只需要实现加解密功能就可以了,签名功能就不用另外写代码了。
上篇文章中我们介绍过,Tink 的使用上 是不用我们管理密钥的,它会随机的为我们生成一个用来加密的密钥,同时也会为这个密钥分配一个 ID 值,并将其记录到 Keyset 中。解密的时候就通过这个 ID 值去 Keyset 中查找对就在的密钥去解密。
我们调用 AES GMC 实现的 AEAD 功能,仅仅需要传入明文和可选的额外数据就可以了。
密钥的准备流程
在 Tink 中,要实现 AEAD 的加解密功能,大致流程如下:
- 创建模板
密钥的创建是要通过预先定义好的密钥模板(KeyTemplate)的,假如想使用 AES GCM 加密算法,则是通过 AES128GCMKeyTemplate()
或 AES256GCMKeyTemplate()
来生成密钥模板的。
- 创建密钥
创建密钥的过程是通过 KeyManager 来完成的,它主要的功能就是生成随机的 KeyID 和 Key,这里的 Key 就是用来加解密数据使用的。
- 创建 PrimitiveSet
创建的密钥会被加入到 PrimitiveSet 中,为什么有个 Set 呢?这是因为如果你有多个 Key 的话,加密是通过设置为 Primary 的 Key 来完成的,解密则会查找指定的 KeyID Key 来完成。
- 使用 Primitive
完成了上面的步骤之后,就可以通过调用 Primitive 的 Encrypt 或 Decrypt 来执行加解密任务了。
上面的步骤,只需要通过两行代码就可以完成,也就是下面的这两行:
kh, _ := keyset.NewHandle(aead.AES256CTRHMACSHA256KeyTemplate())
a, _ := aead.New(kh)
是不是相当的简单,现在代码中的 a 就是 Primitive,可以通过它实现加解密的操作。
密文的构成
通过对上面知识的介绍,现在我们能了解到 Tink 中一些关键的实现方法,大致就能推导出密文的组成方式。
比如,前面的介绍中使用的加密示例,用来加密字符串 "this data needs to be encrypted":
ct, err := a.Encrypt([]byte("this data needs to be encrypted"), []byte("associated data"))
运行后,输出的密文结果就是,它由 128 个十六进制数组成:
0187574a607e5a6e321cd16ea125546ef145d0f9e250a7ae4ce5ef259a715a7025cacdeb8e004c1bb98b2db96de439ed9023e8c5636b05f462dd298c597789c7
那它的构成是通过下面的内容组合而成的:
- prefix: 1 byte,使用 Tink 就默认是 1;
- key id: 4 byte,随机生成的 32 位数字;
- IV: 12 byte,随机生成的 iv,GCM 中应该叫 nonce;
- GCM tag: 16 byte,GCM 生成的认证数据,将和明文混合;
- ciphertext: 密文的长度,它和明文拥有一样的长度,如何用 byte 统计,上面的 "this data needs to be encrypted" 长度为 31 byte。
发现最终的结果就是 64 byte(1 + 4 + 12 + 16 + 31),打印成十六进制数就是 128 个,和上面的结果一致。
一般情况下,我们不需要去刻意的观察它的长度,只需要知道使用 Tink AEAD 加密的内容,一般都会是以 01 开头,而它使用密钥 ID 就是 01 后面的 8 个十六进制数(4 字节),也就是上面密文中的 87574a60 这串数字来表示的。
AES CTR 实现
了解了 AES GCM 的实现后,其它的加密模式就比较好理解了。
虽然 AES CTR 模式的实现比较复杂一点,那是因为 CTR 模式不带有对密文进行签名的功能,所以就需要使用 HMAC 来对密文进行签名。举个 AES 256 CTR 模板的例子:
// AES256CTRHMACSHA256KeyTemplate is a KeyTemplate that generates an AES-CTR-HMAC-AEAD key with the following parameters:
// - AES key size: 32 bytes
// - AES CTR IV size: 16 bytes
// - HMAC key size: 32 bytes
// - HMAC tag size: 32 bytes
// - HMAC hash function: SHA256
func AES256CTRHMACSHA256KeyTemplate() *tinkpb.KeyTemplate {
return createAESCTRHMACAEADKeyTemplate(32, 16, 32, 32, commonpb.HashType_SHA256)
}
我们就可以看到 AES、CTR、HMAC256 分别代表着加密算法、加密模式、签名算法,它们一起使用,就会带来一大堆的参数了。通过简单的推理也可以知道,采用 AES256CTRHMACSHA256KeyTemplate 这个 KeyTemplate 会比其它的两种 KeyTemplate 产生更长的密文。
虽然看起来很复杂,但通过 Tink 的封装,大量底层的逻辑操作都是通过 Primitive 提供的 非常简单的 Encrypt 和 Decrypt 接口来实现的,大大简化心智负担。这就相当于现在和计算机沟通,只用通过使用高级编程语言来完成,不用来编写二进制来控制计算机是一样的道理。
ChaCha20 和 XChaCha20 Poly1305 实现
ChaCha20 和 XChaCha20 它们使用的密钥长度都是 32 byte 长,而它们唯一的区别就是使用的 NonceSize 的长度不一样,Chacha20 使用的 NonceSize 是 12 byte,而 XChaCha20 合适的 NonceSize 是 24 byte。
最关键的就是它们的加密实现中,也同时采用 Poly1305 签名算法实现了签名,所以就不用像 AES CTR 那样,在加密完成之后再对密文签名,大大的简化了它的处理逻辑。
它密文的构成方式和 AES GCM 是差不多的,是 1 字节的 prefix,4 字节的 KeyID,然后剩下的就是密文。这里的密文是 nonce (nonce 你可以理解为随机字符)和密文拼接在一起形成的。
这两个算法,一般选择用在没有对 AES 做过优化的处理器平台上,比如嵌入式处理器,而 armv8 之后,就做了对 AES 的优化处理。
总结
由于 Tink 的封装的特别好,所以作为使用者来说,这些底层的内容是可以不用了解的。但对于加解密这样关键的操作来说,知道一些框架底层的原理还是大有好处的。总比搞错了技术细节导致无法对密文解密花的时间和金钱上的损失少得多。
而且通过对 Tink 的学习,学可以掌握 Google 对代码进行封装的能力,可以将非常复杂的操作都封装到低层来实现,上层只需要调用简单的几个 API 就可以了。这样的写代码的抽象水平,可以好好的领悟下。
现在,我们知道了一些非常安全的实现对称加密算法的方式,当然也可以将其用到其它的地方,不至于犯一些低级的错误,也不需要再考虑什么 CBC、ECB 等这样的加密模式,也不会忘记对密文进行签名。
这样,会对以后开发加解密相关的功能更有信心。