| [ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Wed Oct 14 08:20:33 2026 | Cross-referenced by PHPXref |