Key Rotation Cookbook#
This page shows the preferred Epicrypt key-rotation pattern once your application has:
one active key for new writes
one or more fallback keys for rollover reads
a short rotation window where successful fallback reads trigger re-issue or re-encryption
Core Model#
Use the same model across domains:
active key: used for all new writes
fallback keys: accepted only during a rotation window
matched key id: recorded when you need to know which candidate succeeded
used fallback key: tells you whether the value should be rewritten under the active key
Represent that with KeyRing:
use Infocyph\Epicrypt\Security\KeyRing;
$ring = new KeyRing([
'k2026-q1' => $previousKey,
'k2026-q2' => $activeKey,
], 'k2026-q2');
Protected Strings#
Use decryptWithAnyKeyResult() when a protected value may have been encrypted with a previous key version.
use Infocyph\Epicrypt\DataProtection\StringProtector;
use Infocyph\Epicrypt\Security\Policy\SecurityProfile;
$protector = StringProtector::forProfile();
$result = $protector->decryptWithAnyKeyResult($ciphertext, $ring);
$plaintext = $result->plaintext;
if ($result->usedFallbackKey) {
$ciphertext = $protector->reencryptWithAnyKey($ciphertext, $ring, $activeKey);
}
Envelope-Protected Data#
EnvelopeProtector follows the same flow.
use Infocyph\Epicrypt\DataProtection\EnvelopeProtector;
use Infocyph\Epicrypt\Security\Policy\SecurityProfile;
$protector = EnvelopeProtector::forProfile(SecurityProfile::MODERN);
$result = $protector->decryptWithAnyKeyResult($encodedEnvelope, $ring);
if ($result->usedFallbackKey) {
$encodedEnvelope = $protector->reencryptWithAnyKey($encodedEnvelope, $ring, $activeMasterKey);
}
Wrapped Secrets#
Wrapped secrets should also move forward when a previous master key matches.
use Infocyph\Epicrypt\Password\Secret\WrappedSecretManager;
$manager = new WrappedSecretManager();
$result = $manager->unwrapWithAnyKeyResult($wrappedSecret, $ring);
$secret = $result->plaintext;
if ($result->usedFallbackKey) {
$wrappedSecret = $manager->rewrapWithAnyKey($wrappedSecret, $ring, $activeMasterSecret);
}
JWT and Signed Payload Verification#
When you only need verification metadata, prefer verifyWithAnyKeyResult().
$result = $jwt->verifyWithAnyKeyResult($token, $ring);
if (!$result->verified) {
throw new RuntimeException('Token verification failed.');
}
if ($result->usedFallbackKey) {
// Re-issue the token with the active signing key when appropriate.
}
Files#
Protected files should be re-encrypted into a new destination or rotated in place.
use Infocyph\Epicrypt\DataProtection\FileProtector;
use Infocyph\Epicrypt\Security\Policy\SecurityProfile;
$files = FileProtector::forProfile(SecurityProfile::MODERN);
$result = $files->reencryptWithAnyKey(
'/secure/archive.epc',
'/secure/archive.current.epc',
$ring,
$activeFileKey,
);
if ($result->usedFallbackKey) {
// The file was read with a fallback key and is now on the active key.
}
Operational Guidance#
keep fallback keys only during an active rollover window
rewrite on successful fallback reads when doing so is safe for your workflow
remove retired keys after the rotation window closes
prefer
SecurityProfile::MODERNfor new writes