$plain, 'hashes' => $hashes, ); } /** * Hash a normalized backup code using site auth salt. */ public static function hash_backup_code( $code ) { $normalized = strtoupper( preg_replace( '/[^A-Za-z0-9]/', '', (string) $code ) ); return hash_hmac( 'sha256', 'aio-2fa-backup:' . $normalized, wp_salt( 'auth' ) ); } /** * Verify and consume a one-time backup code for a given user. */ public static function verify_and_consume_backup_code( $user_id, $code ) { $normalized = strtoupper( preg_replace( '/[^A-Za-z0-9]/', '', (string) $code ) ); if ( strlen( $normalized ) < 8 ) { return false; } $stored_hashes = get_user_meta( $user_id, '_aio_2fa_backup_codes', true ); if ( ! is_array( $stored_hashes ) || empty( $stored_hashes ) ) { return false; } $candidate_hash = self::hash_backup_code( $normalized ); foreach ( $stored_hashes as $idx => $saved_hash ) { if ( is_string( $saved_hash ) && hash_equals( $saved_hash, $candidate_hash ) ) { unset( $stored_hashes[ $idx ] ); update_user_meta( $user_id, '_aio_2fa_backup_codes', array_values( $stored_hashes ) ); return true; } } return false; } /** * Clean RFC 4648 Base32 decoder without error-suppression operators. */ private static function base32_decode( $secret ) { if ( empty( $secret ) || ! is_string( $secret ) ) { return ''; } $secret = strtoupper( rtrim( trim( $secret ), '=' ) ); if ( $secret === '' ) { return ''; } $flipped = array_flip( str_split( self::$_base32Map ) ); $bits = ''; $len = strlen( $secret ); for ( $i = 0; $i < $len; $i++ ) { $ch = $secret[ $i ]; if ( ! isset( $flipped[ $ch ] ) ) { return ''; } $bits .= str_pad( decbin( $flipped[ $ch ] ), 5, '0', STR_PAD_LEFT ); } $output = ''; $byte_count = (int) floor( strlen( $bits ) / 8 ); for ( $i = 0; $i < $byte_count; $i++ ) { $output .= chr( bindec( substr( $bits, $i * 8, 8 ) ) ); } return $output; } }