ForcePilot/backend/package/yuxi/channel/extensions/wechatpay_notify/crypto.py

136 lines
4.2 KiB
Python
Raw Normal View History

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
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