feat: 企微回调服务基建(加解密+事件分发,默认关闭)

This commit is contained in:
ABAPPLO
2026-09-01 15:08:00 +08:00
parent ca3810a3d0
commit b243562e60
5 changed files with 393 additions and 0 deletions

View File

@@ -0,0 +1,97 @@
import {
createCipheriv,
createDecipheriv,
createHash,
randomBytes,
timingSafeEqual,
} from "node:crypto";
export class WecomCallbackCryptoError extends Error {
status: number;
constructor(message: string, status = 400) {
super(message);
this.name = "WecomCallbackCryptoError";
this.status = status;
}
}
export function resolveWecomCallbackAesKey(encodingAesKey: string): Buffer {
const key = String(encodingAesKey ?? "").trim();
if (!/^[A-Za-z0-9]{43}$/.test(key)) {
throw new WecomCallbackCryptoError(
"企业微信回调 EncodingAESKey 非法(需 43 位字母数字)",
500,
);
}
return Buffer.from(`${key}=`, "base64");
}
export function verifyWecomCallbackSignature(
token: string,
timestamp: string,
nonce: string,
encrypt: string,
): string {
return createHash("sha1")
.update([token, timestamp, nonce, encrypt].sort().join(""))
.digest("hex");
}
export function wecomSignaturesMatch(
expected: string,
received: string,
): boolean {
const a = Buffer.from(String(expected ?? ""));
const b = Buffer.from(String(received ?? ""));
if (a.length !== b.length || a.length === 0) return false;
return timingSafeEqual(a, b);
}
export function encryptWecomCallback(
plainMessage: string,
receiveId: string,
encodingAesKey: string,
): string {
const key = resolveWecomCallbackAesKey(encodingAesKey);
const random = randomBytes(16);
const message = Buffer.from(String(plainMessage ?? ""), "utf8");
const receive = Buffer.from(String(receiveId ?? ""), "utf8");
const length = Buffer.alloc(4);
length.writeUInt32BE(message.length, 0);
const cipher = createCipheriv("aes-256-cbc", key, key.subarray(0, 16));
const encrypted = Buffer.concat([
cipher.update(Buffer.concat([random, length, message, receive])),
cipher.final(),
]);
return encrypted.toString("base64");
}
export function decryptWecomCallback(
encrypted: string,
encodingAesKey: string,
): { message: string; receiveId: string } {
const key = resolveWecomCallbackAesKey(encodingAesKey);
const data = Buffer.from(String(encrypted ?? "").trim(), "base64");
if (data.length === 0 || data.length % 16 !== 0) {
throw new WecomCallbackCryptoError("企业微信回调密文长度非法");
}
let raw: Buffer;
try {
const decipher = createDecipheriv("aes-256-cbc", key, key.subarray(0, 16));
raw = Buffer.concat([decipher.update(data), decipher.final()]);
} catch {
throw new WecomCallbackCryptoError("企业微信回调密文解密失败");
}
if (raw.length < 20) {
throw new WecomCallbackCryptoError("企业微信回调明文长度非法");
}
const messageLength = raw.readUInt32BE(16);
if (messageLength <= 0 || 20 + messageLength > raw.length) {
throw new WecomCallbackCryptoError("企业微信回调消息长度字段非法");
}
return {
message: raw.subarray(20, 20 + messageLength).toString("utf8"),
receiveId: raw.subarray(20 + messageLength).toString("utf8"),
};
}