phalcon/cphalcon · error · DecryptionFailed

Could not decrypt data

Error message

Could not decrypt data

What it means

Crypt's internal decryptGcmCcm/decryptCbc path calls openssl_decrypt(); when it returns false (OpenSSL could not decrypt), Phalcon throws DecryptionFailed('Could not decrypt data'). Unlike Mismatch (signing), this is OpenSSL itself rejecting the input: key length not matching the cipher, corrupted ciphertext/IV, or mode/cipher mismatch.

Source

Thrown at phalcon/Encryption/Crypt.zep:838

                cipher,
                decryptKey,
                OPENSSL_RAW_DATA,
                iv,
                authTag,
                authData
            );
        } else {
            let decrypted = openssl_decrypt(
                cipherText,
                cipher,
                decryptKey,
                OPENSSL_RAW_DATA,
                iv
            );
        }

        if (false === decrypted) {
            throw new DecryptionFailed();
        }

        return decrypted;
    }

    /**
     * @param string $mode
     * @param string $input
     * @param int    $blockSize
     *
     * @return string
     * @throws Exception
     */
    protected function encryptGetPadded(
        string mode,
        string input,
        int blockSize
    ) -> string {

View on GitHub (pinned to b7419de9cd)

Solutions

  1. Use keys of the exact byte length the cipher requires (32 bytes for aes-256-*); derive from passwords with hash('sha256', $password, true) or PBKDF2/argon2 rather than passing the password raw.
  2. Ensure the payload reaches decrypt() byte-identical: store base64 (encryptBase64/decryptBase64) or BLOB-safe columns, never URL params or trimmed strings.
  3. Verify cipher, mode, and auth-tag handling match the encrypt side (for GCM, the tag is appended to the ciphertext by Phalcon - keep storage big enough).
  4. Catch DecryptionFailed and Mismatch together at the call site and treat both as 'unreadable payload'.

Example fix

// before
$crypt->setKey('my-password');                 // 11 bytes, wrong for aes-256
$plain = $crypt->decrypt($blob);               // openssl_decrypt -> false -> throws

// after
$crypt->setKey(hash('sha256', 'my-password', true)); // exact 32 bytes
$plain = $crypt->decrypt($blob);
Defensive patterns

Strategy: try-catch

Validate before calling

// Pre-flight the most common cause: exact key length for the cipher
$keyLengths = ['aes-128' => 16, 'aes-192' => 24, 'aes-256' => 32];
$prefix = substr($crypt->getCipher(), 0, 7); // e.g. 'aes-256'
if (isset($keyLengths[$prefix]) && strlen($crypt->getKey()) !== $keyLengths[$prefix]) {
    throw new \RuntimeException("Key must be {$keyLengths[$prefix]} bytes for {$crypt->getCipher()}");
}

Try / catch

use Phalcon\Encryption\Crypt\Exception\DecryptionFailed;
use Phalcon\Encryption\Crypt\Exception\Mismatch;

try {
    $plain = $crypt->decrypt($payload);
} catch (DecryptionFailed | Mismatch $e) {
    // unreadable payload: wrong key/length or corrupted bytes - reject, never echo raw
    $logger->warning('Undecryptable payload: ' . $e->getMessage());
    throw new AppException\UnrecoverablePayload('Stored data cannot be decrypted', 0, $e);
}

Prevention

When it happens

Trigger: Decrypting with a key of the wrong length for the cipher (e.g., a 16-byte passphrase with aes-256); ciphertext or IV bytes corrupted/truncated (but still >= ivLength so the length check passed); data encrypted with a different cipher or by another library; GCM tag bytes lost because storage stripped trailing bytes.

Common situations: Using a human password directly as the key instead of a derived 32-byte key; DB columns/base64 layers dropping trailing NUL bytes from the appended auth tag; switching ciphers after data was stored; decrypting data from an external system with a different padding/signing scheme.

Related errors


AI-assisted analysis of phalcon/cphalcon@b7419de9cd (2026-08-21). Data as JSON: /api/errors/6d552f36bd941a85. Report an issue: GitHub.