目录
什么是对称加密
对称密钥算法(Symmetric-key algorithm),又称为对称加密、私钥加密、共享密钥加密,是密码学中的一类加密算法。
对称加密的特点是,在加密和解密时使用相同的密钥,或是使用两个可以简单地相互推算的密钥。
这一个或一组密钥需要在两个或多个成员之间共享,以便维持专属的通讯联系。
对称加密的优点是速度快,缺点是需要共享密钥,安全性不足。
常见的对称加密算法有 AES、SM4、ChaCha20、3DES、Salsa20、DES、Blowfish、IDEA、RC5、RC6、Camellia。
对称加密的基本过程
对称加密算法可分为两大类型:
分组加密: 先将明文切分成一个个固定大小的块,再对每个块进行加密,这种方式被称为分组加密或块加密,有的资料称呼为"分组密码"或"块密码"。
流加密: 将密钥扩展到与密文等长后,用扩展后的密钥与明文按比特位做异或运算。
相比分组加密,流加密具有速度快,消耗少的优点,在网络通信的某些特定场景比较有优势。然而流加密的发展落后于分组加密,其安全性、可扩展性、使用灵活性上,目前认为还是比不上分组加密的,同时某些分组加密算法可以兼具流加密的部分特点。因此对称加密的主流仍然是分组加密。
常见的流加密算法如RC4、ChaCha20等等,它们的安全强度主要取决于扩展后密钥的随机性。
无线网络通信中常常使用RC4; TLS通信协议的最新版本TLS 1.3中,出现了支持ChaCha20的密码套件。
流加密不是本篇的学习重点,这里只简单了解一下,接下来开始学习分组加密,后文提到对称加密时,一般都是特指分组加密。
分组加密的基本过程
分组加密的基本流程如下所述:
- 将明文字节数组(byte[])切分为固定长度的块(block),例如每个块16个字节(即128位)。
- 对每一个块进行块加密,块加密的算法需要使用密钥,并且该过程是可逆的,即使用相同密钥可以对加密结果进行解密;块加密不会改变块的长度。
- 将加密后的块拼接起来,得到密文字节数组(byte[])。
- 解密过程也是一样的,先对密文字节数组进行固定长度的分组,然后对每个块进行块解密,然后将解密后的块拼接起来,得到明文字节数组。
分组加密有三个重点:
- 一是如何分组,明文的长度是不固定的,当明文长度不是块长度的整数倍时,要不要填充?如何填充?这时就要引入填充算法。
- 二是分组之后的各个块以何种形式组织起来实现整体的加解密,这个就是分组模式。
- 三是如何实现针对每个块的加解密,这个是不同的分组加密算法的核心部分。
填充算法
使用分组加密算法对明文进行分组时,有时需要事先对明文字节数组进行填充,使其长度变为块长度的整数倍。这就是分组加密的填充算法,或者说填充规则。
但并不是所有的分组加密算法都需要做明文填充,这取决于分组模式,事实上只有ECB、CBC、PCBC等分组模式需要填充明文。
目前主流的分组加密填充规则是PKCS7Padding。
PKCS7算法
假定分组加密的块长度为BlockSize, 明文字节数组长度为SrcSize, 则按下面的公式计算padding。padding即是明文需要补位的字节数,也是补位的每个字节的数值,解密时通过最后一个字节的数值判断需要去除多少个填充字节。
padding = BlockSize - SrcSize % BlockSize
举例说明:
假设BlockSize为16,那么:
如果padding为1,就在明文尾部填充1个1;
如果padding为13,就在明文尾部填充13个13;
如果padding为16,就在明文尾部填充16个16;
padding为16代表明文长度正好是16的整数倍,但此时仍然在明文尾部补充16个16,这样在解密时就仍然是通过最后一个字节的数值来判断需要去掉多少个填充的字节,而不需要对明文长度正好是16整数倍的情况做特殊处理。
PKCS5算法
PKCS5是PKCS7的子集,BlockSize值固定为8,其他规则一样。
目前PKCS5基本不再使用,因为主流的AES与SM4的BlockSize都是16。
从文字密码到比特序列密码
- 计算机的操作对象并不是文字,而是由 0 和1排列而成的比特序列。
- 将现实世界中的东西映射为比特序列的操作称为编码( encoding )。
XOR
由于 X0R 和加法运算很相似,因此一般用 + 和O组合而成的符号®来表示 X0R。
举例: 一枚棋子,翻转两遍,还是那枚棋子
-
由于两个相同的数进行 XOR 运算的结果一定为 0 , 因此如果将 A ㊉B的结果再与B进行XOR运算,则结果会变回 A。
-
只要选择一个合适的 B,仅仅使用 XOR 就可以实现一个高强度的密码。
-
对于密码技术来说,“是否可以预测” 是非常重要的一点。能够产生不可预测的比特序列,对于密码技术的贡献是巨大的。这种不可预测的比特序列就称为随机数。
一次性密码本——绝对不会被破译的密码
只要通过暴力破解法对密钥空间进行遍历,无论什么密文总有一天也都能够被破解。然而,一次性密码本( one-time pad )却是一个例外。即便用暴力破解法遍历整个密钥空间,一次性密码本也绝对无法被破译。
一次性密码本的加密
一次性密码本是一种非常简单的密码,它的原理是 “将明文与一串随机的比特序列进行
X0R 运算”。如果将硬币的正面设为 0, 反面设为 1,则通过不断掷硬币就能够产生这样一串随机的比特序列。
- 随机生成一个和明文长度相同的bit串,再与明文异或运算
—次性密码本的解密
解密就是加密的反向运算。也就是说,用密文和密钥进行 XOR 运算,就可以得到明文。
—次性密码本是无法破译的
- 这里说的无法破译,并不是指在现实的时间内难以破译,而是指即便拥有一种运算能力无穷大的计算机,可以在一瞬间遍历任意大小的密钥空间,也依然无法破译。
- 由于明文中所有可能的排列组合都会出现,因此我们无法判断其中哪一个才是正确的明文( 也就是用哪个密钥才能够正确解密 )。
- 一 次 性 密 码 本 是 无 条 件 安 全的( unconditionally secure ), 在理论上是无法破译的( theoretically unbreakable )
为什么没有被使用
- 密钥的配送:破译需要的密钥也需要一并发送出去,既然能安全的发送密钥,还对信息加密什么?
- 密钥的保存:密钥必须安全的保存,既然能安全的保存密码,还不能安全的保存信息?
- 密钥的同步:比特位不允许错位
DES
- DES ( Data Encryption Standard )是 1977 年美国联邦信息处理标准( FIPS ) 中所采用的一种对称密码。
- 由于 DES 的密文可以在短时间内被破译,因此除了用它来解密以前的密文以外,现在不应该再使用 DES 了。
加密和解密
- DES 是一种将 64 比特的明文加密成 64 比特的密文的对称密码算法,它的密钥长度是 56 比特。尽管从规格上来说,DES 的密钥长度是 64 比特,但由于每隔 7 比特会设置一个用于错误检查的比特,因此实质上其密钥长度是 56 比特。
- DES 是以 64 比特的明文( 比特序列 )为一个单位来进行加密的,这个 64 比特的单位称为分组。一般来说,以分组为单位进行处理的密码算法称为分组密码( block cipher ),DES 就是分组密码的一种。
- DES 每次只能加密 64 比特的数据,如果要加密的明文比较长,就需要对 DES 加密进行迭代( 反复 ),而迭代的具体方式就称为
模式( mode )。
DES 的结构
(1 ) 将输入的数据等分为左右两部分。
(2) 将输人的右侧直接发送到输出的右侧。
(3) 将输入的右侧发送到轮函数。
(4) 轮函数根据右侧数据和子密钥,计算出一串看上去是随机的比特序列。
(5) 将上一步得到的比特序列与左侧数据进行 XOR 运算,并将结果作为加密后的左侧。
-
但是,这样一来 “右侧” 根本就没有被加密,因此我们需要用不同的子密钥对一轮的处理重复若干次,并在每两轮处理之间将左侧和右侧的数据对调。
-
Feistel 网络的解密操作只要按照相反的顺序来使用子密钥就可以完成了,而 Feistel 网络本身的结构,在加密和解密时都是完全相同的
性质
- Feistel 网络的轮数可以任意增加。
- 加密时无论使用任何函数作为轮函数都可以正确解密。
- 加密和解密可以用完全相同的结构来实现
3DES
三重 DES( triple-DES )是为了增加 DES 的强度,将 DES 重复 3 次所得到的一种密码算法,也称为 TDEA ( Triple Date Encryption Algorithm ),通常缩写为 3DES。
- 明文经过三次 DES 处理才能变成最后的密文,由于 DES 密钥的长度实质上是 56 比特,因
此三重 DES 的密钥长度就是 56 x 3=168 比特。 - 加密—解密—加密:目的是为了让三重 DES 能够兼容普通的 DES,当三重 DES 中所有的密钥都相同时,三重 DES 也就等同于普通的 DES 了。
- 尽管三重 DES 目前还被银行等机构使用,但其处理速度不高,除了特别重视向下兼容性的情况以外,很少被用于新的用途。
AES
- AES ( Advanced Encryption Standard )是取代其前任标准( DES )而成为新标准的一种对称密码算法。
- Rijndael 是由比利时密码学家 Joan Daemen 和 Vincent Rijmen设计的分组密码算法,于2000 年被选为新一代的标准密码算法—AES
- 在 AES 的规格中,分组长度固定为 128 比特,密钥长度只有 128、 192 和 256比特三种。
Rijndael 的加密和解密
- 和 DES —样,Rijndael 算法也是由多个轮构成的,其中每一轮分为 SubBytes、ShiftRows、MixColumns 和 AddRoundKey 共 4 个步骤。
- Rijndael没有使用Feistel 网络,而是使用了 SPN 结构。
- Rijndael 的输入分组为 128 比特,也就是 16 字节。
-
- SubBytes:将每一个字节替换为另一个字节
- SubBytes:将每一个字节替换为另一个字节
-
- ShiftRows:将以 4 字节为单位的行(n>w )按照一定的规则向左平移,且每一行平移的字节数是不同的。
- ShiftRows:将以 4 字节为单位的行(n>w )按照一定的规则向左平移,且每一行平移的字节数是不同的。
-
- MixColumns:是对一个 4 字节的值进行比特运算,将其变为另外一个 4 字节值。
- MixColumns:是对一个 4 字节的值进行比特运算,将其变为另外一个 4 字节值。
-
- AddRoundKey:将 MixColumns 的输出与轮密钥进行 XOR
- AddRoundKey:将 MixColumns 的输出与轮密钥进行 XOR
分组密码的模式———分组密码是如何迭代的
DES 和 AES 都属于分组密码,它们只能加密固定长度的明文。如果需要加密任意长度的明文,就需要对分组密码进行迭代,而分组密码的迭代方法就称为分组密码的 “模式”。
分组密码与流密码
- 密码算法可以分为分组密码和流密码两种。
- 分组密码( block cipher )是每次只能处理特定长度的一块数据的一类密码算法,这里的"—块” 就称为分组( block )。此外,一个分组的比特数就称为分组长度( block length) ,AES 的分组长度为 128 比特,因此 AES —次可加密 128 比特的明文,并生成 128 比特的密文。
- 流密码( stream cipher )是对数据流进行连续处理的一类密码算法。流密码中一般以 1 比特、
8 比特或 32 比特等为单位进行加密和解密。 - 千万不能使用 ECB模式。
模式
分组之后,接下来分组加密算法要解决的问题就是: 如何将分组后的各个块组织起来,协同实现明文整体的加解密。这个问题就是分组密码工作模式要解决的问题。常见的分组密码工作模式(简称分组模式或加密模式)有:
- ECB : 电子密码本(Electronic codebook)
- CBC : 密码块链接(Cipher-block chaining)
- PCBC : 填充密码块链接(Propagating cipher-block chaining), 也被称为 明文密码块链接(Plaintext cipher-block chaining)
- CFB : 密文反馈(Cipher feedback)
- OFB : 输出反馈(Output feedback)
- CTR : 计数器模式(Counter mode), 也被称为 ICM整数计数模式(Integer Counter Mode), 或 SIC模式(Segmented Integer Counter)
- GCM : 伽罗瓦/计数器模式(Galois/Counter Mode)
其中CBC、CTR、GCM是较为常用的分组模式,他们都需要一个随机初始化向量IV。
明文分组与密文分组
- 明文分组:即分组后的明文。明文分组的长度与分组密码算法的分
组长度是相等的。 - 密文分组:是指使用分组密码算法将明文分组加密之后所生成的密文。
ECB模式
ECB 模式的全称是 Electronic CodeBook 模式。在 ECB 模式中,将明文分组加密之后的结果将直接成为密文分组
当最后一个明文分组的内容小于分组长度时,需要用一些特定的数据进行填充( padding )。
总结:
- ECB 模式是所有模式中最简单的一种。
- 明文分组与密文分组是一一对应的关系,因此,如果明文中存在多个相同的明文分组,则这些明文分组最终都将被转换为相同的密文分组,只要观察一下密文,就可以知道明文中存在怎样的重复组合。
- ECB 模式的一大弱点,就是可以在不破译密文的情况下操纵明文,即只要将密文分组元素顺序改变就可以造成破坏
CBC模式(CBC, 密码块链接(Cipher-block chaining))
- CBC 模式是将前一个密文分组与当前明文分组的内容混合起来进行加密的,这样就可以避免 ECB 模式的弱点。
- CBC 模式的全称是 Cipher Block Chaining 模式( 密文分组链接模式 ),之所以叫这个名字,
是因为密文分组是像链条一样相互连接在一起的。
ECB 模式只进行了加密,而 CBC 模式则在加密之前进行了一次 XOR。
初始化向量
当加密第一个明文分组时,由于不存在 “前一个密文分组”,因此需要事先准备一个长度为—个分组的比特序列来代替 “前一个密文分组”,这个比特序列称为初始化向量( InitializationVector ), 通常缩写为 IV。一般来说,每次加密时都会随机产生一个不同的比特序列来作为初始化向量。
CBC 模式的特点
- 明文分组在加密之前一定会与 “前一个密文分组” 进行 XOR 运算,因此即便明文分组 1 和2 的值是相等的,密文分组 1 和 2 的值也不一定是相等的。这样一来,ECB 模式的缺陷在 CBC模式中就不存在了。
- 有一个分组损坏了(例如由于硬盘故障导致密文分组的值发生了改变等),解密时最多只会有 2 个分组受到数据损坏的影响
CBC 模式的应用实例
确保互联网安全的通信协议之一 SS1/TLS, 就是使用CBC 模式来确保通信的机密性的,如使用CBC 模式三重 DES 的 3DES_EDE_CBC 以及 CBC 模式 256 比特 AES 的 AES_256_CBC 等。
CFB 模式
-
CFB 模式的全称是 Cipher FeedBack 模式( 密文反馈模式 )
-
在 CFB 模式中,前一个密文分组会被送回到密码算法的输人端。所谓反馈,这里指的就是返回输入端的意思。
-
在生成第一个密文分组时,由于不存在前一个输出的数据,因此需要使用初始化向量( IV )来代替,这一点和 CBC 模式是相同的
OFB 模式
- OFB 模式的全称是 OutputFeedback 模式( 输出反馈模式 )。在 OFB 模式中,密码算法的输出会反馈到密码算法的输入中
CTR 模式
CTR 模式的全称是 CcmnTeR 模式( 计数器模式 )。CTR 模式是一种通过将逐次累加的计数器进行加密来生成密钥流的流密码
应该使用哪种模式
AES块加密过程
AES的BlockSize是16字节(128位),密码支持128/192/256位,其块加密过程:
- 对密钥进行扩展(expandKey)得到轮密钥,128/196/256位的密钥分别是16/24/32字节,会被扩展为长度为44/52/60的轮密钥数组(元素是int整数),会在后续的n轮计算中按4个一组分开使用,使用11/13/15轮。
- 将明文块以4个字节为单位切分为4个小块,并将每个小块转为整数int。
- 第一轮计算,将4个小块的整数值与分别与轮密钥的前四个整数(即第一轮密钥)进行异或运算,这个计算被称为密钥加法层(也叫轮密钥加,英文Add Round Key)。
- 从轮密钥的第5个元素开始,每四个元素一轮,与前一轮计算结果做设计好的系列运算,分别是字节代换层(SubByte)、行位移层(Shift Rows)、列混淆层(Mix Column)和密钥加法层,实际在代码中就是一个复杂的含有各种位移、异或等运算的公式。128/196/256位的密钥得来的轮密钥分别会在这里重复9/11/13轮。
- 最后一轮计算,使用轮密钥中最后4个元素,只做字节代换层、行位移层、密钥加法层。
- 将最终的结果拼接起来,就是本次块加密的结果密文块。
SM4加解密过程
SM4是我国国家标准的商用密码体系中提供的一种分组密码算法,可以参考国标GB/T 32907-2016。
SM4的BlockSize为16字节128位,密钥长度也是16字节128位,不支持其他长度。SM4的块加密的计算轮次固定为32轮,块加密和块解密使用相同的轮密钥,只是块解密时将轮密钥逆序使用即可。
SM4的块加密过程:
- 对密钥进行扩展,得到一个长度为32的int数组作为轮密钥数组。每轮计算使用一个int整数作为轮密钥。
- 对明文块做4个字节为单位的切分,得到4个字,SM4里定义一个字为4字节。
- 对4个字做32轮迭代计算,每轮通过轮函数F,输入最近4个字和本轮密钥计算下一个字。如第一轮输入前四个字和第一轮密钥计算第5个字,第二轮输入第2~5个字和第二轮密钥计算第6个字,以此类推,计算32轮后,一共有36个字。
- 将32轮迭代后的最后4个字进行反序拼接,得到16个字节的密文块。
对称加密的密钥与IV
对称加密算法的密钥和IV通常都要求使用一个伪随机数,最好是密码学安全的伪随机数。有时直接用某个PRNG或CSPRNG生成,有时则使用密钥派生算法派生。
import (
...
"crypto/rand"
...
)
func GetRandomBytes(len int) ([]byte, error) {
if len < 0 {
return nil, errors.New("len must be larger than 0")
}
buffer := make([]byte, len)
n, err := rand.Read(buffer)
if err != nil {
return nil, err
}
if n != len {
return nil, fmt.Errorf("buffer not filled. Requested [%d], got [%d]", len, n)
}
return buffer, nil
}
上面获取伪随机数的语句n, err := rand.Read(buffer)使用的是golang的crypto/rand包,对应的是使用系统熵值的CSPRNG,生成的是一个密码学安全的伪随机数。