Skip to content
47 changes: 40 additions & 7 deletions src/wp-admin/includes/secrets-site-health.php
Comment thread
ericmann marked this conversation as resolved.
Resolved
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ function wp_secrets_site_health_result( string $test, string $label, string $sta
}

/**
* Tests whether the site key comes from a dedicated constant or a weaker fallback.
* Tests whether the site key is defined, and in the recommended form.
*
* @since 7.2.0
*
Expand All @@ -82,7 +82,9 @@ function wp_secrets_site_health_result( string $test, string $label, string $sta
* @phpstan-return array{test: string, label: string, status: 'good'|'recommended'|'critical', badge: array{label: string, color: string}, description: string}
*/
function wp_secrets_site_health_test_key_source(): array {
$key_source = _wp_secrets_get_key_manager()->get_keyring()->get_key_source();
$keyring = _wp_secrets_get_key_manager()->get_keyring();
$key_source = $keyring->get_key_source();
$key_status = $keyring instanceof WP_Secrets_Config_Key_Provider ? $keyring->get_key_status() : null;

if ( str_contains( $key_source, 'broken' ) ) {
return wp_secrets_site_health_result(
Expand All @@ -96,7 +98,7 @@ function wp_secrets_site_health_test_key_source(): array {
);
}

if ( str_contains( $key_source, 'legacy interpretation' ) ) {
if ( WP_Secrets_Config_Key_Provider::KEY_STATUS_LEGACY === $key_status ) {
return wp_secrets_site_health_result(
'secrets_api_key_source',
__( 'Secrets API key should be regenerated' ),
Expand All @@ -108,14 +110,45 @@ function wp_secrets_site_health_test_key_source(): array {
);
}

if ( str_contains( $key_source, 'LOGGED_IN_KEY' ) ) {
if ( WP_Secrets_Config_Key_Provider::KEY_STATUS_UNDEFINED === $key_status || WP_Secrets_Config_Key_Provider::KEY_STATUS_UNUSABLE === $key_status ) {
$is_defined = WP_Secrets_Config_Key_Provider::KEY_STATUS_UNUSABLE === $key_status;
$example = sprintf( "define( 'WP_SECRETS_KEY', '%s' );", base64_encode( random_bytes( 32 ) ) );

return wp_secrets_site_health_result(
'secrets_api_key_source',
__( 'Secrets API is using a fallback key' ),
__( 'Secrets API has no encryption key' ),
'recommended',
sprintf(
'<p>%s</p>',
esc_html__( 'No dedicated WP_SECRETS_KEY constant is defined. Secrets are encrypted using a key derived from existing salts, which works but is not the recommended configuration. Define WP_SECRETS_KEY with a value from `wp secret generate-key`.' )
'<p>%s</p><p>%s</p><p><code>%s</code></p><p>%s</p>',
$is_defined
? sprintf(
/* translators: 1: WP_SECRETS_KEY, 2: wp-config.php, 3: wp-config-sample.php */
__( '%1$s is defined in %2$s, but its value cannot be used: it is empty, still the placeholder from %3$s, or the same as one of the authentication keys and salts. Plugins cannot store API keys and other credentials with the Secrets API until it is fixed.' ),
'<code>WP_SECRETS_KEY</code>',
'<code>wp-config.php</code>',
'<code>wp-config-sample.php</code>'
)
: sprintf(
/* translators: 1: WP_SECRETS_KEY, 2: wp-config.php */
__( '%1$s is not defined in %2$s. Plugins cannot store API keys and other credentials with the Secrets API until it is.' ),
'<code>WP_SECRETS_KEY</code>',
'<code>wp-config.php</code>'
),
$is_defined
? sprintf(
/* translators: 1: WP_SECRETS_KEY, 2: wp-config.php */
__( 'Replace the %1$s line in %2$s with this one:' ),
'<code>WP_SECRETS_KEY</code>',
'<code>wp-config.php</code>'
)
: sprintf(
/* translators: 1: wp-config.php, 2: Translated version of "That's all, stop editing! Happy publishing." */
__( 'Add this line to %1$s, above the line reading %2$s:' ),
'<code>wp-config.php</code>',
'<code>/* ' . __( 'That&#8217;s all, stop editing! Happy publishing.' ) . ' */</code>'
),
esc_html( $example ),
esc_html__( 'A new value is generated each time this page loads. Keep a copy of the one you use with your backups: secrets stored with it cannot be read without it.' )
)
);
}
Expand Down
71 changes: 71 additions & 0 deletions src/wp-includes/secrets.php
Original file line number Diff line number Diff line change
Expand Up @@ -693,6 +693,77 @@ function wp_secrets_provider_is_writable(): bool {
return _wp_secrets_get_provider()->is_writable();
}

/**
* Whether the Secrets API can store and read secrets on this site.
*
* A plugin can check this before it moves a credential into the Secrets API, and
* keep its existing storage while it returns false:
*
* if ( wp_is_secrets_api_available() ) {
* // Move the credential, then use wp_set_secret() and wp_get_secret().
* } else {
* // Keep using the existing storage.
* }
*
* Returns false when the secrets.php drop-in failed to load, when the default
* keyring has no usable WP_SECRETS_KEY, or when WP_SECRETS_KEY cannot unwrap the
* stored root key. The last happens when a site that already stored secrets adds
* or changes the key without re-wrapping the root key.
*
* A provider or keyring from a working drop-in manages its own keys, so it is
* reported as available. Its backend can still be unreachable on a given request,
* which each call reports as a WP_Error.
*
* Whether the provider accepts writes is a separate question. See
* wp_secrets_provider_is_writable().
*
* @since 7.2.0
*
* @global WP_Secrets_Provider $wp_secrets_provider Optional. Provider override set by the drop-in.
*
* @return bool
*/
function wp_is_secrets_api_available(): bool {
if ( _wp_secrets_get_provider() instanceof WP_Secrets_Broken_Provider ) {
return false;
}

if ( isset( $GLOBALS['wp_secrets_provider'] ) ) {
return true;
}

$keyring = _wp_secrets_get_key_manager()->get_keyring();

if ( $keyring instanceof WP_Secrets_Broken_Keyring ) {
return false;
}

if ( $keyring instanceof WP_Secrets_Config_Key_Provider ) {
$is_usable = in_array(
$keyring->get_key_status(),
array( WP_Secrets_Config_Key_Provider::KEY_STATUS_BASE64, WP_Secrets_Config_Key_Provider::KEY_STATUS_LEGACY ),
true
);

if ( ! $is_usable ) {
return false;
}

$root_key = _wp_secrets_get_key_manager()->get_root_key( false );

if ( ! is_wp_error( $root_key ) ) {
wp_secrets_memzero( $root_key );

return true;
}

// With no root key stored yet, the first write creates one under this key.
return false === get_site_option( WP_Secrets_Key_Manager::ROOT_KEY_OPTION );
}

return true;
}

/**
* A human-readable description of what is protecting this site's secrets.
*
Expand Down
Loading
Loading