90 lines
4.3 KiB
JavaScript
90 lines
4.3 KiB
JavaScript
layui.define(function (exports) {
|
||
"use strict";
|
||
|
||
class WeChatJsonCrypto {
|
||
constructor(token, encodingAesKey, appId) {
|
||
if (!token || !appId || encodingAesKey.length !== 43 || !/^[A-Za-z0-9+\/]{43}$/.test(encodingAesKey)) {
|
||
throw new Error("配置错误:Token/AppID不能为空,EncodingAESKey需为43位标准Base64字符");
|
||
}
|
||
this.token = token;
|
||
this.appId = appId;
|
||
this.blockSize = 32;
|
||
this.encodingAesKey = CryptoJS.enc.Base64.parse(encodingAesKey + '=');
|
||
if (!this.encodingAesKey.words?.length) throw new Error("密钥解析失败");
|
||
}
|
||
|
||
// ✅ 核心修复1:加密时直接传入明文WordArray(非{ ciphertext: ... })
|
||
async encrypt(data) {
|
||
const jsonBytes = new TextEncoder().encode(JSON.stringify(data, null, 0));
|
||
const appIdBytes = new TextEncoder().encode(this.appId);
|
||
const random = crypto.getRandomValues(new Uint8Array(16));
|
||
const lenBytes = new Uint8Array(new DataView(new ArrayBuffer(4)).buffer);
|
||
new DataView(lenBytes.buffer).setUint32(0, jsonBytes.length, false);
|
||
|
||
// 拼接明文: random(16) + len(4) + json + appId
|
||
const plain = new Uint8Array(20 + jsonBytes.length + appIdBytes.length);
|
||
plain.set(random); plain.set(lenBytes, 16);
|
||
plain.set(jsonBytes, 20); plain.set(appIdBytes, 20 + jsonBytes.length);
|
||
|
||
// PKCS#7填充
|
||
const pad = this.blockSize - (plain.length % this.blockSize);
|
||
const padded = new Uint8Array(plain.length + pad).fill(pad, plain.length);
|
||
padded.set(plain);
|
||
|
||
const iv = CryptoJS.lib.WordArray.create(this.encodingAesKey.words.slice(0, 4));
|
||
const encrypted = CryptoJS.AES.encrypt(
|
||
CryptoJS.lib.WordArray.create(padded), // ← 修复点:直接传明文
|
||
this.encodingAesKey,
|
||
{ iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.NoPadding }
|
||
);
|
||
return CryptoJS.enc.Base64.stringify(encrypted.ciphertext);
|
||
}
|
||
|
||
async decrypt(encryptedBase64) {
|
||
try {
|
||
const cipherWA = CryptoJS.enc.Base64.parse(encryptedBase64);
|
||
if (cipherWA.sigBytes < 32) return null;
|
||
|
||
const iv = CryptoJS.lib.WordArray.create(this.encodingAesKey.words.slice(0, 4));
|
||
const decryptedWA = CryptoJS.AES.decrypt(
|
||
{ ciphertext: cipherWA },
|
||
this.encodingAesKey,
|
||
{ iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.NoPadding }
|
||
);
|
||
|
||
// 转Uint8Array
|
||
const decrypted = new Uint8Array(decryptedWA.sigBytes);
|
||
for (let i = 0; i < decryptedWA.sigBytes; i++) {
|
||
decrypted[i] = (decryptedWA.words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
|
||
}
|
||
|
||
// ✅ 核心修复2:严格PKCS#7填充验证与移除
|
||
const padLen = decrypted[decrypted.length - 1];
|
||
if (padLen < 1 || padLen > this.blockSize || decrypted.length < padLen) return null;
|
||
for (let i = decrypted.length - padLen; i < decrypted.length; i++) {
|
||
if (decrypted[i] !== padLen) return null;
|
||
}
|
||
const plain = decrypted.slice(0, -padLen); // 移除填充
|
||
|
||
if (plain.length < 20) return null;
|
||
const jsonLen = new DataView(plain.slice(16, 20).buffer).getUint32(0, false);
|
||
if (jsonLen <= 0 || 20 + jsonLen > plain.length) return null;
|
||
|
||
const jsonStr = new TextDecoder().decode(plain.slice(20, 20 + jsonLen));
|
||
// ✅ 核心修复3:AppID直接解码(无字符过滤)
|
||
const fromAppId = new TextDecoder().decode(plain.slice(20 + jsonLen));
|
||
if (fromAppId !== this.appId) return null;
|
||
|
||
return JSON.parse(jsonStr);
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
generateSignature(timestamp, nonce, encryptMsg) {
|
||
return CryptoJS.SHA1([this.token, timestamp, nonce, encryptMsg].sort().join('')).toString(CryptoJS.enc.Hex);
|
||
}
|
||
}
|
||
// 导出模块
|
||
exports('JsonCrypto', WeChatJsonCrypto);
|
||
}); |