[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-includes/secrets/ -> class-wp-secrets-provider.php (source)

   1  <?php
   2  /**
   3   * Secrets API: WP_Secrets_Provider class
   4   *
   5   * @package WordPress
   6   * @subpackage Secrets
   7   * @since 7.2.0
   8   */
   9  
  10  /**
  11   * Base class for a secrets provider.
  12   *
  13   * A provider is responsible for a secret: storing it, protecting it, and returning
  14   * it to a caller. It is the outermost extension point in the Secrets API, and the
  15   * one a hosting platform extends.
  16   *
  17   * WordPress ships WP_Secrets_Libsodium_Provider, which encrypts with libsodium and
  18   * stores ciphertext in the options tables. That provider is the default rather than
  19   * a privileged case: a platform that protects credentials in a key management
  20   * service, a hardware security module, or its own control panel extends this
  21   * same class.
  22   *
  23   * A provider must be stronger than the default, never weaker. Storing a plaintext
  24   * where the default would have stored ciphertext is what this class exists to
  25   * prevent. Receiving a value over an authenticated channel and holding it in a
  26   * hardware security module satisfies that requirement.
  27   *
  28   * WordPress cannot verify any of this. A provider is loaded from a drop-in, which is
  29   * fully trusted code: it runs before plugins, and could already read every secret by
  30   * implementing the keyring. get_protection_boundary() and is_writable() are
  31   * declarations rather than enforcement. They exist so that a developer reviewing a
  32   * drop-in can see what it claims, Site Health can report where credentials are
  33   * protected, and a settings screen can determine that a write will be refused before
  34   * an operator enters a credential. They are not a security boundary.
  35   *
  36   * Every implementation shares these contracts:
  37   *
  38   * - Three states, never collapsed. get() returns a WP_Secret, null when the secret
  39   *   does not exist, or WP_Error when it exists but cannot be produced. Reporting an
  40   *   unreachable backend as absent turns an outage into an apparently deleted
  41   *   credential.
  42   * - Fail closed. A provider that cannot answer returns WP_Error. It never
  43   *   substitutes a weaker source, and never returns a partial or placeholder value.
  44   * - No filter on the retrieval path. A provider replaces a component; it is not a
  45   *   hook. Nothing is given the opportunity to observe or alter a value in transit.
  46   * - Never persist a value more weakly than the provider protects it. A provider that
  47   *   fetches over the network must not write the plaintext into the persistent object
  48   *   cache. WP_Secret cannot round-trip a plaintext through wp_cache_set(), and
  49   *   caching the raw value alongside it would undo that. Memoize within the request
  50   *   only.
  51   *
  52   * Implementations extend this class. It is abstract rather than an interface so
  53   * that a later release can add a method with a default implementation, and every
  54   * existing subclass keeps working.
  55   *
  56   * @since 7.2.0
  57   */
  58  abstract class WP_Secrets_Provider {
  59  
  60      /**
  61       * Protection happens inside WordPress.
  62       *
  63       * The provider holds ciphertext that WordPress produced, and WordPress holds the
  64       * key material.
  65       *
  66       * @since 7.2.0
  67       * @var string
  68       */
  69      const BOUNDARY_WORDPRESS = 'wordpress';
  70  
  71      /**
  72       * Protection happens outside WordPress.
  73       *
  74       * The provider is itself the encryption boundary, and WordPress is a consumer of a
  75       * credential it does not protect.
  76       *
  77       * @since 7.2.0
  78       * @var string
  79       */
  80      const BOUNDARY_PROVIDER = 'provider';
  81  
  82      /**
  83       * Retrieves a secret.
  84       *
  85       * Returning null means "I am not responsible for this name" as well as "this
  86       * name does not exist". The two are the same answer from a caller's point of
  87       * view, and keeping them distinct would require a provider to enumerate names
  88       * it has never heard of.
  89       *
  90       * @since 7.2.0
  91       *
  92       * @param string $name    The secret's name.
  93       * @param string $version A WP_Secret_Version constant. A provider with no
  94       *                        version history returns null for PREVIOUS, which is
  95       *                        absence rather than an error.
  96       * @param bool   $network Whether this is a network-scope secret.
  97       * @return WP_Secret|null|WP_Error
  98       *
  99       * @phpstan-param WP_Secret_Version::CURRENT|WP_Secret_Version::PREVIOUS $version
 100       */
 101      abstract public function get( string $name, string $version, bool $network = false );
 102  
 103      /**
 104       * Stores a secret.
 105       *
 106       * A provider whose credentials are managed elsewhere, by a control panel, host
 107       * tooling, or a KMS with its own access policy, returns WP_Error here with code
 108       * WP_SECRETS_ERROR_PROVIDER_READ_ONLY, and reports false from is_writable() so
 109       * that callers can find that out without attempting the write first.
 110       *
 111       * Implementations are responsible for firing the `wp_secret_changed` action once
 112       * the write has succeeded, with `$network` as its last argument. The API does
 113       * not fire it on the provider's behalf, because only the provider knows the
 114       * prior fingerprint without paying for an additional read. An audit hook that
 115       * stopped firing once a host installed a provider would be unreliable in exactly
 116       * the situation it matters most, so WP_Secrets_Provider_Conformance checks it.
 117       * A write that fails fires nothing.
 118       *
 119       * Writing the value a secret already holds is not a rotation. A settings form
 120       * that saves every field on every submit does this routinely, so a provider
 121       * that keeps a previous version must not discard it for an unchanged write;
 122       * doing so would end a rotation's grace period early, with every consumer
 123       * still on the old credential losing access. WP_Secrets_Provider_Conformance
 124       * checks this. Treating the write as a no-op that fires nothing is preferred,
 125       * but a provider that cannot tell the value is unchanged without an additional
 126       * read may write it and report it.
 127       *
 128       * @since 7.2.0
 129       *
 130       * @param string      $name           The secret's name.
 131       * @param string      $value          The plaintext value.
 132       * @param bool        $network        Whether this is a network-scope secret.
 133       * @param bool        $needs_rotation Mark the stored secret as needing rotation.
 134       *                                    Set for values that arrived from somewhere
 135       *                                    less protected than this provider, such as
 136       *                                    an imported option. A provider with nowhere
 137       *                                    to record this may ignore it, but must not
 138       *                                    report it as honored.
 139       * @param string|null $action         Overrides the action reported to
 140       *                                    `wp_secret_changed`; null means the
 141       *                                    provider decides between 'created' and
 142       *                                    'updated'.
 143       * @return true|WP_Error
 144       */
 145      abstract public function set(
 146          string $name,
 147          #[\SensitiveParameter]
 148          $value,
 149          bool $network = false,
 150          bool $needs_rotation = false,
 151          ?string $action = null
 152      );
 153  
 154      /**
 155       * Deletes a secret.
 156       *
 157       * Deleting a secret that does not exist is a success.
 158       *
 159       * Fires `wp_secret_changed` with the 'deleted' action once a secret has been
 160       * deleted, for the reason given on set(). A provider that can tell nothing was
 161       * there to delete fires nothing.
 162       *
 163       * @since 7.2.0
 164       *
 165       * @param string $name    The secret's name.
 166       * @param bool   $network Whether this is a network-scope secret.
 167       * @return true|WP_Error
 168       */
 169      abstract public function delete( string $name, bool $network = false );
 170  
 171      /**
 172       * Clears a secret's previous version, if this provider keeps one.
 173       *
 174       * A provider with no version history treats this as a successful no-op: there
 175       * is nothing to retire, which is the state the caller asked for.
 176       *
 177       * Fires `wp_secret_changed` with the 'retired' action once a previous version
 178       * has been cleared, for the reason given on set(). Nothing fires when there was
 179       * no previous version to clear.
 180       *
 181       * @since 7.2.0
 182       *
 183       * @param string $name    The secret's name.
 184       * @param bool   $network Whether this is a network-scope secret.
 185       * @return true|WP_Error
 186       */
 187      abstract public function retire_previous( string $name, bool $network = false );
 188  
 189      /**
 190       * Lists secrets by name and metadata. Never values.
 191       *
 192       * @since 7.2.0
 193       *
 194       * @param string $name_prefix Restrict to names beginning with this prefix, or
 195       *                            '' for all of them.
 196       * @param bool   $network     Whether to list network-scope secrets.
 197       * @return array|WP_Error List of arrays with keys 'name', 'fingerprint',
 198       *                        'created', 'has_previous', 'needs_rotation'.
 199       *
 200       * @phpstan-return list<array{name: string, fingerprint: string, created: int, has_previous: bool, needs_rotation: bool}>|WP_Error
 201       */
 202      abstract public function list_secrets( string $name_prefix = '', bool $network = false );
 203  
 204      /**
 205       * A short human-readable name for this provider, for Site Health.
 206       *
 207       * Describes the protection, not the vendor's marketing: "AWS KMS
 208       * (alias/wp-secrets)" tells an operator something, "SuperSecure Pro" does not.
 209       * Never key material, never a credential, never a value.
 210       *
 211       * @since 7.2.0
 212       *
 213       * @return string
 214       */
 215      abstract public function get_label(): string;
 216  
 217      /**
 218       * Where secrets this provider holds are actually protected.
 219       *
 220       * One of the BOUNDARY_* constants. This is what lets Site Health and a future
 221       * admin screen tell an operator whether their credentials are protected by
 222       * WordPress's own libsodium envelope or by something outside it, a question
 223       * they currently have no way to answer, and the one hosts asked to be able to
 224       * answer honestly.
 225       *
 226       * @since 7.2.0
 227       *
 228       * @return string
 229       *
 230       * @phpstan-return self::BOUNDARY_*
 231       */
 232      abstract public function get_protection_boundary(): string;
 233  
 234      /**
 235       * Whether wp_set_secret() can succeed against this provider.
 236       *
 237       * False for a provider whose credentials are managed by host tooling or a
 238       * control panel. Declared rather than discovered so a settings screen can
 239       * disable its save control before an operator types a credential into a field
 240       * that will only reject it.
 241       *
 242       * @since 7.2.0
 243       *
 244       * @return bool
 245       */
 246      abstract public function is_writable(): bool;
 247  }


Generated : Wed Nov 11 08:20:02 2026 Cross-referenced by PHPXref