ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

从零手写DES算法:Java纯手工实现,无依赖、全流程详解

从零手写DES算法:Java纯手工实现,无依赖、全流程详解

如果你刷过不少加密算法相关的帖子,八成见过类似的评论:“反正都是调包,谁还看原理?”工程里我确实更愿意直接调现成库,但这次要聊的是一个完全相反的体验:把DES算法在Java里从零纯手工实现一遍,不引入任何依赖,所有置换表、S盒、Feistel结构、16轮子密钥调度全部自己写。标题里那句“无需导包,简单易懂还很全”不是夸张,我的实现里连java.util.Arrays都没有 import,核心逻辑就是一堆数组、循环和位运算。

DES 的全称是 Data Encryption Standard,数据加密标准,属于对称分组密码。它一次处理一个64比特的数据块,密钥名义上也是64比特,实际参与运算的只有56比特,剩下8比特是奇偶校验位。整个算法固定跑16轮,核心是一个叫作 Feistel 的结构。虽然现在生产环境早就是AES的天下,但DES在密码学教学、面试手写算法、内部演示系统里依然是常青树,因为它的模块足够清晰,又不像AES那样把字节代换和列混合搅在一起,特别适合拿来“练手”。

这篇文章适合三类人:正在学密码学、不想只背结论的学生;需要在某个内部演示或作业里交付一份完整DES实现的开发者;以及想练一练位运算和底层功底的Java工程师。我会把设计思路、完整代码、结果验证和我在实际操作里踩过的坑都摊开讲,你按着顺序把代码块拼起来就能跑。

1. 整体拆解:DES 的设计骨架

1.1 Feistel 结构:一轮到底做了什么

DES 的核心是 Feistel 结构。这个结构最大的好处是:加密和解密使用完全相同的轮函数,只是子密钥使用顺序相反,也就是说你不需要为解密单独设计一套算法。

先看一轮加密的流程。64比特的输入经过初始置换 IP 后,分成左右两半,各32比特,记为 L 和 R。之后重复16轮,每轮干的事就两行:

newL = R newR = L XOR F(R, K)

这里 F 是轮函数,K 是这一轮的子密钥。一轮结束后,左右交换位置,进入下一轮。你可以把它想象成两个人互相换手上的东西:右手的东西原封不动交给左手,左手的东西则跟“右手经过某种计算后”的结果做异或,再交给右手。异或这个操作是可逆的,而且只需要再异或一次同样的值就能恢复,所以十六轮做完,只要把子密钥倒着用一遍,就能逐步还原出原来的 L 和 R。

需要注意一个细节:16轮做完之后,输出的前32位是 R16,后32位是 L16,而不是 L16||R16。这看起来像“少交换了一次”,但实际上是为了让加解密流程保持一致。如果你写代码时把左右顺序搞反,加密结果就会完全错掉。

整个流程用文字描述就是:明文块 -> IP置换 -> 左右拆开 -> Feistel循环16轮 -> 最后拼接成 R16||L16 -> FP逆置换 -> 密文块。其中的 IP 和 FP 互为逆置换,作用是把数据重新“洗牌”,让算法在硬件实现上更顺,也把相邻比特分散开。

1.2 56位密钥如何变成16个48位子密钥

DES 的密钥名义上是64比特,也就是8个字节,但每个字节的最后一位是奇偶校验位,不参与运算。所以真正有用的只有56比特,而这56比特还要进一步扩展成16个48位的子密钥。

密钥调度的流程是:

  1. 把64位密钥做 PC-1 置换,去掉8位校验位,得到56位。
  2. 把56位分成两半:C 和 D,各28位。
  3. 每一轮分别把 C 和 D 循环左移,移动位数由固定表决定。
  4. 把左移后的 C、D 拼接成56位,再做 PC-2 压缩置换,得到48位的子密钥。

循环左移的位数表是1,1,2,2,2,2,2,2,1,2,2,2,2,2,2,1。也就是说前两轮各移1位,第3到第8轮各移2位,第9轮移1位,后面七轮各移2位。这个表没有特别神秘的来源,就是标准里写死的,你把表抄对就行。

PC-1 和 PC-2 都是置换,不是压缩后再加入什么新位,只是从固定位置抽取比特。PC-1 从64位里挑56位,PC-2 从56位里挑48位。两次置换下来,每一轮子密钥使用的实际密钥比特都不太一样,这保证了16轮之间不会出现重复模式。

