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/**
10 * Core class used to generate and validate keys used to enter Recovery Mode.
11 *
12 * @since 5.2.0
13 */
14#[AllowDynamicProperties]
15final class WP_Recovery_Mode_Key_Service {
16
17 /**
18 * The option name used to store the keys.
19 *
20 * @since 5.2.0
21 * @var string
22 */
23 private $option_name = 'recovery_keys';
24
25 /**
26 * Creates a recovery mode token.
27 *
28 * @since 5.2.0
29 *
30 * @return string A random string to identify its associated key in storage.
31 */
32 public function generate_recovery_mode_token() {
33 return wp_generate_password( 22, false );
34 }
35
36 /**
37 * Creates a recovery mode key.
38 *
39 * @since 5.2.0
40 * @since 6.8.0 The stored key is now hashed using wp_fast_hash() instead of phpass.
41 *
42 * @param string $token A token generated by {@see generate_recovery_mode_token()}.
43 * @return string Recovery mode key.
44 */
45 public function generate_and_store_recovery_mode_key( $token ) {
46 $key = wp_generate_password( 22, false );
47
48 $records = $this->get_keys();
49
50 $records[ $token ] = array(
51 'hashed_key' => wp_fast_hash( $key ),
52 'created_at' => time(),
53 );
54
55 $this->update_keys( $records );
56
57 /**
58 * Fires when a recovery mode key is generated.
59 *
60 * @since 5.2.0
61 *
62 * @param string $token The recovery data token.
63 * @param string $key The recovery mode key.
64 */
65 do_action( 'generate_recovery_mode_key', $token, $key );
66
67 return $key;
68 }
69
70 /**
71 * Verifies if the recovery mode key is correct.
72 *
73 * Recovery mode keys can only be used once; the key will be consumed in the process.
74 *
75 * @since 5.2.0
76 *
77 * @param string $token The token used when generating the given key.
78 * @param string $key The plain text key.
79 * @param int $ttl Time in seconds for the key to be valid for.
80 * @return true|WP_Error True on success, error object on failure.
81 */
82 public function validate_recovery_mode_key( $token, $key, $ttl ) {
83 $records = $this->get_keys();
84
85 if ( ! isset( $records[ $token ] ) ) {
86 return new WP_Error( 'token_not_found', __( 'Recovery Mode not initialized.' ) );
87 }
88
89 $record = $records[ $token ];
90
91 $this->remove_key( $token );
92
93 if ( ! is_array( $record ) || ! isset( $record['hashed_key'], $record['created_at'] ) ) {
94 return new WP_Error( 'invalid_recovery_key_format', __( 'Invalid recovery key format.' ) );
95 }
96
97 if ( ! wp_verify_fast_hash( $key, $record['hashed_key'] ) ) {
98 return new WP_Error( 'hash_mismatch', __( 'Invalid recovery key.' ) );
99 }
100
101 if ( time() > $record['created_at'] + $ttl ) {
102 return new WP_Error( 'key_expired', __( 'Recovery key expired.' ) );
103 }
104
105 return true;
106 }
107
108 /**
109 * Removes expired recovery mode keys.
110 *
111 * @since 5.2.0
112 *
113 * @param int $ttl Time in seconds for the keys to be valid for.
114 */
115 public function clean_expired_keys( $ttl ) {
116
117 $records = $this->get_keys();
118
119 foreach ( $records as $key => $record ) {
120 if ( ! isset( $record['created_at'] ) || time() > $record['created_at'] + $ttl ) {
121 unset( $records[ $key ] );
122 }
123 }
124
125 $this->update_keys( $records );
126 }
127
128 /**
129 * Removes a used recovery key.
130 *
131 * @since 5.2.0
132 *
133 * @param string $token The token used when generating a recovery mode key.
134 */
135 private function remove_key( $token ) {
136
137 $records = $this->get_keys();
138
139 if ( ! isset( $records[ $token ] ) ) {
140 return;
141 }
142
143 unset( $records[ $token ] );
144
145 $this->update_keys( $records );
146 }
147
148 /**
149 * Gets the recovery key records.
150 *
151 * @since 5.2.0
152 * @since 6.8.0 Each key is now hashed using wp_fast_hash() instead of phpass.
153 * Existing keys may still be hashed using phpass.
154 *
155 * @return array {
156 * Associative array of token => data pairs, where the data is an associative
157 * array of information about the key.
158 *
159 * @type array ...$0 {
160 * Information about the key.
161 *
162 * @type string $hashed_key The hashed value of the key.
163 * @type int $created_at The timestamp when the key was created.
164 * }
165 * }
166 */
167 private function get_keys() {
168 return (array) get_option( $this->option_name, array() );
169 }
170
171 /**
172 * Updates the recovery key records.
173 *
174 * @since 5.2.0
175 * @since 6.8.0 Each key should now be hashed using wp_fast_hash() instead of phpass.
176 *
177 * @param array $keys {
178 * Associative array of token => data pairs, where the data is an associative
179 * array of information about the key.
180 *
181 * @type array ...$0 {
182 * Information about the key.
183 *
184 * @type string $hashed_key The hashed value of the key.
185 * @type int $created_at The timestamp when the key was created.
186 * }
187 * }
188 * @return bool True on success, false on failure.
189 */
190 private function update_keys( array $keys ) {
191 return update_option( $this->option_name, $keys, false );
192 }
193}
194