[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

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

   1  <?php
   2  /**
   3   * Secrets API: WP_Secret class
   4   *
   5   * @package WordPress
   6   * @subpackage Secrets
   7   * @since 7.2.0
   8   */
   9  
  10  /**
  11   * A decrypted secret value, returned by wp_get_secret().
  12   *
  13   * Masking is total and unconditional. Every representation short of an explicit call
  14   * to reveal() yields the placeholder '[secret:{name}]' – printing, logging, JSON
  15   * encoding, or dumping an instance never exposes the plaintext.
  16   *
  17   * The plaintext is never a declared property of this class. It lives in a private
  18   * static table keyed by spl_object_id(), which is what makes the masking unconditional
  19   * rather than something every magic method has to individually get right: var_export()
  20   * in particular ignores __debugInfo() and __toString() and serializes declared
  21   * properties directly, so if the plaintext lived in one there would be no way to mask
  22   * it there at all.
  23   *
  24   * @since 7.2.0
  25   */
  26  final class WP_Secret implements JsonSerializable {
  27  
  28      /**
  29       * Plaintext values, keyed by the owning instance's spl_object_id().
  30       *
  31       * Deliberately not an SplObjectStorage keyed by the instance itself: that holds a
  32       * strong reference to its keys, which would keep every WP_Secret ever constructed
  33       * alive – and its plaintext un-zeroed – for the life of the request. Keying by
  34       * the integer id instead leaves an instance's own reference count to govern its
  35       * lifetime normally, so __destruct() fires and zeroes the value the moment nothing
  36       * else references the instance.
  37       *
  38       * @since 7.2.0
  39       * @var array<int, string>
  40       */
  41      private static $vault = array();
  42  
  43      /**
  44       * The secret's namespaced name.
  45       *
  46       * @since 7.2.0
  47       * @var string
  48       */
  49      private $name;
  50  
  51      /**
  52       * Keyed fingerprint of the plaintext.
  53       *
  54       * Not sensitive. It is a one-way, per-site, per-key digest rather than the value
  55       * itself.
  56       *
  57       * @since 7.2.0
  58       * @var string
  59       */
  60      private $fingerprint;
  61  
  62      /**
  63       * Why this secret's value is unavailable to PHP, or null when it is available.
  64       *
  65       * Set only for a secret a provider can prove exists but will not hand over –
  66       * an HSM that will sign with a key but never export it, a broker that releases
  67       * a credential to a subsystem rather than to WordPress. Such a secret is still
  68       * a real secret with a real name and fingerprint; what it is not is readable,
  69       * and reveal() says so rather than returning something misleading.
  70       *
  71       * @since 7.2.0
  72       * @var WP_Error|null
  73       */
  74      private $withheld_reason = null;
  75  
  76      /**
  77       * Constructor.
  78       *
  79       * Throws, where the rest of this API returns WP_Error for a caller mistake.
  80       * That is not an inconsistency to be tidied up later: a constructor has no
  81       * return channel, so the only alternatives are throwing or building a
  82       * half-valid WP_Secret and letting it fail somewhere less obvious. Every
  83       * function that *can* return WP_Error does – see
  84       * this file's own reasoning below. The same rule covers the serialization
  85       * and clone refusals below, which are magic methods with the same problem
  86       * and an additional one: silently permitting them would leak a plaintext.
  87       *
  88       * Nothing outside this API constructs a WP_Secret in normal use; the values
  89       * passed here come from _wp_secrets_get() having just decrypted them.
  90       *
  91       * @since 7.2.0
  92       *
  93       * @param string $name        The secret's namespaced name.
  94       * @param string $value       The decrypted plaintext.
  95       * @param string $fingerprint Keyed fingerprint of $value.
  96       *
  97       * @throws InvalidArgumentException If $name or $fingerprint is empty, or if $value
  98       *                                  is not a string. $value may be empty.
  99       */
 100  	public function __construct( string $name, $value, string $fingerprint ) {
 101          if ( '' === $name ) {
 102              throw new InvalidArgumentException( 'WP_Secret requires a non-empty name.' );
 103          }
 104  
 105          if ( ! is_string( $value ) ) {
 106              throw new InvalidArgumentException( 'WP_Secret values must be strings.' );
 107          }
 108  
 109          if ( '' === $fingerprint ) {
 110              throw new InvalidArgumentException( 'WP_Secret requires a non-empty fingerprint.' );
 111          }
 112  
 113          $this->name        = $name;
 114          $this->fingerprint = $fingerprint;
 115  
 116          self::$vault[ spl_object_id( $this ) ] = $value;
 117      }
 118  
 119      /**
 120       * Returns the decrypted plaintext.
 121       *
 122       * This is the only path to the stored plaintext anywhere in the API.
 123       *
 124       * Returns WP_Error for a secret whose provider will not release the value to
 125       * PHP at all – see withheld(). That case does not arise for the provider
 126       * WordPress ships, which decrypts eagerly and therefore always has a value in
 127       * hand, so most callers will never see it. It is in the signature because a
 128       * return type cannot be widened after adoption: leaving reveal() as
 129       * `string` would permanently foreclose brokered and use-only credentials,
 130       * which is a decision better made deliberately than by omission.
 131       *
 132       * @since 7.2.0
 133       *
 134       * @return string|WP_Error The plaintext, or WP_Error if this provider does not
 135       *                         release values to PHP.
 136       */
 137  	public function reveal() {
 138          if ( null !== $this->withheld_reason ) {
 139              return $this->withheld_reason;
 140          }
 141  
 142          return self::$vault[ spl_object_id( $this ) ];
 143      }
 144  
 145      /**
 146       * Builds a secret whose value exists but is not available to PHP.
 147       *
 148       * For a provider that can prove a credential exists, and can name and
 149       * fingerprint it, but will not release it – typically because releasing it
 150       * would defeat the point of where it is held. Everything except reveal()
 151       * behaves normally, so such a secret still lists, still reports a stable
 152       * fingerprint, and still masks itself in every output path.
 153       *
 154       * @since 7.2.0
 155       *
 156       * @param string   $name        The secret's name.
 157       * @param string   $fingerprint A stable per-site digest, from the provider.
 158       * @param WP_Error $reason      Why the value cannot be produced. Returned
 159       *                              verbatim by reveal().
 160       *
 161       * @throws InvalidArgumentException If $name or $fingerprint fail the constructor's
 162       *                                  checks.
 163       *
 164       * @return WP_Secret
 165       */
 166  	public static function withheld( string $name, string $fingerprint, WP_Error $reason ): WP_Secret {
 167          $secret = new self( $name, '', $fingerprint );
 168  
 169          $secret->withheld_reason = $reason;
 170  
 171          return $secret;
 172      }
 173  
 174      /**
 175       * Returns the keyed fingerprint of the plaintext.
 176       *
 177       * Stable for a given value on a given site; not a value-recovery oracle.
 178       *
 179       * @since 7.2.0
 180       *
 181       * @return string
 182       */
 183  	public function fingerprint(): string {
 184          return $this->fingerprint;
 185      }
 186  
 187      /**
 188       * Returns the secret's namespaced name.
 189       *
 190       * @since 7.2.0
 191       *
 192       * @return string
 193       */
 194  	public function get_name(): string {
 195          return $this->name;
 196      }
 197  
 198      /**
 199       * Builds the masked placeholder used everywhere a plaintext would otherwise appear.
 200       *
 201       * @since 7.2.0
 202       *
 203       * @return string
 204       *
 205       * @phpstan-return non-falsy-string
 206       */
 207  	private function mask(): string {
 208          return '[secret:' . $this->name . ']';
 209      }
 210  
 211      /**
 212       * Masks the instance for string conversion.
 213       *
 214       * Covers error_log( $secret ) and any other implicit string coercion.
 215       *
 216       * @since 7.2.0
 217       *
 218       * @return string
 219       *
 220       * @phpstan-return non-falsy-string
 221       */
 222  	public function __toString(): string {
 223          return $this->mask();
 224      }
 225  
 226      /**
 227       * Masks the instance for var_dump().
 228       *
 229       * Note that print_r() and var_export() have no equivalent hook. They are safe
 230       * regardless, because the plaintext is never a declared property for them to
 231       * enumerate; they will surface the (non-sensitive) name and fingerprint rather
 232       * than this placeholder. See docs/journal/open-questions.md for the documented
 233       * var_export() limitation.
 234       *
 235       * @since 7.2.0
 236       *
 237       * @return array<string, string>
 238       */
 239  	public function __debugInfo(): array {
 240          return array( 'value' => $this->mask() );
 241      }
 242  
 243      /**
 244       * Masks the instance for json_encode().
 245       *
 246       * @since 7.2.0
 247       *
 248       * @return string
 249       *
 250       * @phpstan-return non-falsy-string
 251       */
 252  	public function jsonSerialize(): string {
 253          return $this->mask();
 254      }
 255  
 256      /**
 257       * Refuses serialization outright.
 258       *
 259       * @since 7.2.0
 260       *
 261       * @throws LogicException Always.
 262       *
 263       * @return void
 264       */
 265  	public function __sleep() {
 266          throw new LogicException( 'WP_Secret cannot be serialized.' );
 267      }
 268  
 269      /**
 270       * Refuses unserialization outright.
 271       *
 272       * @since 7.2.0
 273       *
 274       * @throws LogicException Always.
 275       *
 276       * @return void
 277       */
 278  	public function __wakeup() {
 279          throw new LogicException( 'WP_Secret cannot be unserialized.' );
 280      }
 281  
 282      /**
 283       * Refuses serialization outright.
 284       *
 285       * PHP calls this in preference to __sleep() when both are defined, so both must
 286       * refuse independently.
 287       *
 288       * @since 7.2.0
 289       *
 290       * @throws LogicException Always.
 291       *
 292       * @return void
 293       */
 294  	public function __serialize() {
 295          throw new LogicException( 'WP_Secret cannot be serialized.' );
 296      }
 297  
 298      /**
 299       * Refuses unserialization outright.
 300       *
 301       * @since 7.2.0
 302       *
 303       * @param array<mixed> $data Ignored.
 304       *
 305       * @throws LogicException Always.
 306       *
 307       * @return void
 308       */
 309  	public function __unserialize( array $data ): void {
 310          throw new LogicException( 'WP_Secret cannot be unserialized.' );
 311      }
 312  
 313      /**
 314       * Refuses cloning outright.
 315       *
 316       * A clone would be a second, unaudited reference to the same plaintext with an
 317       * independent lifetime – and, since the vault is keyed by object id rather than
 318       * copied, a bare `clone` would otherwise leave the clone's reveal() silently
 319       * reading nothing.
 320       *
 321       * @since 7.2.0
 322       *
 323       * @throws LogicException Always.
 324       */
 325  	public function __clone() {
 326          throw new LogicException( 'WP_Secret cannot be cloned.' );
 327      }
 328  
 329      /**
 330       * Zeroes the plaintext and removes it from the vault.
 331       *
 332       * @since 7.2.0
 333       */
 334  	public function __destruct() {
 335          $id = spl_object_id( $this );
 336  
 337          if ( isset( self::$vault[ $id ] ) ) {
 338              wp_secrets_memzero( self::$vault[ $id ] );
 339              unset( self::$vault[ $id ] );
 340          }
 341      }
 342  }


Generated : Wed Oct 14 08:20:33 2026 Cross-referenced by PHPXref