diff --git a/README.md b/README.md index d617ee78..6c6ca597 100644 --- a/README.md +++ b/README.md @@ -204,7 +204,7 @@ wp config delete [--type=] [--config-file=] Generates a wp-config.php file. ~~~ -wp config create [--dbname=] [--dbuser=] [--dbpass|db_password=] [--dbhost=] [--dbprefix=] [--dbcharset=] [--dbcollate=] [--locale=] [--extra-php] [--skip-salts] [--skip-check] [--force] [--config-file=] [--insecure] [--ssl] +wp config create [--dbname=] [--dbuser=] [--dbpass|db_password=] [--dbhost=] [--dbprefix=] [--dbcharset=] [--dbcollate=] [--locale=] [--extra-php] [--skip-salts] [--skip-secrets-key] [--skip-check] [--force] [--config-file=] [--insecure] [--ssl] ~~~ Creates a new wp-config.php with database constants, and verifies that @@ -257,6 +257,9 @@ the database constants are correct. [--skip-salts] If set, keys and salts won't be generated, but should instead be passed via `--extra-php`. + [--skip-secrets-key] + If set, `WP_SECRETS_KEY` won't be generated. WordPress 7.2 and later need it to store secrets with the Secrets API, so pass it via `--extra-php` instead. + [--skip-check] If set, the database connection is not checked. @@ -576,7 +579,7 @@ wp config shuffle-salts [...] [--force] [--config-file=] [--insecure **OPTIONS** [...] - One ore more keys to shuffle. If none are provided, this falls back to the default WordPress Core salt keys. + One ore more keys to shuffle. If none are provided, this falls back to the default WordPress Core salt keys. `WP_SECRETS_KEY` is not a salt and cannot be shuffled: changing it makes every stored secret unreadable. [--force] If an unknown key is requested to be shuffled, add it instead of throwing a warning. diff --git a/features/config-create.feature b/features/config-create.feature index 0dffcb36..46419c3e 100644 --- a/features/config-create.feature +++ b/features/config-create.feature @@ -302,6 +302,69 @@ Feature: Create a wp-config file define( 'AUTH_SALT', """ + @require-php-7.0 + Scenario: Configure with a Secrets API key generated + Given an empty directory + And WP files + + When I run `wp config create {CORE_CONFIG_SETTINGS} --skip-check` + Then the wp-config.php file should contain: + """ + define( 'WP_SECRETS_KEY', + """ + + When I run `wp config get WP_SECRETS_KEY` + Then STDOUT should match /^[A-Za-z0-9+\/]{43}=$/ + + Scenario: Configure without a Secrets API key + Given an empty directory + And WP files + + When I run `wp config create {CORE_CONFIG_SETTINGS} --skip-check --skip-secrets-key` + Then the wp-config.php file should not contain: + """ + WP_SECRETS_KEY + """ + + Scenario: Configure with a Secrets API key passed via --extra-php + Given an empty directory + And WP files + + When I run `wp config create {CORE_CONFIG_SETTINGS} --skip-check --extra-php="define( 'WP_SECRETS_KEY', 'my-own-key' );"` + Then the wp-config.php file should contain: + """ + define( 'WP_SECRETS_KEY', 'my-own-key' ); + """ + + When I run `wp config get WP_SECRETS_KEY` + Then STDOUT should be: + """ + my-own-key + """ + + When I run `grep -c WP_SECRETS_KEY wp-config.php` + Then STDOUT should be: + """ + 1 + """ + + Scenario: Configure with a Secrets API key passed via --extra-php in upper case + Given an empty directory + And WP files + + When I run `wp config create {CORE_CONFIG_SETTINGS} --skip-check --extra-php="DEFINE( 'WP_SECRETS_KEY', 'my-own-key' );"` + And I run `grep -c WP_SECRETS_KEY wp-config.php` + Then STDOUT should be: + """ + 1 + """ + + When I run `wp config get WP_SECRETS_KEY` + Then STDOUT should be: + """ + my-own-key + """ + Scenario: Values are properly escaped to avoid creating invalid config files Given an empty directory And WP files diff --git a/features/config-shuffle-salts.feature b/features/config-shuffle-salts.feature index 3acfcc81..61b4e62c 100644 --- a/features/config-shuffle-salts.feature +++ b/features/config-shuffle-salts.feature @@ -265,3 +265,31 @@ Feature: Refresh the salts in the wp-config.php file """ {NEW_KEY_ORIG} """ + + Scenario: The Secrets API key is not a salt + Given an empty directory + And WP files + And I run `wp config create {CORE_CONFIG_SETTINGS} --skip-check` + And I run `wp config get WP_SECRETS_KEY` + And save STDOUT as {SECRETS_KEY} + + When I run `wp config shuffle-salts` + And I run `wp config get WP_SECRETS_KEY` + Then STDOUT should be: + """ + {SECRETS_KEY} + """ + + When I try `wp config shuffle-salts WP_SECRETS_KEY` + Then STDERR should contain: + """ + WP_SECRETS_KEY is not a salt and cannot be shuffled + """ + And the return code should be 1 + + When I try `wp config shuffle-salts WP_SECRETS_KEY --force` + Then STDERR should contain: + """ + WP_SECRETS_KEY is not a salt and cannot be shuffled + """ + And the return code should be 1 diff --git a/src/Config_Command.php b/src/Config_Command.php index 6b6ec671..8a802e4a 100644 --- a/src/Config_Command.php +++ b/src/Config_Command.php @@ -165,6 +165,9 @@ private static function get_initial_locale() { * [--skip-salts] * : If set, keys and salts won't be generated, but should instead be passed via `--extra-php`. * + * [--skip-secrets-key] + * : If set, `WP_SECRETS_KEY` won't be generated. WordPress 7.2 and later need it to store secrets with the Secrets API, so pass it via `--extra-php` instead. + * * [--skip-check] * : If set, the database connection is not checked. * @@ -311,6 +314,7 @@ public function create( $_, $assoc_args ) { [ 'keys-and-salts' => false, 'keys-and-salts-alt' => '', + 'secrets-key' => false, 'extra-php' => '', ] ); @@ -337,6 +341,25 @@ public function create( $_, $assoc_args ) { $template_args['extra-php'] = $extra_php; } + /* + * WordPress 7.2 and later need WP_SECRETS_KEY to store secrets. It is a base64-encoded + * random 32-byte value, unlike the salts, and it is left alone if --extra-php defines it. + * PHP function names are case-insensitive and constant names are not, so only `define` + * is matched in any case. + */ + $extra_php_defines_secrets_key = is_string( $template_args['extra-php'] ) + && preg_match( '/(?i:define)\s*\(\s*[\'"]WP_SECRETS_KEY[\'"]/', $template_args['extra-php'] ); + + if ( ! Utils\get_flag_value( $assoc_args, 'skip-secrets-key' ) && ! $extra_php_defines_secrets_key ) { + try { + // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- The Secrets API reads the key as base64. + $assoc_args['secrets-key'] = base64_encode( random_bytes( 32 ) ); + $template_args['secrets-key'] = true; + } catch ( Exception $e ) { + WP_CLI::warning( 'Could not generate WP_SECRETS_KEY: ' . $e->getMessage() . ' Add it to the wp-config.php file to use the Secrets API.' ); + } + } + $command_root = Path::phar_safe( dirname( __DIR__ ) ); $out = Utils\mustache_render( "{$command_root}/templates/wp-config.mustache", $template_args ); @@ -441,6 +464,10 @@ public function create( $_, $assoc_args ) { $config_transformer->update( 'constant', $name, $assoc_args[ $arg_name ], [ 'add' => true ] ); } } + + if ( ! empty( $template_args['secrets-key'] ) ) { + $config_transformer->update( 'constant', 'WP_SECRETS_KEY', $assoc_args['secrets-key'], [ 'add' => true ] ); + } } catch ( Throwable $exception ) { $cleanup_error = ''; if ( $created_config_file && file_exists( $assoc_args['config-file'] ) ) { @@ -1246,7 +1273,7 @@ public function has( $args, $assoc_args ) { * ## OPTIONS * * [...] - * : One ore more keys to shuffle. If none are provided, this falls back to the default WordPress Core salt keys. + * : One ore more keys to shuffle. If none are provided, this falls back to the default WordPress Core salt keys. `WP_SECRETS_KEY` is not a salt and cannot be shuffled: changing it makes every stored secret unreadable. * * [--force] * : If an unknown key is requested to be shuffled, add it instead of throwing a warning. @@ -1275,6 +1302,11 @@ public function shuffle_salts( $args, $assoc_args ) { $keys = $args; $force = Utils\get_flag_value( $assoc_args, 'force', false ); + // Secrets stored with the Secrets API cannot be read once WP_SECRETS_KEY changes. + if ( in_array( 'WP_SECRETS_KEY', $keys, true ) ) { + WP_CLI::error( 'WP_SECRETS_KEY is not a salt and cannot be shuffled: changing it makes every secret stored with the Secrets API unreadable.' ); + } + $has_keys = ( ! empty( $keys ) ) ? true : false; if ( empty( $keys ) ) { diff --git a/templates/wp-config.mustache b/templates/wp-config.mustache index 9bcc9e69..da1bbadc 100644 --- a/templates/wp-config.mustache +++ b/templates/wp-config.mustache @@ -63,6 +63,18 @@ define( 'WP_CACHE_KEY_SALT', '{{wp-cache-key-salt}}' ); {{keys-and-salts-alt}} /**#@-*/ +{{#secrets-key}} + +/** + * Encryption key for the Secrets API. + * + * Secrets stored by WordPress and plugins are encrypted with this key. + * Unlike the keys and salts above, changing it makes every stored secret unreadable. + * + * @since 7.2.0 + */ +define( 'WP_SECRETS_KEY', '' ); +{{/secrets-key}} /** * WordPress database table prefix.