帮你快速理解、总结文档立即下载

事件加解密

最近更新时间:2026-08-10 14:23:30
我的收藏

术语说明

EncodingAESKey:注册应用提供的数据加密密钥。用于消息体的加密,长度固定为43个字符,从a-z, A-Z, 0-9共62个字符中选取,是 AESKey 的 Base64 编码。解码后即为32字节长的 AESKey。
AESKey:AESKey=Base64_Decode(EncodingAESKey + “=”),是 AES 算法的密钥,长度为32字节。AES 采用 CBC 模式,数据采用 PKCS#7 填充;IV 初始向量大小为16字节,取 AESKey 前16字节。具体请参见:Cryptographic Message Syntax
msg 为消息体明文,格式为 JSON。

加解密方案说明

明文 msg 的加密过程

msg_encrypt = Base64_Encode( AES_Encrypt[msg + $key] )
AES 加密的 buf 由明文 msg 和 $key 组成。
注意:
开发者无需自行加密,加密由会议业务内部实现。

加密方案对应的解密方案

取出返回的 JSON 中的 data 字段。
对密文 BASE64 解码:aes_msg=Base64_Decode(data)
使用 AESKey 做 AES 解密:msg=AES_Decrypt(aes_msg)

解密示例代码

Go
Java
C++
PHP
Python
package main

import (
"crypto/aes"
"crypto/cipher"
"crypto/subtle"
"encoding/base64"
"errors"
"fmt"
"strings"
)

// aesKeySize 固定使用 AES-256。
const aesKeySize = 32

var (
ErrBadKeyLength = errors.New("aes: 密钥 base64 解码后必须为 32 字节 (AES-256)")
ErrEmptyCiphertext = errors.New("aes: 密文为空")
ErrCiphertextNotAligned = errors.New("aes: 密文长度不是 AES 块大小(16)的整数倍")
ErrBadPadding = errors.New("aes: PKCS#7 填充无效")
)

// AllowZeroPadding 为 true 时接受零填充(ZeroPadding)密文,即末字节为 0x00 时
// 不剥离任何字节。仅在对端使用 ZeroPadding 时开启;标准 PKCS#7 请保持 false。
var AllowZeroPadding = false

// aesDecrypt 用 AES-256-CBC 解密 base64 编码的密文,IV 取自密钥前 16 字节。
func aesDecrypt(encryptedText, key string) (string, error) {
decodedKey, err := base64.StdEncoding.DecodeString(padBase64(key))
if err != nil {
return "", fmt.Errorf("解码密钥失败: %w", err)
}
if len(decodedKey) != aesKeySize {
return "", fmt.Errorf("%w, 实际 %d 字节", ErrBadKeyLength, len(decodedKey))
}
aesKey := make([]byte, aesKeySize)
copy(aesKey, decodedKey)

// IV 取密钥前 16 字节,拷贝一份避免与 aesKey 共享底层数组
iv := make([]byte, aes.BlockSize)
copy(iv, aesKey[:aes.BlockSize])

decodedText, err := base64.StdEncoding.DecodeString(encryptedText)
if err != nil {
return "", fmt.Errorf("解码密文失败: %w", err)
}
// CryptBlocks 要求输入非空且为块大小整数倍,否则 panic
if len(decodedText) == 0 {
return "", ErrEmptyCiphertext
}
if len(decodedText)%aes.BlockSize != 0 {
return "", fmt.Errorf("%w, 实际 %d 字节", ErrCiphertextNotAligned, len(decodedText))
}

block, err := aes.NewCipher(aesKey)
if err != nil {
return "", fmt.Errorf("创建 AES cipher 失败: %w", err)
}
// 原地解密,decodedText 被覆盖为明文
cipher.NewCBCDecrypter(block, iv).CryptBlocks(decodedText, decodedText)

plaintext, err := pkcs7Unpad(decodedText, aes.BlockSize)
if err != nil {
return "", err
}
return string(plaintext), nil
}

// padBase64 为标准 base64 字符串补齐 '=' padding。
func padBase64(s string) string {
if m := len(s) % 4; m != 0 {
s += strings.Repeat("=", 4-m)
}
return s
}

