420 lines
15 KiB
PHP
420 lines
15 KiB
PHP
<?php
|
|
/*
|
|
* Plugin Name: WP Multi 2FA
|
|
* Plugin URI: https://git.viper.ipv64.net/M_Viper/WP-Multi-2FA
|
|
* Description: 2FA mit Backup-Codes und Admin-Erzwingung.
|
|
* Version: 1.0
|
|
* Author: M_Viper
|
|
* Author URI: https://m-viper.de
|
|
* Requires at least: 6.7.2
|
|
* Tested up to: 6.7.2
|
|
* PHP Version: 7.2
|
|
* License: GPL2
|
|
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
|
* Support: [Microsoft Teams Support](https://teams.live.com/l/community/FEAzokphpZTJ2u6OgI)
|
|
* Support: [Telegram Support](https://t.me/M_Viper04)
|
|
*/
|
|
|
|
if (!defined('ABSPATH')) exit;
|
|
|
|
// === 1. Plugin-Zeile in der Plugin-Liste erweitern ===
|
|
add_filter('plugin_row_meta', 'wp_multi_2fa_plugin_meta_links', 10, 2);
|
|
function wp_multi_2fa_plugin_meta_links($links, $file) {
|
|
if (plugin_basename(__FILE__) === $file) {
|
|
$update_link = '<a href="https://git.viper.ipv64.net/M_Viper/WP-Multi-2FA/releases" target="_blank" style="color: #d63638; font-weight: bold;">🔴 Neue Updates auf Gitea</a>';
|
|
$links[] = $update_link;
|
|
}
|
|
return $links;
|
|
}
|
|
|
|
// === 2. Admin-Dashboard-Hinweis bei neuer Version ===
|
|
add_action('admin_init', 'wp_multi_2fa_check_update_notice');
|
|
function wp_multi_2fa_check_update_notice() {
|
|
if (!current_user_can('update_plugins')) return;
|
|
|
|
$transient = get_transient('wp_multi_2fa_gitea_version');
|
|
|
|
if ($transient === false) {
|
|
$response = wp_remote_get('https://git.viper.ipv64.net/api/v1/repos/M_Viper/WP-Multi-2FA/releases/latest');
|
|
if (is_wp_error($response)) return;
|
|
|
|
$data = json_decode(wp_remote_retrieve_body($response));
|
|
if (isset($data->tag_name)) {
|
|
$latest = ltrim($data->tag_name, 'v'); // Entfernt evtl. "v" vor der Versionsnummer
|
|
set_transient('wp_multi_2fa_gitea_version', $latest, 12 * HOUR_IN_SECONDS);
|
|
} else {
|
|
return;
|
|
}
|
|
} else {
|
|
$latest = $transient;
|
|
}
|
|
|
|
if (!function_exists('get_plugin_data')) {
|
|
require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
|
}
|
|
|
|
$plugin_data = get_plugin_data(__FILE__);
|
|
$current_version = $plugin_data['Version'];
|
|
|
|
if (version_compare($latest, $current_version, '>')) {
|
|
add_action('admin_notices', function () use ($latest) {
|
|
?>
|
|
<div class="notice notice-warning is-dismissible">
|
|
<p><strong>WP Multi 2FA:</strong> Eine neue Version (<?= esc_html($latest); ?>) ist verfügbar.
|
|
<a href="https://git.viper.ipv64.net/M_Viper/WP-Multi-2FA/releases" target="_blank">Jetzt ansehen auf Gitea</a>.</p>
|
|
</div>
|
|
<?php
|
|
});
|
|
}
|
|
}
|
|
|
|
class WP_2FA_Plugin {
|
|
private $option_name = 'wp2fa_users';
|
|
|
|
public function __construct() {
|
|
add_action('show_user_profile', [$this, 'show_2fa_settings']);
|
|
add_action('edit_user_profile', [$this, 'show_2fa_settings']);
|
|
add_action('personal_options_update', [$this, 'save_2fa_settings']);
|
|
add_action('edit_user_profile_update', [$this, 'save_2fa_settings']);
|
|
|
|
// Login flow
|
|
add_action('wp_login', [$this, 'after_login'], 10, 2);
|
|
add_action('init', [$this, 'handle_2fa']);
|
|
|
|
// Einstellungen > Allgemein
|
|
add_action('admin_init', [$this, 'register_settings_field']);
|
|
|
|
// Session für 2FA-Code speichern
|
|
if (!session_id()) {
|
|
session_start();
|
|
}
|
|
}
|
|
|
|
private function get_user_2fa_data($user_id) {
|
|
$all = get_option($this->option_name, []);
|
|
return $all[$user_id] ?? null;
|
|
}
|
|
|
|
private function set_user_2fa_data($user_id, $data) {
|
|
$all = get_option($this->option_name, []);
|
|
$all[$user_id] = $data;
|
|
update_option($this->option_name, $all);
|
|
}
|
|
|
|
public function show_2fa_settings($user) {
|
|
if (!current_user_can('edit_user', $user->ID)) return;
|
|
|
|
$data = $this->get_user_2fa_data($user->ID);
|
|
$enabled = $data['enabled'] ?? false;
|
|
$secret = $data['secret'] ?? null;
|
|
$backup_codes = $data['backup_codes'] ?? [];
|
|
|
|
if (!$secret && $enabled) {
|
|
$secret = $this->generate_secret();
|
|
$data['secret'] = $secret;
|
|
$backup_codes = $this->generate_backup_codes();
|
|
$data['backup_codes'] = $backup_codes;
|
|
$this->set_user_2fa_data($user->ID, $data);
|
|
}
|
|
|
|
?>
|
|
<h2>2-Faktor-Authentifizierung</h2>
|
|
<table class="form-table">
|
|
<tr>
|
|
<th><label for="wp2fa_enabled">2FA aktivieren</label></th>
|
|
<td>
|
|
<input type="checkbox" name="wp2fa_enabled" id="wp2fa_enabled" value="1" <?php checked($enabled); ?>>
|
|
<p class="description">Aktiviere die Zwei-Faktor-Authentifizierung für dein Konto.</p>
|
|
</td>
|
|
</tr>
|
|
<?php if ($enabled && $secret): ?>
|
|
<tr>
|
|
<th>QR-Code (für Authenticator-App)</th>
|
|
<td>
|
|
<img src="<?php echo esc_url($this->get_qr_code_url($user->user_email, $secret)); ?>" alt="QR Code" />
|
|
<p>Scanne diesen QR-Code mit einer App wie Google Authenticator oder Authy.</p>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<th>Backup-Codes</th>
|
|
<td>
|
|
<textarea readonly rows="5" cols="30" style="font-family: monospace;"><?php echo implode("\n", $backup_codes); ?></textarea><br>
|
|
<button type="button" onclick="downloadBackupCodes()">Backup-Codes herunterladen</button>
|
|
<script>
|
|
function downloadBackupCodes() {
|
|
const codes = <?php echo json_encode($backup_codes); ?>;
|
|
const blob = new Blob([codes.join("\n")], {type: 'text/plain'});
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = 'backup-codes.txt';
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
</script>
|
|
<p>Bewahre diese Codes sicher auf. Jeder Code kann einmal zur Anmeldung verwendet werden.</p>
|
|
</td>
|
|
</tr>
|
|
<?php endif; ?>
|
|
</table>
|
|
<?php
|
|
}
|
|
|
|
public function save_2fa_settings($user_id) {
|
|
if (!current_user_can('edit_user', $user_id)) return;
|
|
|
|
$data = $this->get_user_2fa_data($user_id) ?? [];
|
|
|
|
$enabled = isset($_POST['wp2fa_enabled']) && $_POST['wp2fa_enabled'] == '1';
|
|
|
|
if ($enabled && empty($data['secret'])) {
|
|
$data['secret'] = $this->generate_secret();
|
|
$data['backup_codes'] = $this->generate_backup_codes();
|
|
}
|
|
|
|
if (!$enabled) {
|
|
// Deaktivieren: löschen
|
|
$data = [];
|
|
} else {
|
|
$data['enabled'] = true;
|
|
}
|
|
|
|
$this->set_user_2fa_data($user_id, $data);
|
|
}
|
|
|
|
private function generate_secret() {
|
|
$chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
|
$secret = '';
|
|
for ($i=0; $i < 16; $i++) {
|
|
$secret .= $chars[random_int(0, 31)];
|
|
}
|
|
return $secret;
|
|
}
|
|
|
|
private function generate_backup_codes() {
|
|
$codes = [];
|
|
for ($i = 0; $i < 10; $i++) {
|
|
$codes[] = strtoupper(bin2hex(random_bytes(4)));
|
|
}
|
|
return $codes;
|
|
}
|
|
|
|
private function get_qr_code_url($email, $secret) {
|
|
$issuer = rawurlencode(get_bloginfo('name'));
|
|
$label = rawurlencode(get_bloginfo('name') . ':' . $email);
|
|
$otpauth = "otpauth://totp/{$label}?secret={$secret}&issuer={$issuer}";
|
|
$chl = urlencode($otpauth);
|
|
return "https://api.qrserver.com/v1/create-qr-code/?size=200x200&data={$chl}";
|
|
}
|
|
|
|
// Nach Login prüfen, ob 2FA nötig
|
|
public function after_login($user_login, $user) {
|
|
if (!$this->needs_2fa($user->ID)) return;
|
|
|
|
$_SESSION['wp2fa_user'] = $user->ID;
|
|
wp_logout();
|
|
wp_redirect(site_url('/wp-login.php?action=wp2fa'));
|
|
exit;
|
|
}
|
|
|
|
private function needs_2fa($user_id) {
|
|
$data = $this->get_user_2fa_data($user_id);
|
|
$admin_forced = get_option('wp2fa_admin_forced', false);
|
|
return ($data['enabled'] ?? false) || $admin_forced;
|
|
}
|
|
|
|
// 2FA Handler: Formular verarbeiten und ggf. anzeigen (Popup)
|
|
public function handle_2fa() {
|
|
if (!isset($_GET['action']) || $_GET['action'] !== 'wp2fa') return;
|
|
|
|
$user_id = $_SESSION['wp2fa_user'] ?? 0;
|
|
if (!$user_id) {
|
|
wp_redirect(wp_login_url());
|
|
exit;
|
|
}
|
|
|
|
$error = '';
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
$code = strtoupper(trim($_POST['wp2fa_code'] ?? ''));
|
|
|
|
$data = $this->get_user_2fa_data($user_id);
|
|
if (!$data) {
|
|
wp_redirect(wp_login_url());
|
|
exit;
|
|
}
|
|
|
|
$secret = $data['secret'] ?? '';
|
|
$backup_codes = $data['backup_codes'] ?? [];
|
|
|
|
if ($this->verify_code($secret, $code)) {
|
|
// Erfolgreicher Authenticator-Code
|
|
wp_set_auth_cookie($user_id);
|
|
unset($_SESSION['wp2fa_user']);
|
|
wp_redirect(admin_url());
|
|
exit;
|
|
} elseif (in_array($code, $backup_codes, true)) {
|
|
// Backup-Code erfolgreich, diesen Code löschen
|
|
$backup_codes = array_diff($backup_codes, [$code]);
|
|
$data['backup_codes'] = $backup_codes;
|
|
$this->set_user_2fa_data($user_id, $data);
|
|
|
|
wp_set_auth_cookie($user_id);
|
|
unset($_SESSION['wp2fa_user']);
|
|
wp_redirect(admin_url());
|
|
exit;
|
|
} else {
|
|
$error = 'Falscher Code!';
|
|
}
|
|
}
|
|
|
|
$this->show_2fa_form($error);
|
|
exit;
|
|
}
|
|
|
|
private function show_2fa_form($error = '') {
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>2-Faktor-Authentifizierung</title>
|
|
<style>
|
|
body {
|
|
font-family: Arial, sans-serif;
|
|
background: #222;
|
|
margin: 0; padding: 0; height: 100vh;
|
|
display: flex; align-items: center; justify-content: center;
|
|
}
|
|
#wp2fa-modal {
|
|
background: #fff;
|
|
padding: 30px;
|
|
border-radius: 8px;
|
|
box-shadow: 0 0 15px rgba(0,0,0,0.5);
|
|
width: 320px;
|
|
text-align: center;
|
|
position: relative;
|
|
}
|
|
#wp2fa-modal h1 {
|
|
margin-top: 0;
|
|
margin-bottom: 20px;
|
|
}
|
|
#wp2fa-modal input[type="text"] {
|
|
font-size: 18px;
|
|
padding: 10px;
|
|
width: 100%;
|
|
box-sizing: border-box;
|
|
margin-bottom: 20px;
|
|
text-align: center;
|
|
letter-spacing: 4px;
|
|
font-family: monospace;
|
|
}
|
|
#wp2fa-modal button {
|
|
background-color: #0073aa;
|
|
border: none;
|
|
color: white;
|
|
padding: 10px 20px;
|
|
font-size: 16px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
}
|
|
#wp2fa-modal .error {
|
|
color: red;
|
|
margin-bottom: 15px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="wp2fa-modal" role="dialog" aria-modal="true" aria-labelledby="wp2fa-title">
|
|
<h1 id="wp2fa-title">2-Faktor-Authentifizierung</h1>
|
|
<?php if ($error): ?>
|
|
<div class="error"><?php echo esc_html($error); ?></div>
|
|
<?php endif; ?>
|
|
<form method="post">
|
|
<label for="wp2fa_code">Gib deinen Authenticator- oder Backup-Code ein:</label><br>
|
|
<input type="text" name="wp2fa_code" id="wp2fa_code" autocomplete="one-time-code" required maxlength="8" autofocus><br>
|
|
<button type="submit">Bestätigen</button>
|
|
</form>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
<?php
|
|
}
|
|
|
|
private function verify_code($secret, $code) {
|
|
// TOTP-Verifikation (30-Sekunden Fenster)
|
|
$timeSlice = floor(time() / 30);
|
|
for ($i = -1; $i <= 1; $i++) {
|
|
$calc = $this->get_totp_code($secret, $timeSlice + $i);
|
|
if ($calc === $code) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private function get_totp_code($secret, $timeSlice) {
|
|
$secretkey = $this->base32_decode($secret);
|
|
|
|
$time = pack('N*', 0) . pack('N*', $timeSlice);
|
|
$hash = hash_hmac('sha1', $time, $secretkey, true);
|
|
$offset = ord(substr($hash, -1)) & 0x0F;
|
|
$truncatedHash = substr($hash, $offset, 4);
|
|
|
|
$code = unpack('N', $truncatedHash)[1] & 0x7FFFFFFF;
|
|
$code = $code % 1000000;
|
|
|
|
return str_pad($code, 6, '0', STR_PAD_LEFT);
|
|
}
|
|
|
|
private function base32_decode($b32) {
|
|
$alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
|
$b32 = strtoupper($b32);
|
|
$l = strlen($b32);
|
|
$n = 0;
|
|
$j = 0;
|
|
$binary = '';
|
|
|
|
for ($i = 0; $i < $l; $i++) {
|
|
$n = $n << 5; // 5 bits per char
|
|
$n = $n + strpos($alphabet, $b32[$i]);
|
|
$j += 5;
|
|
if ($j >= 8) {
|
|
$j -= 8;
|
|
$binary .= chr(($n & (0xFF << $j)) >> $j);
|
|
}
|
|
}
|
|
return $binary;
|
|
}
|
|
|
|
// Registrierung des Admin-Feldes in Einstellungen > Allgemein
|
|
public function register_settings_field() {
|
|
register_setting('general', 'wp2fa_admin_forced', [
|
|
'type' => 'boolean',
|
|
'description' => '2FA für alle Benutzer erzwingen',
|
|
'sanitize_callback' => [$this, 'sanitize_bool'],
|
|
'default' => false,
|
|
]);
|
|
|
|
add_settings_field(
|
|
'wp2fa_admin_forced',
|
|
'2FA für alle Benutzer erzwingen',
|
|
[$this, 'render_admin_forced_field'],
|
|
'general'
|
|
);
|
|
}
|
|
|
|
public function render_admin_forced_field() {
|
|
$forced = get_option('wp2fa_admin_forced', false);
|
|
?>
|
|
<input type="checkbox" id="wp2fa_admin_forced" name="wp2fa_admin_forced" value="1" <?php checked($forced, true); ?> />
|
|
<label for="wp2fa_admin_forced">Wenn aktiviert, müssen alle Benutzer 2FA nutzen, auch wenn sie es nicht selbst aktiviert haben.</label>
|
|
<?php
|
|
}
|
|
|
|
public function sanitize_bool($value) {
|
|
return ($value === '1' || $value === 1 || $value === true) ? true : false;
|
|
}
|
|
}
|
|
|
|
new WP_2FA_Plugin();
|