介绍 Google Tink 加密库

2023-05-16, 星期二, 11:00

Cyber Security培训

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 的代码自己解析密钥集。