// pkcs7Unpad 校验并去除 PKCS#7 填充。
func pkcs7Unpad(data []byte, blockSize int) ([]byte, error) {
n := len(data)
if n == 0 || n%blockSize != 0 {
return nil, ErrBadPadding
}
padding := int(data[n-1])

if padding == 0 && AllowZeroPadding {
return data, nil
}
if padding == 0 || padding > blockSize || padding > n {
return nil, fmt.Errorf("%w: 填充长度 %d", ErrBadPadding, padding)
}

// 常量时间比较,缓解 padding oracle 时序侧信道
want := make([]byte, padding)
for i := range want {
want[i] = byte(padding)
}
if subtle.ConstantTimeCompare(data[n-padding:], want) != 1 {
return nil, ErrBadPadding
}
return data[:n-padding], nil
}

func main() {
encryptedText := "your base64 encoded encrypted text"
key := "your base64 encoded key"

decryptedText, err := aesDecrypt(encryptedText, key)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Decrypted Text:", decryptedText)
}


Running Environment

Operating System: Ubuntu 24.04.3 LTS / x86_64

Runtime Version: go version go1.21.4 linux/amd64

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.util.Arrays;
import java.util.Base64;

/**
* AES/CBC 解密工具。
*
* <ul>
* <li>密钥经 base64 解码后直接作为 AES key,长度决定 AES-128/192/256</li>
* <li>IV 取自密钥前 16 字节</li>
* <li>Cipher 使用 NoPadding,PKCS#7 填充由本类手工剥离</li>
* </ul>
*/
public final class Main {

private static final int BLOCK_SIZE = 16;
/** AES 允许的密钥长度(字节):AES-128 / AES-192 / AES-256 */
private static final int[] VALID_KEY_LENGTHS = {16, 24, 32};

/**
* 为 true 时接受零填充(ZeroPadding)密文,即末字节为 0x00 时不剥离任何字节。
* 仅在对端使用 ZeroPadding 时开启;标准 PKCS#7 请保持 false。
*/
public static volatile boolean allowZeroPadding = false;

private Main() {
}

public static String decrypt(String encryptedText, String key) throws GeneralSecurityException {
if (encryptedText == null || key == null) {
throw new IllegalArgumentException("密文和密钥均不能为 null");
}

byte[] keyBytes = decodeBase64(padBase64(key), "密钥");
if (!isValidKeyLength(keyBytes.length)) {
throw new InvalidKeyLengthException(keyBytes.length);
}

byte[] encryptedBytes = decodeBase64(encryptedText, "密文");
if (encryptedBytes.length == 0) {
throw new IllegalBlockSizeException("密文为空");
}
if (encryptedBytes.length % BLOCK_SIZE != 0) {
throw new IllegalBlockSizeException(
"密文长度不是 AES 块大小(" + BLOCK_SIZE + ")的整数倍, 实际 "
+ encryptedBytes.length + " 字节");
}

byte[] ivBytes = Arrays.copyOf(keyBytes, BLOCK_SIZE);

Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
cipher.init(Cipher.DECRYPT_MODE,
new SecretKeySpec(keyBytes, "AES"),
new IvParameterSpec(ivBytes));
byte[] decryptedBytes = cipher.doFinal(encryptedBytes);

byte[] unpaddedData = unpadPkcs7(decryptedBytes);

try {
return new String(unpaddedData, StandardCharsets.UTF_8);
} finally {
// 敏感数据及时清零,减少在堆中的驻留时间
Arrays.fill(decryptedBytes, (byte) 0);
Arrays.fill(unpaddedData, (byte) 0);
Arrays.fill(keyBytes, (byte) 0);
}
}

/** 校验并剥离 PKCS#7 填充。 */
private static byte[] unpadPkcs7(byte[] data) throws BadPaddingException {
int len = data.length;
if (len == 0 || len % BLOCK_SIZE != 0) {
throw new BadPaddingException("PKCS#7 填充无效: 数据长度 " + len);
}

// Java 的 byte 有符号,必须 & 0xFF 转为无符号,否则末字节 >= 0x80 时得到负数
int paddingLength = data[len - 1] & 0xFF;

if (paddingLength == 0 && allowZeroPadding) {
return data.clone();
}
if (paddingLength < 1 || paddingLength > BLOCK_SIZE) {
throw new BadPaddingException("PKCS#7 填充无效: 填充长度 " + paddingLength);
}

// 全部填充字节都要校验;用 MessageDigest.isEqual 做常量时间比较,
// 缓解 padding oracle 时序侧信道
byte[] actual = Arrays.copyOfRange(data, len - paddingLength, len);
byte[] expected = new byte[paddingLength];
Arrays.fill(expected, (byte) paddingLength);
if (!MessageDigest.isEqual(actual, expected)) {
throw new BadPaddingException("PKCS#7 填充无效");
}

return Arrays.copyOf(data, len - paddingLength);
}

/** 为标准 base64 字符串补齐 '=' padding。 */
private static String padBase64(String s) {
int remainder = s.length() % 4;
if (remainder == 0) {
return s;
}
return s + "=".repeat(4 - remainder);
}

/** base64 解码,把 IllegalArgumentException 转为受检异常。 */
private static byte[] decodeBase64(String s, String what) throws GeneralSecurityException {
try {
return Base64.getDecoder().decode(s);
} catch (IllegalArgumentException e) {
throw new GeneralSecurityException(what + " base64 解码失败: " + e.getMessage(), e);
}
}

private static boolean isValidKeyLength(int len) {
for (int valid : VALID_KEY_LENGTHS) {
if (valid == len) {
return true;
}
}
return false;
}

/** 密钥长度非法,便于调用方区分处理。 */
public static class InvalidKeyLengthException extends GeneralSecurityException {
private static final long serialVersionUID = 1L;

public InvalidKeyLengthException(int actual) {
super("AES 密钥 base64 解码后长度必须为 16/24/32 字节, 实际 " + actual + " 字节");
}
}

public static void main(String[] args) {
String encryptedText = "your base64 encoded encrypted text";
String key = "your base64 encoded key";

try {
String decryptedText = decrypt(encryptedText, key);
System.out.println("Decrypted Text: " + decryptedText);
} catch (NoSuchPaddingException | IllegalBlockSizeException | BadPaddingException e) {
// 数据/填充相关,通常意味着密钥不对或密文被篡改
System.err.println("解密失败(数据或密钥错误): " + e.getMessage());
} catch (GeneralSecurityException e) {
System.err.println("解密失败: " + e.getMessage());
} catch (IllegalArgumentException e) {
System.err.println("参数错误: " + e.getMessage());
}
}
}
// AES-256-CBC解密(IV 取自密钥前 16 字节,PKCS#7 手工去填充)
//
// 编译:
// g++ -std=c++17 -O2 aes_decrypt.cpp -o aes_decrypt \\
// -I$(brew --prefix openssl@3)/include -L$(brew --prefix openssl@3)/lib -lcrypto
//
// macOS Apple Silicon 若 Homebrew 的 OpenSSL 只有 x86_64 版本,需补 -arch x86_64,
// 并在运行时设置 DYLD_LIBRARY_PATH=$(brew --prefix openssl@3)/lib

