PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 3.0.1
Defender Security – Malware Scanner, Login Security & Firewall v3.0.1
6.3.0 6.2.3 6.2.4 6.2.0 6.2.1 6.2.2 6.1.0 5.3.1 5.4.0 5.4.1 5.5.0 5.5.1 5.6.0 5.6.1 5.6.2 5.7.0 5.7.1 5.7.2 5.8.0 5.8.1 5.9.0 6.0.0 6.0.1 3.0.1 3.1.0 All 141 releases
defender-security / src / component / two-factor / providers / backup-codes.php

backup-codes.php in Defender Security – Malware Scanner, Login Security & Firewall 3.0.1, at src/component/two-factor/providers/backup-codes.php

338 lines 8.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace WP_Defender\Component\Two_Factor\Providers;
4
5 use WP_Defender\Component\Two_Factor\Two_Factor_Provider;
6
7 /**
8 * Class Backup_Codes
9 * @since 2.8.0
10 * @package WP_Defender\Component\Two_Factor\Providers
11 */
12 class Backup_Codes extends Two_Factor_Provider {
13 static $slug = 'backup-codes';
14
15 /**
16 * The number backup codes.
17 *
18 * @type int
19 */
20 public const BACKUP_CODE_COUNT = 5;
21
22 /**
23 * @type int
24 */
25 public const BACKUP_CODE_SIZE = 8;
26
27 /**
28 * @type string
29 */
30 public const BACKUP_CODE_CHARACTERS = '1234567890';
31
32 /**
33 * The user meta key for backup codes.
34 *
35 * @type string
36 */
37 public const BACKUP_CODE_VALUES = 'wd_2fa_backup_codes';
38
39 /**
40 * @type string
41 */
42 public const BACKUP_CODE_START = 'wd_2fa_backup_codes_is_activated';
43
44 /**
45 * @type string
46 */
47 protected $is_activated;
48
49 /**
50 * @type string
51 */
52 protected $description;
53
54 public function __construct() {
55 add_action( 'admin_notices', array( $this, 'admin_notices' ) );
56 add_action( 'wd_2fa_init_provider_' . self::$slug, array( &$this, 'init_provider' ) );
57 add_action( 'wd_2fa_user_options_' . self::$slug, array( &$this, 'user_options' ) );
58 }
59
60 /**
61 * @param \WP_User $user
62 */
63 public function init_provider( $user ) {
64 $this->is_activated = get_user_meta( $user->ID,self::BACKUP_CODE_START, true );
65 $this->description = empty( $this->is_activated )
66 ? __( 'Generate non-expirable backup codes that can be used to log in once.', 'wpdef' )
67 : __( 'Each backup code can only be used to log in once.', 'wpdef' );
68 }
69
70 /**
71 * Get the name of the provider.
72 *
73 * @return string
74 */
75 public function get_label() {
76 return __( 'Backup Codes', 'wpdef' );
77 }
78
79 /**
80 * @return string
81 */
82 public function get_login_label() {
83 return $this->get_label();
84 }
85
86 /**
87 * @return string
88 */
89 public function get_user_label() {
90 return __( 'Backup codes', 'wpdef' );
91 }
92
93 /**
94 * Get the desc of the provider.
95 *
96 * @return string
97 */
98 public function get_description() {
99 return $this->description;
100 }
101
102 /**
103 * Whether this 2FA provider is configured and codes are available for the user specified.
104 *
105 * @param \WP_User $user
106 *
107 * @return bool
108 */
109 public function is_available_for_user( $user ) {
110 // Does this user have available codes?
111 if ( 0 < self::get_unused_codes_for_user( $user ) ) {
112 return true;
113 }
114
115 return false;
116 }
117
118 public function authentication_form() {
119 ?>
120 <p class="wpdef-2fa-label"><?php echo $this->get_label(); ?></p>
121 <p class="wpdef-2fa-text">
122 <?php echo __( 'Enter one of your recovery codes to log in to your account.', 'wpdef' ); ?>
123 </p>
124 <input type="text" autofocus value="" autocomplete="off" name="backup-codes">
125 <button class="button button-primary float-r" type="submit"><?php _e( "Authenticate", 'wpdef' ) ?></button>
126 <?php
127 }
128
129 /**
130 * Return the number of unused codes for the specified user.
131 *
132 * @param \WP_User $user
133 *
134 * @return int
135 */
136 public static function get_unused_codes_for_user( $user ) {
137 $backup_codes = get_user_meta( $user->ID, self::BACKUP_CODE_VALUES, true );
138 if ( is_array( $backup_codes ) && ! empty( $backup_codes ) ) {
139 return count( $backup_codes );
140 }
141
142 return 0;
143 }
144
145 /**
146 * Generate a random string for an auth code.
147 *
148 * @return string
149 */
150 private static function get_code() {
151 $code = '';
152 $chars = self::BACKUP_CODE_CHARACTERS;
153 for ( $i = 0; $i < self::BACKUP_CODE_SIZE; $i++ ) {
154 $code .= substr( $chars, wp_rand( 0, strlen( $chars ) - 1 ), 1 );
155 }
156
157 return $code;
158 }
159
160 /**
161 * Show an admin notice when backup codes have run out.
162 */
163 public function admin_notices() {
164 $user = wp_get_current_user();
165 if ( ! is_object( $user ) ) {
166 return;
167 }
168 // Is User role from common list checked?
169 if ( empty( array_intersect( $user->roles, $this->get_model()->user_roles ) ) ) {
170 return;
171 }
172 // Return if the provider is not enabled.
173 if ( ! in_array( self::$slug, $this->get_component()->get_enabled_providers_for_user( $user->ID ), true ) ) {
174 return;
175 }
176 // Return if we are not out of codes.
177 if ( $this->is_available_for_user( $user ) ) {
178 return;
179 }
180 ?>
181 <div class="error">
182 <p>
183 <span>
184 <?php
185 echo wp_kses(
186 sprintf(
187 /* translators: %s: URL to regenerate code */
188 __( 'You\'ve used all generated backup codes. Please <a href="%s">generate</a> new codes accordingly.', 'wpdef' ),
189 esc_url( get_edit_user_link( $user->ID ) . '#wpdef-2fa-backup-codes' )
190 ),
191 array( 'a' => array( 'href' => true ) )
192 );
193 ?>
194 <span>
195 </p>
196 </div>
197 <?php
198 }
199
200 /**
201 * Display auth method.
202 *
203 * @param \WP_User $user
204 */
205 public function user_options( $user ) {
206 $cond = $this->get_component()->is_checked_enabled_provider_by_slug( $user, self::$slug )
207 && $this->is_available_for_user( $user );
208 $attr = $cond ? '' : ' disabled';
209 // Check whether codes were generated before or not.
210 if ( empty( $this->is_activated ) ) {
211 $button_text = __( 'Generate Backup Codes', 'wpdef' );
212 $class = 'hidden';
213 $count = 0;
214 $show_notice = 'no';
215 } else {
216 $button_text = __( 'Get New Codes', 'wpdef' );
217 $class = $cond ? '' : 'hidden';
218 $count = self::get_unused_codes_for_user( $user );
219 $show_notice = 0 === $count ? 'yes' : 'no';
220 }
221 $this->get_controller()->render_partial(
222 'two-fa/providers/backup-codes',
223 array(
224 'number_of_codes' => self::display_number_of_codes( $count ),
225 'url' => $this->get_url( 'generate_backup_codes' ),
226 'user' => $user,
227 'attr' => $attr,
228 'class' => $class,
229 'show_notice' => $show_notice,
230 'button_text' => $button_text,
231 )
232 );
233 }
234
235 /**
236 * Generate backup codes.
237 *
238 * @param \WP_User $user
239 * @param array $args
240 *
241 * @return array
242 */
243 public static function generate_codes( $user, $args = '' ) {
244 $codes = array();
245 $codes_hashed = array();
246 // Check for arguments.
247 if ( isset( $args['number'] ) ) {
248 $num_codes = (int) $args['number'];
249 } else {
250 $num_codes = self::BACKUP_CODE_COUNT;
251 }
252 // Add a flag if it haven't existed yet.
253 if ( empty( get_user_meta( $user->ID,self::BACKUP_CODE_START, true ) ) ) {
254 update_user_meta( $user->ID,self::BACKUP_CODE_START, 1 );
255 }
256 // Append or replace (default).
257 if ( isset( $args['method'] ) && 'append' === $args['method'] ) {
258 $codes_hashed = (array) get_user_meta( $user->ID, self::BACKUP_CODE_VALUES, true );
259 }
260
261 for ( $i = 0; $i < $num_codes; $i++ ) {
262 $code = self::get_code();
263 $codes_hashed[] = wp_hash_password( $code );
264 $codes[] = $code;
265 unset( $code );
266 }
267
268 update_user_meta( $user->ID, self::BACKUP_CODE_VALUES, $codes_hashed );
269
270 // Unhashed values.
271 return $codes;
272 }
273
274 /**
275 * Delete codes.
276 *
277 * @param \WP_User $user
278 * @param string $code_hashed
279 */
280 public function delete_code( $user, $code_hashed ) {
281 $backup_codes = get_user_meta( $user->ID, self::BACKUP_CODE_VALUES, true );
282 // Delete the current code from the list since it's been used.
283 $backup_codes = array_flip( $backup_codes );
284 unset( $backup_codes[ $code_hashed ] );
285 $backup_codes = array_values( array_flip( $backup_codes ) );
286 // Update the backup code master list.
287 update_user_meta( $user->ID, self::BACKUP_CODE_VALUES, $backup_codes );
288 }
289
290 /**
291 * @param int $count
292 *
293 * @return string
294 */
295 public static function display_number_of_codes( $count ) {
296 return sprintf(
297 /* translators: %s: count */
298 _n( '%s unused code remaining', '%s unused codes remaining', $count, 'wpdef' ),
299 $count
300 );
301 }
302
303 /**
304 * @param \WP_User $user
305 * @param string $code
306 *
307 * @return bool|\WP_Error
308 */
309 public function validate_code( $user, $code ) {
310 $backup_codes = get_user_meta( $user->ID, self::BACKUP_CODE_VALUES, true );
311
312 if ( is_array( $backup_codes ) && ! empty( $backup_codes ) ) {
313 foreach ( $backup_codes as $key => $code_hashed ) {
314 if ( wp_check_password( $code, $code_hashed, $user->ID ) ) {
315 $this->delete_code( $user, $code_hashed );
316
317 return true;
318 }
319 }
320 }
321
322 return new \WP_Error( 'opt_fail', __( 'ERROR: Invalid verification code.', 'wpdef' ) );
323 }
324
325 /**
326 * @param \WP_User $user
327 *
328 * @return bool|\WP_Error
329 */
330 public function validate_authentication( $user ) {
331 $backup_code = isset( $_POST['backup-codes'] )
332 ? sanitize_text_field( wp_unslash( $_POST['backup-codes'] ) )
333 : false;
334
335 return $this->validate_code( $user, filter_var( $backup_code, FILTER_SANITIZE_STRING ) );
336 }
337 }
338