VOOZH about

URL: https://deepwiki.com/hypervel/encryption/5.2-key-rotation-and-previous-keys

⇱ Key Rotation and Previous Keys | hypervel/encryption | DeepWiki


Loading...
Menu

Key Rotation and Previous Keys

Purpose and Scope

This document explains the key rotation mechanism in the hypervel/encryption package, including how to configure previous encryption keys, how the decryption system automatically falls back to legacy keys, and best practices for rotating keys in production without data loss. For information about generating new keys, see Key Generation Command. For details on the overall encryption implementation, see The Encrypter Implementation.

Overview of Key Rotation

The encryption system supports zero-downtime key rotation through a previous keys mechanism. When an application needs to rotate its encryption key for security reasons, it can continue decrypting existing encrypted data while new encryptions use the updated key.

Key Rotation Principle

OperationKey Usage
EncryptionAlways uses the current key ($this->key)
DecryptionAttempts current key first, then tries each previous key in order

This asymmetric approach ensures that:

  • All new encrypted data uses the latest key
  • Legacy encrypted data remains accessible during the transition period
  • No immediate re-encryption of existing data is required
  • Multiple key rotations can be tracked simultaneously

Sources: src/Encrypter.php15-23 src/Encrypter.php86-117 src/Encrypter.php134-178

Decryption with Key Fallback

The getAllKeys Method

The Encrypter class provides three key-related accessor methods:


The getAllKeys() method combines the current key with all previous keys using array spread syntax:

return [$this->key, ...$this->previousKeys];

Sources: src/Encrypter.php280-302

Decryption Flow with Key Iteration

The decryption process iterates through all available keys to find one that successfully decrypts the payload:


Key Implementation Details:

The loop at src/Encrypter.php148-167 implements the following logic:

  1. For CBC modes (non-AEAD): Validates MAC before attempting decryption

    • If MAC is invalid, skip to next key immediately
    • This optimization avoids expensive decrypt operations for wrong keys
  2. For GCM modes (AEAD): Skip MAC validation, rely on authenticated encryption

    • OpenSSL will fail the decrypt operation if authentication fails
  3. If decryption succeeds ($decrypted !== false), the loop breaks immediately

  4. After the loop, two final validations occur:

    • If MAC mode and no valid MAC found: throw DecryptException
    • If decryption never succeeded: throw DecryptException

Sources: src/Encrypter.php134-178 src/Encrypter.php239-256 src/Encrypter.php272-278

Configuring Previous Keys

Configuration Structure

Previous keys are configured through the previous_keys configuration array:


Sources: publish/encryption.php12-21 src/EncryptionFactory.php17-32

Environment Variable Format

The default configuration file shows how to specify previous keys via environment variables:

File: publish/encryption.php

The APP_PREVIOUS_KEYS environment variable should be a comma-separated list of base64-encoded keys:

APP_KEY=base64:current_key_here
APP_PREVIOUS_KEYS=base64:old_key_1,base64:old_key_2,base64:old_key_3

The configuration processes this via:


The array_filter() removes empty strings, allowing clean handling when the variable is unset.

Sources: publish/encryption.php16-20

Factory Processing

The EncryptionFactory processes previous keys during instantiation:

File: src/EncryptionFactory.php

At src/EncryptionFactory.php28-31 the factory:

  1. Extracts previous_keys from config (defaults to empty array)
  2. Maps each key through parseKey() to handle base64: prefix
  3. Passes the parsed array to previousKeys() method

The parseKey() method at src/EncryptionFactory.php49-56 strips the base64: prefix and decodes:


Sources: src/EncryptionFactory.php17-32 src/EncryptionFactory.php49-56

Validation Requirements

The previousKeys() method validates each provided key at src/Encrypter.php307-320:

ValidationRequirementException on Failure
Cipher CompatibilityEach key must be valid for the current cipherRuntimeException
Key LengthKey byte length must match cipher requirementsRuntimeException

Cipher-Specific Key Length Requirements:

CipherRequired Key Length
aes-128-cbc16 bytes
aes-256-cbc32 bytes
aes-128-gcm16 bytes
aes-256-gcm32 bytes

Important Constraint: All previous keys must be compatible with the current cipher. You cannot decrypt AES-128 data with an AES-256 configured encrypter, even if the old key is provided in previous_keys. The cipher algorithm must remain consistent.

Sources: src/Encrypter.php304-320 src/Encrypter.php35-40 src/Encrypter.php62-71