#include <cstring>
#include <iostream>
#include <stdexcept>
#include <string>
#include <vector>

// AES_set_decrypt_key / AES_cbc_encrypt 在 OpenSSL 3.x 已标记 deprecated,
// 此处局部静默相关告警。迁移建议见文末。
#ifdef __clang__
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
#elif defined(__GNUC__)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
#endif

#include <openssl/aes.h>
#include <openssl/bio.h>
#include <openssl/buffer.h>
#include <openssl/crypto.h>
#include <openssl/evp.h>

namespace aescbc {

constexpr size_t kAesKeySize = 32; // AES-256
constexpr size_t kBlockSize = 16; // AES_BLOCK_SIZE

// 为 true 时接受零填充(ZeroPadding)密文,即末字节为 0x00 时不剥离任何字节。
// 仅在对端使用 ZeroPadding 时开启;标准 PKCS#7 请保持 false。
bool g_allow_zero_padding = false;

class DecryptError : public std::runtime_error {
public:
explicit DecryptError(const std::string& msg) : std::runtime_error(msg) {}
};

// RAII 包装 BIO 链,确保异常路径也能释放。
class BioChain {
public:
explicit BioChain(BIO* bio) : bio_(bio) {}
~BioChain() {
if (bio_ != nullptr) BIO_free_all(bio_);
}
BioChain(const BioChain&) = delete;
BioChain& operator=(const BioChain&) = delete;
BIO* get() const { return bio_; }

private:
BIO* bio_;
};

// 为标准 base64 字符串补齐 '=' padding。
std::string PadBase64(const std::string& s) {
const size_t remainder = s.size() % 4;
if (remainder == 0) return s;
return s + std::string(4 - remainder, '=');
}

// base64 解码,返回二进制字节。
//
// 注意:结果必须用 std::string(ptr, len) 构造,且 len 取自 BIO_read 的返回值。
// 密钥与密文是任意二进制,含 0x00 是常态;若从 char* 隐式构造会在首个 0x00 处截断。
std::string Base64Decode(const std::string& encoded, const char* what) {
if (encoded.empty()) return std::string();

BIO* b64 = BIO_new(BIO_f_base64());
if (b64 == nullptr) throw DecryptError("BIO_new(base64) 失败");
BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL);

BIO* bmem = BIO_new_mem_buf(encoded.data(), static_cast<int>(encoded.size()));
if (bmem == nullptr) {
BIO_free_all(b64);
throw DecryptError("BIO_new_mem_buf 失败");
}
BioChain chain(BIO_push(b64, bmem));

// base64 解码后长度必定 <= 输入长度
std::vector<unsigned char> buffer(encoded.size());
const int decoded_len =
BIO_read(chain.get(), buffer.data(), static_cast<int>(buffer.size()));
if (decoded_len < 0) {
throw DecryptError(std::string(what) + " base64 解码失败");
}

return std::string(reinterpret_cast<char*>(buffer.data()),
static_cast<size_t>(decoded_len));
}

