DES从入门到cpp实现
一、定义
DES(Data Encryption Standard)是 1977 年 NIST 标准化的对称分组密码:
| 属性 | 值 |
|---|---|
| 分组大小 | 64 bit(8 字节) |
| 密钥大小 | 64 bit(8 字节),其中实际有效 56 bit,另 8 bit 是校验位 |
| 结构 | Feistel 网络,16 轮迭代 |
| 输出 | 64 bit 密文 |
| 现状 | 已被攻破(密钥太短),新项目用 AES |
二、原理
先来看流程图
妈的好复杂,你肯定这样想吧,我也这么想。
我们来拆解看看,这是两条线,简化为密钥扩展和数据加密。
先看明文的前些部分,我们看到了填充与分组。DES算法 是以64位为一组分组加密的,所以当输入的明文长度,不满足64的整数倍时,需要涉及到数据的填充。而分组的意思就和之前哈希算法分块差不多,他将填充后的原始消息进行分组,每组64位,依次对这N组进行加密,最终产出N组密文。
算法特点
参考DES加密算法原理及代码实现_des算法代码-CSDN博客
- 使用56位的密钥(一般密钥说是64位,实际上有效位只有56位,其余8位是校验位)
- 以64位位单位,对数据分组进行加密和解密(密文与明文长度相同,均为64位)。
- DES加密与解密使用同一密钥(属于对称加密)
- DES的保密性依赖于密钥。(DES的加密算法是公开的,密钥是保密的)
明文前置处理
填充部分省略了,这个后面会说,我们直接看填充后的8字节。
1.DES使用大端序,所以需要大端加载明文
uint64_t data = 0;for (int i = 0; i < 8; ++i) data = (data << 8) | block[i];2.IP 置换:一个 64→64 的位重排。规则是”输入第 58 位移到输出第 1 位,输入第 50 位移到输出第 2 位……”。

IP表58 50 42 34 26 18 10 260 52 44 36 28 20 12 462 54 46 38 30 22 14 664 56 48 40 32 24 16 857 49 41 33 25 17 9 159 51 43 35 27 19 11 361 53 45 37 29 21 13 563 55 47 39 31 23 15 7- 表是一个 8×8 的矩阵。
- 读取顺序:从左到右,从上到下。
- 数值含义:输出块第
i位的数据,来自于输入块的第IP[i]位。
IP 表第一行第一个数字是 58。这表示:输入明文第 58 位(从左往右数)的数据,经过 IP 置换后,成为了输出块的第 1 位(最左边)。
换句话讲,分组输入进来的原文不是64bit吗?按照这个表查,索引1的位置上是58,就代表明文的第58位将来会填在输出的第一位。
初始置换就是将明文打乱重排序。并生成L0 , R0。
L0取明文前32字节,R0取明文后32字节。当然这里说的是经过IP打乱后的明文。
密钥前置处理
好了,我们再来看看密钥这一侧的前置处理。
DES加密的过程,明文M共经历了16次运算迭代,每次迭代的数据长度是48 ,因此我们需要16组48位的加密秘钥来进行加密。
首先密钥是不会填充的,他必须得输入8字节。

