Google Tink 03:密钥的保存

前面介绍了如何使用 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,这才是正确的做法。

始终记住,这篇文章的场景只适合在无法使用密钥管理系统的情况下采用。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,657评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,662评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,143评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,732评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,837评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,036评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,126评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,868评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,315评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,641评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,773评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,859评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,584评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,676评论 2 351