w7corp/easywechat · error · InvalidArgumentException
Decrypt AES ECB failed.
Error message
Decrypt AES ECB failed.
What it means
Thrown when openssl_decrypt() returns false for the aes-256-ecb decryption of a base64 payload. The only internal caller is Pay\Server::decodeXmlMessage (src/Pay/Server.php:229), which decrypts the legacy WeChat Pay v2 XML callback field req_info using md5($v2SecretKey) as the key. Because OPENSSL_RAW_DATA still verifies PKCS7 padding, a wrong V2 key (garbage plaintext fails the padding check), a key that is not exactly 32 bytes, or ciphertext that fails strict base64_decode all make OpenSSL return false and raise this exception.
Source
Thrown at src/Kernel/Support/AesEcb.php:44
return \base64_encode($ciphertext);
}
/**
* @throws InvalidArgumentException
*/
public static function decrypt(string $ciphertext, string $key, ?string $iv = null): string
{
$plaintext = openssl_decrypt(
base64_decode($ciphertext, true) ?: '',
'aes-256-ecb',
$key,
OPENSSL_RAW_DATA,
(string) $iv
);
if ($plaintext === false) {
throw new InvalidArgumentException(openssl_error_string() ?: 'Decrypt AES ECB failed.');
}
return $plaintext;
}
}
View on GitHub (pinned to f0cf0a8b83)
Solutions
- Verify the V2 key is the exact 32-char API v2 key from the WeChat merchant console and that the decrypt key is md5($v2Key) (32 hex chars, valid for aes-256).
- Log base64_decode($reqInfo, true) before decrypting; if it returns false the XML layer upstream mangled req_info — fix parsing instead of keys.
- Assert strlen($key) === 32 before calling; aes-256-* ciphers only accept 32-byte keys.
- If keys are correct and it still fails, reproduce with the openssl CLI to see the raw OpenSSL error (usually a padding error proving the key is wrong).
Example fix
// before: raw v2 key used directly (wrong length for aes-256) $cipher = AesEcb::decrypt($reqInfo, $v2Key, iv: ''); // after: the v2 protocol derives a 32-byte hex key via md5() $cipher = AesEcb::decrypt($reqInfo, md5($v2Key), iv: '');
Defensive patterns
Strategy: validation
Validate before calling
$key = md5($v2Key);
if (strlen($key) !== 32) { throw new RuntimeException('aes-256-ecb key must be 32 bytes'); }
if ($reqInfo === '' || base64_decode($reqInfo, true) === false) { throw new RuntimeException('req_info is not valid base64'); }
$plain = AesEcb::decrypt($reqInfo, $key, iv: ''); Try / catch
try { $plain = AesEcb::decrypt($reqInfo, md5($v2Key), iv: ''); } catch (\EasyWeChat\Kernel\Exceptions\InvalidArgumentException $e) { \Log::warning('v2 callback decrypt failed: '.$e->getMessage()); return new \Nyholm\Psr7\Response(200, [], 'fail'); } Prevention
- Keep the V2 key in one env-driven config value and derive the md5 key in a single place
- Assert strlen(md5($v2Key)) === 32 at boot
- Hand the SDK the raw callback body instead of pre-parsing the XML yourself
When it happens
Trigger: Handling a WeChat Pay v2 XML callback (refund notifications etc.) with a wrong or rotated V2 secret key; the req_info CDATA value truncated or re-encoded by earlier XML handling so base64_decode($c, true) fails; calling AesEcb::decrypt() directly with strlen($key) !== 32 (e.g. the raw 32-char key re-hashed, or a raw 16-byte md5 digest instead of the 32-char hex string).
Common situations: V2 key rotated in the merchant console but the app config still has the old one; custom code passing the raw key or md5($key, true) (16 bytes) instead of md5($key) (32 hex chars); test environments pointing at production keys; XML middleware stripping CDATA sections.
Related errors
- Decrypt failed
- Encrypt AES CBC error.
- Encrypt AES ECB failed.
- Encrypt failed
- Read the $certificate failed, please check it whether or nor
AI-assisted analysis of w7corp/easywechat@f0cf0a8b83 (2026-08-21).
Data as JSON: /api/errors/06769bfe057bb73c.
Report an issue: GitHub.