1.3 轮函数F:扩展、异或、S盒、置换

轮函数 F 是DES里最容易抄错的部分,它由四个动作拼成:

  • E扩展置换:把32位的 R 扩展成48位。方法是把32位分成8组,每组4位,每组复制首尾两个相邻位,变成6位。所以8组就是48位。
  • 与子密钥异或:把扩展后的48位跟这一轮的48位子密钥逐位异或。
  • S盒替换:48位分成8组,每组6位,分别送进8个不同的S盒。每个S盒输出4位,总共得到32位。
  • P置换:把32位结果按 P 表重新排列。

S盒是整个DES唯一的非线性部件。你可以把它理解成一张查表:6位输入,首尾两位拼成行号,中间四位拼成列号,查表得到0到15之间的一个数,转成4位二进制输出。如果DES没有S盒,整个算法就会退化成一堆线性变换的组合,破解会变得极其容易。S盒表的具体数值是设计者精心挑选的,用来抵抗差分分析和线性分析,咱们做实现的时候不需要理解每个数的来历,但抄表绝对不能抄错。

2. Java 实现与核心代码解析

2.1 类骨架与全部常数表

实现DES不需要任何第三方库,我把所有逻辑封在一个DES类里。类的结构分成四块:常数表、位工具方法、核心算法、对外接口。下面这段是常数区,我建议你直接复制到类的最开头。

