> ## Content Index
> Fetch the complete content index at: https://blog.vercanti.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# RSA
- URL: https://blog.vercanti.com/rsa/
- Published: 2026-08-28T14:35:04.000Z
- Updated: 2026-08-28T14:57:59.000Z
- Description: RSA 是最常见的非对称加密算法。公钥加密，私钥解密；或私钥签名，公钥验签。JS 逆向中通常只需要用公钥加密（还原加密过程），私钥由服务端持有，无法获取。 逆向时在源码中找到的公钥常见两种形式： 还有一种是直接给出模数（n）和指数（e）： 依赖库：pip install pycryptodome 或 pip install cryptography 搜索 JSEncrypt 或 -----BEGIN PUBLIC KEY----- 快速定位：这两个关键词几乎覆盖了 Web 端 RSA 的所有使用场景；其次搜索 setPublicKey、encrypt、B
- Author: yellowdog
- Tags: js逆向, 加密算法

> 官方文档：<https://www.rfc-editor.org/rfc/rfc8017>  
> 适用场景：JS 逆向中提取 RSA 公钥、还原加密参数生成逻辑

RSA 是最常见的非对称加密算法。公钥加密，私钥解密；或私钥签名，公钥验签。JS 逆向中通常只需要用公钥加密（还原加密过程），私钥由服务端持有，无法获取。

---

## 关键概念

| 概念   | 说明                                      |
| ---- | --------------------------------------- |
| 公钥   | 可公开，用于加密或验签                             |
| 私钥   | 保密，用于解密或签名                              |
| 密钥长度 | 常见 1024 / 2048 / 4096 位                 |
| 填充方式 | PKCS#1 v1.5（含随机字节，同一明文每次密文不同）、OAEP（更安全） |
| 输出格式 | 通常为 Base64                              |

---

## 公钥格式

逆向时在源码中找到的公钥常见两种形式：

```
-----BEGIN PUBLIC KEY-----          ← SPKI 格式（X.509）
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A...
-----END PUBLIC KEY-----

-----BEGIN RSA PUBLIC KEY-----      ← PKCS#1 格式
MIIBCgKCAQEA...
-----END RSA PUBLIC KEY-----

```

还有一种是直接给出模数（n）和指数（e）：

```javascript
// 源码中常见
var rsa = new JSEncrypt();
rsa.setPublicKey('-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----');

```

---

## Python 实现

依赖库：`pip install pycryptodome` 或 `pip install cryptography`

### 使用 pycryptodome

```python
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP, PKCS1_v1_5
from Crypto.Signature import pkcs1_15
from Crypto.Hash import SHA256
import base64

# ── 生成密钥对（逆向时通常不需要，直接用现成公钥）──────────────

def generate_rsa_keypair(bits=2048):
    key = RSA.generate(bits)
    return {
        'private': key.export_key().decode(),
        'public':  key.publickey().export_key().decode()
    }

# ── PKCS#1 v1.5 加密（常见于老系统）──────────────────────────

def rsa_pkcs1_encrypt(plaintext: str, public_key_pem: str) -> str:
    key    = RSA.import_key(public_key_pem)
    cipher = PKCS1_v1_5.new(key)
    return base64.b64encode(cipher.encrypt(plaintext.encode())).decode()

def rsa_pkcs1_decrypt(ciphertext_b64: str, private_key_pem: str) -> str:
    key    = RSA.import_key(private_key_pem)
    cipher = PKCS1_v1_5.new(key)
    return cipher.decrypt(base64.b64decode(ciphertext_b64), None).decode()

# ── OAEP 加密（更安全，现代推荐）─────────────────────────────

def rsa_oaep_encrypt(plaintext: str, public_key_pem: str) -> str:
    key    = RSA.import_key(public_key_pem)
    cipher = PKCS1_OAEP.new(key)
    return base64.b64encode(cipher.encrypt(plaintext.encode())).decode()

def rsa_oaep_decrypt(ciphertext_b64: str, private_key_pem: str) -> str:
    key    = RSA.import_key(private_key_pem)
    cipher = PKCS1_OAEP.new(key)
    return cipher.decrypt(base64.b64decode(ciphertext_b64)).decode()

# ── 签名与验签 ────────────────────────────────────────────────

def rsa_sign(message: str, private_key_pem: str) -> str:
    key       = RSA.import_key(private_key_pem)
    msg_hash  = SHA256.new(message.encode())
    signature = pkcs1_15.new(key).sign(msg_hash)
    return base64.b64encode(signature).decode()

def rsa_verify(message: str, signature_b64: str, public_key_pem: str) -> bool:
    key       = RSA.import_key(public_key_pem)
    msg_hash  = SHA256.new(message.encode())
    try:
        pkcs1_15.new(key).verify(msg_hash, base64.b64decode(signature_b64))
        return True
    except (ValueError, TypeError):
        return False

# ── 从模数和指数构造公钥（逆向中常见）────────────────────────

def rsa_from_n_e(n_hex: str, e: int = 65537) -> str:
    """从十六进制模数和指数构造公钥 PEM"""
    n   = int(n_hex, 16)
    key = RSA.construct((n, e))
    return key.publickey().export_key().decode()

```

