PluginProbe ʕ •ᴥ•ʔ
WP 2FA – Two-factor authentication for WordPress / 2.5.0
WP 2FA – Two-factor authentication for WordPress v2.5.0
4.0.0 1.7.1 2.0.0 2.0.1 2.1.0 2.2.0 2.2.1 2.3.0 2.4.0 2.4.1 2.4.2 2.5.0 2.6.0 2.6.1 2.6.2 2.6.3 2.6.4 2.7.0 2.8.0 2.9.0 2.9.1 2.9.2 2.9.3 3.0.0 3.0.1 3.1.0 3.1.1 3.1.1.2 trunk 1.2.0 1.3.0 1.4.0 1.4.1 1.4.2 1.5.0 1.5.1 1.5.2 1.6.0 1.6.1 1.6.2 1.7.0
wp-2fa / includes / classes / Admin / Methods / class-backup-codes.php
wp-2fa / includes / classes / Admin / Methods Last commit date
Traits 2 years ago class-backup-codes.php 2 years ago index.php 2 years ago
class-backup-codes.php
284 lines
1 <?php
2 /**
3 * Responsible for WP2FA user's backup codes manipulation.
4 *
5 * @package wp2fa
6 * @subpackage backup-codes
7 *
8 * @copyright %%YEAR%% Melapress
9 * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
10 *
11 * @see https://wordpress.org/plugins/wp-2fa/
12 */
13
14 declare(strict_types=1);
15
16 namespace WP2FA\Methods;
17
18 use WP2FA\Admin\Settings_Page;
19 use WP2FA\Admin\Helpers\User_Helper;
20 use WP2FA\Admin\Controllers\Settings;
21 use WP2FA\Authenticator\Authentication;
22 use WP2FA\Admin\Methods\Traits\Login_Attempts;
23
24 /**
25 * Class for handling backup codes.
26 *
27 * @since 0.1-dev
28 *
29 * @package WP2FA
30 */
31 if ( ! class_exists( '\WP2FA\Methods\Backup_Codes' ) ) {
32 /**
33 * Backup code class, for handling backup code generation and such.
34 */
35 class Backup_Codes {
36
37 use Login_Attempts;
38
39 /**
40 * Holds the name of the meta key for the allowed login attempts.
41 *
42 * @var string
43 *
44 * @since 2.0.0
45 */
46 private static $logging_attempts_meta_key = WP_2FA_PREFIX . 'backup-login-attempts';
47
48 /**
49 * Key used for backup codes.
50 *
51 * @var string
52 */
53 public const BACKUP_CODES_META_KEY = 'wp_2fa_backup_codes';
54
55 /**
56 * The number backup codes.
57 *
58 * @var int
59 */
60 public const NUMBER_OF_CODES = 10;
61
62 /**
63 * The name of the method.
64 *
65 * @var string
66 *
67 * @since 2.0.0
68 */
69 public static $method_name = 'backup_codes';
70
71 /**
72 * The login attempts class.
73 *
74 * @var \WP2FA\Admin\Controllers\Login_Attempts
75 *
76 * @since 2.0.0
77 */
78 private static $login_attempts = null;
79
80 /**
81 * Lets build!
82 */
83 public static function init() {
84 \add_filter( WP_2FA_PREFIX . 'backup_methods_list', array( __CLASS__, 'add_backup_method' ), 10, 2 );
85 \add_filter( WP_2FA_PREFIX . 'backup_methods_enabled', array( __CLASS__, 'check_backup_method' ), 10, 2 );
86 \add_action( 'wp_ajax_wp2fa_run_ajax_generate_json', array( __CLASS__, 'run_ajax_generate_json' ) );
87
88 \add_action( WP_2FA_PREFIX . 'remove_backup_methods_for_user', array( __CLASS__, 'remove_backup_methods_for_user' ) );
89 }
90
91 /**
92 * Generate backup codes.
93 *
94 * @param object $user User data.
95 * @param string $args possible args.
96 */
97 public static function generate_codes( $user, $args = '' ) {
98 $codes = array();
99 $codes_hashed = array();
100
101 // Check for arguments.
102 if ( isset( $args['number'] ) ) {
103 $num_codes = (int) $args['number'];
104 } else {
105 $num_codes = self::NUMBER_OF_CODES;
106 }
107
108 // Append or replace (default).
109 if ( isset( $args['method'] ) && 'append' === $args['method'] ) {
110 $codes_hashed = (array) get_user_meta( $user->ID, self::BACKUP_CODES_META_KEY, true );
111 }
112
113 for ( $i = 0; $i < $num_codes; ++$i ) {
114 $code = Authentication::get_code();
115 $codes_hashed[] = wp_hash_password( $code );
116 $codes[] = $code;
117 unset( $code );
118 }
119
120 update_user_meta( $user->ID, self::BACKUP_CODES_META_KEY, $codes_hashed );
121
122 // Unhashed.
123 return $codes;
124 }
125
126 /**
127 * Removes the backup method (user meta key) from the database.
128 *
129 * @param \WP_User,int,null $user - The user to remove method for.
130 *
131 * @return void
132 *
133 * @since 2.5.0
134 */
135 public static function remove_backup_methods_for_user( $user ) {
136 if ( ! Settings::is_provider_enabled_for_role( User_Helper::get_user_role( $user ), self::get_method_name() ) ) {
137 \delete_user_meta( $user->ID, self::BACKUP_CODES_META_KEY );
138 }
139 }
140
141 /**
142 * Generate codes and check remaining amount for user.
143 */
144 public static function run_ajax_generate_json() {
145 $user = wp_get_current_user();
146
147 check_ajax_referer( 'wp-2fa-backup-codes-generate-json-' . $user->ID, 'nonce' );
148
149 // Setup the return data.
150 $codes = self::generate_codes( $user );
151
152 $count = self::codes_remaining_for_user( $user );
153 $i18n = array(
154 'count' => esc_html(
155 sprintf(
156 /* translators: %s: count */
157 _n( '%s unused code remaining.', '%s unused codes remaining.', $count, 'wp-2fa' ),
158 $count
159 )
160 ),
161 /* translators: %s: the site's domain */
162 'title' => esc_html__( 'Two-Factor Backup Codes for %s', 'wp-2fa' ),
163 );
164
165 // Send the response.
166 wp_send_json_success(
167 array(
168 'codes' => $codes,
169 'i18n' => $i18n,
170 )
171 );
172 }
173
174 /**
175 * Grab number of unused backup codes within the users position.
176 *
177 * @param object $user User data.
178 *
179 * @return int Count of codes.
180 */
181 public static function codes_remaining_for_user( $user ) {
182 $backup_codes = get_user_meta( $user->ID, self::BACKUP_CODES_META_KEY, true );
183 if ( is_array( $backup_codes ) && ! empty( $backup_codes ) ) {
184 return count( $backup_codes );
185 }
186
187 return 0;
188 }
189
190 /**
191 * Validate backup codes.
192 *
193 * @param object $user User data.
194 * @param string $code The code we are checking.
195 *
196 * @return bool Is is valid or not.
197 */
198 public static function validate_code( $user, $code ) {
199 $backup_codes = get_user_meta( $user->ID, self::BACKUP_CODES_META_KEY, true );
200 if ( is_array( $backup_codes ) && ! empty( $backup_codes ) ) {
201 foreach ( $backup_codes as $code_index => $code_hashed ) {
202 if ( wp_check_password( $code, $code_hashed, $user->ID ) ) {
203 self::delete_code( $user, $code_hashed );
204 self::clear_login_attempts( $user );
205
206 return true;
207 }
208 }
209 }
210 self::increase_login_attempts( $user );
211
212 return false;
213 }
214
215 /**
216 * Delete code once its used.
217 *
218 * @param object $user User data.
219 * @param string $code_hashed Code to delete.
220 */
221 public static function delete_code( $user, $code_hashed ) {
222 $backup_codes = get_user_meta( $user->ID, self::BACKUP_CODES_META_KEY, true );
223
224 // Delete the current code from the list since it's been used.
225 $backup_codes = array_flip( $backup_codes );
226 unset( $backup_codes[ $code_hashed ] );
227 $backup_codes = array_values( array_flip( $backup_codes ) );
228
229 // Update the backup code master list.
230 update_user_meta( $user->ID, self::BACKUP_CODES_META_KEY, $backup_codes );
231 }
232
233 /**
234 * Add the method to the existing backup methods array.
235 *
236 * @param array $backup_methods - Array with the currently supported backup methods.
237 *
238 * @since 2.0.0
239 */
240 public static function add_backup_method( array $backup_methods ): array {
241 return array_merge(
242 $backup_methods,
243 array(
244 self::$method_name => array(
245 'wizard-step' => '2fa-wizard-config-backup-codes',
246 'button_name' => sprintf(
247 /* translators: URL with more information about the backup codes */
248 esc_html__( 'Login with a backup code: you will get 10 backup codes and you can use one of them when you need to login and you cannot generate a code from the app. %s', 'wp-2fa' ),
249 '<a href="https://melapress.com/2fa-backup-codes/" target="_blank">' . esc_html__( 'More information.', 'wp-2fa' ) . '</a>'
250 ),
251 ),
252 )
253 );
254 }
255
256 /**
257 * Changes the global backup methods array - removes the method if it is not enabled.
258 *
259 * @param array $backup_methods - Array with all global backup methods.
260 * @param \WP_User $user - User to check for is that method enabled.
261 *
262 * @since 2.0.0
263 */
264 public static function check_backup_method( array $backup_methods, \WP_User $user ): array {
265 $enabled = Settings_Page::are_backup_codes_enabled( User_Helper::get_user_role( $user ) );
266
267 if ( ! $enabled ) {
268 unset( $backup_methods[ self::$method_name ] );
269 }
270
271 return $backup_methods;
272 }
273
274 /**
275 * Returns the name of the method.
276 *
277 * @since 2.0.0
278 */
279 public static function get_method_name(): string {
280 return self::$method_name;
281 }
282 }
283 }
284