chore: 重写初始提交(清空历史,整理后全量提交)

This commit is contained in:
ywxapp
2026-08-16 16:54:14 +08:00
commit 6c1a106bc1
1808 changed files with 238144 additions and 0 deletions
+185
View File
@@ -0,0 +1,185 @@
<?php
// +----------------------------------------------------------------------
// | YwxApp [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Author: ywxapp<admin@ywxapp.cn>
// +----------------------------------------------------------------------
namespace ywxapp\utils;
/**
* 微信公众号消息加解密类 (JSON数组版) - 互通修复版
* ✅ 严格PKCS#7填充验证 | ✅ 二进制安全随机数 | ✅ JSON字节一致性保障
* 实测环境:PHP 7.4+ / Node.js 18+ / Chrome 115+
*/
class JsonCrypto
{
private $token;
private $encodingAesKey;
private $appId;
const BLOCK_SIZE = 32;
public function __construct($token, $encodingAesKey, $appId)
{
if (empty($token) || empty($appId) || strlen($encodingAesKey) !== 43) {
throw new Exception("配置错误:Token/AppID不能为空,EncodingAESKey需为43位Base64字符串");
}
// 严格验证Base64字符集(防空格/换行等污染)
if (!preg_match('/^[A-Za-z0-9+\/]{43}$/', $encodingAesKey)) {
throw new Exception("EncodingAESKey包含非法字符(仅允许A-Z, a-z, 0-9, +, /");
}
$this->token = $token;
$this->encodingAesKey = base64_decode($encodingAesKey . '=', true);
if ($this->encodingAesKey === false || strlen($this->encodingAesKey) !== 32) {
throw new Exception("EncodingAESKey解码失败或长度错误(应为32字节)");
}
$this->appId = $appId;
}
/**
* 加密消息 (数组 -> 标准JSON -> 拼接 -> PKCS#7 -> AES)
* ✅ 与JS完全对齐:JSON无空格、无转义、严格UTF-8字节
*/
public function encrypt(array $data): string
{
// 核心修复1:强制生成与JS完全一致的JSON字节流
$jsonPayload = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if ($jsonPayload === false) {
throw new Exception("JSON编码失败: " . json_last_error_msg());
}
// 移除所有空格/换行(确保与JS的JSON.stringify(data, null, 0)字节一致)
$jsonPayload = preg_replace('/\s+/', '', $jsonPayload);
// 核心修复2:使用二进制安全随机数(与JS crypto.getRandomValues 对齐)
$random = random_bytes(16); // PHP 7.0+ 原生支持
// 拼接明文: random(16) + len(4, big-endian) + json + appId
$msgLength = strlen($jsonPayload);
$msgLengthBin = pack('N', $msgLength); // 大端序4字节
$toBeEncrypted = $random . $msgLengthBin . $jsonPayload . $this->appId;
// PKCS#7填充
$paddedText = $this->pkcs7Pad($toBeEncrypted);
// AES-256-CBC加密(IV=Key前16字节,无填充)
$encrypted = openssl_encrypt(
$paddedText,
'AES-256-CBC',
$this->encodingAesKey,
OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING,
substr($this->encodingAesKey, 0, 16)
);
if ($encrypted === false) {
throw new Exception("AES加密失败: " . openssl_error_string());
}
return base64_encode($encrypted);
}
/**
* 解密消息 (Base64 -> AES -> PKCS#7验证 -> 解析)
* ✅ 严格填充验证 | ✅ 长度边界检查 | ✅ AppID原始字节比对
*/
public function decrypt(string $encryptedBase64): ?array
{
$ciphertext = base64_decode($encryptedBase64, true);
if ($ciphertext === false || strlen($ciphertext) < 32) {
return null;
}
$decrypted = openssl_decrypt(
$ciphertext,
'AES-256-CBC',
$this->encodingAesKey,
OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING,
substr($this->encodingAesKey, 0, 16)
);
if ($decrypted === false) {
return null;
}
// 核心修复3:严格PKCS#7填充验证(与JS逻辑完全一致)
$unpadded = $this->pkcs7Unpad($decrypted);
if ($unpadded === null || strlen($unpadded) < 20) {
return null;
}
// 解析长度字段(大端序4字节)
$msgLengthBin = substr($unpadded, 16, 4);
if (strlen($msgLengthBin) !== 4) return null;
$msgLength = unpack('N', $msgLengthBin)[1];
// 严格边界检查(防越界)
if ($msgLength <= 0 || (20 + $msgLength) > strlen($unpadded)) {
return null;
}
// 提取JSON和AppID(原始字节比对,禁止trim/过滤)
$jsonPayload = substr($unpadded, 20, $msgLength);
$fromAppId = substr($unpadded, 20 + $msgLength);
// 核心修复4:AppID原始字节比对(禁止字符过滤!)
if ($fromAppId !== $this->appId) {
// 调试用(生产环境建议移除)
error_log("AppID mismatch! Expected: " . bin2hex($this->appId) . ", Got: " . bin2hex($fromAppId));
return null;
}
$data = json_decode($jsonPayload, true, 512, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);
return is_array($data) ? $data : null;
}
/**
* 生成签名(与JS逻辑严格一致)
*/
public function generateSignature($timestamp, $nonce, $encryptMsg): string
{
$tmpArr = [$this->token, (string)$timestamp, (string)$nonce, (string)$encryptMsg];
sort($tmpArr, SORT_STRING);
return sha1(implode('', $tmpArr));
}
// =============== 私有辅助方法(关键修复区) ===============
/**
* PKCS#7填充(安全实现)
*/
private function pkcs7Pad(string $text): string
{
$padLen = self::BLOCK_SIZE - (strlen($text) % self::BLOCK_SIZE);
return $text . str_repeat(chr($padLen), $padLen);
}
/**
* PKCS#7严格验证与移除(核心修复!)
* ✅ 验证填充长度范围
* ✅ 验证所有填充字节值一致性
* ✅ 验证填充长度不超过原文长度
* ❌ 任何异常立即返回null(与JS行为一致)
*/
private function pkcs7Unpad(string $text): ?string
{
$textLen = strlen($text);
if ($textLen === 0) return null;
$pad = ord($text[$textLen - 1]);
// 严格验证1:填充长度合法性
if ($pad < 1 || $pad > self::BLOCK_SIZE || $pad > $textLen) {
return null;
}
// 严格验证2:所有填充字节必须等于$pad值
for ($i = $textLen - $pad; $i < $textLen; $i++) {
if (ord($text[$i]) !== $pad) {
return null;
}
}
return substr($text, 0, -$pad);
}
}