Salsa20流密码算法的密钥扩展与初始化过程
Salsa20是一个由Daniel J. Bernstein设计的流密码算法,以其简洁、高速和良好的安全性著称。它的核心是一个基于哈希函数的伪随机数生成器。今天,我将为你详细讲解Salsa20算法在开始生成密钥流之前的关键准备阶段:密钥扩展与初始化过程。这个过程负责将一个短密钥和一个随机数(nonce)扩展并填充到一个固定的内部状态矩阵中,为后续的“哈希/置换”轮函数提供输入。
1. 算法背景与核心组件
在进入正题前,我们先明确几个核心概念和参数:
- 密钥(Key):Salsa20支持两种密钥长度:128位(16字节)和256位(32字节)。这是加解密的共享秘密。
- 随机数(Nonce):一个必须唯一的数值(通常为64位或96位,这里以原始Salsa20/20的64位为例),用于同样的密钥加密不同消息时产生不同的密钥流。它不需要保密,但绝不能重复使用。
- 块计数器(Block Counter):一个64位的计数器,标识当前加密的是消息的第几个64字节块。它确保每个块的状态输入都是唯一的。
- 内部状态(State):一个4x4的矩阵,每个元素是一个32位(4字节)的字。总共16个字,即64字节。算法的核心是将(密钥,随机数,计数器)填充到这个矩阵中,然后对该矩阵进行一系列混淆和扩散操作(轮函数),最终将结果与原始状态相加,得到64字节的密钥流块。
2. 常量字符串(Sigma/Tau Constants)
这是密钥扩展过程的第一步,也是设计的精妙之处。算法定义了特定的常量字符串,作为状态矩阵的固定填充。这些常量充当“轮密钥”的“盐”,确保即使密钥全为零,初始状态也不是全零,增加了算法的混淆性。
- 对于256位密钥,使用的常量是字符串“expand 32-byte k”的ASCII码,表示为4个32位字:
0x61707865, 0x3320646e, 0x79622d32, 0x6b206574。我们记作sigma[0],sigma[1],sigma[2],sigma[3]。 - 对于128位密钥,使用的常量是字符串“expand 16-byte k”的ASCII码:
0x61707865, 0x3120646e, 0x79622d36, 0x6b206574。我们记作tau[0],tau[1],tau[2],tau[3]。
为什么需要它们? 如果没有这些与密钥无关的固定常量,当密钥为全0时,初始状态矩阵大部分为0,经过轮函数后可能产生弱随机性。这些常量打破了这种对称性。
3. 初始化过程详解:构建4x4状态矩阵
现在我们来看如何将密钥、随机数、计数器和常量组装成那个4x4的状态矩阵。我们以Salsa20/20(20轮)的经典参数为例:256位密钥(32字节)、64位随机数(8字节)、64位块计数器(8字节)。
状态矩阵有16个位置,编号为0到15。填充规则如下:
-
填充固定常量:
- 位置0:放入
sigma[0](0x61707865) - 位置5:放入
sigma[1](0x3320646e) - 位置10:放入
sigma[2](0x79622d32) - 位置15:放入
sigma[3](0x6b206574)
这四个点像矩阵的四个“锚点”,位置是固定的。
- 位置0:放入
-
填充密钥:
- 将256位密钥(32字节)看作8个32位字:
K0, K1, K2, K3, K4, K5, K6, K7。 - 位置1, 2, 3, 4:依次放入
K0, K1, K2, K3(密钥的前16字节)。 - 位置11, 12, 13, 14:依次放入
K4, K5, K6, K7(密钥的后16字节)。
- 将256位密钥(32字节)看作8个32位字:
-
填充块计数器:
- 将64位块计数器看作2个32位字:
Counter0(低32位),Counter1(高32位)。在实际实现中,常将其视为一个64位小端序整数,但分解为两个字填充。 - 位置6:放入
Counter0 - 位置7:放入
Counter1
- 将64位块计数器看作2个32位字:
-
填充随机数:
- 将64位随机数看作2个32位字:
N0, N1。 - 位置8:放入
N0 - 位置9:放入
N1
- 将64位随机数看作2个32位字:
最终,状态矩阵 X 如下所示(每个单元格是一个32位字):
| 索引 | 0 | 1 | 2 | 3 |
|---|---|---|---|---|
| 0 | sigma[0] |
K0 |
K1 |
K2 |
| 1 | K3 |
sigma[1] |
Counter0 |
Counter1 |
| 2 | N0 |
N1 |
sigma[2] |
K4 |
| 3 | K5 |
K6 |
K7 |
sigma[3] |
一个生动的比喻:你可以把这个状态矩阵想象成一个已划好固定座位(常量)的4x4餐桌。密钥被分成两段,坐在了第一排和第四排的特定位置。随机数(客人号)和计数器(菜序号)这对“客人身份标识”坐在了中间两排。这样就为每一“桌”加密(每个64字节块)准备了一份独一无二的“座位表”。
4. 如果使用128位密钥呢?
如果密钥是128位(16字节,即4个32位字 K0, K1, K2, K3),过程略有不同:
- 常量使用
tau[0..3],填充位置不变(0,5,10,15)。 - 密钥字会被重复使用来填满8个密钥槽位:
- 位置1, 2, 3, 4:依次放入
K0, K1, K2, K3。 - 位置11, 12, 13, 14:再次依次放入
K0, K1, K2, K3。
- 位置1, 2, 3, 4:依次放入
- 计数器和随机数的填充位置与256位密钥版本完全相同。
5. 初始化之后:从状态到密钥流
构建好这个初始状态矩阵 X 后,Salsa20的核心轮函数(一系列针对行的“四字运算”和对角的“四字运算”,共20轮)开始工作。我们记轮函数为 R(X),它对这个矩阵进行混淆和扩散。
密钥流块 并不是直接输出 R(X),而是将轮函数输出的矩阵与原始的初始状态矩阵 X 按字相加(模2^32),得到最终矩阵 Y:
Y = X + R(X)(这里的+是16个字的逐字模加)
然后,将64字节的矩阵 Y 按小端字节序展开,就得到了一个64字节的密钥流块。这个密钥流块与相同长度的明文进行逐字节异或,即完成加密或解密。
为什么是 X + R(X) 而不是直接输出 R(X)?
这是一个巧妙的设计,被称为“Davies-Meyer结构”在流密码中的一种应用。它使得算法是可逆的(从输出和X可以求R(X)),但这个性质对攻击者没有直接帮助,同时它极大地简化了轮函数的设计——轮函数R本身不需要是可逆的,这给了设计者更大的自由度来使用高效的运算。而最后的加法确保了输出与初始状态强相关,防止固定点攻击。
总结
Salsa20的密钥扩展与初始化过程,实质上是一个高度结构化、确定性的状态填充过程:
- 引入常量:使用预定义的“sigma”或“tau”常量,破坏对称性,增加混淆。
- 分割与填充:将输入的密钥、随机数和计数器分割成32位字。
- 矩阵组装:按照一个精心设计的位置映射表,将这些字和常量填入一个4x4的矩阵中,确保密钥材料、可变输入(随机数、计数器)和固定常量充分交织。
- 准备处理:这个组装好的矩阵,就是Salsa20轮函数的输入。经过20轮(或12轮、8轮的变体)的置换后,与自身相加,产出最终的密钥流。
这个过程虽然看起来只是简单的“摆放数据”,但其位置设计确保了在后续轮函数中,密钥、随机数和计数器的每一个比特都能快速扩散到整个状态中,是Salsa20算法简洁、高效且安全的重要基石。