RSA

RSA 是最常见的非对称加密算法。公钥加密,私钥解密;或私钥签名,公钥验签。JS 逆向中通常只需要用公钥加密(还原加密过程),私钥由服务端持有,无法获取。 逆向时在源码中找到的公钥常见两种形式: 还有一种是直接给出模数(n)和指数(e): 依赖库:pip install pycryptodome 或 pip install cryptography 搜索 JSEncrypt 或 -----BEGIN PUBLIC KEY----- 快速定位:这两个关键词几乎覆盖了 Web 端 RSA 的所有使用场景;其次搜索 setPublicKey、encrypt、B

分享

官方文档: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):

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

Python 实现

依赖库:pip install pycryptodomepip install cryptography

使用 pycryptodome

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 库

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(浏览器,逆向中最常见)

// 浏览器环境,源码中常见此写法
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

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

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 的所有使用场景;其次搜索 setPublicKeyencryptBigInteger(RSA 底层大数运算)。

Hook JSEncrypt.prototype.encrypt 打印明文

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-----" 后再导入。


参见

魔法数字速查
Base64与编码
代码片段大全
签名算法还原

阅读更多

Web 安全基础

1. HTML 转义(服务端渲染必须): 2. CSP(Content Security Policy): 3. HttpOnly Cookie:防止 JS 读取会话 Cookie: 4. 前端框架防护: 攻击者在第三方网站构造一个表单,诱导已登录用户提交,浏览器会自动携带目标站的 Cookie。 触发条件: 1. 用户已登录目标网站(Cookie 有效) 2. 目标 API 仅凭 Cookie 识别用户身份 3. 请求来源未验证 1. CSRF Token(推荐): 2. SameSite Cookie: 3. 验证 Origin/Referer 头:

By yellowdog

HTTP 协议深度指南

HTTP(HyperText Transfer Protocol)是 Web 的基础传输协议,基于 TCP/IP,采用请求/响应模型。 相关文档:Web安全基础(/web-an-quan-ji-chu/) FastAPI完全指南(/fastapi-wan-quan-zhi-nan/) Nginx完全指南(/nginx-wan-quan-zhi-nan/) 幂等性:多次执行相同请求,服务器状态结果相同。PUT /users/1 多次执行结果一致;POST /users 每次创建新资源,非幂等。 浏览器直接从本地缓存读取,不向服务器发送请求。 缓存命中时,状

By yellowdog

系统设计基础

SLA 对照表: 选择建议:无状态服务(Web 层、API 层)优先水平扩展;数据库初期垂直扩展,达到瓶颈后考虑分库分表或读写分离。 缓存穿透(查询不存在的 key,每次都打到 DB): 缓存击穿(热点 key 过期,瞬间大量请求打到 DB): 缓存雪崩(大量 key 同时过期,或缓存服务宕机): 令牌桶 Python 实现: Redis 实现分布式限流(滑动窗口): URL 命名规则: Cursor 分页响应格式: 雪花算法结构(64 bit): 定义:分布式系统不能同时满足以下三个特性: 在分布式环境中 P 是必须保证的,所以实际是 CP vs AP

By yellowdog

算法思路与模板

二分查找要求序列有序,每次将搜索范围缩减一半,时间复杂度 O(log n)。 两个指针从两端向中间收缩,常用于有序数组。 滑动窗口维护一个满足条件的区间 left, right,right 不断向右扩张,条件不满足时收缩 left。 滑动窗口通用框架: 1. 确定"子问题":原问题可以分解为哪些规模更小的同类问题 2. 定义 dpi 或 dpij 的含义,要足够清晰 3. 推导状态转移方程 4. 确定初始状态(边界条件) 5. 确定计算顺序(确保依赖的子问题先计算) 每件物品最多选一次。dpj = 容量为 j 时的最大价值,逆序遍历容量防止重复选取。 每

By yellowdog