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

98 lines
3.0 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 { 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 WechatRefundNotifyResource = {
refund_id: string;
out_refund_no: string;
transaction_id?: string;
out_trade_no?: string;
refund_status: 'SUCCESS' | 'PROCESSING' | 'ABNORMAL' | 'CLOSED';
amount?: { refund?: number; total?: number; payer_refund?: 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();
}