1<?php
2/**
3 * WP_Application_Passwords class
4 *
5 * @package WordPress
6 * @since 5.6.0
7 */
8
9/**
10 * Class for displaying, modifying, and sanitizing application passwords.
11 *
12 * @package WordPress
13 */
14#[AllowDynamicProperties]
15class WP_Application_Passwords {
16
17 /**
18 * The application passwords user meta key.
19 *
20 * @since 5.6.0
21 *
22 * @var string
23 */
24 const USERMETA_KEY_APPLICATION_PASSWORDS = '_application_passwords';
25
26 /**
27 * The option name used to store whether application passwords are in use.
28 *
29 * @since 5.6.0
30 *
31 * @var string
32 */
33 const OPTION_KEY_IN_USE = 'using_application_passwords';
34
35 /**
36 * The generated application password length.
37 *
38 * @since 5.6.0
39 *
40 * @var int
41 */
42 const PW_LENGTH = 24;
43
44 /**
45 * Checks if application passwords are being used by the site.
46 *
47 * This returns true if at least one application password has ever been created.
48 *
49 * @since 5.6.0
50 *
51 * @return bool
52 */
53 public static function is_in_use() {
54 $network_id = get_main_network_id();
55 return (bool) get_network_option( $network_id, self::OPTION_KEY_IN_USE );
56 }
57
58 /**
59 * Creates a new application password.
60 *
61 * @since 5.6.0
62 * @since 5.7.0 Returns WP_Error if application name already exists.
63 * @since 6.8.0 The hashed password value now uses wp_fast_hash() instead of phpass.
64 *
65 * @param int $user_id User ID.
66 * @param array $args {
67 * Arguments used to create the application password.
68 *
69 * @type string $name The name of the application password.
70 * @type string $app_id A UUID provided by the application to uniquely identify it.
71 * }
72 * @return array|WP_Error {
73 * Application password details, or a WP_Error instance if an error occurs.
74 *
75 * @type string $0 The generated application password in plain text.
76 * @type array $1 {
77 * The details about the created password.
78 *
79 * @type string $uuid The unique identifier for the application password.
80 * @type string $app_id A UUID provided by the application to uniquely identify it.
81 * @type string $name The name of the application password.
82 * @type string $password A one-way hash of the password.
83 * @type int $created Unix timestamp of when the password was created.
84 * @type null $last_used Null.
85 * @type null $last_ip Null.
86 * }
87 * }
88 */
89 public static function create_new_application_password( $user_id, $args = array() ) {
90 if ( ! empty( $args['name'] ) ) {
91 $args['name'] = sanitize_text_field( $args['name'] );
92 }
93
94 if ( empty( $args['name'] ) ) {
95 return new WP_Error( 'application_password_empty_name', __( 'An application name is required to create an application password.' ), array( 'status' => 400 ) );
96 }
97
98 $new_password = wp_generate_password( static::PW_LENGTH, false );
99 $hashed_password = self::hash_password( $new_password );
100
101 $new_item = array(
102 'uuid' => wp_generate_uuid4(),
103 'app_id' => empty( $args['app_id'] ) ? '' : $args['app_id'],
104 'name' => $args['name'],
105 'password' => $hashed_password,
106 'created' => time(),
107 'last_used' => null,
108 'last_ip' => null,
109 );
110
111 $passwords = static::get_user_application_passwords( $user_id );
112 $passwords[] = $new_item;
113 $saved = static::set_user_application_passwords( $user_id, $passwords );
114
115 if ( ! $saved ) {
116 return new WP_Error( 'db_error', __( 'Could not save application password.' ) );
117 }
118
119 $network_id = get_main_network_id();
120 if ( ! get_network_option( $network_id, self::OPTION_KEY_IN_USE ) ) {
121 update_network_option( $network_id, self::OPTION_KEY_IN_USE, true );
122 }
123
124 /**
125 * Fires when an application password is created.
126 *
127 * @since 5.6.0
128 * @since 6.8.0 The hashed password value now uses wp_fast_hash() instead of phpass.
129 *
130 * @param int $user_id The user ID.
131 * @param array $new_item {
132 * The details about the created password.
133 *
134 * @type string $uuid The unique identifier for the application password.
135 * @type string $app_id A UUID provided by the application to uniquely identify it.
136 * @type string $name The name of the application password.
137 * @type string $password A one-way hash of the password.
138 * @type int $created Unix timestamp of when the password was created.
139 * @type null $last_used Null.
140 * @type null $last_ip Null.
141 * }
142 * @param string $new_password The generated application password in plain text.
143 * @param array $args {
144 * Arguments used to create the application password.
145 *
146 * @type string $name The name of the application password.
147 * @type string $app_id A UUID provided by the application to uniquely identify it.
148 * }
149 */
150 do_action( 'wp_create_application_password', $user_id, $new_item, $new_password, $args );
151
152 return array( $new_password, $new_item );
153 }
154
155 /**
156 * Gets a user's application passwords.
157 *
158 * @since 5.6.0
159 *
160 * @param int $user_id User ID.
161 * @return array {
162 * The list of application passwords.
163 *
164 * @type array ...$0 {
165 * @type string $uuid The unique identifier for the application password.
166 * @type string $app_id A UUID provided by the application to uniquely identify it.
167 * @type string $name The name of the application password.
168 * @type string $password A one-way hash of the password.
169 * @type int $created Unix timestamp of when the password was created.
170 * @type int|null $last_used The Unix timestamp of the GMT date the application password was last used.
171 * @type string|null $last_ip The IP address the application password was last used by.
172 * }
173 * }
174 */
175 public static function get_user_application_passwords( $user_id ) {
176 $passwords = get_user_meta( $user_id, static::USERMETA_KEY_APPLICATION_PASSWORDS, true );
177
178 if ( ! is_array( $passwords ) ) {
179 return array();
180 }
181
182 $save = false;
183
184 foreach ( $passwords as $i => $password ) {
185 if ( ! isset( $password['uuid'] ) ) {
186 $passwords[ $i ]['uuid'] = wp_generate_uuid4();
187 $save = true;
188 }
189 }
190
191 if ( $save ) {
192 static::set_user_application_passwords( $user_id, $passwords );
193 }
194
195 return $passwords;
196 }
197
198 /**
199 * Gets a user's application password with the given UUID.
200 *
201 * @since 5.6.0
202 *
203 * @param int $user_id User ID.
204 * @param string $uuid The password's UUID.
205 * @return array|null {
206 * The application password if found, null otherwise.
207 *
208 * @type string $uuid The unique identifier for the application password.
209 * @type string $app_id A UUID provided by the application to uniquely identify it.
210 * @type string $name The name of the application password.
211 * @type string $password A one-way hash of the password.
212 * @type int $created Unix timestamp of when the password was created.
213 * @type int|null $last_used The Unix timestamp of the GMT date the application password was last used.
214 * @type string|null $last_ip The IP address the application password was last used by.
215 * }
216 */
217 public static function get_user_application_password( $user_id, $uuid ) {
218 $passwords = static::get_user_application_passwords( $user_id );
219
220 foreach ( $passwords as $password ) {
221 if ( $password['uuid'] === $uuid ) {
222 return $password;
223 }
224 }
225
226 return null;
227 }
228
229 /**
230 * Checks if an application password with the given name exists for this user.
231 *
232 * @since 5.7.0
233 *
234 * @param int $user_id User ID.
235 * @param string $name Application name.
236 * @return bool Whether the provided application name exists.
237 */
238 public static function application_name_exists_for_user( $user_id, $name ) {
239 $passwords = static::get_user_application_passwords( $user_id );
240
241 foreach ( $passwords as $password ) {
242 if ( strtolower( $password['name'] ) === strtolower( $name ) ) {
243 return true;
244 }
245 }
246
247 return false;
248 }
249
250 /**
251 * Updates an application password.
252 *
253 * @since 5.6.0
254 * @since 6.8.0 The actual password should now be hashed using wp_fast_hash().
255 *
256 * @param int $user_id User ID.
257 * @param string $uuid The password's UUID.
258 * @param array $update {
259 * Information about the application password to update.
260 *
261 * @type string $uuid The unique identifier for the application password.
262 * @type string $app_id A UUID provided by the application to uniquely identify it.
263 * @type string $name The name of the application password.
264 * @type string $password A one-way hash of the password.
265 * @type int $created Unix timestamp of when the password was created.
266 * @type int|null $last_used The Unix timestamp of the GMT date the application password was last used.
267 * @type string|null $last_ip The IP address the application password was last used by.
268 * }
269 * @return true|WP_Error True if successful, otherwise a WP_Error instance is returned on error.
270 */
271 public static function update_application_password( $user_id, $uuid, $update = array() ) {
272 $passwords = static::get_user_application_passwords( $user_id );
273
274 foreach ( $passwords as &$item ) {
275 if ( $item['uuid'] !== $uuid ) {
276 continue;
277 }
278
279 if ( ! empty( $update['name'] ) ) {
280 $update['name'] = sanitize_text_field( $update['name'] );
281 }
282
283 $save = false;
284
285 if ( ! empty( $update['name'] ) && $item['name'] !== $update['name'] ) {
286 $item['name'] = $update['name'];
287 $save = true;
288 }
289
290 if ( $save ) {
291 $saved = static::set_user_application_passwords( $user_id, $passwords );
292
293 if ( ! $saved ) {
294 return new WP_Error( 'db_error', __( 'Could not save application password.' ) );
295 }
296 }
297
298 /**
299 * Fires when an application password is updated.
300 *
301 * @since 5.6.0
302 * @since 6.8.0 The password is now hashed using wp_fast_hash() instead of phpass.
303 * Existing passwords may still be hashed using phpass.
304 *
305 * @param int $user_id The user ID.
306 * @param array $item {
307 * The updated application password details.
308 *
309 * @type string $uuid The unique identifier for the application password.
310 * @type string $app_id A UUID provided by the application to uniquely identify it.
311 * @type string $name The name of the application password.
312 * @type string $password A one-way hash of the password.
313 * @type int $created Unix timestamp of when the password was created.
314 * @type int|null $last_used The Unix timestamp of the GMT date the application password was last used.
315 * @type string|null $last_ip The IP address the application password was last used by.
316 * }
317 * @param array $update The information to update.
318 */
319 do_action( 'wp_update_application_password', $user_id, $item, $update );
320
321 return true;
322 }
323
324 return new WP_Error( 'application_password_not_found', __( 'Could not find an application password with that id.' ) );
325 }
326
327 /**
328 * Records that an application password has been used.
329 *
330 * @since 5.6.0
331 *
332 * @param int $user_id User ID.
333 * @param string $uuid The password's UUID.
334 * @return true|WP_Error True if the usage was recorded, a WP_Error if an error occurs.
335 */
336 public static function record_application_password_usage( $user_id, $uuid ) {
337 $passwords = static::get_user_application_passwords( $user_id );
338
339 foreach ( $passwords as &$password ) {
340 if ( $password['uuid'] !== $uuid ) {
341 continue;
342 }
343
344 // Only record activity once a day.
345 if ( $password['last_used'] + DAY_IN_SECONDS > time() ) {
346 return true;
347 }
348
349 $password['last_used'] = time();
350 $password['last_ip'] = $_SERVER['REMOTE_ADDR'];
351
352 $saved = static::set_user_application_passwords( $user_id, $passwords );
353
354 if ( ! $saved ) {
355 return new WP_Error( 'db_error', __( 'Could not save application password.' ) );
356 }
357
358 return true;
359 }
360
361 // Specified application password not found!
362 return new WP_Error( 'application_password_not_found', __( 'Could not find an application password with that id.' ) );
363 }
364
365 /**
366 * Deletes an application password.
367 *
368 * @since 5.6.0
369 *
370 * @param int $user_id User ID.
371 * @param string $uuid The password's UUID.
372 * @return true|WP_Error Whether the password was successfully found and deleted, a WP_Error otherwise.
373 */
374 public static function delete_application_password( $user_id, $uuid ) {
375 $passwords = static::get_user_application_passwords( $user_id );
376
377 foreach ( $passwords as $key => $item ) {
378 if ( $item['uuid'] === $uuid ) {
379 unset( $passwords[ $key ] );
380 $saved = static::set_user_application_passwords( $user_id, $passwords );
381
382 if ( ! $saved ) {
383 return new WP_Error( 'db_error', __( 'Could not delete application password.' ) );
384 }
385
386 /**
387 * Fires when an application password is deleted.
388 *
389 * @since 5.6.0
390 *
391 * @param int $user_id The user ID.
392 * @param array $item The data about the application password.
393 */
394 do_action( 'wp_delete_application_password', $user_id, $item );
395
396 return true;
397 }
398 }
399
400 return new WP_Error( 'application_password_not_found', __( 'Could not find an application password with that id.' ) );
401 }
402
403 /**
404 * Deletes all application passwords for the given user.
405 *
406 * @since 5.6.0
407 *
408 * @param int $user_id User ID.
409 * @return int|WP_Error The number of passwords that were deleted or a WP_Error on failure.
410 */
411 public static function delete_all_application_passwords( $user_id ) {
412 $passwords = static::get_user_application_passwords( $user_id );
413
414 if ( $passwords ) {
415 $saved = static::set_user_application_passwords( $user_id, array() );
416
417 if ( ! $saved ) {
418 return new WP_Error( 'db_error', __( 'Could not delete application passwords.' ) );
419 }
420
421 foreach ( $passwords as $item ) {
422 /** This action is documented in wp-includes/class-wp-application-passwords.php */
423 do_action( 'wp_delete_application_password', $user_id, $item );
424 }
425
426 return count( $passwords );
427 }
428
429 return 0;
430 }
431
432 /**
433 * Sets a user's application passwords.
434 *
435 * @since 5.6.0
436 *
437 * @param int $user_id User ID.
438 * @param array $passwords {
439 * The list of application passwords.
440 *
441 * @type array ...$0 {
442 * @type string $uuid The unique identifier for the application password.
443 * @type string $app_id A UUID provided by the application to uniquely identify it.
444 * @type string $name The name of the application password.
445 * @type string $password A one-way hash of the password.
446 * @type int $created Unix timestamp of when the password was created.
447 * @type int|null $last_used The Unix timestamp of the GMT date the application password was last used.
448 * @type string|null $last_ip The IP address the application password was last used by.
449 * }
450 * }
451 * @return int|bool User meta ID if the key didn't exist (ie. this is the first time that an application password
452 * has been saved for the user), true on successful update, false on failure or if the value passed
453 * is the same as the one that is already in the database.
454 */
455 protected static function set_user_application_passwords( $user_id, $passwords ) {
456 return update_user_meta( $user_id, static::USERMETA_KEY_APPLICATION_PASSWORDS, $passwords );
457 }
458
459 /**
460 * Sanitizes and then splits a password into smaller chunks.
461 *
462 * @since 5.6.0
463 *
464 * @param string $raw_password The raw application password.
465 * @return string The chunked password.
466 */
467 public static function chunk_password(
468 #[\SensitiveParameter]
469 $raw_password
470 ) {
471 $raw_password = preg_replace( '/[^a-z\d]/i', '', $raw_password );
472
473 return trim( chunk_split( $raw_password, 4, ' ' ) );
474 }
475
476 /**
477 * Hashes a plaintext application password.
478 *
479 * @since 6.8.0
480 *
481 * @param string $password Plaintext password.
482 * @return string Hashed password.
483 */
484 public static function hash_password(
485 #[\SensitiveParameter]
486 string $password
487 ): string {
488 return wp_fast_hash( $password );
489 }
490
491 /**
492 * Checks a plaintext application password against a hashed password.
493 *
494 * @since 6.8.0
495 *
496 * @param string $password Plaintext password.
497 * @param string $hash Hash of the password to check against.
498 * @return bool Whether the password matches the hashed password.
499 */
500 public static function check_password(
501 #[\SensitiveParameter]
502 string $password,
503 string $hash
504 ): bool {
505 if ( ! str_starts_with( $hash, '$generic$' ) ) {
506 /*
507 * If the hash doesn't start with `$generic$`, it is a hash created with `wp_hash_password()`.
508 * This is the case for application passwords created before 6.8.0.
509 */
510 return wp_check_password( $password, $hash );
511 }
512
513 return wp_verify_fast_hash( $password, $hash );
514 }
515}
516