class-wp-recovery-mode-key-service.php 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. <?php
  2. /**
  3. * Error Protection API: WP_Recovery_Mode_Key_Service class
  4. *
  5. * @package WordPress
  6. * @since 5.2.0
  7. */
  8. /**
  9. * Core class used to generate and validate keys used to enter Recovery Mode.
  10. *
  11. * @since 5.2.0
  12. */
  13. #[AllowDynamicProperties]
  14. final class WP_Recovery_Mode_Key_Service {
  15. /**
  16. * The option name used to store the keys.
  17. *
  18. * @since 5.2.0
  19. * @var string
  20. */
  21. private $option_name = 'recovery_keys';
  22. /**
  23. * Creates a recovery mode token.
  24. *
  25. * @since 5.2.0
  26. *
  27. * @return string A random string to identify its associated key in storage.
  28. */
  29. public function generate_recovery_mode_token() {
  30. return wp_generate_password( 22, false );
  31. }
  32. /**
  33. * Creates a recovery mode key.
  34. *
  35. * @since 5.2.0
  36. *
  37. * @global PasswordHash $wp_hasher
  38. *
  39. * @param string $token A token generated by {@see generate_recovery_mode_token()}.
  40. * @return string Recovery mode key.
  41. */
  42. public function generate_and_store_recovery_mode_key( $token ) {
  43. global $wp_hasher;
  44. $key = wp_generate_password( 22, false );
  45. if ( empty( $wp_hasher ) ) {
  46. require_once ABSPATH . WPINC . '/class-phpass.php';
  47. $wp_hasher = new PasswordHash( 8, true );
  48. }
  49. $hashed = $wp_hasher->HashPassword( $key );
  50. $records = $this->get_keys();
  51. $records[ $token ] = array(
  52. 'hashed_key' => $hashed,
  53. 'created_at' => time(),
  54. );
  55. $this->update_keys( $records );
  56. /**
  57. * Fires when a recovery mode key is generated.
  58. *
  59. * @since 5.2.0
  60. *
  61. * @param string $token The recovery data token.
  62. * @param string $key The recovery mode key.
  63. */
  64. do_action( 'generate_recovery_mode_key', $token, $key );
  65. return $key;
  66. }
  67. /**
  68. * Verifies if the recovery mode key is correct.
  69. *
  70. * Recovery mode keys can only be used once; the key will be consumed in the process.
  71. *
  72. * @since 5.2.0
  73. *
  74. * @param string $token The token used when generating the given key.
  75. * @param string $key The unhashed key.
  76. * @param int $ttl Time in seconds for the key to be valid for.
  77. * @return true|WP_Error True on success, error object on failure.
  78. */
  79. public function validate_recovery_mode_key( $token, $key, $ttl ) {
  80. $records = $this->get_keys();
  81. if ( ! isset( $records[ $token ] ) ) {
  82. return new WP_Error( 'token_not_found', __( 'Recovery Mode not initialized.' ) );
  83. }
  84. $record = $records[ $token ];
  85. $this->remove_key( $token );
  86. if ( ! is_array( $record ) || ! isset( $record['hashed_key'], $record['created_at'] ) ) {
  87. return new WP_Error( 'invalid_recovery_key_format', __( 'Invalid recovery key format.' ) );
  88. }
  89. if ( ! wp_check_password( $key, $record['hashed_key'] ) ) {
  90. return new WP_Error( 'hash_mismatch', __( 'Invalid recovery key.' ) );
  91. }
  92. if ( time() > $record['created_at'] + $ttl ) {
  93. return new WP_Error( 'key_expired', __( 'Recovery key expired.' ) );
  94. }
  95. return true;
  96. }
  97. /**
  98. * Removes expired recovery mode keys.
  99. *
  100. * @since 5.2.0
  101. *
  102. * @param int $ttl Time in seconds for the keys to be valid for.
  103. */
  104. public function clean_expired_keys( $ttl ) {
  105. $records = $this->get_keys();
  106. foreach ( $records as $key => $record ) {
  107. if ( ! isset( $record['created_at'] ) || time() > $record['created_at'] + $ttl ) {
  108. unset( $records[ $key ] );
  109. }
  110. }
  111. $this->update_keys( $records );
  112. }
  113. /**
  114. * Removes a used recovery key.
  115. *
  116. * @since 5.2.0
  117. *
  118. * @param string $token The token used when generating a recovery mode key.
  119. */
  120. private function remove_key( $token ) {
  121. $records = $this->get_keys();
  122. if ( ! isset( $records[ $token ] ) ) {
  123. return;
  124. }
  125. unset( $records[ $token ] );
  126. $this->update_keys( $records );
  127. }
  128. /**
  129. * Gets the recovery key records.
  130. *
  131. * @since 5.2.0
  132. *
  133. * @return array Associative array of $token => $data pairs, where $data has keys 'hashed_key'
  134. * and 'created_at'.
  135. */
  136. private function get_keys() {
  137. return (array) get_option( $this->option_name, array() );
  138. }
  139. /**
  140. * Updates the recovery key records.
  141. *
  142. * @since 5.2.0
  143. *
  144. * @param array $keys Associative array of $token => $data pairs, where $data has keys 'hashed_key'
  145. * and 'created_at'.
  146. * @return bool True on success, false on failure.
  147. */
  148. private function update_keys( array $keys ) {
  149. return update_option( $this->option_name, $keys );
  150. }
  151. }