chore: 重写初始提交(清空历史,整理后全量提交)
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user