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

  1. 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).
  2. Log base64_decode($reqInfo, true) before decrypting; if it returns false the XML layer upstream mangled req_info — fix parsing instead of keys.
  3. Assert strlen($key) === 32 before calling; aes-256-* ciphers only accept 32-byte keys.
  4. 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

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


AI-assisted analysis of w7corp/easywechat@f0cf0a8b83 (2026-08-21). Data as JSON: /api/errors/06769bfe057bb73c. Report an issue: GitHub.