ForcePilot/backend/package/yuxi/channel/extensions/wechatpay_notify/crypto.py
Kris 87a8931db3 feat(channel): 添加微信客服、微信公众号和微信支付通知渠道扩展
新增微信客服、微信公众号、微信支付通知三个渠道扩展。

微信客服渠道扩展功能模块:
- account: 账户管理
- config: 渠道配置管理
- gateway: SSE/WebSocket 网关接入
- webhook: Webhook 事件处理
- outbound: 外发消息管理
- streaming: 流式消息处理
- pairing: 用户配对与绑定
- security: 安全校验
- crypto: 加解密处理
- dedupe: 消息去重
- customer: 客户管理
- servicer: 客服管理
- session: 会话管理
- status: 会话状态管理
- media: 媒体资源处理
- statistics: 统计功能
- sync: 数据同步
- upgrade: 升级处理

微信公众号渠道扩展功能模块:
- config: 渠道配置管理
- gateway: SSE/WebSocket 网关接入
- webhook: Webhook 事件处理
- outbound: 外发消息管理
- streaming: 流式消息处理
- pairing: 用户配对与绑定
- security: 安全校验
- crypto: 加解密处理
- dedupe: 消息去重
- passive_reply: 被动回复
- message: 消息处理
- broadcast: 群发消息
- template: 模板消息
- menu: 菜单管理
- qrcode: 二维码管理
- user: 用户管理
- media: 媒体资源处理
- status: 会话状态管理

微信支付通知渠道扩展功能模块:
- config: 渠道配置管理
- webhook: Webhook 事件处理
- crypto: 加解密与签名校验
- cert_manager: 证书管理
- event_router: 事件路由
- dedupe: 消息去重
- pay_repo: 支付数据仓库
- query_client: 查询客户端
- arq_tasks: 异步任务
- callback_compensator: 回调补偿
2026-05-21 12:00:30 +08:00

136 lines
4.2 KiB
Python

from __future__ import annotations
import base64
import logging
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
logger = logging.getLogger(__name__)
MAX_TIMESTAMP_DRIFT_SECONDS = 300
class SignatureVerificationError(Exception):
pass
class DecryptionError(Exception):
pass
class WechatPayCrypto:
def __init__(self, api_v3_key: str):
if len(api_v3_key) != 64:
raise ValueError("APIv3 key must be 32 bytes (64 hex characters)")
self._api_v3_key = api_v3_key
@staticmethod
def build_authorization_signature(
method: str,
url: str,
body: str,
timestamp: str,
nonce_str: str,
private_key_pem: str,
) -> str:
from cryptography.hazmat.primitives.serialization import load_pem_private_key
private_key = load_pem_private_key(private_key_pem.encode("utf-8"), password=None, backend=default_backend())
sign_message = f"{method}\n{url}\n{timestamp}\n{nonce_str}\n{body}\n"
signature = private_key.sign(sign_message.encode("utf-8"), padding.PKCS1v15(), hashes.SHA256())
return base64.b64encode(signature).decode("utf-8")
def verify_signature(
self,
body: bytes,
signature: str,
timestamp: str,
nonce: str,
serial_no: str,
platform_cert_pem: str,
) -> bool:
try:
dr = _check_timestamp_drift(timestamp)
if dr:
logger.warning("WechatPay signature timestamp drift: %s", dr)
return False
except (ValueError, TypeError):
return False
sign_message = f"{timestamp}\n{nonce}\n{body.decode('utf-8')}\n"
try:
cert = x509.load_pem_x509_certificate(platform_cert_pem.encode("utf-8"), default_backend())
public_key = cert.public_key()
public_key.verify(
base64.b64decode(signature),
sign_message.encode("utf-8"),
padding.PKCS1v15(),
hashes.SHA256(),
)
return True
except Exception:
logger.warning("WechatPay signature verification failed for serial=%s", serial_no)
return False
def decrypt_resource(
self,
ciphertext: str,
nonce: str,
associated_data: str,
) -> str:
try:
aesgcm = AESGCM(bytes.fromhex(self._api_v3_key))
plaintext = aesgcm.decrypt(
nonce.encode("utf-8"),
base64.b64decode(ciphertext),
associated_data.encode("utf-8") if associated_data else None,
)
return plaintext.decode("utf-8")
except Exception as e:
raise DecryptionError(f"AES-GCM decryption failed: {e}") from e
def verify_signature_with_raw_key(
self,
body: bytes,
signature: str,
timestamp: str,
nonce: str,
public_key_pem: str,
) -> bool:
from cryptography.hazmat.primitives.serialization import load_pem_public_key
dr = _check_timestamp_drift(timestamp)
if dr:
logger.warning("WechatPay signature timestamp drift: %s", dr)
return False
sign_message = f"{timestamp}\n{nonce}\n{body.decode('utf-8')}\n"
try:
public_key = load_pem_public_key(public_key_pem.encode("utf-8"), default_backend())
public_key.verify(
base64.b64decode(signature),
sign_message.encode("utf-8"),
padding.PKCS1v15(),
hashes.SHA256(),
)
return True
except Exception:
logger.warning("WechatPay signature verification failed with raw public key")
return False
def _check_timestamp_drift(timestamp: str) -> str | None:
import time
ts = int(timestamp)
now = int(time.time())
diff = abs(now - ts)
if diff > MAX_TIMESTAMP_DRIFT_SECONDS:
return f"drift={diff}s > {MAX_TIMESTAMP_DRIFT_SECONDS}s"
return None