// 校验并剥离 PKCS#7 填充。
//
// 注意:padding 可达255,必须先校验范围。否则 data.end() - padding
// 会构成迭代器越界(未定义行为)。
std::string UnpadPkcs7(const std::vector<unsigned char>& data) {
const size_t n = data.size();
if (n == 0 || n % kBlockSize != 0) {
throw DecryptError("PKCS#7 填充无效: 数据长度 " + std::to_string(n));
}

const size_t padding = static_cast<size_t>(data[n - 1]);

if (padding == 0 && g_allow_zero_padding) {
return std::string(reinterpret_cast<const char*>(data.data()), n);
}
if (padding < 1 || padding > kBlockSize || padding > n) {
throw DecryptError("PKCS#7 填充无效: 填充长度 " + std::to_string(padding));
}

// 全部填充字节都要校验;CRYPTO_memcmp 为常量时间比较,缓解 padding oracle
std::vector<unsigned char> expected(padding, static_cast<unsigned char>(padding));
if (CRYPTO_memcmp(data.data() + (n - padding), expected.data(), padding) != 0) {
throw DecryptError("PKCS#7 填充无效");
}

return std::string(reinterpret_cast<const char*>(data.data()), n - padding);
}

std::string AesDecrypt(const std::string& encrypted_text, const std::string& key) {
const std::string decoded_key = Base64Decode(PadBase64(key), "密钥");
if (decoded_key.size() != kAesKeySize) {
throw DecryptError("AES 密钥 base64 解码后必须为 32 字节 (AES-256), 实际 " +
std::to_string(decoded_key.size()) + " 字节");
}
unsigned char aes_key[kAesKeySize];
std::memcpy(aes_key, decoded_key.data(), kAesKeySize);

unsigned char iv[kBlockSize];
std::memcpy(iv, aes_key, kBlockSize);

const std::string decoded_text = Base64Decode(encrypted_text, "密文");
if (decoded_text.empty()) {
throw DecryptError("密文为空");
}
if (decoded_text.size() % kBlockSize != 0) {
throw DecryptError("密文长度不是 AES 块大小(16)的整数倍, 实际 " +
std::to_string(decoded_text.size()) + " 字节");
}

AES_KEY aes_decrypt_key;
if (AES_set_decrypt_key(aes_key, 256, &aes_decrypt_key) < 0) {
OPENSSL_cleanse(aes_key, sizeof(aes_key));
throw DecryptError("AES_set_decrypt_key 失败");
}

// AES_cbc_encrypt 会就地修改 iv,故传入副本
std::vector<unsigned char> decrypted_bytes(decoded_text.size());
unsigned char iv_copy[kBlockSize];
std::memcpy(iv_copy, iv, kBlockSize);
AES_cbc_encrypt(reinterpret_cast<const unsigned char*>(decoded_text.data()),
decrypted_bytes.data(), decoded_text.size(), &aes_decrypt_key,
iv_copy, AES_DECRYPT);

std::string result;
try {
result = UnpadPkcs7(decrypted_bytes);
} catch (...) {
OPENSSL_cleanse(aes_key, sizeof(aes_key));
OPENSSL_cleanse(&aes_decrypt_key, sizeof(aes_decrypt_key));
if (!decrypted_bytes.empty())
OPENSSL_cleanse(decrypted_bytes.data(), decrypted_bytes.size());
throw;
}

// 敏感数据及时清零(OPENSSL_cleanse 不会被编译器优化掉)
OPENSSL_cleanse(aes_key, sizeof(aes_key));
OPENSSL_cleanse(&aes_decrypt_key, sizeof(aes_decrypt_key));
if (!decrypted_bytes.empty())
OPENSSL_cleanse(decrypted_bytes.data(), decrypted_bytes.size());
return result;
}

} // namespace aescbc

