JavaScript逆向之非对称加密算法

163 阅读9分钟

什么是非对称加密

非对称加密,又称为公钥加密 (Public-key Cryptography),其最核心的特点是: 加密和解密使用的是一对不同的密钥:一个公钥 (Public Key) 和一个私钥 (Private Key)

  • 公钥 (Public Key):可以随意公开,任何人都可以获取。
  • 私钥 (Private Key):必须由所有者自己严格保管,绝不能泄露。

这对密钥是通过复杂的数学算法生成的,它们之间存在着一种特殊的单向关系:通过公钥加密的数据,只能用对应的私钥解密;通过私钥加密(签名)的数据,也只能用对应的公钥解密(验证)

非对称加密的两种核心用途

这对神奇的密钥对带来了两种截然不同的、但都极为重要的应用。

  1. 用于加密(保证机密性) 目标:A 要发送一条只有 B 能看的机密消息。 流程

    1. B 先生成一对公钥和私钥,并将公钥告诉 A(可以通过任何不安全的渠道,比如邮件、网站)。

    2. A 使用 B 的公钥来加密消息。

    3. A 将加密后的密文发送给 B。

    4. B 收到密文后,使用自己私有的、从未泄露过的私钥来解密,从而得到原始消息。 在这个过程中,即使攻击者截获了 B 的公钥和加密后的密文,也无法解密,因为他没有 B 的私钥。

  2. 用于数字签名(保证真实性、完整性和不可否认性) 目标:A 要发送一条消息,并向所有人证明这条消息确实是 A 发的,且未被篡改。 流程

    1. A 先对要发送的消息计算一个哈希值(摘要)。
    2. A 使用自己的私钥对这个哈希值进行“加密”,这个加密后的结果就是数字签名 (Digital Signature)。
    3. A 将原始消息和数字签名一起发送出去。
    4. 接收方(任何人)收到后,执行验证: a. 使用 A 的公钥对数字签名进行“解密”,得到一个哈希值(我们称之为 H1)。 b. 对收到的原始消息重新计算一次哈希值(我们称之为 H2)。 c. 比较 H1 和 H2 是否完全相等。

    如果相等,则证明了两件事:

    • 真实性/身份验证:因为只有 A 的私钥才能生成能被 A 的公钥解开的签名,所以证明消息确实来自 A。
    • 完整性/不可否认性:因为消息的哈希值能对上,证明消息在传输过程中未被篡改。A 也无法否认自己发送过这条消息。

主要特点(优点和缺点)

优点

  1. 解决了密钥分发问题: 不再需要事先安全地传递密钥,只需要公开分发公钥即可。
  2. 支持数字签名: 实现了身份验证和不可否认性,这是对称加密无法做到的。

缺点

  1. 速度极慢 (Very Slow): 非对称加密涉及复杂的数学运算(如大数分解、离散对数),其计算速度比对称加密慢几个数量级(通常是 100 到 1000 倍)。
  2. 不适合加密大量数据: 正因为速度慢,直接用非对称加密来加密大文件或视频是不切实际的。

混合加密:现实世界中的最佳实践

既然对称加密快,而非对称加密解决了密钥分发问题,那么将它们结合起来就是完美的方案。这就是混合加密系统 (Hybrid Encryption System),也是 HTTPS/TLS 等现代安全协议的工作核心。

流程如下

  1. 非对称加密阶段:客户端使用服务器的公钥,加密一个随机生成的、临时的对称加密密钥,然后发送给服务器。
  2. 服务器使用自己的私钥解密,安全地获取了这个临时对称密钥。
  3. 对称加密阶段:现在,客户端和服务器都有了同一个共享的对称密钥。后续所有的通信都使用这个密钥,通过**速度飞快的对称加密算法(如 AES-GCM)**来进行。

这样,既利用了非对称加密的安全性来交换密钥,又利用了对称加密的高效率来传输数据。

非对称加密算法分类

RSA

算法介绍

RSA 是第一个,也是历史上最重要、应用最广泛的非对称加密算法。它的诞生彻底改变了密码学的面貌,使得在不安全的网络(如互联网)上进行安全通信和身份验证成为可能。它的名字来源于三位发明者的姓氏首字母:Rivest、Shamir 和 Adleman。 RSA 的安全性并非基于复杂的算法流程,而是依赖于一个非常简单的数论难题:大整数质因数分解 (Integer Factorization Problem)。

这个难题可以通俗地理解为:

  • 乘法(简单): 给你两个非常大的素数(比如几百位数长),让你将它们相乘得到一个结果。这个计算用计算机可以瞬间完成。
  • 分解(极其困难): 反过来,给你那个巨大的乘积结果,让你找出最初是哪两个素数相乘得到的。这个计算对于目前最强大的计算机来说,如果数字足够大,需要耗费数百年甚至更长的时间,在计算上被认为是不可行的。

在 RSA 中:

  • 公钥 的生成与那个巨大的乘积有关。
  • 私钥 的生成与那两个原始的巨大素数有关。

因此,全世界的人都知道你的公钥(那个大乘积),但只有你知道那两个“秘密”的素数因子,所以只有你能生成对应的私钥。这就是 RSA 安全性的根基。

JavaScript 实现

需先安装node-rsa

npm install node-rsa
// 1. 引入 node-rsa 库
const NodeRSA = require('node-rsa');

// 2. 生成一个新的 2048 位的密钥对
//    b: 密钥长度(位数),推荐 2048 或以上
const key = new NodeRSA({ b: 2048 });

console.log("✅ 1. RSA 密钥对已生成。");

// 导出 PEM 格式的公钥,以便分享
const publicKey = key.exportKey('public');
// console.log("公钥 (PEM 格式):\n", publicKey);

