import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'crypto'; /** 微信消息推送:Token/timestamp/nonce[/Encrypt] 字典序拼接后 SHA1 */ export function wechatMsgSha1(...parts: string[]): string { return createHash('sha1').update([...parts].sort().join('')).digest('hex'); } export function verifyWechatUrlSignature( token: string, timestamp: string, nonce: string, signature: string, ): boolean { if (!token || !timestamp || !nonce || !signature) return false; return wechatMsgSha1(token, timestamp, nonce) === signature; } export function verifyWechatMsgSignature( token: string, timestamp: string, nonce: string, encrypt: string, msgSignature: string, ): boolean { if (!token || !timestamp || !nonce || !encrypt || !msgSignature) return false; return wechatMsgSha1(token, timestamp, nonce, encrypt) === msgSignature; } function decodeAesKey(encodingAESKey: string): Buffer { const key = Buffer.from(`${encodingAESKey.trim()}=`, 'base64'); if (key.length !== 32) { throw new Error(`EncodingAESKey 无效(解码后应为 32 字节,实际 ${key.length})`); } return key; } /** * 解密微信安全模式 Encrypt 字段。 * FullStr = random(16) + msg_len(4 BE) + msg + appid */ export function decryptWechatEncrypt( encryptBase64: string, encodingAESKey: string, expectedAppId?: string, ): string { const aesKey = decodeAesKey(encodingAESKey); const iv = aesKey.subarray(0, 16); const decipher = createDecipheriv('aes-256-cbc', aesKey, iv); const decrypted = Buffer.concat([ decipher.update(Buffer.from(encryptBase64, 'base64')), decipher.final(), ]); if (decrypted.length < 20) { throw new Error('解密结果过短'); } const msgLen = decrypted.readUInt32BE(16); const msgStart = 20; const msgEnd = msgStart + msgLen; if (msgEnd > decrypted.length) { throw new Error('解密消息长度非法'); } const msg = decrypted.subarray(msgStart, msgEnd).toString('utf8'); const appId = decrypted.subarray(msgEnd).toString('utf8'); if (expectedAppId && appId && appId !== expectedAppId) { throw new Error(`appid 不匹配: got=${appId}`); } return msg; } /** 加密回包(一般回复 success 明文即可,此函数供需要加密回包时使用) */ export function encryptWechatReply( plain: string, encodingAESKey: string, appId: string, ): string { const aesKey = decodeAesKey(encodingAESKey); const iv = aesKey.subarray(0, 16); const random = randomBytes(16); const msg = Buffer.from(plain, 'utf8'); const msgLen = Buffer.alloc(4); msgLen.writeUInt32BE(msg.length, 0); const full = Buffer.concat([random, msgLen, msg, Buffer.from(appId, 'utf8')]); const cipher = createCipheriv('aes-256-cbc', aesKey, iv); return Buffer.concat([cipher.update(full), cipher.final()]).toString('base64'); } /** 简易 XML 标签提取(微信推送字段无嵌套结构) */ export function parseSimpleXml(xml: string): Record { const out: Record = {}; const re = /<([A-Za-z0-9_]+)>(?:|([^<]*))<\/\1>/g; let m: RegExpExecArray | null; while ((m = re.exec(xml))) { out[m[1]] = (m[2] ?? m[3] ?? '').trim(); } return out; } export function parseWechatPushBody(raw: string): Record { const trimmed = raw.trim(); if (!trimmed) return {}; if (trimmed.startsWith('{')) { return JSON.parse(trimmed) as Record; } return parseSimpleXml(trimmed); } export type WechatTradeManageEvent = { event: string; toUserName?: string; fromUserName?: string; createTime?: number; transactionId?: string; merchantId?: string; subMerchantId?: string; merchantTradeNo?: string; payTime?: number; shippedTime?: number; estimatedSettlementTime?: number; /** 1 手动确认;2 自动确认(结算推送才有) */ confirmReceiveMethod?: number; confirmReceiveTime?: number; settlementTime?: number; msg?: string; raw: Record; }; function num(v: unknown): number | undefined { if (v == null || v === '') return undefined; const n = typeof v === 'number' ? v : Number(v); return Number.isFinite(n) ? n : undefined; } function str(v: unknown): string | undefined { if (v == null) return undefined; const s = String(v).trim(); return s || undefined; } export function normalizeTradeManageEvent(body: Record): WechatTradeManageEvent { return { event: str(body.Event ?? body.event) || '', toUserName: str(body.ToUserName), fromUserName: str(body.FromUserName), createTime: num(body.CreateTime), transactionId: str(body.transaction_id), merchantId: str(body.merchant_id), subMerchantId: str(body.sub_merchant_id), merchantTradeNo: str(body.merchant_trade_no), payTime: num(body.pay_time), shippedTime: num(body.shipped_time), estimatedSettlementTime: num(body.estimated_settlement_time), confirmReceiveMethod: num(body.confirm_receive_method), confirmReceiveTime: num(body.confirm_receive_time), settlementTime: num(body.settlement_time), msg: str(body.msg), raw: body, }; }