#ifdef __clang__
#pragma clang diagnostic pop
#elif defined(__GNUC__)
#pragma GCC diagnostic pop
#endif

#ifndef AES_DECRYPT_NO_MAIN
int main() {
const std::string encrypted_text = "your base64 encoded encrypted text";
const std::string key = "your base64 encoded key";

try {
const std::string decrypted_text = aescbc::AesDecrypt(encrypted_text, key);
std::cout << "Decrypted Text: " << decrypted_text << std::endl;
} catch (const aescbc::DecryptError& e) {
std::cerr << "解密失败: " << e.what() << std::endl;
return 1;
}
return 0;
}
#endif

<?php
/**
* AES-256-CBC 解密(IV 取自密钥前 16 字节)。
*
* 注意:openssl_decrypt 配合 OPENSSL_RAW_DATA 会自动校验并剥离 PKCS#7 填充,
* 因此 aes_decrypt() 无需手工处理填充,填充非法时直接返回 false。
* 若需要与手工剥离填充的实现(Go/Java/C++/Python)严格对齐,
* 或需兼容零填充(ZeroPadding)密文,请改用 aes_decrypt_manual_unpad()。
*/

declare(strict_types=1);

const AES_BLOCK_SIZE = 16;
const AES_KEY_SIZE = 32; // AES-256
const AES_CIPHER = 'AES-256-CBC';

/** 解密相关异常基类 */
class DecryptException extends RuntimeException {}
class InvalidKeyLengthException extends DecryptException {}

/** 为标准 base64 字符串补齐 '=' padding。 */
function pad_base64(string $s): string
{
$remainder = strlen($s) % 4;
if ($remainder !== 0) {
$s .= str_repeat('=', 4 - $remainder);
}
return $s;
}

/** 严格模式 base64 解码,非法字符会报错而非被静默丢弃。 */
function b64decode_strict(string $s, string $what): string
{
$decoded = base64_decode($s, true);
if ($decoded === false) {
throw new DecryptException("{$what} base64 解码失败(非法 base64 字符或 padding 错误)");
}
return $decoded;
}