### 使用 cryptography 库

```python
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.asymmetric import rsa, padding
import base64

# 加载公钥
def load_public_key(pem: str):
    return serialization.load_pem_public_key(pem.encode())

# OAEP 加密
def rsa_oaep_encrypt(plaintext: str, public_key_pem: str) -> str:
    pub_key    = load_public_key(public_key_pem)
    ciphertext = pub_key.encrypt(
        plaintext.encode(),
        padding.OAEP(
            mgf=padding.MGF1(algorithm=hashes.SHA256()),
            algorithm=hashes.SHA256(),
            label=None
        )
    )
    return base64.b64encode(ciphertext).decode()

# PKCS#1 v1.5 加密
def rsa_pkcs1_encrypt(plaintext: str, public_key_pem: str) -> str:
    pub_key    = load_public_key(public_key_pem)
    ciphertext = pub_key.encrypt(plaintext.encode(), padding.PKCS1v15())
    return base64.b64encode(ciphertext).decode()

```

---

## JavaScript 实现

### 使用 JSEncrypt（浏览器，逆向中最常见）

```javascript
// 浏览器环境，源码中常见此写法
const JSEncrypt = require('jsencrypt').JSEncrypt;

const publicKey = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----`;

function rsaEncrypt(plaintext, pubKey) {
    const rsa = new JSEncrypt();
    rsa.setPublicKey(pubKey);
    return rsa.encrypt(plaintext);  // 返回 Base64 字符串
}

// 解密（需要私钥，逆向中通常无法获取）
function rsaDecrypt(ciphertext, privKey) {
    const rsa = new JSEncrypt();
    rsa.setPrivateKey(privKey);
    return rsa.decrypt(ciphertext);
}

console.log(rsaEncrypt('hello', publicKey));

```

### 使用 node-forge

```javascript
const forge = require('node-forge');

const publicKeyPem = `-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----`;

// PKCS#1 v1.5 加密
function rsaEncryptPkcs1(plaintext, pubKeyPem) {
    const publicKey = forge.pki.publicKeyFromPem(pubKeyPem);
    const encrypted = publicKey.encrypt(plaintext, 'RSAES-PKCS1-V1_5');
    return forge.util.encode64(encrypted);
}

// OAEP 加密
function rsaEncryptOaep(plaintext, pubKeyPem) {
    const publicKey = forge.pki.publicKeyFromPem(pubKeyPem);
    const encrypted = publicKey.encrypt(plaintext, 'RSA-OAEP', {
        md: forge.md.sha256.create(),
        mgf1: { md: forge.md.sha256.create() }
    });
    return forge.util.encode64(encrypted);
}

// 签名（需要私钥）
function rsaSign(message, privKeyPem) {
    const privateKey = forge.pki.privateKeyFromPem(privKeyPem);
    const md = forge.md.sha256.create();
    md.update(message, 'utf8');
    return forge.util.encode64(privateKey.sign(md));
}

```

### 使用 Node.js 内置 crypto

```javascript
const crypto = require('crypto');
const fs     = require('fs');

// 加载公钥文件
const publicKey = fs.readFileSync('public.pem', 'utf8');

// OAEP 加密
function rsaOaepEncrypt(plaintext, pubKey) {
    return crypto.publicEncrypt(
        {
            key:        pubKey,
            padding:    crypto.constants.RSA_PKCS1_OAEP_PADDING,
            oaepHash:   'sha256'
        },
        Buffer.from(plaintext)
    ).toString('base64');
}

// PKCS#1 v1.5 加密
function rsaPkcs1Encrypt(plaintext, pubKey) {
    return crypto.publicEncrypt(
        { key: pubKey, padding: crypto.constants.RSA_PKCS1_PADDING },
        Buffer.from(plaintext)
    ).toString('base64');
}

// 私钥解密（OAEP）
function rsaOaepDecrypt(ciphertextB64, privKey) {
    return crypto.privateDecrypt(
        {
            key:      privKey,
            padding:  crypto.constants.RSA_PKCS1_OAEP_PADDING,
            oaepHash: 'sha256'
        },
        Buffer.from(ciphertextB64, 'base64')
    ).toString('utf8');
}

