Skip to content

marko/encryption-openssl

OpenSSL encryption driver --- encrypts and decrypts data using AES-256-GCM with authenticated encryption. Each encryption generates a random IV, produces a GCM authentication tag, and encodes the result as a portable base64 payload. Decryption verifies the authentication tag, detecting any tampering or key mismatch.

Implements EncryptorInterface from marko/encryption.

Terminal window
composer require marko/encryption-openssl

This automatically installs marko/encryption. Requires the ext-openssl PHP extension.

Set the encryption key and cipher in your config:

config/encryption.php
use Marko\Config\Env;
return [
'key' => Env::string('ENCRYPTION_KEY', ''),
'cipher' => Env::string('ENCRYPTION_CIPHER', 'aes-256-gcm'),
'previous_keys' => Env::list('ENCRYPTION_PREVIOUS_KEYS', []),
'aad_fallback' => Env::bool('ENCRYPTION_AAD_FALLBACK', true),
];

Generate a key:

Terminal window
php -r "echo base64_encode(random_bytes(32)) . PHP_EOL;"

The key must be base64-encoded and exactly as long as the cipher’s key: 32 bytes for aes-256-gcm, 16 bytes for aes-128-gcm. A key of any other length throws EncryptionException::invalidKeyLength() at construction instead of being silently truncated. Each previous_keys entry is validated the same way. See Rotating the Key and Binding Ciphertext with Associated Data.

Once configured, inject EncryptorInterface as usual --- the OpenSSL driver is used automatically:

use Marko\Encryption\Contracts\EncryptorInterface;
class SecureStorage
{
public function __construct(
private EncryptorInterface $encryptor,
) {}
public function store(
string $sensitiveData,
): string {
return $this->encryptor->encrypt($sensitiveData);
}
public function retrieve(
string $encrypted,
): string {
return $this->encryptor->decrypt($encrypted);
}
}

The encryptor throws specific exceptions for different failure modes:

use Marko\Encryption\Exceptions\DecryptionException;
use Marko\Encryption\Exceptions\EncryptionException;
try {
$value = $this->encryptor->decrypt($token);
} catch (DecryptionException $e) {
// Invalid payload, wrong key, or tampered data
}

An EncryptionException is thrown at construction time if the key or a previous key is missing, invalid, or the wrong length for the cipher, if the configured cipher is not recognized by OpenSSL (EncryptionException::invalidCipher()), or if the cipher is recognized but is not an AEAD mode (EncryptionException::nonAeadCipher()). Only AEAD ciphers (those ending in -gcm or -ccm) are accepted; the default aes-256-gcm satisfies this requirement. An EncryptionException is also thrown during encrypt() if the OpenSSL operation fails. A DecryptionException is thrown for malformed payloads, corrupted data, mismatched associated data, or when neither the current key nor any previous key decrypts the value. Before decrypting, the payload’s IV must match the cipher’s IV length (DecryptionException::invalidIvLength()) and its authentication tag must be the full 16 bytes (DecryptionException::invalidTagLength()). OpenSSL only verifies as many tag bytes as it is given, so a truncated tag would otherwise let an attacker forge ciphertext by brute-forcing a few tag bytes.

Replace the encryptor with a Preference to use a different cipher or add logging:

use Marko\Core\Attributes\Preference;
use Marko\Encryption\OpenSsl\OpenSslEncryptor;
#[Preference(replaces: OpenSslEncryptor::class)]
class LoggingEncryptor extends OpenSslEncryptor
{
public function encrypt(
string $value,
string $aad = '',
): string {
$result = parent::encrypt($value, $aad);
// Log encryption event...
return $result;
}
}

Implements all methods from EncryptorInterface. See marko/encryption for the full contract.

MethodDescription
encrypt(string $value, string $aad = ''): stringEncrypt a value with the current key, authenticating $aad, and return a base64-encoded payload containing the IV, ciphertext, and GCM tag
decrypt(string $encrypted, string $aad = ''): stringDecrypt a payload with the current key, then each previous key, verifying the authentication tag and $aad before returning the plaintext; retries with empty $aad while aad_fallback is on

The encrypted output is a base64-encoded JSON object containing three fields:

  • iv --- Base64-encoded initialization vector (random per encryption)
  • value --- Base64-encoded ciphertext
  • tag --- Base64-encoded GCM authentication tag (always 16 bytes; shorter tags are rejected)

The associated data is not stored in the payload; the caller supplies it again on decryption.