import { createDecipheriv, createVerify, timingSafeEqual } from 'crypto'; export type WechatPayNotifyResource = { transaction_id: string; out_trade_no: string; trade_state: string; trade_state_desc?: string; amount?: { total?: number; payer_total?: number }; }; export type WechatPayNotifyEnvelope = { id: string; create_time: string; event_type: string; resource_type: string; summary: string; resource: { algorithm: string; ciphertext: string; associated_data?: string; nonce: string; original_type?: string; }; }; export function decryptPayResource( apiV3Key: string, associatedData: string, nonce: string, ciphertext: string, ): WechatPayNotifyResource { const key = Buffer.from(apiV3Key, 'utf8'); const buf = Buffer.from(ciphertext, 'base64'); const authTag = buf.subarray(buf.length - 16); const data = buf.subarray(0, buf.length - 16); const decipher = createDecipheriv('aes-256-gcm', key, Buffer.from(nonce, 'utf8')); if (associatedData) { decipher.setAAD(Buffer.from(associatedData, 'utf8')); } decipher.setAuthTag(authTag); const decoded = Buffer.concat([decipher.update(data), decipher.final()]); return JSON.parse(decoded.toString('utf8')) as WechatPayNotifyResource; } export function verifyPaySignature(params: { platformPublicKeyPem: string; timestamp: string; nonce: string; body: string; signature: string; }): boolean { const message = `${params.timestamp}\n${params.nonce}\n${params.body}\n`; const verifier = createVerify('RSA-SHA256'); verifier.update(message); verifier.end(); const ok = verifier.verify(params.platformPublicKeyPem, params.signature, 'base64'); if (!ok) return false; const ts = Number(params.timestamp); if (!Number.isFinite(ts)) return false; const skewMs = Math.abs(Date.now() - ts * 1000); return skewMs <= 5 * 60 * 1000; } export function safeEqual(a: string, b: string): boolean { const ba = Buffer.from(a); const bb = Buffer.from(b); if (ba.length !== bb.length) return false; return timingSafeEqual(ba, bb); } /** * 规范化 .env / system_config 中的 PEM: * - 去掉外层引号(DB/表单常把整段含引号写入) * - 把字面量 \\n 转成真实换行 * OpenSSL 报 1E08010C DECODER unsupported 时多半是这两类污染。 */ export function normalizePemEnv(raw: string | undefined | null): string { if (!raw) return ''; let value = String(raw).trim(); if ( (value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")) ) { value = value.slice(1, -1).trim(); } value = value.replace(/\\r\\n/g, '\n').replace(/\\n/g, '\n').replace(/\r\n/g, '\n'); return value.trim(); }