```

---

## 逆向中的定位方法

| 特征                    | 说明                           |
| --------------------- | ---------------------------- |
| BEGIN PUBLIC KEY      | 源码中直接硬编码公钥字符串                |
| setPublicKey / setKey | JSEncrypt 的 API              |
| publicKeyFromPem      | node-forge                   |
| rsa.encrypt           | 常见函数名                        |
| 密文每次不同                | PKCS#1 v1.5 填充含随机字节，可此识别 RSA |

---

## 注意事项

- RSA 加密的数据长度受限：1024 位密钥最多加密 117 字节（PKCS#1 v1.5），因此 RSA 通常只用于加密对称密钥（如 AES key）
- 逆向时通常只能做加密，无法解密（没有私钥）
- 若源码使用 `new JSEncrypt()` 并从接口动态获取公钥，需抓包找到公钥

---

## 最佳实践

**搜索 `JSEncrypt` 或 `-----BEGIN PUBLIC KEY-----` 快速定位**：这两个关键词几乎覆盖了 Web 端 RSA 的所有使用场景；其次搜索 `setPublicKey`、`encrypt`、`BigInteger`（RSA 底层大数运算）。

**Hook `JSEncrypt.prototype.encrypt` 打印明文**：

```javascript
const _encrypt = JSEncrypt.prototype.encrypt
JSEncrypt.prototype.encrypt = function(str) {
  console.log('[RSA encrypt input]', str)
  const result = _encrypt.call(this, str)
  console.log('[RSA encrypt output]', result)
  return result
}

```

**公钥从接口动态获取时先抓包再 Hook**：若公钥通过接口下发，先抓包记录公钥内容；然后在 Python 中用相同公钥和 padding 方案重现加密：`from Crypto.PublicKey import RSA; from Crypto.Cipher import PKCS1_v1_5`。

**区分 PKCS#1 v1.5 和 OAEP 填充**：JSEncrypt 默认使用 PKCS#1 v1.5；`SubtleCrypto` 通常使用 OAEP。Python 中对应 `PKCS1_v1_5.new(key).encrypt(data)` 和 `PKCS1_OAEP.new(key).encrypt(data)`，填充方案不同结果不同。

**RSA 加密非确定性**：同一明文每次加密结果不同（因为 PKCS#1 v1.5 / OAEP 含随机填充），不能通过对比密文验证。需用私钥解密或抓包后对比明文。

---

## 常见陷阱

### 陷阱：用 Python 加密后结果与 JS 不同

**现象：** Python `PKCS1_v1_5.new(key).encrypt(data)` 结果与 JS `JSEncrypt.encrypt(data)` 不同。  
**原因：** 两者输出都正确——PKCS#1 v1.5 含随机 padding，每次加密结果天然不同。服务端用私钥解密后得到相同明文。  
**解决：** 不能比较密文；验证方式是将 Python 加密的结果发给接口，看服务端能否正确解密处理。

### 陷阱：Base64 编码与 Hex 编码混淆

**现象：** Python RSA 加密结果是字节，转为十六进制后与 JS 的 Base64 输出对不上。  
**原因：** JSEncrypt 默认输出 Base64，Python `pycryptodome` 返回 bytes，需要手动 `base64.b64encode(result).decode()`。  
**解决：** 统一输出格式后再对比；注意标准 Base64 和 URL-safe Base64（`+` → `-`，`/` → `_`）的区别。

### 陷阱：公钥格式不同导致解析失败

**现象：** 接口返回的公钥没有 `-----BEGIN PUBLIC KEY-----` 头，Python RSA 库解析报错。  
**原因：** 有些接口返回裸 Base64 编码的公钥（DER 格式），JSEncrypt 会自动识别，但 `RSA.import_key()` 需要完整 PEM 格式。  
**解决：** 手动拼接头尾：`"-----BEGIN PUBLIC KEY-----\n" + raw_key + "\n-----END PUBLIC KEY-----"` 后再导入。

---

## 参见

[魔法数字速查](https://blog.vercanti.com/jia-mi-suan-fa-mo-fa-shu-zi-su-cha/)  
[Base64与编码](https://blog.vercanti.com/base64-yu-bian-ma/)  
[代码片段大全](https://blog.vercanti.com/js-ni-xiang-hook-yu-dai-ma-pian-duan-da-quan/)  
[签名算法还原](https://blog.vercanti.com/qian-ming-suan-fa-huan-yuan/)