avatar

leviegu

2026年1月17日
使用 Wasm 作为加解密黑盒探索
#技术#AI润色

最近在定位线上问题时,我在打包产物里发现了一段连续字符。全局搜索后确认它是用于数据加解密的密钥。前端代码天然透明,把密钥硬编码进代码里显然不合适。常见的补救手段是对加解密逻辑做混淆,以提高逆向成本。

const AES_KEY = "xs10qllw4FTcQ8YUrM56triR4Ds4NrmB";
const AES_IV = "okukJvVE8fHIX1MymHg7gOo8w15Tj0He";
const get = () => {
return [AES_KEY, AES_IV];
};
const params = get();

将这段示例代码混淆后,可读性会大幅下降,代码里会充斥着诸如 0x13db5c 之类的常量与变量。但在浏览器里仍可能通过调试拿到 AES_KEYAES_IV 的真实值。根因在于密钥写死在前端:在缺少服务端参与的前提下,前端能做到的通常只是“增加难度”,而不是“彻底隐藏”。

Wasm 加解密黑盒

在运行时,密钥必然会以某种形式出现在执行路径上(哪怕是被拆分、拼接、解码后再使用)。如果能把“密钥如何被构造出来”的过程放进一个黑盒里,至少可以显著降低被直接定位/提取的概率。但如前所述,前端代码透明,严格意义上的黑盒并不存在。

一个更现实的方向是:把关键逻辑迁移到 WebAssembly 中,获得一种“近似黑盒”的效果。把加解密实现用 Go/Rust 等语言重写并编译为 wasm 文件,再由前端调用,这对分析打包后的 JS、以及通过调试直接定位密钥,都有一定的对抗收益。

创建 WebAssembly 项目的步骤这里不展开;我选择 Rust 作为重构语言。

crypto-aes-wasm/src/crypto_params.rs
pub const KEY: [u8; 16] = [
0x78, 0x73, 0x31, 0x30, 0x71, 0x6c, 0x6c, 0x77, 0x34, 0x46, 0x54, 0x63, 0x51, 0x38, 0x59, 0x55,
];
pub const IV: [u8; 16] = [
0x6f, 0x6b, 0x75, 0x6b, 0x4a, 0x76, 0x56, 0x45, 0x38, 0x66, 0x48, 0x49, 0x58, 0x31, 0x4d, 0x79,
];
crypto-aes-wasm/src/lib.rs
#[wasm_bindgen]
pub fn encrypt(plain_text: &str) -> String {
let ct = aes128::encrypt_cbc(
&crypto_params::KEY,
&crypto_params::IV,
plain_text.as_bytes(),
);
key_iv_codec::hex_encode(&ct)
}
pub fn decrypt(cipher_text_hex: &str) -> Result<String, JsValue> {
let ct =
key_iv_codec::hex_decode(cipher_text_hex).map_err(|e| JsValue::from_str(&e.to_string()))?;
let pt = aes128::decrypt_cbc(&crypto_params::KEY, &crypto_params::IV, &ct)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
String::from_utf8(pt).map_err(|_| JsValue::from_str("invalid utf-8 plaintext"))
}

重构思路大致如下:

引入 wasm 后,简单验证如下:

项目耗时(ms)吞吐(MiB/s)
WASM encrypt--
WASM decrypt--
WebCrypto AES-CBC encrypt -> hex--
WebCrypto AES-CBC decrypt (from hex) -> string--

将编译后的 wasm 转换为 wat 格式后,可以尝试直接在文本中检索密钥。你会发现转换后的代码呈现出一种类似 LISP 的结构化风格:符号密集、片段离散,想从中快速定位“散落的密钥”会难很多。

(module
(type $t0 (func (param i32 i32)))
(type $t1 (func (param i32 i32) (result i32)))
(type $t2 (func (param i32 i32 i32) (result i32)))
(type $t3 (func (param i32)))
(type $t4 (func (param i32 i32 i32)))
(type $t5 (func (param i32 i32 i32 i32) (result i32)))
(type $t6 (func (result i32 i32 i32 i32)))
(type $t7 (func (result i32 i32)))
(type $t8 (func))
(type $t9 (func (param i32 i32) (result externref)))
(type $t10 (func (param i32) (result i32)))
(type $t11 (func (param i32 i32 i32 i32 i32)))
(type $t12 (func (param i32 i32 i32 i32 i32 i32)))
(type $t13 (func (param i32 i32) (result i32 i32 i32 i32)))
(type $t14 (func (param i32 i32) (result i32 i32)))
(import "./crypto_aes_wasm_bg.js" "__wbindgen_init_externref_table" (func $./crypto_aes_wasm_bg.js.__wbindgen_init_externref_table (type $t8)))
(import "./crypto_aes_wasm_bg.js" "__wbindgen_cast_0000000000000001" (func $./crypto_aes_wasm_bg.js.__wbindgen_cast_0000000000000001 (type $t9)))
(func $f2 (type $t10) (param $p0 i32) (result i32)
(local $l1 i32) (local $l2 i32) (local $l3 i32) (local $l4 i32) (local $l5 i32) (local $l6 i32) (local $l7 i32) (local $l8 i32) (local $l9 i32) (local $l10 i64)
(global.set $g0
(local.tee $l8
(i32.sub
(global.get $g0)
(i32.const 16))))
(block $B0
// ...more

需要强调的是:把密钥“藏进 wasm”并不意味着密钥真正安全。只要攻击者能在前端调用 encrypt / decrypt,就仍然可以把它当作一个加解密 Oracle 来使用,最终的效果更多是“提高提取密钥的门槛”,而非“阻止功能被滥用”。要从根本上解决密钥暴露问题,仍需要服务端参与(例如密钥不下发、按会话派生、请求签名等)。