
附 Java / Python / Node 三端实测对齐方案所有密文向量均为本地实跑结果前几天群里又有朋友甩过来一段 Java 代码问“为什么我加密出来的密文对方 Python 那边死活解不开密钥明明是同一个。”我看了眼他的密钥我的密钥123。问题就出在这儿——DES 要的是 8 个字节不是 8 个字符。这串密钥用 UTF-8 编码是 4×3 3 15 个字节Java 端SecretKeySpec直接抛异常对方那边拿到的是另一套逻辑生成的密文自然对不上。说实话DES 这算法我本来以为早就该进博物馆了。但现实是银行老接口、政企对接、物联网设备的存量固件、十几年前的 ERP 数据库字段……只要你做对接迟早要和它碰面。而且一碰面80% 的时间不是花在“怎么加密”而是花在“为什么两边加出来的东西不一样”。这篇就把 DES / 3DES 的原理、安全边界、跨语言对齐和常见坑一次性讲清楚。一、先搞懂 DES 到底在算什么DESData Encryption Standard是 1977 年被美国定为标准的对称分组加密算法基于 Feistel 网络结构参数值说明分组长度64 bit8 字节数据必须补齐到 8 的整数倍密钥长度64 bit其中56 bit 有效每字节最低位是奇偶校验位直接被丢弃轮数16 轮每轮用不同的 48 bit 子密钥核心部件8 个 S 盒6 进 4 出唯一的非线性来源算法的“心脏”一次加密的流程明文 64bit → IP 初始置换 → 拆成 L0 / R0 各 32bit → 16 轮 FeistelLi Ri-1 , Ri Li-1 XOR f(Ri-1, Ki) → 左右交换后合并 → FP 逆初始置换 → 密文 64bit这里有两个容易被忽略的点1. 密钥的 8 个校验位不是白给的64 位密钥里只有 56 位参与运算。所以12345678和把每个字节最高位改掉的另一个 8 字节串加密结果完全相同。这也是为什么“密钥看起来不一样密文却一样”不是 bug。2. 明文长度几乎永远不是 8 的整数倍于是就有了填充Padding。Hello, DES!是 11 字节PKCS#5 填充后补 5 个0x05变成 16 字节48 65 6c 6c 6f 2c 20 44 45 53 21 05 05 05 05 05填充方式不一致是跨语言解密失败的头号原因——比密钥写错还常见。二、它到底还安不安全不安全。而且不是“理论上不安全”是已经被人真金白银破过。时间事件成本耗时1997RSA DES Challenge互联网分布式穷举志愿者算力96 天1998.07EFF 造出专用机器Deep Crack约 25 万美元56 小时1999.01Deep Crack distributed.net 联合同上 志愿者22 小时 15 分2007COPACOBANAFPGA 阵列约 1 万美元一周量级今天云上 FPGA 实例 / 大规模 GPU 集群按小时计费小时级2^56 ≈ 7.2 × 10^16这个空间在 1977 年是天文数字在今天是一条能预算出来的账单。那 3DES 呢3DESTriple DES / DESede是 EDE 结构加密(K1) → 解密(K2) → 加密(K3)。三密钥版本有效强度约 112 位目前暴力穷举不现实。但它有两个硬伤⚠️分组长度还是 64 bit。这就是 2016 年的Sweet32攻击CVE-2016-2183在同一密钥下传输约 2^32 个分组≈32 GB后生日攻击开始能恢复部分明文。TLS、VPN 这种长连接大流量场景首当其冲。⚠️双密钥变体K1K2K116 字节密钥有效强度只有 80 位可被中间相遇攻击打穿NIST 早已不推荐。监管口径也很明确标准态度NIST SP 800-673DES 仅限兼容旧系统新系统不要用NIST SP 800-131A Rev.22023 年之后禁止用 3DES 加密解密仅限遗留数据PCI DSS要求强加密DES 系列不满足商用密码相关规范推荐 SM4同样是 128 bit 分组避开了 Sweet32 类问题结论DES 只能用于“读懂老系统”绝不能用于“保护新数据”。新做设计直接上 AES-256-GCM口令场景用 bcrypt / Argon2。三、ECB 还是 CBC这一步选错后面全白干对比项ECBCBC是否需要 IV不需要需要 8 字节 IV相同明文块产生相同密文块不同典型问题图案泄露经典的“企鹅图”IV 复用导致前缀可识别现存老系统占比极高很多老接口的默认值高对接建议照抄对方的但心里清楚它不安全IV 必须和对方完全一致两个坑坑 1ECB 会泄露数据结构。相同明文块 → 相同密文块。加密一张位图能看出轮廓加密结构化 JSON 能猜出哪些字段重复。这也是为什么 ECB 在任何新设计里都是错的。坑 2全零 IV 的 CBC 是“半个 ECB”。很多老系统以及大量在线工具图省事直接固定 IV 8 个0x00。后果是同样的密钥下前缀相同的明文密文前缀也相同。批量加密用户名、手机号这类数据时攻击者不用解密就能判断“哪些记录是一样的”。对接老系统时你要做的第一件事不是写代码而是确认对方的四件套算法(DES / 3DES) 模式(ECB / CBC) 填充(PKCS5 / ZeroPad / NoPad) IV(全零 / 随机 / 密钥前8字节)这四个里有任意一个对不上密文就完全不同而且报错信息通常毫无参考价值。四、跨语言对齐同一份密钥三端跑出同一串密文这是本文最实用的部分。我用同一组参数在 Java / Node / Python 三端实测明文Hello, DES! DES 密钥12345678 8 字节 3DES 密钥123456789012345678901234 24 字节 填充PKCS#5 / PKCS#7 IV8 字节全零4.1 JavaSunJCEimportjavax.crypto.Cipher;importjavax.crypto.spec.IvParameterSpec;importjavax.crypto.spec.SecretKeySpec;importjava.nio.charset.StandardCharsets;importjava.util.Base64;publicclassDesDemo{publicstaticvoidmain(String[]args)throwsException{StringplainHello, DES!;byte[]key12345678.getBytes(StandardCharsets.UTF_8);// 必须 8 字节// DES / CBC / PKCS5PaddingIV 全零CiphercbcCipher.getInstance(DES/CBC/PKCS5Padding);cbc.init(Cipher.ENCRYPT_MODE,newSecretKeySpec(key,DES),newIvParameterSpec(newbyte[8]));StringctBase64.getEncoder().encodeToString(cbc.doFinal(plain.getBytes(StandardCharsets.UTF_8)));System.out.println(DES/CBC ct);// 注意Cipher.getInstance(DES) 默认就是 DES/ECB/PKCS5PaddingCipherecbCipher.getInstance(DES);ecb.init(Cipher.ENCRYPT_MODE,newSecretKeySpec(key,DES));System.out.println(DES/ECB Base64.getEncoder().encodeToString(ecb.doFinal(plain.getBytes(StandardCharsets.UTF_8))));// 3DES算法名是 DESede不是 3DES / TripleDESbyte[]key3123456789012345678901234.getBytes(StandardCharsets.UTF_8);// 24 字节CiphertdesCipher.getInstance(DESede/CBC/PKCS5Padding);tdes.init(Cipher.ENCRYPT_MODE,newSecretKeySpec(key3,DESede),newIvParameterSpec(newbyte[8]));System.out.println(3DES/CBC Base64.getEncoder().encodeToString(tdes.doFinal(plain.getBytes(StandardCharsets.UTF_8))));}}4.2 Node.js内置 cryptoconstcryptorequire(crypto);constkeyBuffer.from(12345678,utf8);// 8 字节constkey3Buffer.from(123456789012345678901234,utf8);// 24 字节constivBuffer.alloc(8,0);// 全零 IVconstrun(alg,k,ivArg){constccrypto.createCipheriv(alg,k,ivArg);returnBuffer.concat([c.update(Hello, DES!,utf8),c.final()]).toString(base64);};console.log(DES/CBC ,run(des-cbc,key,iv));console.log(DES/ECB ,run(des-ecb,key,null));// ECB 传 nullconsole.log(3DES/CBC ,run(des-ede3-cbc,key3,iv));console.log(3DES/ECB ,run(des-ede3,key3,null));// 注意名字是 des-ede3⚠️Node 17 必踩的坑OpenSSL 3 默认把 DES 归到 legacy provider直接跑会报Error: error:0308010C:digital envelope routines::unsupported错误码ERR_OSSL_EVP_UNSUPPORTED。三种解法任选其一# 方式一启动参数最快node--openssl-legacy-provider app.js# 方式二环境变量NODE_OPTIONS--openssl-legacy-providernodeapp.js方式三改openssl.cnf把legacy_sect加进默认 provider 列表生产环境推荐别改启动脚本。我本机 Node v24 OpenSSL 3.5.6 实测不加参数必挂。4.3 Pythonpycryptodomepipinstallpycryptodome# 注意包名是 pycryptodome导入名是 Cryptoimportbase64fromCrypto.CipherimportDES,DES3fromCrypto.Util.Paddingimportpad,unpad plainbHello, DES!keyb12345678# 长度不对直接 ValueErrorkey3b123456789012345678901234# DES / ECB / PKCS7等价于 Java 的 PKCS5ct_ecbDES.new(key,DES.MODE_ECB).encrypt(pad(plain,DES.block_size))print(DES/ECB ,base64.b64encode(ct_ecb).decode())# DES / CBC / 全零 IVct_cbcDES.new(key,DES.MODE_CBC,ivb\x00*8).encrypt(pad(plain,DES.block_size))print(DES/CBC ,base64.b64encode(ct_cbc).decode())# 3DESprint(3DES/ECB ,base64.b64encode(DES3.new(key3,DES3.MODE_ECB).encrypt(pad(plain,8))).decode())print(3DES/CBC ,base64.b64encode(DES3.new(key3,DES3.MODE_CBC,ivb\x00*8).encrypt(pad(plain,8))).decode())# 解密 去填充print(解密 ,unpad(DES.new(key,DES.MODE_ECB).decrypt(ct_ecb),DES.block_size))4.4 三端实测结果可直接当对拍基准算法组合Base64 密文DES / ECB / PKCS5fy7lEyzVC0iQwyShwfm6VgDES / CBC / PKCS5IV 全零fy7lEyzVC0gYec/52AT5pw3DES / ECB / PKCS56qwauLn9riP/CWoQez2o6w3DES / CBC / PKCS5IV 全零6qwauLn9riP4VQDnULEJ6AJavaSunJCE、NodeOpenSSL 3.5.6legacy provider、纯 Python DES 实现三方结果完全一致。拿到别人的密文对不上时先跑一遍这张表如果对方的DES/CBC结果和你一致说明算法参数没问题锅在密钥编码或明文编码上如果连这张表都对不上那是你本地环境的 DES 实现没启用。顺便一个能省很多事的观察密文长度 ceil((明文字节数 1) / 8) * 8Base64 后是 24 个字符fy7lEyzVC0iQwyShwfm6Vg。如果对方给的密文 Base64 长度不是 4 的倍数那大概率中间被截断或者被换行符污染了先查传输链路别查算法。五、7 个高频坑按出现频率排序#坑现象解法18 字节 ≠ 8 个字符中文密钥getBytes(UTF-8)每字 3 字节直接超长密钥只用 ASCII或用MessageDigest.getInstance(MD5).digest(pwd)生成恰好 16 字节再截 8 字节老系统常见做法但等于把强度降到口令级2密钥编码方式不统一一边用原文字节一边用 Hex/Base64 解码和对方明确写清key.getBytes()还是Hex.decodeHex(key)3Java 默认模式Cipher.getInstance(DES)悄悄就是DES/ECB/PKCS5Padding永远显式写全DES/CBC/PKCS5Padding4Node / OpenSSL 3 禁用 legacyERR_OSSL_EVP_UNSUPPORTED--openssl-legacy-provider或 cnf 里启用 legacy provider5填充方式不一致解密末尾多出乱码\x05\x05\x05\x05\x05或抛 padding 异常PKCS#5 / PKCS#7 等价ZeroPadding 需自己截断对方用 NoPadding 时你必须手工补齐6Base64 被换行MimeEncoder每 76 字符插\r\n对方atob直接报错统一用Base64.getEncoder()或接收端先replaceAll(\\s,)73DES 双密钥误用16 字节密钥能跑但有效强度仅 80 位统一用 24 字节三密钥存量数据要从 K1K2 升到 K1K2K3只能解密后重新加密并做双写过渡再补两个冷门但会咬人的❌弱密钥DES 有 4 个弱密钥如全0x01、全0xFE和 6 对半弱密钥用它们加密等于没加密E(E(x)) x。测试用例里千万别拿00000000当密钥去验证“算法是否正确”。❌URL 传输Base64 里的/在 URL 中必须 encode否则会被服务端解成空格解密直接失败。要么URLEncoder.encode要么改用 Base64URL。六、临时对拍不想搭环境的时候排查对接问题时我一般先用本地脚本跑因为要进 CI、要复用、密钥不能外泄。但有时候只是想快速确认一件事“对方到底用的是 ECB 还是 CBC”“IV 是不是全零”“填充是不是 PKCS5”这种纯参数试探翻一个现成的在线对照页比搭工程快。我偶尔会开的一个是 https://leowh.com/tools/des/index.htmlDES / 3DES × ECB / CBC 四种组合收在一个下拉里带一个按长度生成随机密钥的按钮DES 给 8 字节、3DES 给 24 字节适合快速确认“长度是不是差一位”这类低级错误。它页面上也直说了 CBC 用全零 IV这一点在跟老系统对拍时很有用——省得你再去猜对方的 IV 是什么。https://leowh.com/tools/des/index.html不过有两点必须说清楚别把它当生产工具任何在线工具都不要粘贴真实业务密钥和生产数据。哪怕是声称本地计算的页面你也无法验证。测试就用12345678这种公开密钥。浏览器原生crypto.subtle已经不认识 DES 了。Web Crypto 规范里DES-CBC/TripleDES-CBC属于遗留算法Chromium 系早已下线ECB 从来就不在支持列表里。我用 Chrome 153 实测直接调crypto.subtle.importKey(raw, key, {name:DES-CBC}, ...)会抛NotSupportedError: Algorithm: Unrecognized name四种模式全军覆没而同一行代码换成AES-CBC正常返回。所以依赖浏览器原生实现的 DES 页面在 Chrome 里可能点了没反应或直接报错——这不是你操作错了是浏览器端能力被砍了。各家浏览器内核对遗留算法的支持程度并不一致WebKit 系保留得更久所以别把任何一个在线页面当成权威结论来源。换句话说在线页面用来对参数本地脚本用来对结果。七、什么场景还该用 DES什么场景必须换✅还能用 DES / 3DES 的场景读取历史遗留的加密数据存量数据库字段、老日志对接只提供 DES 接口的第三方系统银行、政企、老设备固件短期过渡新旧双写逐步迁移❌绝对不能用的场景存储用户口令 → 用 bcrypt / scrypt / Argon2idDES 加密口令等于把口令暴露给离线爆破新设计的通信加密 → AES-256-GCM或国密 SM4-GCM大流量长连接VPN / TLS 内网隧道→ 64 bit 分组会被 Sweet32 盯上密钥派生 → PBKDF2 / HKDF不要用MD5(口令)当密钥迁移路线建议第 1 步盘点。搜代码库里的 DES / DESede / des-ecb / des-cbc / 3des 第 2 步双写。新数据用 AES-GCM 加密同时保留 DES 密文副本 第 3 步读兼容。解密时按密文版本标记建议加 1 字节前缀标识算法选择算法 第 4 步回刷存量。批量把 DES 密文转成 AES-GCM跑完校验再删副本 第 5 步下线。删除 DES 相关代码和密钥配置别让 legacy provider 一直开着第 3 步的“版本前缀”是最省事的设计新密文以v2:开头无前缀的按 DES 处理。这样解密函数永远不用猜也不会出现“迁移到一半两边都解不开”的灾难。八、常见问题Q1密钥必须是 8 字节可我的口令是变长的怎么办老系统的通行做法是MD5(口令)取 16 字节再取前 8 字节当 DES 密钥。这能跑通但强度取决于口令本身。新系统别学——直接换 AES PBKDF2/Argon2 派生。Q2解密结果是乱码但没报错怎么查按顺序排查① 算法/模式对不对 → ② 填充方式对不对末尾是不是\x05\x05\x05\x05\x05这类规律字节→ ③ IV 对不对CBC 下 IV 错只会导致第一个分组乱码后面正常这是很强的特征→ ④ 字符集对方可能是 GBK你在用 UTF-8。CBC 模式下“只有开头几个字乱码后面全对”几乎 100% 是 IV 不一致这条经验能帮你省一小时。Q33DES 的 16 字节密钥和 24 字节密钥能互通吗不能直接换。16 字节是 K1K2内部按 K3K1 处理24 字节是 K1K2K3密文不同。要升级必须先解密再重新加密且中间态要双写。Q4为什么我生成的随机密钥别人说“强度不够”如果你限定只用可打印 ASCII很多工具的“生成随机密钥”就是这么干的95 个字符每字节实际熵约 log2(95) ≈ 6.57 bit8 字节只有约52.6 bit低于 56 bit 的理论上限。真要生成高强度密钥用原始随机字节SecureRandom/crypto.randomBytes再以 Hex 或 Base64 存储别用可打印字符集。Q5DES 加密后数据会变大多少明文 n 字节 → 密文ceil((n1)/8)*8字节 → Base64 再乘 4/3 向上取整到 4 的倍数。数据库字段长度要按 Base64 后的长度留余量不然会遇到“偶发截断”这种最难查的 bug短文本没事长文本才炸。九、总结一句话记忆DES 的问题从来不是“难”而是参数太多且各家默认值不同算法 / 模式 / 填充 / IV / 密钥编码 / 明文编码六项全对才有相同密文。排查顺序照这个走90% 的对不上能在 10 分钟内定位1. 用本文 4.4 的向量跑本地三端确认环境没问题 2. 对齐六要素算法、模式、填充、IV、密钥编码、字符集前四项就是第三节说的“四件套” 3. 检查 Base64 是否被换行 / URL 编码污染 4. 检查密文长度是否符合 ceil((n1)/8)*8 5. CBC 只有开头乱码 → IV 错全部乱码 → 密钥或模式错末尾多余字节 → 填充错安全底线⚠️DES 只用于读老数据不用于写新数据3DES 在 NIST 口径下 2023 年后也已禁止用于加密。新项目一律 AES-256-GCM 或 SM4-GCM口令一律 bcrypt / Argon2。