Skip to content

feat: add key:rotate command#10174

Open
paulbalandan wants to merge 1 commit intocodeigniter4:4.8from
paulbalandan:key-rotate
Open

feat: add key:rotate command#10174
paulbalandan wants to merge 1 commit intocodeigniter4:4.8from
paulbalandan:key-rotate

Conversation

@paulbalandan
Copy link
Copy Markdown
Member

Description
Adds a key:rotate spark command that demotes the current encryption.key to encryption.previousKeys in .env and generates a fresh key, so existing ciphertext stays decryptable via the KeyRotationDecorator fallback.

The actual key write is delegated to key:generate to avoid duplicating that logic; previousKeys is written first so a failure mid-rotation leaves a stale-but-decryptable .env rather than risking key loss. Options: --force/-f, --prefix, --length, and --keep=N to cap the retained list.

Two small support pieces in system/CLI/:

  • NullInputOutput — an InputOutput sink for silencing a sub-command invoked via AbstractCommand::call().
  • CLI::getInputOutput()@internal getter symmetric to setInputOutput() / resetInputOutput(), so callers can save and restore the prior IO.

Checklist:

  • Securely signed commits
  • Component(s) with PHPDoc blocks, only if necessary or adds value (without duplication)
  • Unit testing, with >80% coverage
  • User guide updated
  • Conforms to style guide

@github-actions github-actions Bot added the 4.8 PRs that target the `4.8` branch. label May 7, 2026
Copy link
Copy Markdown
Contributor

@memleakd memleakd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working on this. I left a few inline questions about .env rewrite edge cases.

unset($_ENV['encryption.previousKeys']);
service('superglobals')->unsetServer('encryption.previousKeys');

$exitCode = $this->callKeyGenerateSilently($prefix, $options['length']);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could --length be validated before writing previousKeys? For example, a negative or zero value reaches key:generate, fails in key generation, and leaves .env partially modified even though rotation did not complete.

$value = implode(',', $previousKeys);
$replacement = "\nencryption.previousKeys = {$value}";

if (! str_contains($contents, 'encryption.previousKeys')) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this treat comments or unrelated text containing encryption.previousKeys as an existing setting? If .env has a comment mentioning this key, the branch below runs, but the replacement regex may not match anything. The command can then continue and rotate encryption.key without actually writing the old key to previousKeys.

return file_put_contents($envFile, $injected !== $contents ? $injected : $contents . $replacement) !== false;
}

$contents = (string) preg_replace(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this also handle DotEnv's supported export encryption.previousKeys = ... syntax? As written, that line would make the earlier str_contains() check true, but this regex would not replace it, so the rotated-out key may not be persisted.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

4.8 PRs that target the `4.8` branch.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants