从DES到AES与RSA:现代密码学核心算法原理与实战应用
1. 项目概述:为什么我们需要了解这些加密算法?
如果你正在开发一个需要用户登录的网站,或者你的应用需要安全地传输一些敏感数据,比如订单信息、个人资料,那么“加密”这个词对你来说就绝不是一个遥远的概念。它就像是你家门的锁,虽然平时看不见摸不着,但一旦缺失,后果不堪设想。我从业十多年,处理过无数因为加密使用不当导致的数据泄露、接口被刷、甚至资金损失的案例。很多开发者,尤其是刚入行的朋友,往往对加密的理解停留在“调用一个库函数”的层面,知其然不知其所以然,这恰恰是最大的安全隐患。
今天,我们就来彻底拆解现代密码学中三位最重要的“门神”:DES、AES和RSA。我不会只给你干巴巴的理论公式,而是会结合我踩过的无数个坑,带你从核心原理、JS/Python双语言实现,再到一个真实的逆向实战案例,让你不仅知道怎么用,更明白为什么要这么用,以及用错了会怎样。你会发现,理解了它们,无论是构建安全的系统,还是应对安全测试、甚至进行一些合法的安全研究(比如分析自家应用的通信是否牢靠),都会游刃有余。
2. 加密算法核心思想与分类:对称与非对称的哲学
在深入细节之前,我们必须建立一个顶层的认知框架。所有的加密算法,大体可以分为两类,这背后的思想截然不同。
2.1 对称加密:同一把钥匙的守护
想象一下你和朋友约定用一个共同的密码本(密钥)来写信。加密和解密用的是同一把钥匙,这就是对称加密。它的特点是速度快,适合加密大量的数据,比如整个文件、数据库内容或网络通信的报文正文。
DES (Data Encryption Standard)和AES (Advanced Encryption Standard)就是对称加密的杰出代表。你可以把它们理解成不同代际的“锁芯”技术。DES是上世纪70年代的老将,而AES则是21世纪的新标准。它们共同的核心挑战在于:密钥如何安全地交给对方?如果传递密钥的通道本身就不安全,那再坚固的锁也无济于事。这就是“密钥分发”难题。
2.2 非对称加密:公私钥的巧妙博弈
为了解决密钥分发问题,非对称加密应运而生。它使用一对数学上紧密关联的钥匙:公钥 (Public Key)和私钥 (Private Key)。公钥可以公开发给任何人,就像你的邮箱地址;私钥则必须严格保密,就像你的邮箱密码。
其精妙之处在于:
- 用公钥加密的内容,只能用对应的私钥解密。(常用场景:加密传输)
- 用私钥签名的内容,可以用对应的公钥验证其真实性。(常用场景:数字签名)
RSA就是非对称加密中最著名、应用最广泛的算法。它完美地解决了对称加密的密钥分发问题:比如,客户端用服务端公开的公钥加密一个随机生成的对称密钥(例如AES密钥),然后发送给服务端,服务端用自己的私钥解密得到这个对称密钥。此后,双方就可以用这个对称密钥进行高速的对称加密通信了。这个过程就是经典的TLS/SSL 握手的核心部分。
注意:非对称加密的计算非常复杂,速度比对称加密慢几个数量级,因此绝不用于直接加密大量数据,通常只用于加密密钥或进行签名。
2.3 混合加密系统:现实的实践
在实际应用中(如HTTPS、SSH),我们几乎总是采用混合加密系统,取两者之长:
- 使用RSA(或ECDH等)这类非对称加密算法,安全地协商或传递一个会话密钥。
- 然后使用AES这类对称加密算法,用这个会话密钥来加密实际传输的业务数据。
这样既解决了密钥分发问题,又保证了数据加密的效率。
3. DES算法详解:昔日标准的功与过
DES是历史上第一个被广泛采用的加密标准,由IBM设计,并在1977年被美国国家标准局(NIST)采纳。理解DES是理解现代分组加密的绝佳起点。
3.1 DES核心原理:Feistel网络结构
DES是一种基于Feistel网络的分组加密算法。这个结构非常巧妙,它使得加密和解密过程可以使用几乎相同的逻辑,简化了硬件实现。它将64位的明文分组,通过一个56位的密钥(实际输入64位,其中8位为奇偶校验位),经过16轮复杂的迭代运算,输出64位的密文。
每一轮的核心操作包括:
- 扩展置换:将32位的半块数据扩展到48位。
- 与子密钥混合:将扩展后的数据与本轮生成的48位子密钥进行异或操作。
- S盒替换:这是DES安全性的核心!将48位数据通过8个不同的S盒(Substitution-box),每个S盒将6位输入映射为4位输出,实现非线性混淆,总共输出32位。S盒的设计是保密的,也是密码学家们主要分析的对象。
- P盒置换:对S盒输出的32位进行固定置换,提供扩散效果。
16轮之后,再进行一次初始置换的逆置换,得到最终的密文。解密过程与加密完全相同,只需将子密钥的使用顺序倒过来即可。
3.2 DES的局限性:为何被淘汰?
尽管设计精妙,但DES如今已不再安全,主要原因有三:
- 密钥长度过短:56位的密钥,在当今的计算能力(尤其是暴力破解和专用硬件如ASIC)面前显得不堪一击。1999年,电子前沿基金会(EFF)制造的专用机器“深 crack”在不到24小时内就破解了DES。
- 分组长度较短:64位的分组在面对“生日攻击”等密码分析时,安全性已显不足。
- 存在理论弱点:其S盒的设计被怀疑可能存在后门(尽管从未被证实),且对差分密码分析和线性密码分析表现出一定的脆弱性。
因此,DES在实际应用中已被AES取代。我们学习它,更多是为了理解密码学发展史和Feistel结构。
3.3 DES的JS/Python实现示例
虽然不应在生产环境使用,但实现一个简易的DES有助于理解。这里我们使用现成的库来演示加解密过程。
Python实现 (使用 pycryptodome 库):
from Crypto.Cipher import DES from Crypto.Util.Padding import pad, unpad import base64 # 密钥必须是8字节(64位)。注意DES实际使用56位,库会自动处理。 key = b'8bytekey' # 8个字节 # 初始化向量,用于CBC等模式,需8字节 iv = b'12345678' # 创建DES cipher对象,使用CBC模式 cipher = DES.new(key, DES.MODE_CBC, iv) # 待加密的文本,需要填充到8字节的倍数 plaintext = "Hello DES World!" plaintext_bytes = plaintext.encode('utf-8') padded_bytes = pad(plaintext_bytes, DES.block_size) # 加密 ciphertext_bytes = cipher.encrypt(padded_bytes) # 通常以Base64形式传输或存储 ciphertext_b64 = base64.b64encode(ciphertext_bytes).decode('utf-8') print(f"密文 (Base64): {ciphertext_b64}") # 解密 cipher_decrypt = DES.new(key, DES.MODE_CBC, iv) decrypted_padded_bytes = cipher_decrypt.decrypt(base64.b64decode(ciphertext_b64)) decrypted_bytes = unpad(decrypted_padded_bytes, DES.block_size) decrypted_text = decrypted_bytes.decode('utf-8') print(f"解密后明文: {decrypted_text}")JavaScript实现 (使用 crypto-js 库):在Node.js或支持CryptoJS的浏览器环境中。
const CryptoJS = require("crypto-js"); // Node.js 中需要安装 const key = CryptoJS.enc.Utf8.parse('8bytekey'); // 8字节密钥 const iv = CryptoJS.enc.Utf8.parse('12345678'); // 8字节IV const plaintext = "Hello DES World!"; // 加密 const encrypted = CryptoJS.DES.encrypt(plaintext, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 // 与Python的PKCS7填充对应 }); console.log("密文 (Base64):", encrypted.toString()); // 解密 const decrypted = CryptoJS.DES.decrypt(encrypted, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); console.log("解密后明文:", decrypted.toString(CryptoJS.enc.Utf8));实操心得:即使使用DES,也务必选择CBC、CFB等带初始化向量(IV)的模式,而不是ECB模式。ECB模式相同的明文块会产生相同的密文块,会泄露数据模式,安全性极差。你可以尝试用两张简单的纯色图片分别用ECB和CBC模式加密,观察加密后的图片,ECB模式的图片轮廓依然可见,而CBC模式则完全随机。
4. AES算法详解:当今的对称加密王者
为了替代DES,NIST在1997年发起征集,最终在2000年选中了由比利时密码学家设计的Rijndael算法,并将其确定为高级加密标准(AES)。AES已成为全球对称加密的事实标准,从Wi-Fi密码(WPA2)到文件压缩(ZIP、RAR),再到HTTPS,无处不在。
4.1 AES核心原理:SPN网络结构
AES采用了与DES不同的SPN(Substitution-Permutation Network,替换-置换网络)结构。它处理128位(16字节)的数据块,密钥长度可以是128位、192位或256位,分别称为AES-128, AES-192, AES-256。轮数取决于密钥长度:10轮(128位)、12轮(192位)或14轮(256位)。
每一轮(除最后一轮稍有不同)都包含四个步骤:
- SubBytes(字节替换):通过一个固定的、非线性的S盒替换每个字节。这是混淆的主要来源。
- ShiftRows(行移位):将状态矩阵(4x4字节)的每一行进行循环左移位。第一行不移,第二行移1位,第三行移2位,第四行移3位。这提供了字节之间的扩散。
- MixColumns(列混合):将状态矩阵的每一列与一个固定的多项式进行矩阵乘法运算。这是AES中扩散效果最强的步骤,使得单个字节的变化在一轮后能影响到整个列。
- AddRoundKey(轮密钥加):将当前的状态与本轮生成的扩展密钥(轮密钥)进行简单的异或操作。
初始时有一个AddRoundKey,最后一轮没有MixColumns步骤。密钥扩展算法将初始密钥扩展成多轮所需的轮密钥。
4.2 AES的工作模式:如何加密长数据?
AES一次只能加密一个128位的块。对于更长的数据,需要选择一种“工作模式”。常见的模式有:
- ECB (Electronic Codebook):绝对不要用!每个块独立加密,相同明文块产生相同密文块,安全性差。
- CBC (Cipher Block Chaining):最常用的模式之一。每个明文块在加密前,先与前一个密文块进行异或。第一个块使用一个随机生成的初始化向量(IV)。需要填充。
- CTR (Counter):将块密码变为流密码。一个计数器(如0,1,2,...)被加密,产生的密钥流与明文进行异或。不需要填充,可以并行加密/解密。
- GCM (Galois/Counter Mode):目前最推荐的模式。它在CTR模式的基础上增加了认证功能,能同时保证机密性和完整性(防篡改)。广泛用于TLS 1.2+。
4.3 AES的JS/Python实现示例(以GCM模式为例)
GCM模式是目前Web和API通信中的最佳实践。
Python实现 (AES-256-GCM):
from Crypto.Cipher import AES from Crypto.Random import get_random_bytes import base64 # 生成随机密钥(256位=32字节)和随机nonce(通常12字节) key = get_random_bytes(32) # AES-256 nonce = get_random_bytes(12) # GCM推荐nonce长度为12字节 plaintext = b"Sensitive data that needs both encryption and authentication." # 创建AES-GCM cipher对象 cipher = AES.new(key, AES.MODE_GCM, nonce=nonce) # 加密并生成认证标签(MAC) ciphertext, tag = cipher.encrypt_and_digest(plaintext) # 在实际传输中,我们需要发送 nonce + ciphertext + tag # 通常将它们打包在一起或分开传输 result_package = nonce + tag + ciphertext print(f"传输包 (Base64): {base64.b64encode(result_package).decode()}") # --- 接收方解密 --- # 假设我们收到了 result_package received_data = base64.b64decode(result_package) nonce_received = received_data[:12] tag_received = received_data[12:28] # tag 通常16字节 ciphertext_received = received_data[28:] cipher_decrypt = AES.new(key, AES.MODE_GCM, nonce=nonce_received) try: decrypted_data = cipher_decrypt.decrypt_and_verify(ciphertext_received, tag_received) print(f"解密并验证成功: {decrypted_data.decode()}") except ValueError: print("解密失败或认证标签验证失败!数据可能被篡改。")JavaScript实现 (AES-256-GCM):现代浏览器和Node.js的Web Crypto API原生支持。
async function aesGcmEncrypt(plaintext, key) { // 编码明文 const encodedText = new TextEncoder().encode(plaintext); // 生成随机IV(在GCM中通常称为nonce),12字节 const iv = crypto.getRandomValues(new Uint8Array(12)); // 生成密钥(示例中从基础密钥派生,实际应从安全来源获取) // 这里假设key是一个CryptoKey对象,已导入或生成 const ciphertext = await crypto.subtle.encrypt( { name: "AES-GCM", iv: iv }, key, // 一个CryptoKey对象 encodedText ); // 返回 IV + 密文。注意Web Crypto的encrypt()结果已包含认证标签。 const result = new Uint8Array(iv.length + ciphertext.byteLength); result.set(iv, 0); result.set(new Uint8Array(ciphertext), iv.length); return result; } async function aesGcmDecrypt(encryptedPackage, key) { const iv = encryptedPackage.slice(0, 12); const ciphertext = encryptedPackage.slice(12); try { const decrypted = await crypto.subtle.decrypt( { name: "AES-GCM", iv: iv }, key, ciphertext ); return new TextDecoder().decode(decrypted); } catch (e) { console.error("解密失败:", e); return null; // 解密或认证失败 } } // 使用示例:需要首先生成一个CryptoKey (async () => { const plaintext = "Sensitive data"; const key = await crypto.subtle.generateKey( { name: "AES-GCM", length: 256, }, true, // 是否可导出 ["encrypt", "decrypt"] ); const encrypted = await aesGcmEncrypt(plaintext, key); console.log("加密后数据:", new Uint8Array(encrypted)); const decrypted = await aesGcmDecrypt(encrypted, key); console.log("解密后:", decrypted); })();注意事项:
- IV/Nonce的重要性:在CBC、GCM等模式下,IV必须随机且不可预测,绝对禁止重复使用相同的密钥和IV组合,否则会严重破坏安全性。GCM模式下重用nonce会导致密钥恢复攻击。
- 密钥管理:对称加密的密钥必须妥善保管。在客户端-服务器场景中,不应将固定的对称密钥硬编码在客户端代码中,而应通过TLS或使用RSA进行密钥交换。
- 填充预言攻击:使用CBC等需要填充的模式时,要注意防范填充预言攻击。确保在验证消息完整性(如通过HMAC)之后再进行解密和移除填充。
5. RSA算法详解:非对称加密的基石
RSA是三位发明者姓氏的首字母组合。它的安全性基于一个简单的数论事实:将两个大质数相乘很容易,但将其乘积因式分解却极其困难。这就是大整数分解问题。
5.1 RSA核心原理:密钥生成与数学运算
密钥生成步骤:
- 随机选择两个不相等的大质数
p和q。 - 计算它们的乘积
n = p * q。n的长度就是RSA密钥的长度(如2048位)。 - 计算欧拉函数
φ(n) = (p-1)*(q-1)。 - 选择一个整数
e,满足1 < e < φ(n),且e与φ(n)互质。通常选择65537(0x10001),因为它二进制表示中1很少,计算效率高。 - 计算
e对于φ(n)的模反元素d,即满足(e * d) mod φ(n) = 1。- 公钥:由
(n, e)组成。 - 私钥:由
(n, d)组成。p,q,φ(n)必须严格保密并销毁。
- 公钥:由
加密与解密:
- 加密:对于明文
m(需要转换为小于n的整数),计算密文c = m^e mod n。 - 解密:对于密文
c,计算明文m = c^d mod n。
签名与验证:
- 签名:用私钥
d对消息摘要H(m)进行“解密”运算:s = H(m)^d mod n,得到签名s。 - 验证:用公钥
e对签名s进行“加密”运算:H'(m) = s^e mod n,然后对比H'(m)与重新计算的消息摘要H(m)是否一致。
5.2 RSA的注意事项与最佳实践
- 密钥长度:1024位RSA已不安全,至少使用2048位,对长期安全要求高的应用建议使用3072或4096位。
- 加密的数据大小限制:RSA算法本身只能加密比模数
n小的数据。对于更长的数据,标准做法是:- 用RSA加密一个随机生成的对称密钥(如AES密钥)。
- 然后用这个对称密钥去加密实际数据。这就是“混合加密”。
- 填充方案:绝对不能直接进行“教科书式RSA”加密(即不对明文进行任何处理直接计算
m^e mod n),这是极不安全的。必须使用标准的填充方案,如:- PKCS#1 v1.5:较老,但仍广泛使用。在某些场景下可能受到攻击(如Bleichenbacher攻击)。
- OAEP (Optimal Asymmetric Encryption Padding):目前推荐的加密填充方案,安全性更强。
- 对于签名,对应的填充方案是PSS。
- 性能:RSA运算非常慢,尤其是解密/签名(私钥运算)。频繁操作应考虑使用ECC(椭圆曲线密码学)替代。
5.3 RSA的JS/Python实现示例(密钥生成、加密、签名)
Python实现 (使用 cryptography 库):
from cryptography.hazmat.primitives.asymmetric import rsa, padding from cryptography.hazmat.primitives import serialization, hashes from cryptography.hazmat.backends import default_backend import base64 # 1. 生成RSA密钥对(2048位) private_key = rsa.generate_private_key( public_exponent=65537, key_size=2048, backend=default_backend() ) public_key = private_key.public_key() # 2. 序列化密钥 # 私钥,通常保存为PEM格式,并加密存储 private_pem = private_key.private_bytes( encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.PKCS8, encryption_algorithm=serialization.BestAvailableEncryption(b'mypassword') # 用密码保护私钥 ) # 公钥,可以公开 public_pem = public_key.public_bytes( encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo ) print("公钥PEM:\n", public_pem.decode()) # 3. 使用公钥加密(OAEP填充) message = b"A secret message for RSA encryption." # 注意:RSA加密有长度限制,对于长消息,应加密一个对称密钥 ciphertext = public_key.encrypt( message, padding.OAEP( mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None ) ) print(f"密文 (Base64): {base64.b64encode(ciphertext).decode()}") # 4. 使用私钥解密 decrypted_message = private_key.decrypt( ciphertext, padding.OAEP( mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None ) ) print(f"解密后明文: {decrypted_message.decode()}") # 5. 使用私钥签名 data_to_sign = b"Important data that needs signing." signature = private_key.sign( data_to_sign, padding.PSS( mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH ), hashes.SHA256() ) print(f"签名 (Base64): {base64.b64encode(signature).decode()}") # 6. 使用公钥验证签名 try: public_key.verify( signature, data_to_sign, padding.PSS( mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH ), hashes.SHA256() ) print("签名验证成功!") except Exception as e: print(f"签名验证失败: {e}")JavaScript实现 (使用 Node.js crypto 模块):
const crypto = require('crypto'); // 1. 生成RSA密钥对 const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', { modulusLength: 2048, publicKeyEncoding: { type: 'spki', format: 'pem' }, privateKeyEncoding: { type: 'pkcs8', format: 'pem', // cipher: 'aes-256-cbc', // 可选:加密私钥 // passphrase: 'my-secret-passphrase' } }); console.log("公钥PEM:\n", publicKey); // 2. 使用公钥加密(OAEP填充) const plaintext = "A secret message"; const encryptedBuffer = crypto.publicEncrypt( { key: publicKey, padding: crypto.constants.RSA_PKCS1_OAEP_PADDING, oaepHash: 'sha256' }, Buffer.from(plaintext) ); console.log("密文 (Base64):", encryptedBuffer.toString('base64')); // 3. 使用私钥解密 const decryptedBuffer = crypto.privateDecrypt( { key: privateKey, padding: crypto.constants.RSA_PKCS1_OAEP_PADDING, oaepHash: 'sha256' }, encryptedBuffer ); console.log("解密后明文:", decryptedBuffer.toString()); // 4. 使用私钥签名 const data = "Important data"; const sign = crypto.createSign('sha256'); sign.update(data); sign.end(); const signature = sign.sign(privateKey); console.log("签名 (Base64):", signature.toString('base64')); // 5. 使用公钥验证签名 const verify = crypto.createVerify('sha256'); verify.update(data); verify.end(); const isVerified = verify.verify(publicKey, signature); console.log("签名验证结果:", isVerified);踩坑实录:我曾经遇到过一个问题,服务端(Python)生成的签名,Node.js端始终验证失败。排查了半天,发现是哈希算法不匹配。Python端默认使用了
PKCS#1 v1.5填充的SHA1,而Node.js端尝试用OAEP填充去验证。务必确保加解密、签名验签双方使用的填充方案、哈希算法完全一致,这是跨语言、跨平台交互时最容易出错的地方。
6. 逆向实战案例:分析一个前端加密通信
现在,让我们把理论应用到实战。假设我们在进行安全评估时,需要分析一个Web应用的登录流程。我们发现登录时,密码被加密了,我们需要理解其加密方式,以评估其安全性或进行合法的接口测试。
场景:一个网站登录时,前端JS对密码进行了加密,POST请求体类似{“encryptedPassword”: “aGVsbG8gd29ybGQ=...”}。
6.1 第一步:定位加密代码
- 打开开发者工具:在登录页面,打开浏览器开发者工具(F12),切换到Network(网络)标签页。
- 触发登录请求:输入测试账号密码(如:user: test, pass: 123456),点击登录,在Network中找到登录的POST请求(通常是
/login或/api/auth)。 - 查看请求负载:点击该请求,在
Payload或Request标签页查看Form Data或Request Payload,确认密码字段已被加密。 - 搜索关键代码:
- 全局搜索:在Sources(源代码)标签页,按
Ctrl+Shift+F(Chrome)进行全局搜索。搜索加密后字符串中可能包含的字段名,如encryptedPassword、password、encrypt、CryptoJS、AES、RSA、encrypt等函数名或库名。 - XHR断点:在
Sources->XHR Breakpoints中添加一个包含/login的断点,然后重新登录。当请求发出时,JS执行会暂停,此时调用栈可以引导你找到发起请求和加密数据的函数。 - 事件监听器断点:在
Sources->Event Listener Breakpoints中勾选Mouse->click,然后点击登录按钮,同样可以追踪到处理登录点击事件的函数。
- 全局搜索:在Sources(源代码)标签页,按
6.2 第二步:分析加密逻辑
假设我们通过搜索找到了类似下面的代码片段(经过混淆或未混淆):
function encryptPassword(password) { var key = CryptoJS.enc.Utf8.parse('1234567890123456'); // 硬编码的AES密钥! var iv = CryptoJS.enc.Utf8.parse('abcdefghijklmnop'); var encrypted = CryptoJS.AES.encrypt(CryptoJS.enc.Utf8.parse(password), key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); return encrypted.ciphertext.toString(CryptoJS.enc.Base64); }分析结果:
- 算法:AES。
- 模式:CBC。
- 密钥:
1234567890123456(16字节,AES-128)。 - IV:
abcdefghijklmnop(16字节)。 - 填充:PKCS7。
- 输出:密文先转换为CryptoJS的内部WordArray,再提取
ciphertext并转为Base64。
安全性评估:
- 严重漏洞:密钥和IV硬编码在前端JS中。这意味着任何访问页面的人都能看到密钥,加密形同虚设。攻击者可以直接解密任何捕获到的密文,或者伪造加密请求。
- 改进方案:前端加密不应依赖硬编码的对称密钥。正确的做法是:
- 后端在用户访问登录页时,动态生成一个RSA公钥(或临时对称密钥的加密密钥)传给前端。
- 前端用这个公钥加密密码(或一个随机生成的AES会话密钥)。
- 后端用对应的私钥解密,获得明文密码或会话密钥。这样能确保传输安全,但最终密码的验证仍需在后端安全进行。
6.3 第三步:编写Python脚本模拟加密
为了进行自动化测试或验证,我们需要用Python复现前端的加密逻辑。
from Crypto.Cipher import AES from Crypto.Util.Padding import pad import base64 def simulate_frontend_encrypt(password): """模拟上述JS前端的AES-CBC加密""" key = b'1234567890123456' # AES-128 iv = b'abcdefghijklmnop' cipher = AES.new(key, AES.MODE_CBC, iv) # JS中Pkcs7填充,Python对应PKCS7 padded_data = pad(password.encode('utf-8'), AES.block_size) ciphertext = cipher.encrypt(padded_data) # JS中返回的是ciphertext的Base64,而不是整个CryptoJS对象的Base64 return base64.b64encode(ciphertext).decode('utf-8') # 测试 plain_password = '123456' encrypted_result = simulate_frontend_encrypt(plain_password) print(f"密码 '{plain_password}' 加密后: {encrypted_result}") # 可以将这个结果与浏览器网络请求中捕获的encryptedPassword字段对比,验证逻辑是否正确。6.4 第四步:深度排查与更复杂场景
有时加密逻辑会更复杂:
- 密钥非硬编码:可能从接口动态获取一个“加密密钥”,这个密钥本身可能被另一个公钥加密过。这时需要追踪这个密钥的来源。
- 使用RSA加密:可能直接使用RSA公钥加密密码。需要找到公钥(通常是一个PEM字符串或模数
n和指数e)。然后用Python的RSA库进行模拟加密。 - 自定义加密或混淆:有些应用会自己实现一些XOR、Base64变种、或组合加密。这时需要耐心地跟读JS代码,理解其每一步操作,并用Python逐一复现。
- 代码混淆:如果JS被严重混淆(变量名变成a,b,c,逻辑被分割),分析难度会大增。可以尝试使用浏览器调试器的“美化(Pretty-print)”功能,并善用断点单步执行,观察变量值的变化来理解逻辑。
逆向心得:
- 目的要合法:此类分析仅用于对自己拥有权限的系统进行安全评估、学习研究或自动化测试,切勿用于非法攻击。
- 从简单入手:先搜索
CryptoJS、encrypt、RSA等明显关键词。很多应用加密并不复杂。- 对比验证:用已知的输入输出(如用
123456作为密码提交一次)来验证你编写的模拟加密脚本是否正确,这是最可靠的调试方法。- 关注网络请求:加密所需的密钥、盐、IV等参数,很可能在页面加载时通过另一个API请求获取,记得检查登录前的网络请求。
7. 常见问题与排查技巧实录
在实际开发和逆向分析中,你会遇到各种各样的问题。这里我整理了一份速查表,涵盖了最常见的一些坑。
| 问题现象 | 可能原因 | 排查思路与解决方案 |
|---|---|---|
| AES解密失败,提示“Padding is incorrect.” | 1. 密钥错误。 2. IV错误。 3. 密文在传输或处理过程中被损坏(如Base64解码错误)。 4. 加密和解密使用的填充模式不一致。 | 1. 确认密钥和IV的字节序列完全一致,包括编码(UTF-8, Hex等)。 2. 打印并对比加密端和解密端的密钥、IV、密文的Hex或Base64值。 3. 确保Base64解码正确,无换行符或空格干扰。 4. 确认两端都是PKCS7/PKCS5填充。 |
| RSA解密失败或验证签名失败 | 1. 公钥私钥不匹配。 2.填充方案不一致(最常见!)。 3. 哈希算法不一致(签名时)。 4. 加密的数据超过了RSA密钥能处理的最大长度。 | 1. 确认使用的是正确的密钥对。 2.重点检查:加密方用OAEP,解密方也必须用OAEP;签名方用PSS,验证方也必须用PSS。跨语言时尤其要查文档确认常量名。 3. 签名时,双方使用的哈希函数(SHA256, SHA1等)必须相同。 4. RSA加密前,检查明文长度。对于OAEP填充,最大明文长度 ≈ 密钥字节数 - 2*哈希长度 - 2。超长数据应改用“混合加密”。 |
| 前端CryptoJS加密,后端解密乱码 | 1. CryptoJS默认输出的是包含盐、IV等信息的OpenSSL格式字符串,而非纯密文。 2. WordArray到字节数组的转换问题。 | 1. 如果前端使用CryptoJS.AES.encrypt(plaintext, key).toString(),后端需要用能解析此格式的库(如Python的cryptography或pycryptodome配合特定方法)。2. 更可靠的做法是前端像我们示例那样,明确指定参数并提取 ciphertext进行Base64编码:encrypted.ciphertext.toString(CryptoJS.enc.Base64),后端直接用此Base64字符串和相同的key/iv解密。 |
| “navicat15 rsa public key not find” 类错误 | 通常发生在使用RSA公钥文件进行连接或激活时。 | 1. 确认公钥文件路径正确且文件存在。 2. 确认公钥文件格式是PEM等受支持的格式。 3. 某些工具可能需要公钥是PKCS#1格式( -----BEGIN RSA PUBLIC KEY-----),而你提供的可能是PKCS#8格式(-----BEGIN PUBLIC KEY-----),需要进行格式转换。可以使用openssl rsa -pubin -in pubkey.pem -RSAPublicKey_out进行转换。 |
| JS逆向时,找不到加密函数入口 | 代码被混淆、压缩或加密逻辑被隐藏。 | 1. 使用XHR/Fetch断点和事件监听器断点,这是定位入口最有效的方法。 2. 在发起登录请求的瞬间,在Console中执行 debugger;语句可能触发调试器(需在特定上下文中)。3. 搜索网络请求负载中的固定字符串或参数名。 4. 如果使用了Webpack等打包工具,可以尝试在Source面板搜索 webpackJsonp或__webpack_require__相关的模块,然后在其内部查找。 |
| 不同语言/平台加密结果不一致 | 1. 默认参数不同(如AES的默认模式、填充)。 2. 字符串编码不同(UTF-8, GBK, Latin-1)。 3. 密钥/IV的派生方式不同。 | 1.显式指定所有参数:算法、模式、填充、IV、密钥、编码。不要依赖任何默认值。 2.统一编码:在所有环节强制使用UTF-8编码。 3.使用Hex或Base64中间表示:在调试时,将密钥、IV、明文、密文都以Hex或Base64格式打印出来,进行逐字节对比。 |
| 性能问题:RSA加密解密太慢 | RSA运算本身就很耗时,尤其是私钥操作(解密/签名)。 | 1.遵循混合加密原则:只用RSA加密一个随机的对称密钥(如32字节的AES-256密钥),然后用对称密钥加密数据。 2.考虑使用ECC:对于需要高性能非对称加密的场景(如TLS握手、大量签名),椭圆曲线密码学(ECC)在相同安全强度下,密钥更短、速度更快。例如,256位的ECC密钥安全性相当于3072位的RSA密钥。 |
掌握DES、AES、RSA这三大算法,就如同掌握了现代数据安全世界的核心拼图。从DES的Feistel结构启蒙,到AES的SPN网络成为中流砥柱,再到RSA的公私钥哲学解决密钥分发难题,它们的演进本身就是一部浓缩的密码学史。真正的安全不在于使用了多么高深的算法,而在于你是否理解了这些算法背后的原理、适用场景和那些微妙的“注意事项”。在逆向实战中,我们看到了一个反面教材——硬编码密钥的前端加密,这提醒我们,错误地使用加密比不使用更危险,因为它会制造一种虚假的安全感。无论是开发还是分析,记住一个原则:显式优于隐式,验证优于假设。明确指定每一个加密参数,并在跨系统交互时进行充分的对比验证,这样才能构建出真正坚固的安全防线。