// --- 示例 1: 加密与解密 ---
console.log("\n--- 示例 1: 加密与解密 ---");

const messageToEncrypt = "This is a secret message!";
console.log("原始消息:", messageToEncrypt);

// 3. 使用公钥加密
//    第二个参数 'base64' 指定了加密后输出的编码格式
const encrypted = key.encrypt(messageToEncrypt, 'base64');
console.log("加密后的 Base64:", encrypted);

// 4. 使用私钥解密
//    第二个参数 'utf8' 指定了解密后输出的编码格式
const decrypted = key.decrypt(encrypted, 'utf8');
console.log("解密后的消息:", decrypted);
console.log("验证成功:", messageToEncrypt === decrypted);


// --- 示例 2: 签名与验证 ---
console.log("\n--- 示例 2: 签名与验证 ---");

const messageToSign = "This message is authentic.";
console.log("待签名的消息:", messageToSign);

// 5. 使用私钥签名
//    第二个参数 'base64' 指定了签名输出的编码格式
const signature = key.sign(messageToSign, 'base64');
console.log("生成的签名 (Base64):", signature);

// 6. 使用公钥验证
//    参数: 原始数据, 签名, 原始数据编码, 签名编码
const isVerified = key.verify(messageToSign, signature, 'utf8', 'base64');
console.log("签名是否有效:", isVerified);

// 尝试验证一个被篡改的消息
const tamperedMessage = "This message is NOT authentic.";
const isTamperedVerified = key.verify(tamperedMessage, signature, 'utf8', 'base64');
console.log("篡改后消息的签名是否有效:", isTamperedVerified);

Python 实现

需先安装pycryptodome

pip install pycryptodome
import base64
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP
from Crypto.Signature import pss
from Crypto.Hash import SHA256

# 1. 生成一个新的 2048 位的 RSA 密钥对
key = RSA.generate(2048)
private_key = key
public_key = key.publickey()

print("✅ 1. RSA 密钥对已生成。")

# 为了方便展示,可以导出 PEM 格式的密钥
# public_pem = public_key.export_key().decode('utf-8')
# print("公钥 (PEM 格式):\n", public_pem)


# --- 示例 1: 加密与解密 (使用 PKCS1_OAEP 填充) ---
print("\n--- 示例 1: 加密与解密 ---")

message_to_encrypt = "This is a secret message!"
# 在 Python 中,加密操作处理的是字节 (bytes),所以需要编码
message_bytes = message_to_encrypt.encode('utf-8')
print(f"原始消息: '{message_to_encrypt}'")

# 2. 使用公钥创建加密器对象
cipher_rsa = PKCS1_OAEP.new(public_key)

# 3. 执行加密
encrypted = cipher_rsa.encrypt(message_bytes)
# 加密后的结果是字节,我们使用 Base64 编码以便显示
encrypted_b64 = base64.b64encode(encrypted).decode('utf-8')
print(f"加密后的 Base64: {encrypted_b64}")

# 4. 使用私钥创建解密器对象
decipher_rsa = PKCS1_OAEP.new(private_key)

# 5. 执行解密
decrypted_bytes = decipher_rsa.decrypt(base64.b64decode(encrypted_b64))
decrypted_message = decrypted_bytes.decode('utf-8')
print(f"解密后的消息: '{decrypted_message}'")
print(f"验证成功: {message_to_encrypt == decrypted_message}")


# --- 示例 2: 签名与验证 (使用 PSS 签名方案) ---
print("\n--- 示例 2: 签名与验证 ---")

message_to_sign = "This message is authentic."
message_hash = SHA256.new(message_to_sign.encode('utf-8'))
print(f"待签名的消息: '{message_to_sign}'")

# 6. 使用私钥创建签名器对象
signer = pss.new(private_key)

# 7. 对消息的哈希值进行签名
signature = signer.sign(message_hash)
signature_b64 = base64.b64encode(signature).decode('utf-8')
print(f"生成的签名 (Base64): {signature_b64}")

# 8. 使用公钥创建验证器对象
verifier = pss.new(public_key)

# 9. 验证签名
#    pycryptodome 的推荐做法是使用 try/except 块来处理验证
try:
    verifier.verify(message_hash, signature)
    print("✅ 签名有效。")
except (ValueError, TypeError):
    print("❌ 签名无效!")

# 尝试验证一个被篡改的消息
tampered_message_hash = SHA256.new(b"This message is NOT authentic.")
try:
    verifier.verify(tampered_message_hash, signature)
    print("✅ 篡改后消息的签名有效。 (这不应该发生!)")
except (ValueError, TypeError):
    print("❌ 篡改后消息的签名无效。 (符合预期)")

逆向技巧

  • 搜索关键词 new JSEncrypt(),JSEncrypt 等,一般会使用 JSEncrypt库,会有 new 一个实例对象的操作

  • 搜索关键词 setPublicKey、setKey、setPrivateKey、getPublicKey 等,一般实现的代码里都含有设置密钥的过程

  • 一般公钥和私钥在后端生成,公钥写死在前端或者通过接口传递到前端

  • RSA 的私钥、公钥、明文、密文长度也有一定对应关系,也可以从这方面初步判断:

    私钥长度 (字节)公钥长度 (字节)明文长度 (字节)密文长度 (字节)
    4281281 ~ 5388
    8122161 ~ 117172
    15883921 ~ 245344

案例

在这里插入图片描述 我们看请求参数是长度为174位字符串,很有可能是RSA非对称加密,因此我们搜索关键词JSEncrypt或者setPublicKey,很容易定位到如下加密位置,可以看出公钥是写在js代码中的 在这里插入图片描述