/**
* 解密 base64 编码的密文,PKCS#7 填充由 openssl 自动处理。
*
* @throws DecryptException
*/
function aes_decrypt(string $encrypted_text, string $key): string
{
$decoded_key = b64decode_strict(pad_base64($key), '密钥');
if (strlen($decoded_key) !== AES_KEY_SIZE) {
throw new InvalidKeyLengthException(
sprintf(
'AES 密钥 base64 解码后必须为 %d 字节 (AES-256), 实际 %d 字节',
AES_KEY_SIZE,
strlen($decoded_key)
)
);
}
$aes_key = $decoded_key;
$iv = substr($aes_key, 0, AES_BLOCK_SIZE);

$decoded_text = b64decode_strict($encrypted_text, '密文');
if ($decoded_text === '') {
throw new DecryptException('密文为空');
}
if (strlen($decoded_text) % AES_BLOCK_SIZE !== 0) {
throw new DecryptException(
sprintf(
'密文长度不是 AES 块大小(%d)的整数倍, 实际 %d 字节',
AES_BLOCK_SIZE,
strlen($decoded_text)
)
);
}

$decrypted_text = openssl_decrypt(
$decoded_text,
AES_CIPHER,
$aes_key,
OPENSSL_RAW_DATA,
$iv
);

// openssl_decrypt 失败返回 false,必须检查,否则会静默得到空字符串
if ($decrypted_text === false) {
$errors = [];
while (($err = openssl_error_string()) !== false) {
$errors[] = $err;
}
throw new DecryptException(
'解密失败(通常意味着密钥错误、密文被篡改或填充非法)'
. ($errors !== [] ? ': ' . implode('; ', $errors) : '')
);
}

return $decrypted_text;
}

/**
* 解密并手工剥离 PKCS#7 填充,行为与 Go/Java/C++/Python 实现一致。
*
* @param bool $allow_zero_padding 为 true 时接受零填充密文(末字节 0x00 时不剥离)
* @throws DecryptException
*/
function aes_decrypt_manual_unpad(
string $encrypted_text,
string $key,
bool $allow_zero_padding = false
): string {
$decoded_key = b64decode_strict(pad_base64($key), '密钥');
if (strlen($decoded_key) !== AES_KEY_SIZE) {
throw new InvalidKeyLengthException(
sprintf('AES 密钥必须为 %d 字节, 实际 %d', AES_KEY_SIZE, strlen($decoded_key))
);
}
$iv = substr($decoded_key, 0, AES_BLOCK_SIZE);

$decoded_text = b64decode_strict($encrypted_text, '密文');
if ($decoded_text === '') {
throw new DecryptException('密文为空');
}
if (strlen($decoded_text) % AES_BLOCK_SIZE !== 0) {
throw new DecryptException('密文长度不是 16 的整数倍');
}

// OPENSSL_ZERO_PADDING 表示"不自动处理填充",而非"使用零填充"
$raw = openssl_decrypt(
$decoded_text,
AES_CIPHER,
$decoded_key,
OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING,
$iv
);
if ($raw === false) {
throw new DecryptException('openssl_decrypt 失败');
}

return unpad_pkcs7($raw, $allow_zero_padding);
}

/**
* 校验并剥离 PKCS#7 填充。
*
* @throws DecryptException
*/
function unpad_pkcs7(string $data, bool $allow_zero_padding = false): string
{
$n = strlen($data);
if ($n === 0 || $n % AES_BLOCK_SIZE !== 0) {
throw new DecryptException("PKCS#7 填充无效: 数据长度 {$n}");
}

$padding = ord($data[$n - 1]); // ord() 返回 0..255,无符号

if ($padding === 0 && $allow_zero_padding) {
return $data;
}
if ($padding < 1 || $padding > AES_BLOCK_SIZE || $padding > $n) {
throw new DecryptException("PKCS#7 填充无效: 填充长度 {$padding}");
}

// 全部填充字节都要校验;hash_equals 为常量时间比较,缓解 padding oracle
$actual = substr($data, $n - $padding);
$expected = str_repeat(chr($padding), $padding);
if (!hash_equals($expected, $actual)) {
throw new DecryptException('PKCS#7 填充无效');
}

return substr($data, 0, $n - $padding);
}

// ===================== 示例入口 =====================
if (PHP_SAPI === 'cli' && realpath($argv[0] ?? '') === realpath(__FILE__)) {
$encrypted_text = 'your base64 encoded encrypted text';
$key = 'your base64 encoded key';

try {
$decrypted_text = aes_decrypt($encrypted_text, $key);
echo 'Decrypted Text: ' . $decrypted_text . PHP_EOL;
} catch (DecryptException $e) {
fwrite(STDERR, '解密失败: ' . $e->getMessage() . PHP_EOL);
exit(1);
}
}

