Engineering

AES-256 encryption in PHP 8: implementation patterns for secure credential storage

AC
Alex ChenFounder & Architect
10 min read

When building platforms that integrate with third-party APIs (like Stripe, Slack, or AWS), you must handle sensitive credentials. Storing these credentials in plain text in a database is a major security vulnerability. If your database is ever compromised, all your customer keys are exposed.

In this post, we'll take a deep dive into how APIPLAY implements its secure credentials vault in PHP 8.2 using AES-256-GCM encryption at rest, including key derivation, IV handling, and safe decryption patterns.

Choosing the Right Cipher: AES-256-GCM

Historically, many developers used AES-256-CBC. While CBC is secure, it is vulnerable to bit-flipping attacks unless paired with a separate HMAC (Hash-based Message Authentication Code) to verify integrity.

In modern PHP, we prefer AES-256-GCM (Galois/Counter Mode). GCM is an authenticated encryption mode. It encrypts the data and generates an authentication tag simultaneously. If any part of the encrypted payload is altered, decryption will fail immediately, preventing tempering attacks natively.

The Implementation Pattern

Below is a clean, robust helper class in PHP 8 implementing AES-256-GCM encryption and decryption:

<?php
declare(strict_types=1);

final class VaultEncryption
{
    private const CIPHER = 'aes-256-gcm';
    private const KEY_BYTES = 32;
    private const IV_BYTES = 12; // Standard GCM IV length
    private const TAG_BYTES = 16; // 128-bit authentication tag

    public static function encrypt(string $plaintext, string $secretKey): string
    {
        $key = hash_hkdf('sha256', $secretKey, self::KEY_BYTES, 'apiplay-vault-key');
        $iv = random_bytes(self::IV_BYTES);
        $tag = ''; // Passed by reference to openssl_encrypt

        $ciphertext = openssl_encrypt(
            $plaintext,
            self::CIPHER,
            $key,
            OPENSSL_RAW_DATA,
            $iv,
            $tag,
            '',
            self::TAG_BYTES
        );

        if ($ciphertext === false) {
            throw new RuntimeException('Encryption failed');
        }

        return base64_encode($iv . $tag . $ciphertext);
    }

    public static function decrypt(string $encoded, string $secretKey): string
    {
        $key = hash_hkdf('sha256', $secretKey, self::KEY_BYTES, 'apiplay-vault-key');
        $data = base64_decode($encoded, true);

        if ($data === false) {
            throw new RuntimeException('Invalid base64 payload');
        }

        $ivLen = self::IV_BYTES;
        $tagLen = self::TAG_BYTES;
        if (strlen($data) < ($ivLen + $tagLen)) {
            throw new RuntimeException('Payload is too short');
        }

        $iv = substr($data, 0, $ivLen);
        $tag = substr($data, $ivLen, $tagLen);
        $ciphertext = substr($data, $ivLen + $tagLen);

        $plaintext = openssl_decrypt(
            $ciphertext,
            self::CIPHER,
            $key,
            OPENSSL_RAW_DATA,
            $iv,
            $tag
        );

        if ($plaintext === false) {
            throw new RuntimeException('Decryption failed / Data tampered');
        }

        return $plaintext;
    }
}

Key Cryptographic Rules for PHP Developers

1. Always Derive Keys (Avoid Raw Keys)

Never use a raw password or database password directly as an encryption key. Use a key derivation function like HKDF (HMAC-based Extract-and-Expand Key Derivation Function) or PBKDF2 to derive a cryptographically strong 256-bit key. This ensures that even if your original passphrase is short, the derived key has proper length and entropy.

2. Never Reuse an Initialization Vector (IV)

In GCM mode, reusing an IV with the same key is a catastrophic failure that allows attackers to recover the plaintext. Always generate a cryptographically secure random IV using random_bytes() for every single encryption operation. Never hardcode an IV or reuse it.

3. Authenticate Before Decrypting

By using AES-256-GCM, PHP automatically verifies the integrity of the data before returning the decrypted text. If an attacker modifies even a single bit of the stored base64 payload, the authentication tag check will fail, and openssl_decrypt will return false. Always handle this failure explicitly by throwing an exception rather than returning empty values.

"Cryptographic security isn't just about choosing a strong algorithm. It's about ensuring integrity at every layer so that the system fails safely and noisily when tampered with."

Securing Key Storage

Even the strongest encryption is useless if the key is stored next to the lock. In production, APIPLAY follows these key segregation guidelines:

  • Environment Segregation: Store the encryption key (APP_KEY) in system environment variables, never inside the code repository.
  • Least Privilege Access: Ensure database servers and storage do not have access to the system configuration files where the encryption key resides.
  • Memory Cleansing: Unset sensitive variables as soon as they are no longer needed to prevent memory dump exposures.

By implementing AES-256-GCM with proper key derivation and IV handling, you can store third-party credentials in your database with absolute confidence, knowing they are fully protected against intrusion.

More from the APIPLAY Blog