public class DES { // 初始置换 IP private static final int[] 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(IP-1) private static final int[] 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 扩展置换:32 -> 48 private static final int[] 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 置换:32 -> 32 private static final int[] 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:64位密钥 -> 56位 private static final int[] 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:56位 -> 48位子密钥 private static final int[] 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 }; // 每轮循环左移位数 private static final int[] SHIFT = { 1,1,2,2,2,2,2,2,1,2,2,2,2,2,2,1 }; // 8个S盒,每个S盒4行16列 private static final int[][] SBOX = { { // 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,0, 15,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,15, 13,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,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 }, { // S4 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 }, { // S5 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 }, { // S6 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 }, { // S7 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 }, { // 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,11 } }; // 辅助方法如下... }

看到这些大表先别慌。以我手写过多遍的经验,最容易被搞坏的恰恰就是这里。常见错误有三种:表抄漏一行、表项写成0到63而不是1到64、S盒里某一行的16个数顺序抄错。置换表本身并不难理解:表里每个数字,都是从原始比特位置取第几个比特,放到结果里第几个位置。因为标准里用1到64编号,代码里取下标时一定要减1。

2.2 位工具方法:用 boolean[] 代替位运算

实现DES最直观的方式是用boolean[]来存放比特位。每个下标存一个布尔值,长度为64就代表64比特。虽然比位运算繁琐一点,但排错极其方便,打印中间结果特别清楚,非常适合教学和展示。让我先介绍几个基础方法。

private static boolean[] slice(boolean[] src, int from, int to) { boolean[] dst = new boolean[to - from]; for (int i = from; i < to; i++) { dst[i - from] = src[i]; } return dst; } private static byte[] sliceBytes(byte[] src, int from, int to) { byte[] dst = new byte[to - from]; for (int i = from; i < to; i++) { dst[i - from] = src[i]; } return dst; } private static boolean[] xor(boolean[] a, boolean[] b) { boolean[] dst = new boolean[a.length]; for (int i = 0; i < a.length; i++) { dst[i] = a[i] ^ b[i]; } return dst; } private static boolean[] leftShift(boolean[] src, int n) { boolean[] dst = new boolean[src.length]; for (int i = 0; i < src.length; i++) { dst[i] = src[(i + n) % src.length]; } return dst; } private static boolean[] permute(boolean[] input, int[] table) { boolean[] output = new boolean[table.length]; for (int i = 0; i < table.length; i++) { output[i] = input[table[i] - 1]; } return output; } private static boolean[] toBits(byte[] block) { boolean[] bits = new boolean[64]; for (int i = 0; i < 8; i++) { for (int j = 0; j < 8; j++) { bits[i * 8 + j] = ((block[i] >>> (7 - j)) & 1) == 1; } } return bits; } private static byte[] toBytes(boolean[] bits) { byte[] block = new byte[8]; for (int i = 0; i < 8; i++) { int v = 0; for (int j = 0; j < 8; j++) { v = (v << 1) | (bits[i * 8 + j] ? 1 : 0); } block[i] = (byte) v; } return block; } public static String toHex(byte[] data) { char[] hex = "0123456789ABCDEF".toCharArray(); char[] out = new char[data.length * 2]; for (int i = 0; i < data.length; i++) { out[i * 2] = hex[(data[i] >>> 4) & 0x0F]; out[i * 2 + 1] = hex[data[i] & 0x0F]; } return new String(out); } public static byte[] fromHex(String s) { byte[] data = new byte[s.length() / 2]; for (int i = 0; i < data.length; i++) { int high = Character.digit(s.charAt(i * 2), 16); int low = Character.digit(s.charAt(i * 2 + 1), 16); data[i] = (byte) ((high << 4) | low); } return data; }

permute是DES的灵魂方法,IP、FP、E、P、PC-1、PC-2 六张表全部由它统一处理。实现思路就是:目标数组的第 i 位,取源数组中table[i] - 1位。因为标准表的编号从1开始,而Java数组下标从0开始,所以这里必须减1。忘了减1,轻则越界,重则整个输出错位。

toBits把8字节变成64个布尔值,遵循“大端风格”:每个字节的最高位对应bits[i*8],最低位对应bits[i*8+7]。如果你习惯小端,后面所有置换表都要跟着改,那会灾难性。所以在整个项目里,统一使用“第一个比特是最高位”的约定。

2.3 子密钥调度:16个48位子密钥怎么生成

有了工具方法,生成子密钥就顺理成章了。注意我这里完全没用到System.arraycopy,就是为了保持“零 import”的纯粹性。

private static boolean[][] generateSubKeys(byte[] key) { boolean[] keyBits = toBits(key); boolean[] key56 = permute(keyBits, PC1); boolean[] c = slice(key56, 0, 28); boolean[] d = slice(key56, 28, 56); boolean[][] subKeys = new boolean[16][48]; for (int i = 0; i < 16; i++) { c = leftShift(c, SHIFT[i]); d = leftShift(d, SHIFT[i]); boolean[] cd = new boolean[56]; for (int j = 0; j < 28; j++) { cd[j] = c[j]; cd[j + 28] = d[j]; } subKeys[i] = permute(cd, PC2); } return subKeys; }

这段代码最需要注意的是leftShift是循环左移,不是普通左移。移出的高位比特要绕回到低位,否则后面PC-2取位时会错位。我把 C 和 D 分别看作两个环,每次移动 n 位,就是把整段数据看成首尾相连的圆环,整体转一格或多格。

子密钥生成是整个流程里最容易“加密成功、解密失败”的地方。如果你在加密函数里用顺序子密钥,解密函数里也用了顺序而不是逆序,密文就会像被搅烂的纸团,怎么都解不回来。后面我会专门讲这个坑。

2.4 F函数:S盒查表与置换

F函数是DES的核心计算模块,这块代码建议自己敲一遍:

private static boolean[] fFunction(boolean[] right32, boolean[] subKey48) { boolean[] expanded = permute(right32, E); boolean[] xored = xor(expanded, subKey48); boolean[] afterSBox = new boolean[32]; for (int i = 0; i < 8; i++) { boolean[] group = slice(xored, i * 6, i * 6 + 6); int row = (group[0] ? 1 : 0) * 2 + (group[5] ? 1 : 0); int col = (group[1] ? 1 : 0) * 8 + (group[2] ? 1 : 0) * 4 + (group[3] ? 1 : 0) * 2 + (group[4] ? 1 : 0); int value = SBOX[i][row * 16 + col]; for (int j = 0; j < 4; j++) { afterSBox[i * 4 + j] = ((value >>> (3 - j)) & 1) == 1; } } return permute(afterSBox, P); }

S盒查表有个经典坑:行号是6位输入的首尾两位拼出来的,中间4位才是列号。第一次写的时候,十个人有八个会把行列搞反。我也翻过车,输出全乱,排查了半天才发现是 row 和 col 的计算写反了。

另外注意value是一个0到15的整数,转成4位布尔数组时,我用(value >>> (3 - j)) & 1取了它的高到低位。如果你希望第1位对应最高位,这个写法就是对的。这个细节和toBits的约定保持一致。

2.5 单块加解密、PKCS5填充与对外接口

现在把上面的模块拼成完整流程。先实现一个runBlock,负责处理8字节的一个块,加密和解密共用这一个方法,只通过参数控制子密钥顺序:

private static byte[] runBlock(byte[] block, byte[] key, boolean encrypt) { if (block.length != 8 || key.length != 8) { throw new IllegalArgumentException("block 和 key 必须是8字节"); } boolean[][] subKeys = generateSubKeys(key); boolean[] bits = toBits(block); boolean[] ip = permute(bits, IP); boolean[] l = slice(ip, 0, 32); boolean[] r = slice(ip, 32, 64); for (int round = 0; round < 16; round++) { int idx = encrypt ? round : 15 - round; boolean[] newL = r; boolean[] newR = xor(l, fFunction(r, subKeys[idx])); l = newL; r = newR; } boolean[] preOutput = new boolean[64]; for (int i = 0; i < 32; i++) { preOutput[i] = r[i]; preOutput[i + 32] = l[i]; } boolean[] outBits = permute(preOutput, FP); return toBytes(outBits); } public static byte[] encryptBlock(byte[] block, byte[] key) { return runBlock(block, key, true); } public static byte[] decryptBlock(byte[] block, byte[] key) { return runBlock(block, key, false); }

加密和解密的流程完全相同,区别只有一个idx。加密时第 i 轮用subKeys[i],解密时第 i 轮用subKeys[15 - i]。这就是Feistel结构最优雅的地方:加解密不用写两套逻辑,反着用子密钥就行。

下面处理多块数据和填充。DES处理的是分组数据,但实际要加密的字节长度不一定正好是8的倍数,所以需要填充。我用的是PKCS5填充:需要补 n 个字节时,每个填充字节的值都写成 n。

public static byte[] encrypt(byte[] data, byte[] key) { int padLen = 8 - (data.length % 8); byte[] padded = new byte[data.length + padLen]; for (int i = 0; i < data.length; i++) { padded[i] = data[i]; } for (int i = data.length; i < padded.length; i++) { padded[i] = (byte) padLen; } byte[] out = new byte[padded.length]; for (int i = 0; i < padded.length; i += 8) { byte[] block = sliceBytes(padded, i, i + 8); byte[] enc = runBlock(block, key, true); for (int j = 0; j < 8; j++) { out[i + j] = enc[j]; } } return out; } public static byte[] decrypt(byte[] cipherData, byte[] key) { if (cipherData.length % 8 != 0) { throw new IllegalArgumentException("密文长度必须是8的倍数"); } byte[] out = new byte[cipherData.length]; for (int i = 0; i < cipherData.length; i += 8) { byte[] block = sliceBytes(cipherData, i, i + 8); byte[] dec = runBlock(block, key, false); for (int j = 0; j < 8; j++) { out[i + j] = dec[j]; } } int padLen = out[out.length - 1] & 0xFF; if (padLen < 1 || padLen > 8) { throw new IllegalStateException("填充长度非法,密钥或数据可能不对"); } byte[] result = new byte[out.length - padLen]; for (int i = 0; i < result.length; i++) { result[i] = out[i]; } return result; }

填充有个容易忽略的边界:即使原始数据长度正好是8的倍数,也要多补一个完整的8字节块,里面每个字节都是8。否则解密时无法区分“数据自然到这里结束”和“刚好还差一组”。亏我当年第一次写填充时没注意,加密一段长度恰好是8的倍数的数据,解密回来尾巴上多出一截“\u0008\u0008\u0008...”,排查了很久。

public static void main(String[] args) { byte[] key = fromHex("133457799BBCDFF1"); byte[] plain = fromHex("0123456789ABCDEF"); byte[] cipher = encryptBlock(plain, key); System.out.println("加密结果: " + toHex(cipher)); byte[] recovered = decryptBlock(cipher, key); System.out.println("解密结果: " + toHex(recovered)); // 带填充的自测:任意长度数据 byte[] selfKey = new byte[] {1,2,3,4,5,6,7,8}; byte[] selfData = new byte[100]; for (int i = 0; i < selfData.length; i++) { selfData[i] = (byte) i; } byte[] enc = encrypt(selfData, selfKey); byte[] dec = decrypt(enc, selfKey); boolean ok = true; for (int i = 0; i < selfData.length; i++) { if (selfData[i] != dec[i]) { ok = false; break; } } System.out.println("多块自检通过: " + ok); } }

把上面所有代码块按顺序放进同一个DES类里,不需要任何 import,直接java DES就能跑。main 方法里先测单块加解密,再用100字节的随机数据测多块填充加解密,两步都过了基本就说明实现没大问题。

3. 验证结果与常见问题排查

3.1 用标准测试向量验证

密码学算法最怕“自以为写对了”。我的做法是先拿教科书标准测试向量跑一遍。用密钥133457799BBCDFF1加密明文0123456789ABCDEF,标准结果应该是85E813540F0AB405。这也是许多教材里都会出现的经典向量,很适合用来验收。

如果你运行上面的 main,应该可以得到:

加密结果: 85E813540F0AB405 解密结果: 0123456789ABCDEF 多块自检通过: true

这里特别说明一下,我调用的是encryptBlock,不是带填充的encrypt。原因是标准测试向量针对的是“单个8字节块”,而encrypt会额外补一个填充块,输出就会变成16字节,没法跟教科书里的值直接对上。

如果加密结果不对,先不要怀疑人生,按优先级排查三件事:S盒数据有没有抄错;行号列号有没有反;IP和FP表有没有抄成同一个方向。我第一次手写时,就是S7中间某行的两个数字顺序抄错,前几轮看着还像样,后面完全乱套。

3.2 高频问题:最容易翻车的四个点

我在实际调试里遇到的高频问题,整理成一张速查表,你可以直接拿去对照:

问题表现大概率原因怎么修
加密结果和标准向量差很远置换表抄错,或S盒行列反了逐表核对,用打印把第一轮输出与参考实现对比
加密正常,解密出来是乱码解密没用逆序子密钥检查idx = encrypt ? round : 15 - round是否写对
加密结果只有前几位对,后面全错某个S盒内部数据顺序错把8个S盒的每行数字打印出来,逐行对照标准表
解密时报“填充长度非法”密钥不对,或数据根本不是DES密文先不对数据进行填充校验,打印最后一个字节看一眼

还有一个新手高频问题:Java 的 byte 是有符号的,十六进制FF会被当成-1。我在toBytes里把v强转成 byte,转回来时用& 0xFF或者>>>处理符号扩展,才不会出现负数捣乱。你要是用byte参与移位、异或,记得时刻警惕符号位。

3.3 调试技巧:如何快速定位问题

写密码学算法,调试工具非常重要。我的经验是准备一个能打印boolean[]的辅助方法,然后在runBlock里临时加几行日志,打印每一轮开始时的L和R。不要一上来就对着64位密文发愁,把问题定位到具体轮次以后,范围会小很多。

// 调试用:把boolean数组打印成01字符串 private static String bitsToString(boolean[] bits) { StringBuilder sb = new StringBuilder(); for (boolean bit : bits) { sb.append(bit ? '1' : '0'); } return sb.toString(); }

另一个很实用的自验思路:单独验证置换表。IP 和 FP 互为逆置换,所以任意64位数据,先做 IP 再做 FP,应该原样返回。如果这个都不成立,说明两张表里至少有一张抄错了。E 和 P 没有这么直观的逆关系,但你可以通过标准测试向量间接验证。

第三个小技巧是分模块单测。把 S 盒查表抽出来,输入已知的6位值,比如101011这种,手算出预期的4位结果,再和代码输出比对。这样能把“F函数”和“主流程”隔离排查。不要等整个算法跑通才看结果,那样一旦出错,排查范围大得吓人。

4. 实操心得与扩展方向

整份代码写下来,我最深的感受是:手写一遍DES后,再去看任何对称加密算法,都多了一层“肌肉记忆”。你不再只是知道“AES比DES安全”这个结论,而是能一眼看出分组密码里常见的套路:扩散靠置换,混淆靠代换,轮密钥靠调度。Feistel结构为什么能保证加解密一致,S盒为什么必须是唯一的非线性部件,这些问题在写代码的过程中都会自动得到答案。

后面如果要继续扩展,这条路是现成的:先做3DES,也就是用两个或三个不同的DES密钥连续加解密,C = E_K1(D_K2(E_K3(P)));再加CBC模式,让每个明文块先与上一块密文异或再加密,第一块用初始化向量IV;再进一步,还可以尝试把同样的Feistel思想扩展到自定义的迷你分组密码上,用来理解AES里的字节代换和列混合到底在解决什么问题。

当然,工程里真正要加密数据的时候,请务必使用成熟的加密库。手写实现的最大价值是学习,生产环境里自己造密码轮子是大忌。但如果你遇到一个不依赖任何第三方库的演示项目,或者需要在一场面试里把DES的每一张置换表都讲清楚,这份DES.java应该能成为一份靠谱的原型。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进