依旧是大端加载,不多说。
PC-1表置换
PC-1表57 49 41 33 25 17 91 58 50 42 34 26 1810 2 59 51 43 35 2719 11 3 60 52 44 3663 55 47 39 31 23 157 62 54 46 38 30 2214 6 61 53 45 37 2921 13 5 28 20 12 4我们可以看到置换后,密钥从原来的64位变成了后面的56位。
DES 密钥的每个字节最低位是奇偶校验位,用于检测密钥传输错误。这 8 位不参与加密,PC-1 直接丢弃。
丢弃的位置是第 8、16、24、32、40、48、56、64 位。
CD瓜分
接着C0和D0各自瓜分走28比特。
16轮循环左移+查表
注意这里是对28比特长度的C和D进行循环左移,左移位数查看下面表格。
依次是C1,D1为C0,D0左移一位,C2,D2为C1,D1左移一位,C3,D3为C2,D2左移两位…
循环左移表轮次: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16左移: 1 1 2 2 2 2 2 2 1 2 2 2 2 2 2 1循环左移中间过程,我们会得到16个Cn和16个Dn,他们会被根据PC-2进行合并压缩。
PC-2 表:14 17 11 24 1 53 28 15 6 21 1023 19 12 4 26 816 7 27 20 13 241 52 31 37 47 5530 40 51 45 33 4844 49 39 56 34 5346 42 50 36 29 32每轮把左移后的 C||D(56bit)合并,用 PC-2 压缩成 48bit 子密钥:PC-2表共48个数据,所以上面拼接出来的的值56会被压缩。
ok,这样我们就能拿到16个子密钥了,每个都是48bit。
为什么是 48bit? 因为 Feistel 里 E 扩展把 R(32bit)扩成 48bit,子密钥必须也是 48bit 才能异或。
ok,到这里我们的明文和密钥处理就完成了,现在正式进入加密过程。
Feistel 网络
| 符号 | 含义 | 长度 | 来源 |
|---|---|---|---|
K_i | 第 i 轮子密钥 | 48 bit | 密钥扩展产生 |
R_i | 第 i 轮后的右半部分 | 32 bit | L_{i-1} ⊕ F(R_{i-1}, K_i) |
L_i | 第 i 轮后的左半部分 | 32 bit | 等于 R_{i-1} |
第一轮的L_1,R_1就是之前的IP置换后瓜分到的C和D
公式L_i = R_{i-1}R_i = L_{i-1} ⊕ F(R_{i-1}, K_i)关键性质:加密和解密结构完全相同,只需子密钥逆序。这是 Feistel 网络的对称美。我们可以看到L_i继承了上一轮的R值,真正在变化的只有R_i.
这里的重点是轮函数F,我们来看看吧。
F函数
F函数就是不断计算Li,Ri的过程,此过程包括四个部分:E盒置换、S盒置换、P盒置换、左右校验,我们简要的总结如下:
E盒扩展置换

E盒表32 1 2 3 4 5 4 5 6 7 8 9 8 9 10 11 12 1312 13 14 15 16 1716 17 18 19 20 2120 21 22 23 24 2524 25 26 27 28 2928 29 30 31 32 1- 输入:32 位
- 输出:48 位
- 读取顺序:从左到右,从上到下
输出第 i 位,来自输入的第 E[i] 位。
例如:
- 输出第 1 位 ← 输入第 32 位
- 输出第 2 位 ← 输入第 1 位
- 输出第 3 位 ← 输入第 2 位
我们可以看到E盒的中间四位是按顺序来的,两边多了两个无关的数字?无关吗?找个规律呢。
每一行的 6 位 = [上一组的最后一位] + [本组的 4 位] + [下一组的第一位]
为什么这么搞?
E 盒的目的不是“增加信息”,而是:
- 把 32 位扩展成 48 位,以便和 48 位子密钥异或。
- 让相邻的位产生重叠,使得 S 盒的输入中,每一位都受到多个输入位的影响。
- 增强扩散(Diffusion):一个输入位的改变,会影响多个 S 盒的输出。
如果只是简单地把 32 位重复一遍凑成 48 位,扩散效果会差很多。E 盒的“边界借位”让每个 S 盒的 6 位输入中,有 2 位来自相邻组,这样 S 盒的输出会互相影响。
子密钥异或
扩展后的数据与对应轮数的密钥进行异或,没啥好说的。
S盒替换
S 盒(Substitution Box)是 DES 里唯一非线性的部分,也是整个 DES 安全性的核心。
把刚刚异或的 48bit 分成 8 组 6bit,每组查一个 S 盒变成 4bit。
8 个 S 盒并行工作:8 × 6 = 48 位输入,8 × 4 = 32 位输出

