Tink 是由 Google 的加密货币和安全工程师编写的开源加密库。Tink 安全简单的 API 可通过以用户为中心的设计、仔细的实现和代码审核以及广泛的测试来降低常见陷阱。帮助没有加密背景的用户安全地实现常见的加密任务。Tink 已部署到数百款 Google 产品和系统中。
Tink 支持的功能有:
- 加密文件
- 加密大型文件或数据流
- 安全数据交换
- 数据加密认证
- 数字签名
Tink 目前支持 C++ / Go / Java / Obj-C 和 Python,Java 用户可以使用 com.google.crypto.tink:tink。由于 Tink 依赖 Protocol Buffers,Tink 团队考虑到维护成本 移除了 JavaScript / TypeScript 的支持。
出于某些原因 Tink 的文档可能难以访问,这时候可以参考项目拆分前 GitHub 仓库中的 Tink for Java HOW-TO。
术语
Primitive(原语 / 基元)
原语是与安全执行某个任务的所有算法对应的数学对象。例如,AEAD 原语由所有满足 Tink 所需的 AEAD 安全属性 的加密算法组成。
原语不绑定编程语言,也没有特定的访问方式,应将原语视为完全数学对象。
Keyset(密钥集)
Tink 使用密钥集来启用密钥轮替。正式地说,密钥集是密钥的非空列表,其中一个密钥被指定为主密钥(用于对新的明文签名和加密的密钥)。此外,密钥集中的密钥会获得唯一 ID(此 ID 通常作为前缀添加到每个生成的密文、签名或标记中,用于指示所使用的密钥)和密钥状态(用于停用密钥,而无需将其从密钥集中移除)。
密钥集中的所有密钥必须是相同基元(如 AEAD)的实现,但可以具有不同的密钥类型(例如,AES-GCM 和 XChaCha20-Poly1305 密钥)。
Keyset handle(密钥集句柄)
用户通过密钥集句柄对密钥集执行操作。密钥集句柄限制了实际敏感密钥材料的暴露。它还会对一个密钥集进行抽象化,使用户能够获得封装整个密钥集的原语。
生成密钥集
最佳实践建议使用中心化的 KMS 创建和管理密钥,不具备这种基础设施的条件而使用明文密钥集时建议借助 podman secret
管理,或是项目加载密钥集后就删除文件(密钥集驻留在内存中其实也是个不好的实践,但相对而言已经足够)。
tinkey create-keyset --key-template AES128_GCM --out ./tinkey-keyset.json
向已有密钥集添加新密钥:
tinkey add-key --in ./old-tinkey-keyset.json --key-template AES128_GCM --out ./new-tinkey-keyset.json
使用公钥密码系统则复杂一些,需要先生成私钥密钥集,再根据私钥密钥集生成公钥密钥集。
tinkey create-keyset --key-template ECDSA_P256 --out src/test/resources/tinkey-keyset-signature-private.json
tinkey create-public-keyset --in src/test/resources/tinkey-keyset-signature-private.json --out src/test/resources/tinkey-keyset-signature-public.json
一个简单的例子
以使用混合加密方案为例,参考 RFC 9180 Hybrid Public Key Encryption,开发者需要选择一种密钥封装机制,一种 KDF,一种 AEAD 方案,它们的配置也要和密文一起发给接收者。
使用 Tink 的话就不必关注实现细节:
// Get the primitive.
HybridEncrypt encryptor = keysetHandle.getPrimitive(HybridEncrypt.class);
byte[] ciphertext = encryptor.encrypt(
PLAIN_TEXT.getBytes(StandardCharsets.UTF_8),
ASSOCIATED_DATA.getBytes(StandardCharsets.UTF_8));
如何解密 Tink 产生的密文
以 com.google.crypto.tink:tink:1.9.0
使用 AEAD 方案和 AES-GCM 密钥加密字符串实现加密为例。
使用单一 AES-GCM 密钥的密钥集:
...
{
"keyData": {
"typeUrl": "type.googleapis.com/google.crypto.tink.AesGcmKey",
"value": "GhDx8O+2oEaTTdeYvALat6KD",
"keyMaterialType": "SYMMETRIC"
},
"keyId": 613509273,
}
...
加密 Hello, world!
获得 0124916899acd80ed748581c85deb53c09732217edde8877cb5dbad21240d6b28bcf8cbe65c6f17d917527121a8c
。
密文串的第一个字节 0x01
表示这是一个 Tink 格式的密文。
接下来 4 个字节是使用密钥的 ID:
assertEquals(613509273L,
Longs.fromBytes(
(byte) 0, (byte) 0, (byte) 0, (byte) 0,
cipherText[1], cipherText[2], cipherText[3], cipherText[4]));
AES-GCM 使用 96-bits 长度的 Nonce,这是密文中接下来的 12 个字节。
System.arraycopy(cipherText, 5, nonce, 0, 12);
AES 加密的密文长度与明文相等,用于 MAC 的 Tag 长度为 16 个字节,合起来是剩下的内容。
byte[] cipherTextWithTag = new byte[13 + 16];
System.arraycopy(cipherText, 17, cipherTextWithTag, 0, 29);
阅读 com.google.crypto.tink.aead.internal.InsecureNonceAesGcmJce
的代码可以了解 Tink 如何利用 Cipher 实现加密,编写对应的解密代码。
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.DECRYPT_MODE,
new SecretKeySpec(key, "AES"),
new GCMParameterSpec(128, nonce));
cipher.updateAAD(associatedData);
byte[] decrypted = cipher.doFinal(cipherTextWithTag);
assertEquals(plainText, decrypted);
不要担心 InsecureNonceAesGcmJce
这个名字,它只是在提醒开发者不要传递预设的 IV 给 Cipher。
目前还没有详细的资料介绍如何读取密钥,可以在调试 com.google.crypto.tink.KeysetHandle#getEntriesFromKeyset
时获取存储在字节数组中的 key,或者干脆参考 Tink 的代码自己解析密钥集。