"""AES-256-CBC 解密。

- 密钥经 base64 解码后作为 AES-256 key
- IV 取自密钥前 16 字节
- PKCS#7 填充由本模块手工剥离

依赖:pycryptodome (pip install pycryptodome)
"""

from __future__ import annotations

import base64
import binascii
import hmac

from Crypto.Cipher import AES

BLOCK_SIZE = 16
AES_KEY_SIZE = 32 # AES-256

# 为 True 时接受零填充(ZeroPadding)密文,即末字节为 0x00 时不剥离任何字节。
# 仅在对端使用 ZeroPadding 时开启;标准 PKCS#7 请保持 False。
ALLOW_ZERO_PADDING = False


class DecryptError(ValueError):
"""解密相关错误的统一基类。"""


class InvalidKeyLengthError(DecryptError):
pass


class BadPaddingError(DecryptError):
pass


def _pad_base64(s: str) -> str:
"""为标准 base64 字符串补齐 '=' padding。"""
remainder = len(s) % 4
if remainder:
s += "=" * (4 - remainder)
return s


def _b64decode(s: str | bytes, what: str) -> bytes:
"""严格模式 base64 解码,非法字符会报错而非被静默忽略。"""
try:
return base64.b64decode(s, validate=True)
except (binascii.Error, ValueError) as exc:
raise DecryptError(f"{what} base64 解码失败: {exc}") from exc


def unpad_pkcs7(data: bytes, block_size: int = BLOCK_SIZE) -> bytes:
"""校验并剥离 PKCS#7 填充。

注意:不可写成 data[:-padding]。Python 中 x[:-0] 等价于 x[:0],
当 padding 为 0 时会静默返回空串;padding > len(data) 时同样如此。
因此必须先校验 padding 取值范围。
"""
n = len(data)
if n == 0 or n % block_size != 0:
raise BadPaddingError(f"PKCS#7 填充无效: 数据长度 {n}")

padding = data[-1] # 索引 bytes 得到 int,无符号

if padding == 0 and ALLOW_ZERO_PADDING:
return data
if padding < 1 or padding > block_size or padding > n:
raise BadPaddingError(f"PKCS#7 填充无效: 填充长度 {padding}")

# 全部填充字节都要校验;compare_digest 为常量时间比较,
# 缓解 padding oracle 时序侧信道
if not hmac.compare_digest(data[-padding:], bytes([padding]) * padding):
raise BadPaddingError("PKCS#7 填充无效")

return data[:-padding]


def aes_decrypt(encrypted_text: str, key: str) -> str:
"""解密 base64 编码的 AES-256-CBC 密文,返回 UTF-8 字符串。"""
decoded_key = _b64decode(_pad_base64(key), "密钥")
if len(decoded_key) != AES_KEY_SIZE:
raise InvalidKeyLengthError(
f"AES 密钥 base64 解码后必须为 {AES_KEY_SIZE} 字节 (AES-256), "
f"实际 {len(decoded_key)} 字节"
)
aes_key = decoded_key
iv = aes_key[:BLOCK_SIZE]

decoded_text = _b64decode(encrypted_text, "密文")
if not decoded_text:
raise DecryptError("密文为空")
if len(decoded_text) % BLOCK_SIZE != 0:
raise DecryptError(
f"密文长度不是 AES 块大小({BLOCK_SIZE})的整数倍, "
f"实际 {len(decoded_text)} 字节"
)

cipher = AES.new(aes_key, AES.MODE_CBC, iv)
plaintext = unpad_pkcs7(cipher.decrypt(decoded_text))

try:
return plaintext.decode("utf-8")
except UnicodeDecodeError as exc:
raise DecryptError(
f"解密结果不是合法 UTF-8(通常意味着密钥错误或密文被篡改): {exc}"
) from exc


def main() -> int:
encrypted_text = "your base64 encoded encrypted text"
key = "your base64 encoded key"

try:
decrypted_text = aes_decrypt(encrypted_text, key)
except DecryptError as exc:
print(f"解密失败: {exc}")
return 1

print("Decrypted Text:", decrypted_text)
return 0


if __name__ == "__main__":
raise SystemExit(main())