每个s盒是一个 4 行 × 16 列 的表,里面存的是 0~15 的数(4 位)
这里以s1为例
S1:14 4 13 1 2 15 11 8 3 10 6 12 5 9 0 7 0 15 7 4 14 2 13 1 10 6 12 11 9 5 3 8 4 1 14 8 13 6 2 11 15 12 9 7 3 10 5 015 12 8 2 4 9 1 7 5 11 3 14 10 0 6 13- 行号:0~3
- 列号:0~15
- 值:0~15
注意这里是从0开始了,与之前的不同。
查表规则:
- 行号 =
b1 b6(第 1 位和第 6 位拼成的 2 位数,范围 0~3) - 列号 =
b2 b3 b4 b5(中间 4 位拼成的 4 位数,范围 0~15) - 查表得到 4 位数,就是输出
大白话:将6bit数据的左右两位拆下来合并做行号,中间剩的做列号
举个例子就是
输入 101100:
b1 b6=10= 2 → 行号 2b2 b3 b4 b5=0110= 6 → 列号 6- 查
S1第 2 行第 6 列 → 值是2→ 二进制0010
所以 S1(101100) = 0010。
完整8个s盒如下
S1:14 4 13 1 2 15 11 8 3 10 6 12 5 9 0 7 0 15 7 4 14 2 13 1 10 6 12 11 9 5 3 8 4 1 14 8 13 6 2 11 15 12 9 7 3 10 5 015 12 8 2 4 9 1 7 5 11 3 14 10 0 6 13
S2:15 1 8 14 6 11 3 4 9 7 2 13 12 0 5 10 3 13 4 7 15 2 8 14 12 0 1 10 6 9 11 5 0 14 7 11 10 4 13 1 5 8 12 6 9 3 2 1513 8 10 1 3 15 4 2 11 6 7 12 0 5 14 9
S3:10 0 9 14 6 3 15 5 1 13 12 7 11 4 2 813 7 0 9 3 4 6 10 2 8 5 14 12 11 15 113 6 4 9 8 15 3 0 11 1 2 12 5 10 14 7 1 10 13 0 6 9 8 7 4 15 14 3 11 5 2 12
S4: 7 13 14 3 0 6 9 10 1 2 8 5 11 12 4 1513 8 11 5 6 15 0 3 4 7 2 12 1 10 14 910 6 9 0 12 11 7 13 15 1 3 14 5 2 8 4 3 15 0 6 10 1 13 8 9 4 5 11 12 7 2 14
S5: 2 12 4 1 7 10 11 6 8 5 3 15 13 0 14 914 11 2 12 4 7 13 1 5 0 15 10 3 9 8 6 4 2 1 11 10 13 7 8 15 9 12 5 6 3 0 1411 8 12 7 1 14 2 13 6 15 0 9 10 4 5 3
S6:12 1 10 15 9 2 6 8 0 13 3 4 14 7 5 1110 15 4 2 7 12 9 5 6 1 13 14 0 11 3 8 9 14 15 5 2 8 12 3 7 0 4 10 1 13 11 6 4 3 2 12 9 5 15 10 11 14 1 7 6 0 8 13
S7: 4 11 2 14 15 0 8 13 3 12 9 7 5 10 6 113 0 11 7 4 9 1 10 14 3 5 12 2 15 8 6 1 4 11 13 12 3 7 14 10 15 6 8 0 5 9 2 6 11 13 8 1 4 10 7 9 5 0 15 14 2 3 12
S8:13 2 8 4 6 15 11 1 10 9 3 14 5 0 12 7 1 15 13 8 10 3 7 4 12 5 6 11 0 14 9 2 7 11 4 1 9 12 14 2 0 6 10 13 15 3 5 8 2 1 14 7 4 10 8 13 15 12 9 0 3 5 6 11p盒置换
16 7 20 21 29 12 28 17 1 15 23 26 5 18 31 10 2 8 24 14 32 27 3 9 19 13 30 6 22 11 4 2532位输入,32位输出,和之前IP表,PC表一个查法
16轮F函数之后
16轮结束后,要进行左右交换,这点很重要。
妈的我就知道你忘了一开始的流程图
将f函数的过程使用k1-k16,迭代16次,即经过16次的交换替代,得到L16 、R16,并将此作为输入进行逆置换得到最后的密文,逆置换的过程同初始置换,不再赘述。
FP表40 8 48 16 56 24 64 32 39 7 47 15 55 23 63 3138 6 46 14 54 22 62 30 37 5 45 13 53 21 61 2936 4 44 12 52 20 60 28 35 3 43 11 51 19 59 2734 2 42 10 50 18 58 26 33 1 41 9 49 17 57 25三、ECB模式和CBC模式
DES 只能处理 8 字节块,实际使用需要配合模式和填充。
7.1 PKCS#7 填充
补到 8 的整数倍:
- 差 N 字节(1 ≤ N ≤ 8)→ 补 N 个值为 N 的字节
- 恰好 8 的倍数 → 补 8 个
0x08
例:明文 5 字节 → 补 3 个 0x03;明文 16 字节 → 补 8 个 0x08。
ECB模式
现在已经不咋用了,不太安全。公式C_i = E(P_i)
特点:
- 每个块独立加密,相同明文块产生相同密文块
- ❌ 不安全:会泄露明文的模式(比如图片 ECB 加密后还能看出轮廓)
- ✅ 优点:可并行、实现简单
CBC模式
加密:C_i = E(P_i ⊕ C_{i-1}),其中 C_0 = IV
解密:P_i = D(C_i) ⊕ C_{i-1}
特点:
- 相同明文块在不同位置产生不同密文
- ✅ 安全(相比 ECB)
- ❌ 不能并行加密(解密可以并行)
- 需要 IV(初始向量),IV 每次加密必须随机
7.4 ECB vs CBC 对比
| 维度 | ECB | CBC |
|---|---|---|
| 相同明文块 | 相同密文 | 不同密文 |
| 安全性 | ❌ 差 | ✅ 好 |
| 并行加密 | ✅ 可 | ❌ 不可 |
| 并行解密 | ✅ 可 | ✅ 可 |
| 需要 IV | ❌ 不需要 | ✅ 需要 |
| 推荐场景 | 不推荐 | 传统主流 |
四、cpp实现
常量表
先来定义常量表备用
#include <iostream>#include <cstdint>#include <cstring>#include <string>#include <vector>#include <array>#include <stdexcept>#include <iomanip>#include <sstream>
// ============================================================// DES 常量表// ============================================================namespace tables {
// IP 初始置换constexpr std::array<uint8_t, 64> IP = { 58,50,42,34,26,18,10, 2, 60,52,44,36,28,20,12, 4, 62,54,46,38,30,22,14, 6, 64,56,48,40,32,24,16, 8, 57,49,41,33,25,17, 9, 1, 59,51,43,35,27,19,11, 3, 61,53,45,37,29,21,13, 5, 63,55,47,39,31,23,15, 7};
// FP 逆初始置换constexpr std::array<uint8_t, 64> FP = { 40, 8,48,16,56,24,64,32, 39, 7,47,15,55,23,63,31, 38, 6,46,14,54,22,62,30, 37, 5,45,13,53,21,61,29, 36, 4,44,12,52,20,60,28, 35, 3,43,11,51,19,59,27, 34, 2,42,10,50,18,58,26, 33, 1,41, 9,49,17,57,25};
// E 扩展constexpr std::array<uint8_t, 48> E = { 32, 1, 2, 3, 4, 5, 4, 5, 6, 7, 8, 9, 8, 9,10,11,12,13, 12,13,14,15,16,17, 16,17,18,19,20,21, 20,21,22,23,24,25, 24,25,26,27,28,29, 28,29,30,31,32, 1};
// P 置换constexpr std::array<uint8_t, 32> P = { 16, 7,20,21,29,12,28,17, 1,15,23,26, 5,18,31,10, 2, 8,24,14,32,27, 3, 9,19,13,30, 6,22,11, 4,25};
// PC-1 密钥置换constexpr std::array<uint8_t, 56> PC1 = { 57,49,41,33,25,17, 9, 1,58,50,42,34,26,18, 10, 2,59,51,43,35,27, 19,11, 3,60,52,44,36, 63,55,47,39,31,23,15, 7,62,54,46,38,30,22, 14, 6,61,53,45,37,29, 21,13, 5,28,20,12, 4};
// PC-2 压缩置换constexpr std::array<uint8_t, 48> PC2 = { 14,17,11,24, 1, 5, 3,28,15, 6,21,10, 23,19,12, 4,26, 8, 16, 7,27,20,13, 2, 41,52,31,37,47,55, 30,40,51,45,33,48, 44,49,39,56,34,53, 46,42,50,36,29,32};
// 循环左移位数表constexpr std::array<uint8_t, 16> SHIFT = { 1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1};
// S 盒constexpr uint8_t SBOX[8][4][16] = { {{14, 4,13, 1, 2,15,11, 8, 3,10, 6,12, 5, 9, 0, 7}, { 0,15, 7, 4,14, 2,13, 1,10, 6,12,11, 9, 5, 3, 8}, { 4, 1,14, 8,13, 6, 2,11,15,12, 9, 7, 3,10, 5, 0}, {15,12, 8, 2, 4, 9, 1, 7, 5,11, 3,14,10, 0, 6,13}}, {{15, 1, 8,14, 6,11, 3, 4, 9, 7, 2,13,12, 0, 5,10}, { 3,13, 4, 7,15, 2, 8,14,12, 0, 1,10, 6, 9,11, 5}, { 0,14, 7,11,10, 4,13, 1, 5, 8,12, 6, 9, 3, 2,15}, {13, 8,10, 1, 3,15, 4, 2,11, 6, 7,12, 0, 5,14, 9}}, {{10, 0, 9,14, 6, 3,15, 5, 1,13,12, 7,11, 4, 2, 8}, {13, 7, 0, 9, 3, 4, 6,10, 2, 8, 5,14,12,11,15, 1}, {13, 6, 4, 9, 8,15, 3, 0,11, 1, 2,12, 5,10,14, 7}, { 1,10,13, 0, 6, 9, 8, 7, 4,15,14, 3,11, 5, 2,12}}, {{ 7,13,14, 3, 0, 6, 9,10, 1, 2, 8, 5,11,12, 4,15}, {13, 8,11, 5, 6,15, 0, 3, 4, 7, 2,12, 1,10,14, 9}, {10, 6, 9, 0,12,11, 7,13,15, 1, 3,14, 5, 2, 8, 4}, { 3,15, 0, 6,10, 1,13, 8, 9, 4, 5,11,12, 7, 2,14}}, {{ 2,12, 4, 1, 7,10,11, 6, 8, 5, 3,15,13, 0,14, 9}, {14,11, 2,12, 4, 7,13, 1, 5, 0,15,10, 3, 9, 8, 6}, { 4, 2, 1,11,10,13, 7, 8,15, 9,12, 5, 6, 3, 0,14}, {11, 8,12, 7, 1,14, 2,13, 6,15, 0, 9,10, 4, 5, 3}}, {{12, 1,10,15, 9, 2, 6, 8, 0,13, 3, 4,14, 7, 5,11}, {10,15, 4, 2, 7,12, 9, 5, 6, 1,13,14, 0,11, 3, 8}, { 9,14,15, 5, 2, 8,12, 3, 7, 0, 4,10, 1,13,11, 6}, { 4, 3, 2,12, 9, 5,15,10,11,14, 1, 7, 6, 0, 8,13}}, {{ 4,11, 2,14,15, 0, 8,13, 3,12, 9, 7, 5,10, 6, 1}, {13, 0,11, 7, 4, 9, 1,10,14, 3, 5,12, 2,15, 8, 6}, { 1, 4,11,13,12, 3, 7,14,10,15, 6, 8, 0, 5, 9, 2}, { 6,11,13, 8, 1, 4,10, 7, 9, 5, 0,15,14, 2, 3,12}}, {{13, 2, 8, 4, 6,15,11, 1,10, 9, 3,14, 5, 0,12, 7}, { 1,15,13, 8,10, 3, 7, 4,12, 5, 6,11, 0,14, 9, 2}, { 7,11, 4, 1, 9,12,14, 2, 0, 6,10,13,15, 3, 5, 8}, { 2, 1,14, 7, 4,10, 8,13,15,12, 9, 0, 3, 5, 6,11}}};
} // namespace tablesPKCS7填充
// ============================================================// PKCS#7 填充// ============================================================namespace pkcs7 {
std::vector<uint8_t> pad(const std::vector<uint8_t>& data, size_t blockSize) { size_t padLen = blockSize - (data.size() % blockSize);//计算出要填充的长度,用分组大小减去数据取模块大小 std::vector<uint8_t> out = data; //拷贝一份避免修改原数据。 out.insert(out.end(), padLen, static_cast<uint8_t>(padLen));//在out的末尾插入padLen个值为padLen的字节。 //static_cast<uint8_t>(padLen):把 padLen(size_t 类型)转成 uint8_t,因为 padLen 最大是 16,能放进一个字节。 return out;}
std::vector<uint8_t> unpad(const std::vector<uint8_t>& data) { if (data.empty()) throw std::runtime_error("empty data");
uint8_t padLen = data.back(); //取最后一个字节,因为填充就是按照长度的数值填充的。
if (padLen == 0 || padLen > 8 || padLen > data.size()) { throw std::runtime_error("invalid PKCS#7 padding"); } for (size_t i = data.size() - padLen; i < data.size(); ++i) { if (data[i] != padLen) { throw std::runtime_error("invalid PKCS#7 padding"); } } return std::vector<uint8_t>(data.begin(), data.end() - padLen);//直接返回一个新的数组,减去了填充的长度即为原始数据长度。}
} // namespace pkcs7DES实现类
前方高能,一大段DES定义即将来袭。
// ============================================================// DES 核心类// ============================================================class DES {public: static constexpr size_t BLOCK_SIZE = 8; static constexpr size_t KEY_SIZE = 8;
//explicit 关键字,禁止隐式类型转换 explicit DES(const std::vector<uint8_t>& key) { if (key.size() != KEY_SIZE) { throw std::invalid_argument("DES key must be 8 bytes"); } generateSubkeys(key.data()); //这个函数是私有函数,在下面,可能把他放到上面会更好阅读点,这里我的。。。 }
void encryptBlock(uint8_t block[BLOCK_SIZE]) const { processBlock(block, true); }
void decryptBlock(uint8_t block[BLOCK_SIZE]) const { processBlock(block, false); }
private: uint64_t subkeys_[16]; //
// -------------------------------------------------------- // 通用位痣,置换函数(注意断句) // table : 置换表,1-based,第 1 位是 MSB // n : 输出位数 // in : 输入值(右对齐,低 inBits 位有效) // inBits : 输入的有效位宽 // -------------------------------------------------------- static uint64_t permute(const uint8_t* table, int n, uint64_t in, int inBits) { //该函数的作用就是查找对应的置换表然后将输入数据指定位置的数置换成表里面的数 uint64_t out = 0; for (int i = 0; i < n; ++i) { int pos = table[i]; uint64_t bit = (in >> (inBits - pos)) & 1ULL; //获取输入数据pos位的比特数 /* 注意1ULL 就是一个无符号长长整型(unsigned long long)的常量 1。 举个例子 in = 0b1101 pos = 2, inBits = 4 inBits - pos = 2 in >> 2 = 0b11 0b11 & 1 = 1 */ out = (out << 1) | bit; //把刚取出的 1 个 bit 拼到输出末尾。 } return out; }
// 28bit 循环左移 static uint32_t rotl28(uint32_t x, int n) { return ((x << n) | (x >> (28 - n))) & 0x0FFFFFFFU; }
// 密钥扩展 void generateSubkeys(const uint8_t key[KEY_SIZE]) { using namespace tables;
// 1. 大端加载 64bit 密钥 uint64_t key64 = 0; for (int i = 0; i < 8; ++i) { key64 = (key64 << 8) | key[i]; }
// 2. PC-1: 64 → 56(输入 64bit) // PC1.data()返回PC1数组第一个元素 uint64_t pc1 = permute(PC1.data(), 56, key64, 64);
// 3. 拆 C / D(各 28bit) uint32_t C = static_cast<uint32_t>((pc1 >> 28) & 0x0FFFFFFFU); uint32_t D = static_cast<uint32_t>(pc1 & 0x0FFFFFFFU);
// 4. 16 轮:左移 + PC-2 压缩 for (int i = 0; i < 16; ++i) { C = rotl28(C, SHIFT[i]); //循环左移位数也是要查表的记得吗?懂我意思?👌 D = rotl28(D, SHIFT[i]);
uint64_t CD = (static_cast<uint64_t>(C) << 28) | D; // 56bit // PC-2 的输入是 56bit,不是 64bit,输出是48bit subkeys_[i] = permute(PC2.data(), 48, CD, 56) & 0xFFFFFFFFFFFFULL; } }
// 轮函数 F static uint32_t feistel(uint32_t R, uint64_t subkey) { using namespace tables;
// E 扩展:输入 32bit uint64_t expanded = permute(E.data(), 48, R, 32);
// 异或子密钥 expanded ^= subkey;
// S 盒:48 → 32,最关键一步 uint32_t sOut = 0; for (int i = 0; i < 8; ++i) { int shift = (7 - i) * 6; //记住咱们s盒是从索引0开始的,每个盒子接受6个bit uint8_t six = static_cast<uint8_t>((expanded >> shift) & 0x3F); int row = ((six & 0x20) >> 4) | (six & 0x01); //行号就是首末两bit int col = (six >> 1) & 0x0F; uint8_t val = SBOX[i][row][col]; sOut = (sOut << 4) | val; }
// P 置换:输入 32bit return static_cast<uint32_t>(permute(P.data(), 32, sOut, 32)); }
// 单块处理 void processBlock(uint8_t block[BLOCK_SIZE], bool encrypt) const { using namespace tables;
// 1. 大端加载 uint64_t data = 0; for (int i = 0; i < 8; ++i) { data = (data << 8) | block[i]; }
// 2. IP 置换:输入 64bit uint64_t ip = permute(IP.data(), 64, data, 64);
// 3. 拆 L / R uint32_t L = static_cast<uint32_t>(ip >> 32); uint32_t R = static_cast<uint32_t>(ip & 0xFFFFFFFFULL);
// 4. 16 轮 Feistel for (int i = 0; i < 16; ++i) { int ki = encrypt ? i : (15 - i); uint32_t F = feistel(R, subkeys_[ki]); uint32_t newR = L ^ F; L = R; R = newR; }
// 5. 左右交换 uint64_t preoutput = (static_cast<uint64_t>(R) << 32) | L;
// 6. FP 置换:输入 64bit uint64_t out = permute(FP.data(), 64, preoutput, 64);
// 7. 大端拆回字节 for (int i = 0; i < 8; ++i) { block[i] = static_cast<uint8_t>((out >> (56 - i * 8)) & 0xFF); } }};ECB模式
我们先来看看ECB模式
// ============================================================// ECB 模式// ============================================================class DESECB {public: explicit DESECB(const std::vector<uint8_t>& key) : des_(key) {}
std::vector<uint8_t> encrypt(const std::vector<uint8_t>& plaintext) const { std::vector<uint8_t> data = pkcs7::pad(plaintext, DES::BLOCK_SIZE);//先对数据进行填充 std::vector<uint8_t> out(data.size()); //声明输出
for (size_t i = 0; i < data.size(); i += DES::BLOCK_SIZE) { uint8_t block[DES::BLOCK_SIZE]; std::memcpy(block, data.data() + i, DES::BLOCK_SIZE); des_.encryptBlock(block); //ECB就这样,对每个块分别加密 std::memcpy(out.data() + i, block, DES::BLOCK_SIZE); } return out; }
std::vector<uint8_t> decrypt(const std::vector<uint8_t>& ciphertext) const { if (ciphertext.size() % DES::BLOCK_SIZE != 0) { throw std::invalid_argument("ciphertext size must be multiple of 8"); }
std::vector<uint8_t> out(ciphertext.size()); for (size_t i = 0; i < ciphertext.size(); i += DES::BLOCK_SIZE) { uint8_t block[DES::BLOCK_SIZE]; std::memcpy(block, ciphertext.data() + i, DES::BLOCK_SIZE); des_.decryptBlock(block); std::memcpy(out.data() + i, block, DES::BLOCK_SIZE); } return pkcs7::unpad(out); }
private: DES des_;};CBC模式
// ============================================================// CBC 模式// ============================================================class DESCBC {public: DESCBC(const std::vector<uint8_t>& key, const std::vector<uint8_t>& iv) : des_(key) { if (iv.size() != DES::BLOCK_SIZE) { throw std::invalid_argument("IV must be 8 bytes"); } std::memcpy(iv_, iv.data(), DES::BLOCK_SIZE); }
std::vector<uint8_t> encrypt(const std::vector<uint8_t>& plaintext) const { std::vector<uint8_t> data = pkcs7::pad(plaintext, DES::BLOCK_SIZE); std::vector<uint8_t> out(data.size());
uint8_t prev[DES::BLOCK_SIZE]; std::memcpy(prev, iv_, DES::BLOCK_SIZE); //记录向量
for (size_t i = 0; i < data.size(); i += DES::BLOCK_SIZE) { uint8_t block[DES::BLOCK_SIZE]; for (size_t j = 0; j < DES::BLOCK_SIZE; ++j) { block[j] = data[i + j] ^ prev[j]; } des_.encryptBlock(block); std::memcpy(out.data() + i, block, DES::BLOCK_SIZE); std::memcpy(prev, block, DES::BLOCK_SIZE); //更新向量用作下一轮的输入向量 } return out; }
std::vector<uint8_t> decrypt(const std::vector<uint8_t>& ciphertext) const { if (ciphertext.size() % DES::BLOCK_SIZE != 0) { throw std::invalid_argument("ciphertext size must be multiple of 8"); }
std::vector<uint8_t> out(ciphertext.size());
uint8_t prev[DES::BLOCK_SIZE]; std::memcpy(prev, iv_, DES::BLOCK_SIZE);
for (size_t i = 0; i < ciphertext.size(); i += DES::BLOCK_SIZE) { uint8_t block[DES::BLOCK_SIZE]; std::memcpy(block, ciphertext.data() + i, DES::BLOCK_SIZE);
uint8_t cipherBlock[DES::BLOCK_SIZE]; std::memcpy(cipherBlock, block, DES::BLOCK_SIZE);
des_.decryptBlock(block); for (size_t j = 0; j < DES::BLOCK_SIZE; ++j) { out[i + j] = block[j] ^ prev[j]; //异或反运算就是异或 } std::memcpy(prev, cipherBlock, DES::BLOCK_SIZE); } return pkcs7::unpad(out); }
private: DES des_; uint8_t iv_[DES::BLOCK_SIZE];};惊为天人啊!根本看不懂!
五、3DES
最后说说这家伙吧。
有两种常见模式,第二种比较常用。
- EEE 模式:
C = E_K3(E_K2(E_K1(P))),三次加密,纯扩展密钥空间。 - EDE 模式:
C = E_K3(D_K2(E_K1(P))),加密-解密-加密。
我们主要说EDE模式,这个模式也有分类,具体就是密钥个数。我们可以用三个,两个,一个。用一个和普通DES没区别,用两个如果前两次加密用同一个密钥的话还是和普通加密没区别,所以用三个才是优质之选。
流程就是加密——解密——加密,当然也可以解密——加密——解密
说直白点就是将原来CBC链式异或过程中的DES加密给改成了这三傻货。我给你瞅瞅原图,你就知道了
下面这个是原来的CBC
这个是3DES-CBC
当然,不可能只有三个分组,这个视具体情况而定。
六、结语
好了,今天就到这里吧,一个人走到这里也挺累的,目前还休息不了,什么时候是个头呢。。。
部分信息可能已经过时









