原语和接口

接下来,我们定义 Tink 中使用的两个重要语言部分(即 PrimitiveInterface),这两个部分是不太正式的,但之后较为正式。

原初

基元是一个数学对象,与安全执行某项任务的所有算法相对应。例如,AEAD 基元包含所有加密算法,这些算法满足 Tink 要求 Aead 的安全属性

我们强调,基元并不局限于编程语言或它们的特定访问方式。相反,应将基元视为纯数学对象。例如,如果我们考虑 AEAD,从根本上说它包含函数对,一个用于加密,另一个用于执行解密。

接口

接口是为用户提供对基元的访问权限的一种方式。例如,我们预计 Tink 将来会提供 Mac 接口,同时还会提供一个 StreamingMac 接口,以便计算未直接加载到内存中的数据 mac。

请注意,我们在此明确区分接口和基元。这应该清楚地表明,这两个接口提供访问权限的数学对象是相同的。

正式定义

对于大多数读者来说,上述直观的说明可能已经足够。 不过,我们认为有时有必要为这些概念提供正式定义。

加密函数

虽然与基元的概念相比,加密函数的概念没有基元的概念重要,但我们需要引入它来正式定义基元。

加密函数

加密函数是一种映射,

\[ f: {\bf K} \times {\bf R} \times {\bf I} \to {\bf O}\]

从集合 \({\bf K}\) (键空间)、集合 \({\bf R} = \{0,1\}^{\infty}\)(随机性,我们假定是无限位串集合)和集合 \({\bf I}\) (输入空间)到集合 \({\bf O}\) (输出空间)。

稍后,您将明白我们添加特定的随机性参数的原因。

例如,我们提供了一种将这些概念映射到 AES-GCM 的可能性。对于每个有效的密钥大小 \(s_k\)、Nonce 大小 \(s_n\)和标记大小\(s_t\),AES-GCM 都包含两个加密函数,一个用于加密,另一个用于解密。这两者将具有相同的键空间 \({\bf K} = \{0,1\}^{s_k}\)。

对于加密函数 \(\mathrm{Enc}\),随机性的前 \(s_n\) 位将用于选择 Nonce。

让 \({\bf B} = \{0,1\}^8\) 表示一个字节。 加密函数的输入空间是 \({\bf I} = {\bf B}^{*} \times {\bf B}^{*}\) 一对任意长度的字节字符串对。键值对的第一个元素表示消息,第二个元素是关联的数据。AES-GCM 标准对输入的长度设有上限,但我们更倾向于允许任意长度,而是在输出空间中添加特殊的错误符号 \(\bot\) 。然后,输出空间变为 \({\bf O} = {\bf B}^* \cup \{\bot\}\),我们可以在其中任意将成功计算的结果定义为标准串联 \((\mathrm{IV} \| \mathrm{ciphertext} \| \mathrm{tag})\) ,并在某些输入过长的情况下输出\(\bot\)。因此,对于固定密钥,加密函数的类型为 \(\mathrm{Enc}_k : {\bf R} \times {\bf B}^* \times {\bf B}^* \rightarrow {\bf B}^* \cup \{\bot\}\)。

对于解密函数, \(\mathrm{Dec}\) 密钥空间相同。恰好,输入空间是一样的: \({\bf I} ={\bf B}^* \times {\bf B}^*\),但现在第一个元素是加密函数的输出,而第二个元素仍然是关联的数据。

输出空间恰好是相同的 \({\bf O} = {\bf B}^* \cup \{\bot\}\) (同样是巧合)。解释方式略有不同,因为 \(\bot\) 通常表示身份验证错误(不过,如果某些输入内容过长,它也会作为输出结果)。

我们强调,上述规范化不是用于规范该标准的唯一选项。例如,您可以将 Nonce 视为输入的一部分,而不是从随机性中读取(这会生成一个完全不同的基元)。或者,可以将输出定义为包含 Nonce、密文和标记的三元组(而不是串联)。或者,可以将键空间限制为(有点随意)到\({\bf K} = \{0,1\}^{128} \cup \{0,1\}^{256}\)。

加密算法:

(对称)加密算法是一种元组

\[(f_1, ... f_k)\]

其中所有函数都具有相同的键空间。加密算法的类型是元组 \((({\bf I}_1, {\bf O}_1), \ldots, ({\bf I}_k, {\bf O}_k))\)。

例如,对于每个有效的密钥、随机数和标记大小的三元组,AES-GCM\({}_{s_k, s_n, s_t}\) 就是一种具有上述 \(\mathrm{Enc}\) 和 \(\mathrm{Dec}\) 两种函数的加密算法。 \((s_k, s_n, s_t)\)

原语和接口

下面,我们将定义加密基元。

原初
基元是一组加密算法,其中所有算法都具有相同类型 \((({\bf I}_1, {\bf O}_1), \ldots, ({\bf I}_k, {\bf O}_k))\),且算法的键空间是成对不相交的。

以 Tink 中的 \(\mathrm{AEAD}\) 基元为例。它有多种算法,其中包括 AES-GCM(密钥大小为 128 和 256 位)、Nonce 大小为 96 位的 AES-EAX、以及 XChaCha20Poly1305。它们具有不相交的密钥空间,但均提供相同的加密函数\(\mathrm{Enc}\) 和 \(\mathrm{Dec}\)。(在此正式讨论中,我们没有看到以某种方式合并不同大小的 AES-GCM 密钥的目的,但当然这样做是可以实现的。)

定义基元

基元的常用思维方式是先定义加密函数的属性,然后将基元视为所有此类算法。

例如,对于 AEAD,我们会认为 \(\mathrm{Dec}_k(\mathrm{Enc}_k(m, a), a) = m\) “始终”满意(除非明文 \(m\) 过长)。此外,我们具有安全属性;例如,对于随机密钥,加密在语义上是安全的。

这样,AEAD 基元就是满足这些属性的所有加密算法的集合。也就是说,实际上,当我们定义特定基元时,就是根据属性来定义它。并不像其定义那样,提供一系列算法。

接口

Tink 中的接口提供对基元的访问权限,因为它允许从输入空间计算输出空间的元素。以 Java 中的 AEAD 接口为例:

public interface Aead {
  byte[] encrypt(byte[] plaintext, byte[] associated_data) throws GeneralSecurityException;
  byte[] decrypt(byte[] ciphertext, byte[] associated_data) throws GeneralSecurityException;
}

请注意,我们不提供随机性。我们允许用户提供输入空间的元素。禁止获取随机性当然是故意的。1

Tink 有时会为单个基元提供多个接口。这一功能非常有用,因为要求有时会有所不同。不过,这样做也需要付出代价:通常,接口提供的接口越多,互操作性就越低。例如,假设某人基于 Tink 编写了一个库,该库要求用户传入 Aead 对象(在内部加密某些内容)。如果 Tink 向 \(\mathrm{AEAD}\) 基元提供了太多不同的接口,很可能是因为用户没有可同时适用于用户选择的密钥和库的实例。因此,添加更多接口是一种需要权衡的因素。


  1. AEAD 加密具有抵御所选密文攻击的特性,只有在未重复使用 Nonce 时才能保证这种特性。Tink 中的 Aead 接口可以防止重复使用 Nonce:用户无法提供 Nonce 作为加密输入,而是为每个加密操作随机生成一个新的 Nonce。