Skip to content

feat: move Key Encryption onto Secrets API - #1107

Open
Pranjal1423 wants to merge 17 commits into
WordPress:developfrom
Pranjal1423:feat/secrets-api-integration
Open

Pranjal1423 wants to merge 17 commits into
WordPress:developfrom
Pranjal1423:feat/secrets-api-integration

Conversation

@Pranjal1423

@Pranjal1423 Pranjal1423 commented Oct 5, 2026 •

Copy link
Copy Markdown

What?

Closes #1012

Moves the Key Encryption experiment onto the proposed WordPress Secrets API feature plugin (slated for WordPress 7.2 Core).

Why?

The Key Encryption experiment originally vendored a subset of ericmann/displace-secrets-manager. That upstream repository has been archived and superseded by ericmann/secrets-api.

To align with the WordPress 7.2 Core roadmap and avoid data loss for existing sites:

  1. Existing stored connector keys (_secret_ai/*) need an automated migration because the new Secrets API prototype fallback only maps unnamespaced names.
  2. The AI plugin should take advantage of wp_get_secret() and friends when present, while retaining the bundled internal provider as a zero-dependency fallback so the "zero-install" experience remains intact.

How?

  1. Secrets_Bridge Integration:

    • Added get_secret(), set_secret(), and delete_secret() methods to Secrets_Bridge.
    • Delegates transparently to wp_get_secret(), wp_set_secret(), and wp_delete_secret() under the ai/ namespace with caller context ['plugin' => 'ai'].
    • Preserves backward compatibility by falling back to the bundled provider if the feature plugin or core functions are not present.
    • Includes transparent read-time promotion if a legacy secret is accessed before the upgrade routine runs.
  2. Automated Migration (V1_5_0):

    • Added V1_5_0 upgrade routine in includes/Admin/Upgrades/V1_5_0.php and registered it in Upgrades.php.
    • Safely decrypts legacy _secret_ai/* options into memory, writes to wp_set_secret(), verifies read-back matches, and only then deletes the legacy option row.
  3. Uninstall Cleanup:

    • Cleans up active secrets stored in the Secrets API via wp_delete_secret() upon plugin uninstallation.
  4. Tests & Docs:

    • Added V1_5_0Test.php and updated Key_EncryptionTest.php.
    • Updated docs/experiments/key-encryption.md and includes/Vendor/Secrets/README.md.

Use of AI Tools

AI assistance: Yes
Tool(s): Google Antigravity
Model(s): Gemini 3.8 Flash
Used for: Implementation, migration routine, and integration tests;
I have reviewed and verified it locally with unit tests and static analysis.

Testing Instructions

  1. In a clean environment with Key Encryption enabled, verify connector API keys are encrypted at rest.
  2. When the Secrets API plugin is active, verify keys are stored and retrieved via wp_get_secret().
  3. Test upgrading a site with existing _secret_ai/* rows: verify the V1_5_0 migration moves keys to the new Secrets API without data loss or user intervention.
  4. Verify deactivation / opt-out restores plaintext credentials via decrypt_all().

Changelog Entry

Changed - Move Key Encryption experiment onto the Secrets API feature plugin with backward-compatible fallback and migration.

Open WordPress Playground Preview

@Pranjal1423
Pranjal1423 requested a review from a team October 5, 2026 07:00
@Pranjal1423
Pranjal1423 requested a review from jeffpaul as a code owner October 5, 2026 07:00
@Pranjal1423
Pranjal1423 marked this pull request as draft October 5, 2026 07:00
@github-actions

github-actions Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: Pranjal1423 <[email protected]>
Co-authored-by: jeffpaul <[email protected]>
Co-authored-by: dkotter <[email protected]>
Co-authored-by: whyisjake <[email protected]>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@Pranjal1423
Pranjal1423 marked this pull request as ready for review October 5, 2026 07:58
@dkotter dkotter added this to the 1.5.0 milestone Oct 5, 2026

@dkotter dkotter left a comment

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.

If I'm following correctly, looks like the changes here require someone to manually install the new Secrets API plugin, correct? If so, that's not the direction we were wanting to go, ideally we can bundle that plugin ourselves, matching what we already do with the older version of that plugin.

@Pranjal1423

Copy link
Copy Markdown
Author

Thanks for clarifying, @dkotter! That makes sense.

I’ll update the PR to bundle the new Secrets API (ericmann/secrets-api) under includes/Vendor/Secrets/, similar to how the older version was vendored.

I’ll:

  • Bundle the new API so the plugin works out of the box without any external dependency.
  • Update Secrets_Bridge to use the bundled API.
  • Keep the existing migration in place so the encrypted keys are safely migrated to the new format.

I’ll push the updated changes shortly!

@codecov

codecov Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 75.70093% with 26 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.13%. Comparing base (26322c3) to head (86741b4).

Files with missing lines Patch % Lines
...udes/Experiments/Key_Encryption/Secrets_Bridge.php 74.73% 24 Missing ⚠️
includes/Admin/Upgrades/V1_5_0.php 80.00% 1 Missing ⚠️
...udes/Experiments/Key_Encryption/Key_Encryption.php 50.00% 1 Missing ⚠️
Additional details and impacted files
@@              Coverage Diff              @@
##             develop    #1107      +/-   ##
=============================================
- Coverage      82.17%   82.13%   -0.05%     
- Complexity      3399     3448      +49     
=============================================
  Files            136      137       +1     
  Lines          13187    13286      +99     
=============================================
+ Hits           10837    10912      +75     
- Misses          2350     2374      +24     
Flag Coverage Δ
unit 82.13% <75.70%> (-0.05%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@Pranjal1423
Pranjal1423 requested a review from dkotter October 7, 2026 08:51

@dkotter dkotter left a comment

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.

On a quick review, this seems to be headed in the right direction, thanks for all the work here!

As I continue to test, one initial question is are we purposely leaving the old Displace plugin code behind? Is there a reason to keep that? I guess I would expect for all of those files to be removed and replaced with the new code

@dkotter

dkotter commented Oct 14, 2026

Copy link
Copy Markdown
Contributor

One other quick thing as I'm reviewing, the Secrets API made it's way into WordPress Core just a few days ago. We should ensure the code we are shipping here includes the latest version of that, noting the code hosted at https://github.com/ericmann/secrets-api may not be the exact same code that was shipped in Core. Should look at the upstream commits to Core to see if there's anything new we need to pull in

@whyisjake whyisjake left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for this, @Pranjal1423. I tested it against core trunk now that the Secrets API has landed (5744bc5803, Trac #66187), and it holds up well on the two things I was most worried about:

No name conflicts on trunk. Core loads wp-includes/secrets.php unconditionally, so a second copy of any wp_*secret* function would fatal. includes/Vendor/Secrets/load.php returns early on function_exists( 'wp_get_secret' ). On 7.2-alpha-64345 the bundled copy never loads, and wp_get_secret resolves to core's file. The option names don't overlap either: the plugin's legacy rows are _secret_ai/* and _secrets_master_key, and core's are _wp_secret_* and _wp_secrets_root_key. ai/<connector>_api_key passes wp_secrets_validate_name().

Existing keys carry over. On trunk I saved a fake key with Key Encryption on using develop (it landed in _secret_ai/anthropic_api_key), switched to this branch, and read it again. It was promoted to _wp_secret_ai/anthropic_api_key, wp_get_secret()->reveal() matched, and get_option() returned the key.

The inline suggestions cover what I found:

  1. A WP_Error from wp_get_secret() is treated as "not set", so a key core can't decrypt (salt or site-key change, broken drop-in) silently disappears, and opt-out restores nothing. This is the one that matters for users.
  2. Read-time promotion and set_secret() delete the legacy row without the read-back check the migration already does.
  3. Uninstall can miss secrets for connectors that are no longer registered, or that a drop-in stores.
  4. Tests for each.

One follow-up that isn't a suggestion: when decrypt_all() leaves a key in get_unreadable_secrets(), the opt-out should probably show an admin notice instead of quietly turning off.

Tested on WordPress 7.2-alpha-64345 (trunk), PHP in wp-env, with the suggestions applied:

  • Key_EncryptionTest, V1_5_0Test, UninstallTest, DeactivationTest: 41 tests, 0 failures (3 skipped).
  • Full PHPUnit suite: 1929 tests, 0 failures, single site and multisite.
  • PHPStan: no errors. PHPCS: clean on the changed lines.
  • Without the suggestions, the full suite also passes on trunk (1926 tests, single site and multisite).

I didn't run the e2e suite against trunk, and I didn't test the bundled path on 7.0.4.

AI assistance: Yes. Tool(s): Claude Code. Used for: reproducing the trunk behavior, drafting these suggestions and tests, and running the checks above.

Comment on lines +342 to +387
/**
* Retrieves a secret value, preferring the global Secrets API when present,
* falling back to the bundled provider.
*
* Transparently promotes any legacy prototype row to the new API on read.
*
* @since 1.5.0
*
* @param string $secret_key Namespaced secret key.
* @return string|null Decrypted secret value, or null if not found.
*/
public function get_secret( string $secret_key ): ?string {
$this->ensure_secrets_api();

if ( function_exists( 'wp_get_secret' ) ) {
$secret = wp_get_secret( $secret_key );
if ( is_object( $secret ) && method_exists( $secret, 'reveal' ) ) {
return $secret->reveal();
}
if ( is_string( $secret ) && '' !== $secret ) {
return $secret;
}
}

// If absent in the new API (or Secrets API unavailable), check if legacy store holds it:
if ( $this->is_legacy_provider_available() ) {
try {
$legacy = Secrets::get( $secret_key, $this->secret_context() );
} catch ( \Throwable $e ) {
$legacy = null;
}

if ( null !== $legacy && '' !== $legacy ) {
// Transparent read-time promotion if new API is present:
if ( function_exists( 'wp_set_secret' ) ) {
$stored = wp_set_secret( $secret_key, $legacy );
if ( false !== $stored && ! is_wp_error( $stored ) ) {
delete_option( Secrets_Provider_Encrypted_Options::OPTION_PREFIX . $secret_key );
}
}
return $legacy;
}
}

return null;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

wp_get_secret() returning a WP_Error is treated as "not set", so a saved key silently disappears.

Core returns a WP_Error when a secret is stored but can't be opened: after WP_SECRETS_KEY, LOGGED_IN_KEY or LOGGED_IN_SALT change, or with a broken secrets.php drop-in. I reproduced it on trunk (7.2-alpha-64345) by saving a key, then reading it with a different WP_SECRETS_KEY:

  • wp_get_secret() → WP_Error secret_key_unavailable
  • get_option( 'connectors_ai_anthropic_api_key' ) → '', so the provider just looks unconfigured
  • decrypt_all() on opt-out → restores 0 keys, with nothing reported

The secret itself is intact; it reads again once the original key is back. So it should be reported, not dropped. This keeps the legacy fallback, never promotes over an unreadable secret, warns once per request through wp_trigger_error(), and exposes get_unreadable_secrets() so the opt-out can show a notice for any key it couldn't restore.

It also routes read-time promotion through a write_and_verify() helper: today the legacy row is deleted as soon as wp_set_secret() returns, without the read-back check maybe_migrate_legacy_secrets() already does.

(The $unreadable property is included here only so the suggestion is self-contained. It's probably better moved up next to $bypass_read_filter.)

Suggested change
/**
* Retrieves a secret value, preferring the global Secrets API when present,
* falling back to the bundled provider.
*
* Transparently promotes any legacy prototype row to the new API on read.
*
* @since 1.5.0
*
* @param string $secret_key Namespaced secret key.
* @return string|null Decrypted secret value, or null if not found.
*/
public function get_secret( string $secret_key ): ?string {
$this->ensure_secrets_api();
if ( function_exists( 'wp_get_secret' ) ) {
$secret = wp_get_secret( $secret_key );
if ( is_object( $secret ) && method_exists( $secret, 'reveal' ) ) {
return $secret->reveal();
}
if ( is_string( $secret ) && '' !== $secret ) {
return $secret;
}
}
// If absent in the new API (or Secrets API unavailable), check if legacy store holds it:
if ( $this->is_legacy_provider_available() ) {
try {
$legacy = Secrets::get( $secret_key, $this->secret_context() );
} catch ( \Throwable $e ) {
$legacy = null;
}
if ( null !== $legacy && '' !== $legacy ) {
// Transparent read-time promotion if new API is present:
if ( function_exists( 'wp_set_secret' ) ) {
$stored = wp_set_secret( $secret_key, $legacy );
if ( false !== $stored && ! is_wp_error( $stored ) ) {
delete_option( Secrets_Provider_Encrypted_Options::OPTION_PREFIX . $secret_key );
}
}
return $legacy;
}
}
return null;
}
/**
* Secrets found stored but unreadable during this request, keyed by secret key.
*
* @since 1.5.0
* @var array<string, \WP_Error>
*/
private array $unreadable = array();
/**
* Retrieves a secret value, preferring the global Secrets API when present,
* falling back to the bundled provider.
*
* Transparently promotes any legacy prototype row to the new API on read.
*
* @since 1.5.0
*
* @param string $secret_key Namespaced secret key.
* @return string|null Decrypted secret value, or null if not found.
*/
public function get_secret( string $secret_key ): ?string {
$this->ensure_secrets_api();
$unreadable = null;
if ( function_exists( 'wp_get_secret' ) ) {
$secret = wp_get_secret( $secret_key );
if ( is_wp_error( $secret ) ) {
// Stored, but not readable: the site key or salts changed, or a drop-in is broken.
$unreadable = $secret;
} elseif ( is_object( $secret ) && method_exists( $secret, 'reveal' ) ) {
unset( $this->unreadable[ $secret_key ] );
return $secret->reveal();
} elseif ( is_string( $secret ) && '' !== $secret ) {
return $secret;
}
}
// If absent in the new API (or Secrets API unavailable), check if legacy store holds it:
if ( $this->is_legacy_provider_available() ) {
try {
$legacy = Secrets::get( $secret_key, $this->secret_context() );
} catch ( \Throwable $e ) {
$legacy = null;
}
if ( null !== $legacy && '' !== $legacy ) {
// Promote on read, but never over an unreadable secret, and delete the
// legacy row only once the new copy reads back intact.
if ( null === $unreadable && $this->write_and_verify( $secret_key, $legacy ) ) {
delete_option( Secrets_Provider_Encrypted_Options::OPTION_PREFIX . $secret_key );
}
return $legacy;
}
}
if ( null !== $unreadable ) {
$this->record_unreadable( $secret_key, $unreadable );
}
return null;
}
/**
* Returns the secret keys found stored but unreadable during this request.
*
* Lets callers such as the Key Encryption opt-out report a key that could not be
* restored instead of treating it as never set.
*
* @since 1.5.0
*
* @return array<string, \WP_Error> Map of secret key to the error the Secrets API returned.
*/
public function get_unreadable_secrets(): array {
return $this->unreadable;
}
/**
* Records an unreadable secret and warns once per request.
*
* @since 1.5.0
*
* @param string $secret_key Namespaced secret key.
* @param \WP_Error $error The error the Secrets API returned.
*/
private function record_unreadable( string $secret_key, \WP_Error $error ): void {
if ( isset( $this->unreadable[ $secret_key ] ) ) {
return;
}
$this->unreadable[ $secret_key ] = $error;
wp_trigger_error(
__METHOD__,
sprintf(
/* translators: 1: Secret name, 2: Error code. */
__( 'The secret "%1$s" is stored but could not be read (%2$s). Restore the site key or salts it was stored with, or save the key again.', 'ai' ),
$secret_key,
$error->get_error_code()
),
E_USER_WARNING
);
}
/**
* Writes a secret through the Secrets API and confirms it reads back unchanged.
*
* @since 1.5.0
*
* @param string $secret_key Namespaced secret key.
* @param string $value Plaintext secret value.
* @return bool True when the stored secret reads back as `$value`.
*/
private function write_and_verify( string $secret_key, string $value ): bool {
if ( ! function_exists( 'wp_set_secret' ) || ! function_exists( 'wp_get_secret' ) ) {
return false;
}
$stored = wp_set_secret( $secret_key, $value );
if ( false === $stored || is_wp_error( $stored ) ) {
return false;
}
$secret = wp_get_secret( $secret_key );
return is_object( $secret ) && method_exists( $secret, 'reveal' ) && hash_equals( $value, (string) $secret->reveal() );
}

Comment on lines +402 to +408
if ( function_exists( 'wp_set_secret' ) ) {
$result = wp_set_secret( $secret_key, $value );
if ( false !== $result && ! is_wp_error( $result ) ) {
delete_option( Secrets_Provider_Encrypted_Options::OPTION_PREFIX . $secret_key );
return true;
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Same read-back check before deleting the legacy row, so a write that doesn't read back falls through to the legacy store instead of removing the only readable copy.

Suggested change
if ( function_exists( 'wp_set_secret' ) ) {
$result = wp_set_secret( $secret_key, $value );
if ( false !== $result && ! is_wp_error( $result ) ) {
delete_option( Secrets_Provider_Encrypted_Options::OPTION_PREFIX . $secret_key );
return true;
}
}
if ( $this->write_and_verify( $secret_key, $value ) ) {
delete_option( Secrets_Provider_Encrypted_Options::OPTION_PREFIX . $secret_key );
return true;
}

Comment on lines +501 to +511
$stored = wp_set_secret( $secret_key, $plaintext );
if ( false === $stored || is_wp_error( $stored ) ) {
continue;
}

// Verify the new secret was written and matches the plaintext before deleting the old row.
$secret = wp_get_secret( $secret_key );
$readback = is_object( $secret ) && method_exists( $secret, 'reveal' ) ? $secret->reveal() : $secret;
if ( $readback !== $plaintext ) {
continue;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This already does the read-back check. Reusing the helper keeps the three paths identical (and compares with hash_equals()).

Suggested change
$stored = wp_set_secret( $secret_key, $plaintext );
if ( false === $stored || is_wp_error( $stored ) ) {
continue;
}
// Verify the new secret was written and matches the plaintext before deleting the old row.
$secret = wp_get_secret( $secret_key );
$readback = is_object( $secret ) && method_exists( $secret, 'reveal' ) ? $secret->reveal() : $secret;
if ( $readback !== $plaintext ) {
continue;
}
// Delete the old row only once the new secret reads back intact.
if ( ! $this->write_and_verify( $secret_key, $plaintext ) ) {
continue;
}

Comment on lines +171 to +176
if ( function_exists( 'wp_delete_secret' ) ) {
$bridge = new Secrets_Bridge();
foreach ( $bridge->get_connector_setting_names() as $connector_id => $setting_name ) {
wp_delete_secret( Secrets_Bridge::SECRET_NAMESPACE . '/' . $connector_id . '_api_key' );
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Deleting by registered connector misses secrets for connectors that are no longer registered. With a secrets.php drop-in those rows aren't in wp_options, so the _wp_secret_ai/ prefix sweep won't catch them either. wp_list_secrets( 'ai' ) covers both. The bundled copy defines it too.

Suggested change
if ( function_exists( 'wp_delete_secret' ) ) {
$bridge = new Secrets_Bridge();
foreach ( $bridge->get_connector_setting_names() as $connector_id => $setting_name ) {
wp_delete_secret( Secrets_Bridge::SECRET_NAMESPACE . '/' . $connector_id . '_api_key' );
}
}
// List through the Secrets API so secrets kept by a `secrets.php` drop-in are
// deleted too, including those of connectors that are no longer registered.
if ( function_exists( 'wp_list_secrets' ) && function_exists( 'wp_delete_secret' ) ) {
$secrets = wp_list_secrets( Secrets_Bridge::SECRET_NAMESPACE );
if ( is_array( $secrets ) ) {
foreach ( $secrets as $secret ) {
wp_delete_secret( $secret['name'] );
}
}
}

$bridge->ensure_secrets_api();
$bridge->ensure_secrets_api();
$this->assertTrue( function_exists( 'wp_get_secret' ) );
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Tests for the cases above, plus one that proves the bundled copy stands down when core provides the API. On trunk with the suggestions applied, all three pass. Without them, the first two fail:

  • test_bridge_reports_an_unreadable_secret: undefined get_unreadable_secrets().
  • test_bridge_promotion_keeps_the_legacy_row_until_it_reads_back: the legacy row is deleted.

They use an option__wp_secret_* filter to make the stored record unreadable, which makes core return secret_record_malformed. They also use wp_trigger_error_always_run, which has been available since WordPress 7.0, so they don't depend on WP_DEBUG.

Suggested change
}
}
/**
* Tests that a secret core stores but cannot read is reported, not treated as never set.
*
* @since 1.5.0
*/
public function test_bridge_reports_an_unreadable_secret(): void {
$bridge = Key_Encryption::get_bridge();
$this->assertTrue( $bridge->set_secret( self::SECRET_KEY, 'sk-unreadable' ) );
// A record the provider cannot open, as after a salt or site-key change.
$corrupt = static function () {
return 'not-a-secret-record';
};
$warnings = array();
$capture = static function ( $function_name, $message ) use ( &$warnings ) {
$warnings[] = $message;
};
add_filter( 'option__wp_secret_' . self::SECRET_KEY, $corrupt );
add_action( 'wp_trigger_error_always_run', $capture, 10, 2 );
add_filter( 'wp_trigger_error_trigger_error', '__return_false' );
try {
$this->assertNull( $bridge->get_secret( self::SECRET_KEY ) );
$this->assertNull( $bridge->get_secret( self::SECRET_KEY ) );
$this->assertArrayHasKey( self::SECRET_KEY, $bridge->get_unreadable_secrets() );
$this->assertCount( 1, $warnings, 'Warns once per request, not on every read.' );
} finally {
remove_filter( 'option__wp_secret_' . self::SECRET_KEY, $corrupt );
remove_action( 'wp_trigger_error_always_run', $capture, 10 );
remove_filter( 'wp_trigger_error_trigger_error', '__return_false' );
}
// The secret was never deleted: it reads again once the record is intact.
$this->assertSame( 'sk-unreadable', $bridge->get_secret( self::SECRET_KEY ) );
$this->assertArrayNotHasKey( self::SECRET_KEY, $bridge->get_unreadable_secrets() );
$bridge->delete_secret( self::SECRET_KEY );
}
/**
* Tests that read-time promotion keeps the legacy row when the new secret does not read back.
*
* @since 1.5.0
*/
public function test_bridge_promotion_keeps_the_legacy_row_until_it_reads_back(): void {
$bridge = Key_Encryption::get_bridge();
$legacy_key = 'ai/readback_key';
$legacy_row = '_secret_' . $legacy_key;
$bridge->is_legacy_provider_available();
$this->assertTrue( Secrets::set( $legacy_key, 'sk-readback', self::SECRET_CONTEXT ) );
// The write succeeds, but the stored record cannot be read back.
$corrupt = static function () {
return 'not-a-secret-record';
};
add_filter( 'option__wp_secret_' . $legacy_key, $corrupt );
try {
$this->assertSame( 'sk-readback', $bridge->get_secret( $legacy_key ) );
$this->assertNotFalse( get_option( $legacy_row, false ), 'The only readable copy must not be deleted.' );
} finally {
remove_filter( 'option__wp_secret_' . $legacy_key, $corrupt );
}
$bridge->delete_secret( $legacy_key );
}
/**
* Tests that the vendored Secrets API stands down when core provides it.
*
* @since 1.5.0
*/
public function test_vendored_secrets_api_stands_down_on_core(): void {
if ( ! file_exists( ABSPATH . WPINC . '/secrets.php' ) ) {
$this->markTestSkipped( 'This WordPress version does not include the Secrets API.' );
}
Key_Encryption::get_bridge()->ensure_secrets_api();
$this->assertSame( ABSPATH . WPINC . '/secrets.php', ( new \ReflectionFunction( 'wp_get_secret' ) )->getFileName() );
$this->assertFalse( class_exists( 'Secrets_API_Prototype_Fallback_Store', false ) );
}

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Move the Key Encryption experiment onto the new Secrets API feature plugin

4 participants