说明
在网站开发中,接口的安全是重中之重,有些付费接口,防止被别人拿去重发等。
有一个方式来保护接口还是非常有必要的。
本来考虑的是JS,但JS加密太弱了,后来采用的是TypeScript生成的wasm,虽然可以生成了wasm,缺点也很明显,能防一下小白。但高手就很难防的住了。
所以采用了Rust语言来生成wasm。
安装
我用的是Visual Studio 2019,第一步,首先是勾选上:使用 C++ 的桌面开发,安装完成之后。
访问官网:https://rustup.rs/下载rustup-init.exe,然后一路默认即可。
如果不安装:使用 C++ 的桌面开发,会提示link错误。
1、创建库命令:
cargo new --lib secure_core
cd secure_core
2、打开目录下的 Cargo.toml 文件,将其替换为以下内容:
[package]
name = "secure_core"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"] # 告诉编译器我们要输出 C 规范的动态库 (WASM 需要)
[dependencies]
wasm-bindgen = "0.2" # Rust 与 JS 交互的桥梁
js-sys = "0.3" # 允许在 Rust 中调用 JS 的原生 API (如 Date.now)
obfstr = "0.4" # 核心:编译期字符串混淆宏!
[dependencies.web-sys]
version = "0.3"
features = [
"Window",
"Navigator",
"Document",
"HtmlCanvasElement",
"Location"
]
3、打开 src/lib.rs
use wasm_bindgen::prelude::*;
use js_sys::{Date, Math, Reflect};
use obfstr::obfstr;
const M: [usize; 40] = [
0x21, 0x20, 0x05, 0x0E, 0x1C, 0x09, 0x27, 0x02, 0x15, 0x0B,
0x28, 0x07, 0x16, 0x12, 0x01, 0x22, 0x0A, 0x19, 0x24, 0x0F,
0x03, 0x1D, 0x0C, 0x25, 0x14, 0x06, 0x1F, 0x1A, 0x13, 0x08,
0x18, 0x21, 0x0D, 0x26, 0x1B, 0x04, 0x23, 0x1E, 0x17, 0x20
];
fn get_key_bytes(hex_str: &str) -> Vec<u8> {
(0..hex_str.len())
.step_by(2)
.map(|i| u8::from_str_radix(&hex_str[i..i + 2], 16).unwrap_or(0))
.collect()
}
fn djb2_hash(data: &str) -> u32 {
let mut hash: u32 = 5381;
for byte in data.bytes() {
hash = hash.wrapping_mul(33).wrapping_add(byte as u32);
}
hash
}
fn check_safe_environment(window: &web_sys::Window) -> bool {
let global = js_sys::global();
if let Ok(has_process) = Reflect::has(&global, &JsValue::from_str("process")) {
if has_process { return false; }
}
let navigator = window.navigator();
if let Ok(webdriver_val) = Reflect::get(&navigator, &JsValue::from_str("webdriver")) {
if webdriver_val.is_truthy() {
return false;
}
}
let hardware_concurrency = navigator.hardware_concurrency();
if hardware_concurrency == 0.0 {
return false;
}
if let Some(document) = window.document() {
if let Ok(canvas) = document.create_element("canvas") {
if Reflect::has(&canvas, &JsValue::from_str("toDataURL")).is_err() {
return false;
}
} else {
return false;
}
}
let hostname = window.location().hostname().unwrap_or_else(|_| "".to_string());
if hostname != "cz.lxjc.com" {
return false;
}
true
}
#[wasm_bindgen]
pub fn pack(d: &str) -> String {
if d.is_empty() {
return String::new();
}
let window = match web_sys::window() {
Some(w) => w,
None => return "41850692d5732be0140cb164a2bc671f".to_string(),
};
if !check_safe_environment(&window) {
return "41850692d5732be0140cb164a2bc671f".to_string();
}
let key_bytes = get_key_bytes(obfstr!("3851094726148290537692751830464068259137"));
let current_ms = Date::now();
let nonce = format!("{:08x}", (Math::random() * 4294967295.0) as u32);
let time_window = (current_ms / 60000.0) as u64;
let token_seed = format!("SUPER_SECRET_{}_{}_{}", obfstr!("3851"), time_window, nonce);
let local_token = format!("{:08x}", djb2_hash(&token_seed));
let base_payload = format!("{}|{}|{}", d, nonce, local_token);
let checksum = format!("{:08x}", djb2_hash(&base_payload));
let payload = format!("{}|{}", base_payload, checksum);
let raw = payload.as_bytes();
let pad_len = 20 - (raw.len() % 20);
let mut padded = raw.to_vec();
padded.resize(raw.len() + pad_len, pad_len as u8);
let mut iv: u32 = 0x5C;
let blocks = padded.len() / 20;
for b in 0..blocks {
let offset = b * 20;
let mut nibbles = vec![0u8; 40];
for i in 0..20 {
nibbles[i * 2] = (padded[offset + i] >> 4) & 0x0F;
nibbles[i * 2 + 1] = padded[offset + i] & 0x0F;
}
let mut permuted = vec![0u8; 40];
for i in 0..40 {
let idx = M[i] - 1;
permuted[i] = nibbles[idx];
}
for i in 0..20 {
padded[offset + i] = (permuted[i * 2] << 4) | permuted[i * 2 + 1];
}
for i in 0..20 {
let mut p = padded[offset + i] as u32;
p = p ^ iv;
p = (p + key_bytes[i] as u32) & 0xFF;
p = ((p << 3) | (p >> 5)) & 0xFF;
let magic = (0xA3 + i as u32 * 17) & 0xFF;
p = p ^ magic;
p = p ^ key_bytes[19 - i] as u32;
padded[offset + i] = p as u8;
iv = p;
}
}
let mut out = String::with_capacity(padded.len() * 2);
for b in padded {
out.push_str(&format!("{:02x}", b));
}
out
}
#[wasm_bindgen]
pub fn unpack_response(hex_str: &str) -> String {
if hex_str.is_empty() || hex_str.len() % 2 != 0 {
return String::new();
}
let rk: [u8; 16] = [
0x75, 0x28, 0x30, 0x46, 0x40, 0x68, 0x25, 0x91,
0x37, 0x38, 0x52, 0x09, 0x47, 0x26, 0x14, 0x82
];
let mut bytes = Vec::new();
for i in (0..hex_str.len()).step_by(2) {
if let Ok(b) = u8::from_str_radix(&hex_str[i..i+2], 16) {
bytes.push(b);
} else {
return String::new();
}
}
let mut state: u32 = 0x4B3C2D1E;
let mut plain = Vec::with_capacity(bytes.len());
for i in 0..bytes.len() {
state = state.wrapping_mul(214013).wrapping_add(2531011);
let rand_byte = ((state >> 16) & 0xFF) as u8;
let key_modifier = rk[i % 16];
let mut c = bytes[i];
c = c.wrapping_sub(i as u8);
let p = c ^ rand_byte ^ key_modifier;
plain.push(p);
}
String::from_utf8(plain).unwrap_or_else(|_| String::new())
}
然后通过CMD输入:cd C:\Users\Administrator\secure_core
再执行:wasm-pack build --target web
即可在目录下,生成了pkg文件。这就是混淆之后的wasm文件。
引用
index.php
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WASM 加解密完整闭环测试</title>
<style>
body { font-family: system-ui; max-width: 600px; margin: 50px auto; padding: 20px; }
textarea { width: 100%; height: 80px; margin-bottom: 10px; padding: 10px; }
button { padding: 10px 20px; background: #007bff; color: white; border: none; cursor: pointer; border-radius: 4px; }
button:hover { background: #0056b3; }
.box { margin-top: 20px; padding: 15px; background: #f8f9fa; border: 1px solid #ddd; word-wrap: break-word; border-radius: 4px; }
.success { color: green; font-weight: bold; }
.error { color: red; font-weight: bold; }
</style>
</head>
<body>
<h2>前端加密 -> 后端解密 闭环测试</h2>
<textarea id="dataInput" placeholder="输入需要保护的明文 (例如: user_id=123&action=login)"></textarea>
<button id="encryptBtn">加密并发送给后端</button>
<div class="box">
<strong>前端生成的密文 (Hex):</strong>
<div id="cipherResult" style="color: #666; font-size: 0.9em; margin-top: 5px;">等待操作...</div>
</div>
<div class="box">
<strong>后端 PHP 返回的解密结果:</strong>
<div id="serverResult" style="margin-top: 5px;">等待服务器响应...</div>
</div>
<script type="module">
import init, { pack, unpack_response } from './pkg/secure_core.js';
async function run() {
try {
await init({ module_or_path: './pkg/secure_core_bg.wasm' });
console.log("引擎加载成功!");
} catch (err) {
alert("WASM 初始化失败: " + err);
return;
}
document.getElementById('encryptBtn').addEventListener('click', async () => {
const rawData = document.getElementById('dataInput').value.trim();
if (!rawData) {
alert("请输入数据");
return;
}
let encryptedHex;
try {
encryptedHex = pack(rawData);
} catch (err) {
alert("加密过程崩溃: " + err);
return;
}
document.getElementById('cipherResult').innerText = encryptedHex;
document.getElementById('serverResult').innerText = "正在发送请求中...";
try {
const response = await fetch('jiemi.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ payload: encryptedHex })
});
const resObject = await response.json();
const resultDiv = document.getElementById('serverResult');
if (resObject && resObject.payload) {
let decryptedResponseText;
try {
decryptedResponseText = unpack_response(resObject.payload);
} catch(decErr) {
resultDiv.innerHTML = `<span class="error">前端解密回程失败:</span> ${decErr}`;
return;
}
const resData = JSON.parse(decryptedResponseText);
if (resData.code === 200) {
resultDiv.innerHTML = `<span class="success">成功提取明文:</span> ${resData.data}`;
} else {
resultDiv.innerHTML = `<span class="error">验证失败 [${resData.code}]:</span> ${resData.msg}`;
}
} else {
resultDiv.innerHTML = `<span class="error">异常响应:</span> 后端未返回合法密文`;
}
} catch (err) {
console.error(err);
document.getElementById('serverResult').innerHTML = `<span class="error">请求后端失败,请检查网络</span>`;
}
});
}
run();
</script>
</body>
</html>
解密:
jiemi.php
<?php
class WasmCrypto
{
private static $m = [
0x21, 0x20, 0x05, 0x0E, 0x1C, 0x09, 0x27, 0x02, 0x15, 0x0B,
0x28, 0x07, 0x16, 0x12, 0x01, 0x22, 0x0A, 0x19, 0x24, 0x0F,
0x03, 0x1D, 0x0C, 0x25, 0x14, 0x06, 0x1F, 0x1A, 0x13, 0x08,
0x18, 0x21, 0x0D, 0x26, 0x1B, 0x04, 0x23, 0x1E, 0x17, 0x20
];
private static $k = "3851094726148290537692751830464068259137";
private static $rk = [
0x75, 0x28, 0x30, 0x46, 0x40, 0x68, 0x25, 0x91,
0x37, 0x38, 0x52, 0x09, 0x47, 0x26, 0x14, 0x82
];
private static function getKeyBytes()
{
$kb = [];
for ($i = 0; $i < 20; $i++) {
$kb[] = hexdec(substr(self::$k, $i * 2, 2));
}
return $kb;
}
public static function djb2_hash($str)
{
$hash = 5381;
for ($i = 0, $len = strlen($str); $i < $len; $i++) {
$hash = (($hash << 5) + $hash + ord($str[$i])) & 0xFFFFFFFF;
}
return sprintf("%08x", $hash);
}
public static function unpack($hexString)
{
$hexString = trim($hexString);
if (empty($hexString) || strlen($hexString) % 2 !== 0) {
return false;
}
$len = strlen($hexString) / 2;
$dec = [];
for ($i = 0; $i < $len; $i++) {
$dec[] = hexdec(substr($hexString, $i * 2, 2));
}
$keyBytes = self::getKeyBytes();
$iv = 0x5C;
$blocks = $len / 20;
for ($b = 0; $b < $blocks; $b++) {
$offset = $b * 20;
for ($i = 0; $i < 20; $i++) {
$c = $dec[$offset + $i];
$p = $c;
$p = $p ^ $keyBytes[19 - $i];
$magic = (0xA3 + $i * 17) & 0xFF;
$p = $p ^ $magic;
$p = (($p >> 3) | ($p << 5)) & 0xFF;
$p = ($p + 256 - $keyBytes[$i]) & 0xFF;
$p = $p ^ $iv;
$iv = $c;
$dec[$offset + $i] = $p;
}
$nibbles = array_fill(0, 40, 0);
for ($i = 0; $i < 20; $i++) {
$nibbles[$i * 2] = ($dec[$offset + $i] >> 4) & 0x0F;
$nibbles[$i * 2 + 1] = $dec[$offset + $i] & 0x0F;
}
$unpermuted = array_fill(0, 40, 0);
for ($i = 0; $i < 40; $i++) {
$idx = self::$m[$i] - 1;
$unpermuted[$idx] = $nibbles[$i];
}
for ($i = 0; $i < 20; $i++) {
$dec[$offset + $i] = ($unpermuted[$i * 2] << 4) | $unpermuted[$i * 2 + 1];
}
}
$padLen = end($dec);
if ($padLen > 0 && $padLen <= 20) {
$dec = array_slice($dec, 0, count($dec) - $padLen);
}
$payloadStr = '';
foreach ($dec as $byte) {
$payloadStr .= chr($byte);
}
return $payloadStr;
}
public static function response_pack($plainText)
{
$bytes = array_values(unpack('C*', $plainText));
$len = count($bytes);
$state = 0x4B3C2D1E;
$out = [];
for ($i = 0; $i < $len; $i++) {
$state = ($state * 214013 + 2531011) & 0xFFFFFFFF;
$rand_byte = ($state >> 16) & 0xFF;
$key_modifier = self::$rk[$i % 16];
$c = $bytes[$i] ^ $rand_byte ^ $key_modifier;
$c = ($c + $i) & 0xFF;
$out[] = sprintf("%02x", $c);
}
return implode('', $out);
}
}
header('Content-Type: application/json; charset=utf-8');
$jsonInput = file_get_contents('php://input');
$requestData = json_decode($jsonInput, true);
$encryptedHex = isset($requestData['payload']) ? $requestData['payload'] : '';
if (empty($encryptedHex)) {
die(json_encode(["payload" => WasmCrypto::response_pack(json_encode(["code" => 400, "msg" => "缺失加密数据"]))]));
}
$decryptedStr = WasmCrypto::unpack($encryptedHex);
if ($decryptedStr !== false) {
$parts = explode('|', $decryptedStr);
if (count($parts) < 4) {
die(json_encode(["payload" => WasmCrypto::response_pack(json_encode(["code" => 403, "msg" => "非法请求:数据格式损坏或来源不受信任"]))]));
}
$checksum_from_client = array_pop($parts);
$local_token_from_client = array_pop($parts);
$nonce = array_pop($parts);
$realData = implode('|', $parts);
$basePayload = $realData . '|' . $nonce . '|' . $local_token_from_client;
$calculated_checksum = WasmCrypto::djb2_hash($basePayload);
if ($calculated_checksum !== $checksum_from_client) {
die(json_encode(["payload" => WasmCrypto::response_pack(json_encode(["code" => 403, "msg" => "非法请求:数据校验失败,涉嫌篡改!"]))]));
}
$now = microtime(true) * 1000;
$current_window = floor($now / 60000);
$previous_window = $current_window - 1;
$next_window = $current_window + 1;
$secret_prefix = "SUPER_SECRET_3851_";
$expected_token_current = WasmCrypto::djb2_hash($secret_prefix . $current_window . '_' . $nonce);
$expected_token_prev = WasmCrypto::djb2_hash($secret_prefix . $previous_window . '_' . $nonce);
$expected_token_next = WasmCrypto::djb2_hash($secret_prefix . $next_window . '_' . $nonce);
if (
$local_token_from_client !== $expected_token_current &&
$local_token_from_client !== $expected_token_prev &&
$local_token_from_client !== $expected_token_next
) {
die(json_encode(["payload" => WasmCrypto::response_pack(json_encode(["code" => 403, "msg" => "非法请求:动态环境令牌失效"]))]));
}
$responseData = json_encode([
"code" => 200,
"msg" => "验证通过",
"data" => $realData
], JSON_UNESCAPED_UNICODE);
echo json_encode(["payload" => WasmCrypto::response_pack($responseData)]);
} else {
$errorData = json_encode(["code" => 403, "msg" => "非法请求:解密彻底失败"], JSON_UNESCAPED_UNICODE);
echo json_encode(["payload" => WasmCrypto::response_pack($errorData)]);
}