Salsa20流密码算法的密钥扩展与初始化过程
字数 3253 2025-12-13 21:10:24

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。填充规则如下:

  1. 填充固定常量

    • 位置0:放入 sigma[0]0x61707865
    • 位置5:放入 sigma[1]0x3320646e
    • 位置10:放入 sigma[2]0x79622d32
    • 位置15:放入 sigma[3]0x6b206574
      这四个点像矩阵的四个“锚点”,位置是固定的。
  2. 填充密钥

    • 将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字节)。
  3. 填充块计数器

    • 将64位块计数器看作2个32位字:Counter0(低32位),Counter1(高32位)。在实际实现中,常将其视为一个64位小端序整数,但分解为两个字填充。
    • 位置6:放入 Counter0
    • 位置7:放入 Counter1
  4. 填充随机数

    • 将64位随机数看作2个32位字:N0, N1
    • 位置8:放入 N0
    • 位置9:放入 N1

最终,状态矩阵 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
  • 计数器和随机数的填充位置与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的密钥扩展与初始化过程,实质上是一个高度结构化、确定性的状态填充过程:

  1. 引入常量:使用预定义的“sigma”或“tau”常量,破坏对称性,增加混淆。
  2. 分割与填充:将输入的密钥、随机数和计数器分割成32位字。
  3. 矩阵组装:按照一个精心设计的位置映射表,将这些字和常量填入一个4x4的矩阵中,确保密钥材料、可变输入(随机数、计数器)和固定常量充分交织。
  4. 准备处理:这个组装好的矩阵,就是Salsa20轮函数的输入。经过20轮(或12轮、8轮的变体)的置换后,与自身相加,产出最终的密钥流。

这个过程虽然看起来只是简单的“摆放数据”,但其位置设计确保了在后续轮函数中,密钥、随机数和计数器的每一个比特都能快速扩散到整个状态中,是Salsa20算法简洁、高效且安全的重要基石。

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 ) 这四个点像矩阵的四个“锚点”,位置是固定的。 填充密钥 : 将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字节)。 填充块计数器 : 将64位块计数器看作2个32位字: Counter0 (低32位), Counter1 (高32位)。在实际实现中,常将其视为一个64位小端序整数,但分解为两个字填充。 位置6:放入 Counter0 位置7:放入 Counter1 填充随机数 : 将64位随机数看作2个32位字: N0, N1 。 位置8:放入 N0 位置9:放入 N1 最终,状态矩阵 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 。 计数器和随机数的填充位置与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算法简洁、高效且安全的重要基石。