diff --git a/system/Commands/Encryption/GenerateKey.php b/system/Commands/Encryption/GenerateKey.php index 7a0d47d0d542..3726360fa8ad 100644 --- a/system/Commands/Encryption/GenerateKey.php +++ b/system/Commands/Encryption/GenerateKey.php @@ -171,36 +171,41 @@ protected function writeNewEncryptionKeyToFile(string $oldKey, string $newKey): } $oldFileContents = (string) file_get_contents($envFile); - $replacementKey = "\nencryption.key = {$newKey}"; - if (! str_contains($oldFileContents, 'encryption.key')) { - return file_put_contents($envFile, $replacementKey, FILE_APPEND) !== false; + // Match an active setting line, preserving any leading whitespace and `export` prefix. + $activePattern = $this->keyPattern($oldKey); + + if (preg_match($activePattern, $oldFileContents) === 1) { + $newFileContents = (string) preg_replace($activePattern, '$1' . $newKey, $oldFileContents, 1); + + return file_put_contents($envFile, $newFileContents) !== false; } - $newFileContents = preg_replace($this->keyPattern($oldKey), $replacementKey, $oldFileContents); + // Match a commented-out setting line (e.g., from the shipped `env` template) and + // uncomment it. The optional `export` prefix is dropped on uncomment for predictability. + $commentedPattern = '/^\h*#\h*(?:export\h+)?encryption\.key\h*=\h*[^\r\n]*$/m'; - if ($newFileContents === $oldFileContents) { - $newFileContents = preg_replace( - '/^[#\s]*encryption.key[=\s]*(?:hex2bin\:[a-f0-9]{64}|base64\:(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?)$/m', - $replacementKey, - $oldFileContents, - ); + if (preg_match($commentedPattern, $oldFileContents) === 1) { + $newFileContents = (string) preg_replace($commentedPattern, "encryption.key = {$newKey}", $oldFileContents, 1); + + return file_put_contents($envFile, $newFileContents) !== false; } - return file_put_contents($envFile, $newFileContents) !== false; + // No setting present (active or commented); append. + return file_put_contents($envFile, "\nencryption.key = {$newKey}", FILE_APPEND) !== false; } /** - * Get the regex of the current encryption key. + * Returns the regex used to locate an active `encryption.key = ...` setting in the `.env` + * contents. The single capture group spans everything up to (and including) the `=` and any + * separating whitespace, so a `preg_replace` substitution preserves an optional `export` + * prefix while rewriting only the value. + * + * The `$oldKey` parameter is retained for backward compatibility with subclasses that + * override this method; it is no longer consulted because the pattern matches any value. */ protected function keyPattern(string $oldKey): string { - $escaped = preg_quote($oldKey, '/'); - - if ($escaped !== '') { - $escaped = "[{$escaped}]*"; - } - - return "/^[#\\s]*encryption.key[=\\s]*{$escaped}$/m"; + return '/^(\h*(?:export\h+)?encryption\.key\h*=\h*)[^\r\n]*$/m'; } } diff --git a/tests/system/Commands/Encryption/GenerateKeyTest.php b/tests/system/Commands/Encryption/GenerateKeyTest.php index a4fb452fd5a2..387bdf7a1641 100644 --- a/tests/system/Commands/Encryption/GenerateKeyTest.php +++ b/tests/system/Commands/Encryption/GenerateKeyTest.php @@ -169,4 +169,40 @@ public function testKeyGenerateWhenNewBase64KeyIsSubsequentlyCommentedOut(): voi $this->assertStringContainsString('was successfully set.', $this->getBuffer()); $this->assertNotSame($key, env('encryption.key', $key), 'Failed replacing the commented out key.'); } + + public function testKeyGenerateReplacesExportPrefixedEncryptionKey(): void + { + $existingKey = 'hex2bin:' . str_repeat('a', 64); + file_put_contents($this->envPath, "export encryption.key = {$existingKey}\n"); + + command('key:generate --force'); + + $contents = (string) file_get_contents($this->envPath); + $this->assertMatchesRegularExpression( + '/^export encryption\.key = hex2bin:[a-f0-9]{64}$/m', + $contents, + 'The `export` prefix should be preserved and the value rewritten.', + ); + $this->assertStringNotContainsString($existingKey, $contents, 'The old key value should be replaced.'); + } + + public function testKeyGenerateNotFooledByCommentMentioningEncryptionKey(): void + { + $envContents = "# Note: encryption.key is set automatically by spark key:generate.\n"; + file_put_contents($this->envPath, $envContents); + + command('key:generate --force'); + + $contents = (string) file_get_contents($this->envPath); + $this->assertStringContainsString( + $envContents, + $contents, + 'The doc comment must be left intact.', + ); + $this->assertMatchesRegularExpression( + '/^encryption\.key = hex2bin:[a-f0-9]{64}$/m', + $contents, + 'A real `encryption.key` setting must be appended even when a comment mentions the name.', + ); + } } diff --git a/user_guide_src/source/changelogs/v4.7.3.rst b/user_guide_src/source/changelogs/v4.7.3.rst index 469a2e8637a0..b7ba0d8d5ed3 100644 --- a/user_guide_src/source/changelogs/v4.7.3.rst +++ b/user_guide_src/source/changelogs/v4.7.3.rst @@ -40,6 +40,7 @@ Bugs Fixed - **CLI:** Fixed a bug where ``CLI::generateDimensions()`` leaked ``stty`` error output (e.g., ``stty: 'standard input': Inappropriate ioctl for device``) to stderr when stdin was not a TTY. - **CLI:** Fixed a bug where ``CLI::generateDimensions()`` leaked ``tput`` error output (``tput: No value for $TERM and no -T specified``) to stderr when the ``stty`` fallback was reached and the ``TERM`` environment variable was not set. - **Commands:** Fixed a bug in the ``env`` command where passing options only would cause the command to throw a ``TypeError`` instead of showing the current environment. +- **Commands:** Fixed a bug in ``key:generate`` command where the regex used to locate the ``encryption.key`` line was fooled by a comment containing the substring (silently writing nothing), and did not handle DotEnv's ``export encryption.key = ...`` syntax. - **Common:** Fixed a bug where the ``command()`` helper function did not properly clean up output buffers, which could lead to risky tests when exceptions were thrown. - **Database:** Fixed a bug where the SQLSRV driver's decrement method was adding instead of subtracting the decrement value when ``$castTextToInt`` was false. - **Database:** Fixed a bug where the PostgreSQL driver's ``increment()`` and ``decrement()`` methods were not working for numeric columns.