1. 项目概述为什么现在还需要了解DES如果你正在学习Java安全编程或者准备面试DESData Encryption Standard这个词大概率会出现在你的学习清单或“八股文”里。很多人可能会疑惑DES不是早就被认为不安全被AES取代了吗为什么我们还要花时间去实现一个“过时”的算法这正是这个项目的价值所在。实现一个DES加解密系统远不止是调用一个API那么简单。它是一次对密码学核心思想的深度实践。DES作为现代分组密码的里程碑其精巧的Feistel网络结构、S盒设计思想是理解AES、SM4等现代对称加密算法的基础。通过亲手实现它你能透彻理解分组加密的工作模式如ECB、CBC、填充方式如PKCS5Padding这些在面试和实际开发中高频出现的概念而不再仅仅是死记硬背“ECB模式不安全”这样的结论。对于Java开发者而言这个项目能帮你打通“应用”与“原理”之间的壁垒。你不仅会知道如何使用javax.crypto.Cipher更能明白当你调用Cipher.getInstance(DES/CBC/PKCS5Padding)时底层究竟发生了什么。这份理解在面对“Java成熟分类”下的复杂系统设计、排查诡异的加解密问题时将是无比宝贵的经验。接下来我将带你从零开始构建一个完整的、可运行的DES加解密系统并深入每一个技术细节。2. 核心原理深度拆解DES不仅仅是56位密钥在动手写代码之前我们必须先吃透DES的原理。很多人对DES的认知停留在“56位密钥已不安全”的层面这远远不够。DES的精华在于其对称分组密码的设计这是一个精妙的系统工程。2.1 Feistel网络结构对称加密的经典范式DES采用Feistel网络这是一种特别的结构其最大优点是加解密过程可以使用相同的算法仅需微调子密钥的使用顺序。这大大简化了硬件和软件的实现。它将64位的明文分组分成左右两半各32位记为L0和R0。然后进行多轮迭代。每一轮的操作可以概括为左输入L[i-1] 右输入R[i-1] 轮函数F 子密钥K[i] 本轮输出 L[i] R[i-1] R[i] L[i-1] XOR F(R[i-1], K[i])这里的XOR异或操作是整个加密过程的核心它是一种可逆运算。经过16轮这样的迭代后最终再将左右两部分合并有时会经过一个最终的置换。注意理解Feistel结构的关键在于每一轮只加密了一半的数据右半部分而左半部分只是简单地被复制。这种结构保证了算法的可逆性。即使轮函数F本身是一个非常复杂的、不可逆的函数整个加密过程依然是可逆的。这是Feistel网络最天才的设计。2.2 轮函数F算法的灵魂所在轮函数F是DES安全性的核心。它接受32位的右半部分输入和48位的子密钥输出一个32位的结果。这个过程包含四个关键步骤扩展置换E-box将32位的输入扩展为48位。这不是简单填充而是通过重复某些位来实现的。目的是让输入的一位能影响下一轮多个S盒的输入从而产生更快的“雪崩效应”。与子密钥异或将扩展后的48位数据与当前轮的48位子密钥进行按位异或。S盒替换Substitution-box这是DES中唯一的非线性部件是混淆的主要来源。将上一步得到的48位数据分成8组每组6位送入8个不同的S盒。每个S盒是一个固定的4行16列的查找表它接收6位输入输出4位。这个压缩过程6位变4位提供了算法的非线性特性。P盒置换Permutation-box将S盒输出的32位数据按照一个固定的P盒进行置换。这是扩散操作目的是让S盒的输出位在下一轮中能更广泛地分布。2.3 密钥调度算法从一把钥匙到多把钥匙DES的输入密钥是64位但其中8位是奇偶校验位每字节的第8位实际有效密钥是56位。密钥调度算法负责将这56位密钥生成16轮所需的16个48位子密钥。 过程大致是先经过一个置换选择PC-1去掉校验位并打乱顺序得到56位有效密钥。将其分成左右各28位的C0和D0。每一轮C和D分别进行循环左移移位数根据轮数固定然后经过另一个置换选择PC-2压缩并打乱生成该轮的48位子密钥。实操心得在手动实现或调试时密钥调度是最容易出错的地方之一。务必仔细核对PC-1、PC-2置换表以及每轮的循环左移位数。一个常见的坑是PC-1置换后原本64位密钥中的第8、16、24...64位校验位已经被丢弃后续所有操作都是基于56位进行的思维上要及时转换。2.4 工作模式与填充如何加密超过64位的数据DES本身只能加密一个64位的分组。实际数据通常更长这就需要工作模式。同时数据长度未必是64位的整数倍这就需要填充。ECB模式电子密码本最简单的模式每个分组独立加密。致命缺点相同的明文分组会生成相同的密文分组无法隐藏数据模式。对于图像等数据加密后可能仍能看到轮廓。绝对不要用于加密有意义的数据。CBC模式密码分组链接最常用的模式之一。每个明文分组在加密前先与前一个密文分组进行异或。第一个分组需要一个初始化向量IV。IV不需要保密但必须不可预测通常随机生成。CBC模式能提供更好的安全性。填充PKCS5Padding或PKCS7Padding是最常用的填充方式。如果最后一个分组缺少n个字节则用数值n填充所有缺失的字节。例如如果缺3字节则填充0x03 0x03 0x03。3. 系统设计与模块划分理解了原理我们就可以开始设计这个Java DES系统了。我们的目标不是简单地封装JDK的Cipher类而是要实现一个教学与理解并重的系统因此会包含“自实现核心算法”和“使用标准库”两种方式。3.1 整体架构设计系统将分为三大核心模块核心算法模块纯Java实现的DES算法包括密钥生成、Feistel轮运算、S/P盒置换等。这部分代码将完全揭示算法细节。JCE适配器模块使用Java Cryptography Extension (JCE)即javax.crypto包下的Cipher、SecretKeyFactory等类实现生产可用的加解密功能。这是企业级应用的标准做法。应用与工具模块提供命令行界面CLI、简单的图形界面可选以及用于测试和验证的辅助工具类如IV生成器、编码转换器等。这种设计既能满足学习者“知其所以然”的需求也能提供“开箱即用”的生产力工具。3.2 核心类图与职责DESCoreEngineDES算法的核心引擎。包含encryptBlock(byte[] block, byte[] key)和decryptBlock(...)方法负责单分组的加解密。KeyScheduler密钥调度器。根据输入的64位密钥生成16轮子密钥。FeistelFunction轮函数F的实现。ModeHandler工作模式处理器。包含processECB(...),processCBC(...)等方法处理分组链接、IV等逻辑。PaddingUtil填充工具类。实现PKCS5Padding的添加与移除。DESJCEWrapperJCE包装类。提供encryptWithJCE(...),decryptWithJCE(...)等简便方法。DESApp主应用程序类整合所有模块提供用户交互入口。3.3 关键数据结构与位操作DES算法本质上是位操作。在Java中我们通常使用byte[]来表示数据但需要频繁进行位级的提取、合并、移位和置换。位操作工具类BitUtils是必不可少的public class BitUtils { // 从字节数组中获取指定位从0开始计数 public static int getBit(byte[] data, int pos) { int byteIndex pos / 8; int bitIndex 7 - (pos % 8); // DES标准常采用大端序位序需注意 return (data[byteIndex] bitIndex) 0x01; } // 设置字节数组中的指定位 public static void setBit(byte[] data, int pos, int bit) { // ... 实现略 } // 循环左移一个28位的半密钥 public static int leftRotate28(int keyHalf, int shift) { return ((keyHalf shift) | (keyHalf (28 - shift))) 0x0FFFFFFF; } // 执行置换操作 public static byte[] permute(byte[] input, int[] table, int outputLen) { byte[] output new byte[(outputLen 7) / 8]; for (int i 0; i outputLen; i) { int srcPos table[i] - 1; // 置换表通常从1开始计数 int bit getBit(input, srcPos); setBit(output, i, bit); } return output; } }踩坑记录位序和字节序是DES实现中最令人头疼的细节。DES标准文档中的位编号是从1开始并且最左边是最高位MSB。而Java的字节数组我们通常认为data[0]的最高位第7位对应整个数据流的最高位。在实现置换如IP、PC-1时必须严格按照表格说明处理每一位的映射关系稍有偏差就会导致加解密失败。建议编写详尽的单元测试针对已知的测试向量进行验证。4. 核心算法模块实现详解这是整个项目最硬核的部分。我们将一步步实现DES的核心。4.1 常量定义置换表与S盒首先我们需要将DES标准中所有固定的置换表和S盒定义为常量。这些是算法的“DNA”。public class DESConstants { // 初始置换IP public static final int[] IP {58, 50, 42, 34, 26, 18, 10, 2, ...}; // 逆初始置换IP^-1 public static final int[] IP_INV {40, 8, 48, 16, 56, 24, 64, 32, ...}; // 扩展置换E public static final int[] E {32, 1, 2, 3, 4, 5, 4, 5, ...}; // P盒置换 public static final int[] P {16, 7, 20, 21, 29, 12, 28, 17, ...}; // PC-1置换 public static final int[] PC1 {57, 49, 41, 33, 25, 17, 9, 1, ...}; // PC-2置换 public static final int[] PC2 {14, 17, 11, 24, 1, 5, 3, 28, ...}; // 每轮循环左移的位数 public static final int[] SHIFT_BITS {1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1}; // 8个S盒每个是一个4x16的二维数组 public static final int[][] 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}, ... }; // ... 定义S2到S8 }将这些表格准确无误地录入是第一步。可以从NIST等权威标准文档中直接复制。4.2 密钥调度算法实现KeyScheduler类的核心是generateSubKeys方法。public class KeyScheduler { public byte[][] generateSubKeys(byte[] key64) { // 1. 应用PC-1置换64位变56位 byte[] key56 BitUtils.permute(key64, DESConstants.PC1, 56); // 2. 分割成C0和D0各28位 int c BitUtils.bytesToInt28(key56, 0); // 前28位 int d BitUtils.bytesToInt28(key56, 28); // 后28位 byte[][] subKeys new byte[16][6]; // 16轮每轮子密钥48位6字节 for (int round 0; round 16; round) { // 3. 循环左移 c BitUtils.leftRotate28(c, DESConstants.SHIFT_BITS[round]); d BitUtils.leftRotate28(d, DESConstants.SHIFT_BITS[round]); // 4. 合并C和D56位然后应用PC-2置换生成48位子密钥 long cd ((c 0x0FFFFFFFL) 28) | (d 0x0FFFFFFFL); byte[] cdBytes BitUtils.longToBytes56(cd); subKeys[round] BitUtils.permute(cdBytes, DESConstants.PC2, 48); } return subKeys; } }注意事项在合并C和D时要确保只取低28位 0x0FFFFFFF因为Java的int是32位左移后高位可能会有垃圾数据。这是一个非常隐蔽的bug来源。4.3 Feistel轮函数与S盒查询实现FeistelFunction类的apply方法是算法的心脏。public class FeistelFunction { public byte[] apply(byte[] r32, byte[] subKey48) { // 1. 扩展置换32位 - 48位 byte[] expanded BitUtils.permute(r32, DESConstants.E, 48); // 2. 与子密钥异或 byte[] xored new byte[6]; for (int i 0; i 6; i) { xored[i] (byte)(expanded[i] ^ subKey48[i]); } // 3. S盒替换48位 - 32位 byte[] substituted new byte[4]; // 32位 4字节 int outPos 0; for (int i 0; i 8; i) { // 8个S盒 // 取6位输入 int blockStart i * 6; int row ((BitUtils.getBit(xored, blockStart) 1) | BitUtils.getBit(xored, blockStart 5)); int col (BitUtils.getBit(xored, blockStart 1) 3) | (BitUtils.getBit(xored, blockStart 2) 2) | (BitUtils.getBit(xored, blockStart 3) 1) | BitUtils.getBit(xored, blockStart 4); // 查询S盒 int sBoxVal DESConstants.S_BOXES[i][row][col]; // S_BOXES是三维数组常量 // 将4位输出写入结果 substituted[outPos / 8] | (sBoxVal (4 - (outPos % 8))); // 注意位写入顺序 outPos 4; } // 4. P盒置换 return BitUtils.permute(substituted, DESConstants.P, 32); } }关键细节S盒查询时6位输入的第一位和最后一位组成行号中间4位组成列号。这是DES标准的规定。行号和列号都是从0开始计算的。写入4位输出到字节数组时要特别注意位的对齐通常是从左到右高位到低位填充。4.4 完整加解密流程整合最后在DESCoreEngine中整合所有部件。public class DESCoreEngine { private KeyScheduler keyScheduler new KeyScheduler(); private FeistelFunction feistel new FeistelFunction(); public byte[] encryptBlock(byte[] block64, byte[] key64) { // 1. 初始置换IP byte[] permuted BitUtils.permute(block64, DESConstants.IP, 64); // 2. 分割成L0和R0 byte[] left Arrays.copyOfRange(permuted, 0, 4); // 32位 byte[] right Arrays.copyOfRange(permuted, 4, 8); // 32位 // 3. 生成16轮子密钥 byte[][] subKeys keyScheduler.generateSubKeys(key64); // 4. 16轮Feistel迭代 for (int round 0; round 16; round) { byte[] temp right; // F(R, K) XOR L byte[] fResult feistel.apply(right, subKeys[round]); right xorBytes(left, fResult, 4); // 异或操作 left temp; } // 5. 最后交换第16轮后不交换但合并前需交换回来 byte[] combined concatenate(right, left); // R16L16 // 6. 逆初始置换IP^-1 return BitUtils.permute(combined, DESConstants.IP_INV, 64); } public byte[] decryptBlock(byte[] block64, byte[] key64) { // 解密过程与加密几乎相同仅子密钥使用顺序相反 byte[][] subKeys keyScheduler.generateSubKeys(key64); // ... 过程类似但在Feistel循环中使用 subKeys[15 - round] } private byte[] xorBytes(byte[] a, byte[] b, int len) { byte[] result new byte[len]; for (int i 0; i len; i) { result[i] (byte)(a[i] ^ b[i]); } return result; } }5. 工作模式与填充的工程化实现单分组加密只是基础我们需要ModeHandler和PaddingUtil来处理真实数据。5.1 PKCS5Padding 实现填充必须在加密前进行在解密后移除。public class PaddingUtil { public static byte[] addPKCS5Padding(byte[] data, int blockSize) { int paddingLen blockSize - (data.length % blockSize); if (paddingLen 0) paddingLen blockSize; // 如果刚好对齐补一整个块 byte[] padded new byte[data.length paddingLen]; System.arraycopy(data, 0, padded, 0, data.length); for (int i data.length; i padded.length; i) { padded[i] (byte) paddingLen; } return padded; } public static byte[] removePKCS5Padding(byte[] paddedData) throws BadPaddingException { int paddingLen paddedData[paddedData.length - 1] 0xFF; // 验证填充有效性 if (paddingLen 0 || paddingLen paddedData.length) { throw new BadPaddingException(Invalid PKCS5 padding length: paddingLen); } for (int i paddedData.length - paddingLen; i paddedData.length; i) { if ((paddedData[i] 0xFF) ! paddingLen) { throw new BadPaddingException(Invalid PKCS5 padding bytes); } } return Arrays.copyOfRange(paddedData, 0, paddedData.length - paddingLen); } }重要提示解密后移除填充时必须验证填充的合法性。直接信任最后一个字节的值是危险的可能导致“Padding Oracle”攻击。虽然我们的演示项目不涉及网络但养成安全编程习惯至关重要。5.2 CBC模式处理器实现CBC模式需要一个初始化向量IV且加解密流程略有不同。public class ModeHandler { private DESCoreEngine engine; public byte[] processCBC(byte[] data, byte[] key, byte[] iv, boolean isEncrypt) { int blockSize 8; // DES块大小8字节 byte[] processed new byte[data.length]; byte[] previousBlock iv.clone(); // 加密时前一个密文块是IV for (int i 0; i data.length; i blockSize) { byte[] currentBlock Arrays.copyOfRange(data, i, i blockSize); if (isEncrypt) { // 加密明文 XOR 前一个密文块 - 加密 - 得到当前密文块 byte[] xored xorBytes(currentBlock, previousBlock, blockSize); byte[] encryptedBlock engine.encryptBlock(xored, key); System.arraycopy(encryptedBlock, 0, processed, i, blockSize); previousBlock encryptedBlock; // 更新前一个密文块 } else { // 解密解密当前密文块 - XOR 前一个密文块 - 得到明文 byte[] decryptedBlock engine.decryptBlock(currentBlock, key); byte[] plainBlock xorBytes(decryptedBlock, previousBlock, blockSize); System.arraycopy(plainBlock, 0, processed, i, blockSize); previousBlock currentBlock; // 更新前一个密文块注意这里是当前密文块 } } return processed; } }踩坑记录CBC模式解密时用于异或的“前一个密文块”是当前正在解密的密文块的前一个块而不是上一轮解密后的明文块。这是一个非常常见的逻辑错误会导致除了第一块外的所有数据解密失败。务必画图理清数据流。6. 使用JCE标准库进行生产级开发虽然手动实现有助于理解但在实际企业级Java开发中我们绝对应该使用经过严格审计和优化的JCEJava Cryptography Extension。6.1 标准加解密流程DESJCEWrapper类封装了标准用法。import javax.crypto.Cipher; import javax.crypto.SecretKey; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.DESKeySpec; import javax.crypto.spec.IvParameterSpec; import java.security.spec.KeySpec; public class DESJCEWrapper { public static byte[] encryptWithJCE(byte[] data, byte[] key, byte[] iv, String mode) throws Exception { // 1. 根据模式字符串获取Cipher实例 // 例如: DES/CBC/PKCS5Padding Cipher cipher Cipher.getInstance(mode); // 2. 从原始密钥字节生成安全的SecretKey对象 KeySpec keySpec new DESKeySpec(key); // DESKeySpec会检查密钥奇偶性可选 SecretKeyFactory keyFactory SecretKeyFactory.getInstance(DES); SecretKey secretKey keyFactory.generateSecret(keySpec); // 3. 初始化Cipher为加密模式并传入IV如果是CBC等模式 if (mode.contains(CBC) || mode.contains(CFB) || mode.contains(OFB)) { IvParameterSpec ivSpec new IvParameterSpec(iv); cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec); } else { cipher.init(Cipher.ENCRYPT_MODE, secretKey); } // 4. 执行加密JCE会自动处理填充和分组 return cipher.doFinal(data); } public static byte[] decryptWithJCE(byte[] encryptedData, byte[] key, byte[] iv, String mode) throws Exception { Cipher cipher Cipher.getInstance(mode); KeySpec keySpec new DESKeySpec(key); SecretKeyFactory keyFactory SecretKeyFactory.getInstance(DES); SecretKey secretKey keyFactory.generateSecret(keySpec); if (mode.contains(CBC) || mode.contains(CFB) || mode.contains(OFB)) { IvParameterSpec ivSpec new IvParameterSpec(iv); cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec); } else { cipher.init(Cipher.DECRYPT_MODE, secretKey); } return cipher.doFinal(encryptedData); } }6.2 密钥管理最佳实践绝对不要在代码中硬编码密钥。在生产环境中使用密钥库Keystore将密钥存储在受密码保护的JKS或PKCS12文件中。从环境变量或配置服务器获取通过System.getenv()或集成Spring Cloud Config等从安全渠道获取加密后的密钥然后在内存中解密使用。密钥生命周期管理定期轮换密钥。DES本身密钥强度不足更应使用AES-256等更强算法并建立密钥轮换机制。// 示例从环境变量读取Base64编码的密钥和IV String base64Key System.getenv(DES_ENCRYPTION_KEY); String base64Iv System.getenv(DES_ENCRYPTION_IV); byte[] key Base64.getDecoder().decode(base64Key); byte[] iv Base64.getDecoder().decode(base64Iv); // 务必验证长度DES密钥应为8字节IV为8字节 if (key.length ! 8) { throw new IllegalArgumentException(Invalid DES key length from environment variable); }7. 常见问题、调试技巧与安全考量即使理解了所有原理在实现和集成过程中你依然会遇到各种问题。7.1 自实现算法验证与调试如何确认你的DES实现是正确的使用标准测试向量Test Vectors。 NIST等机构提供了标准的明文、密钥和密文对照表。这是调试的黄金标准。调试步骤单元测试先行为KeyScheduler、FeistelFunction、DESCoreEngine分别编写测试使用已知的中间结果进行验证。单分组验证找一个标准的测试向量例如明文0x0123456789ABCDEF密钥0x133457799BBCDFF1密文0x85E813540F0AB405测试你的encryptBlock和decryptBlock。逐轮调试如果结果不对打印或调试每一轮加密后的L和R的值与标准中间值对比。问题往往出现在S盒查询、位操作或置换表录入错误。工作模式验证在单分组正确后测试CBC等模式。可以先用JCE生成一个密文然后用你的自实现解密看是否能成功。7.2 与JCE库交互的典型问题NoSuchAlgorithmException或NoSuchPaddingException原因传入Cipher.getInstance()的算法字符串不正确或者运行环境如某些受限的JRE没有提供DES实现。解决检查字符串格式是否为DES/CBC/PKCS5Padding。确保使用的是标准JRE。InvalidKeyException原因密钥长度不是8字节或者密钥的奇偶校验位不正确如果你使用了DESKeySpec它会执行弱校验。解决确保密钥是准确的8字节。对于奇偶校验通常可以忽略使用SecretKeySpec类代替DESKeySpec可以绕过校验SecretKeySpec secretKey new SecretKeySpec(keyBytes, DES);。BadPaddingException原因解密时这是最常见的问题。可能的原因有密钥错误、IV错误、密文在传输中被篡改、或者加密/解密时使用的工作模式或填充方式不匹配。排查这是一条黄金法则——加密和解密时Cipher.getInstance()中的算法字符串必须完全一致。仔细检查key、iv、modeAndPadding字符串是否在两端完全相同。7.3 安全警告与升级建议尽管我们实现了DES但必须清醒认识到DES已不安全56位密钥在现代计算能力下可在短时间内被暴力破解。绝对不要在任何新的、需要安全性的系统中使用DES。ECB模式是危险的如前所述它不能隐藏数据模式。对于任何有意义的数据都应使用CBC、CTR或GCM等更安全的模式。升级到AESJava中替代DES的标准是AES。只需将算法字符串中的DES改为AES并注意密钥长度16, 24, 32字节和IV长度16字节的变化即可。例如Cipher.getInstance(AES/CBC/PKCS5Padding)。考虑认证加密对于需要同时保证机密性和完整性的场景几乎总是需要使用GCM等认证加密模式。Cipher.getInstance(AES/GCM/NoPadding)。7.4 性能考量与优化自实现的Java DES算法性能远低于JCE后者通常有本地优化甚至硬件加速。如果出于学习目的性能不是关键。但如果处理大量数据务必使用JCE。对于JCE可以通过Cipher的update()和doFinal()方法流式处理大文件避免一次性加载到内存。cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec); try (FileInputStream fis new FileInputStream(input.txt); FileOutputStream fos new FileOutputStream(encrypted.dat); CipherOutputStream cos new CipherOutputStream(fos, cipher)) { byte[] buffer new byte[8192]; int bytesRead; while ((bytesRead fis.read(buffer)) ! -1) { cos.write(buffer, 0, bytesRead); } }8. 项目总结与扩展思考完成这个DES加解密系统你收获的远不止一个可运行的程序。你深入理解了Feistel结构、S盒、工作模式、填充这些密码学的基石概念。当你在面试中被问到“DES和AES的区别”、“CBC模式的工作原理”、“为什么ECB不安全”时你能够从实现层面给出令人信服的回答而不是背诵概念。我个人在实际操作中的体会是密码学实现就像做精密的手工容不得半点马虎。一个比特的错误就会导致整个结果面目全非。调试的过程极其痛苦但也正是这个过程强迫你理解每一个步骤的精确含义。当你第一次看到自己实现的DES成功解密出JCE加密的数据时那种成就感是无与伦比的。这个项目还可以进一步扩展实现3DES作为DES向AES过渡的算法3DES使用了两个或三个密钥对数据进行三次DES加密安全性高于DES。尝试修改你的核心引擎来支持它。可视化工具为你的DES引擎开发一个简单的Swing或JavaFX界面实时展示每一轮加密后数据的变化这对于教学演示非常有价值。集成到Web应用创建一个简单的Spring Boot服务提供RESTful API进行加解密并实践如何安全地传输和存储密钥。最后请记住这个项目的终极目的不是让你去使用DES而是为你打开对称加密世界的大门让你有能力去理解和使用更强大、更现代的加密算法。在安全领域“知其然并知其所以然”是构建可靠系统的第一步。