Files
dukang/server/dukang-api/src/integrations/wechat/wechat-msg-crypto.util.ts
T
2026-08-04 21:38:49 +08:00

159 lines
5.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<string, string> {
const out: Record<string, string> = {};
const re = /<([A-Za-z0-9_]+)>(?:<!\[CDATA\[([\s\S]*?)\]\]>|([^<]*))<\/\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<string, unknown> {
const trimmed = raw.trim();
if (!trimmed) return {};
if (trimmed.startsWith('{')) {
return JSON.parse(trimmed) as Record<string, unknown>;
}
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<string, unknown>;
};
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<string, unknown>): 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,
};
}