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"), }; }