Key Rotation Workflow

Step-by-Step Rotation Process


Sources: src/Encrypter.php86-117 src/Encrypter.php134-178 src/EncryptionFactory.php17-32

Configuration Examples

Example 1: Initial Setup (No Previous Keys)

APP_KEY=base64:abc123...
APP_CIPHER=AES-256-CBC

Result: previous_keys = []


Example 2: First Key Rotation

APP_KEY=base64:def456... # New current key
APP_PREVIOUS_KEYS=base64:abc123... # Old key
APP_CIPHER=AES-256-CBC

Result: previous_keys = ['abc123_decoded']


Example 3: Multiple Historical Keys

APP_KEY=base64:ghi789...
APP_PREVIOUS_KEYS=base64:def456...,base64:abc123...
APP_CIPHER=AES-256-CBC

Result: previous_keys = ['def456_decoded', 'abc123_decoded']

The order in the comma-separated list determines the order of decryption attempts after the current key.

Sources: publish/encryption.php14-20

Best Practices and Considerations

Rotation Strategy Table

ScenarioRecommended ApproachRationale
Scheduled RotationGenerate new key → Add to config as current → Move old to previousMaintains access to all data during transition
Security IncidentGenerate new key → Force re-encryption immediately → Remove old keyMinimizes exposure window
Cipher MigrationNot supported via previous keysCipher must be consistent; requires full re-encryption
Performance OptimizationKeep previous keys list under 5 keysEach key adds decryption attempt overhead
Long-term MaintenanceRe-encrypt old data → Remove previous keysReduces technical debt and attack surface

Decryption Performance Impact

Each additional previous key adds computational overhead during decryption of legacy data:


Performance Characteristics:

  • Best Case: Current key succeeds → O(1) operation
  • Worst Case: Last previous key succeeds → O(n) where n = key count
  • CBC Mode Optimization: MAC validation fails fast before expensive decrypt
  • GCM Mode: Must attempt full decrypt for each key

Recommendation: Limit previous_keys array to 3-5 keys maximum. Plan re-encryption of legacy data to remove old keys.

Sources: src/Encrypter.php148-167

Cipher Consistency Requirement

Critical Limitation: Previous keys must use the same cipher as the current key.


Reason: The Encrypter instance is initialized with a single cipher algorithm src/Encrypter.php47-59 which is used for all operations. The validation at src/Encrypter.php309-314 enforces that previous keys match the cipher's required key length.

Implication: To migrate from one cipher to another (e.g., AES-128-CBC → AES-256-GCM), you must:

  1. Create two separate encrypter instances
  2. Decrypt with old encrypter (old cipher + old key)
  3. Re-encrypt with new encrypter (new cipher + new key)
  4. Store re-encrypted data

Sources: src/Encrypter.php47-59 src/Encrypter.php304-320

Security Considerations

ConsiderationRecommendation
Key StorageStore previous keys with same security as current key
Key LifetimeSet maximum lifetime (e.g., 90 days in previous keys)
Rotation FrequencyRotate annually or after security incident
Re-encryption TimelineComplete within 30-90 days of rotation
Audit LoggingLog when fallback keys are used for monitoring
Key DisposalSecurely delete keys after data re-encryption

Recommended Re-encryption Pattern

After rotating keys, re-encrypt legacy data to eliminate dependency on previous keys:


Monitoring: Track which keys are being used for decryption. When previous keys are no longer needed (no decryption attempts fall back to them), they can be safely removed from configuration.

Sources: src/Encrypter.php134-178

Code Reference Summary

Key Methods in Encrypter Class

MethodPurposeLocation
getAllKeys()Returns current key plus all previous keyssrc/Encrypter.php289-294
getPreviousKeys()Returns array of previous keys onlysrc/Encrypter.php296-302
previousKeys(array $keys)Sets and validates previous keyssrc/Encrypter.php304-320
decrypt(string $payload)Iterates through all keys to decryptsrc/Encrypter.php134-178

Configuration Processing

ComponentResponsibilityLocation
publish/encryption.phpDefines configuration structure with previous_keyspublish/encryption.php16-20
EncryptionFactory::__invoke()Parses and applies previous keys during instantiationsrc/EncryptionFactory.php28-31
EncryptionFactory::parseKey()Handles base64: prefix strippingsrc/EncryptionFactory.php49-56

Sources: src/Encrypter.php15-23 src/Encrypter.php289-320 src/EncryptionFactory.php17-56 publish/encryption.php1-21