PluginProbe
The WP Remote WordPress Plugin / 6.76
The WP Remote WordPress Plugin v6.76
6.76 6.72 6.69 6.65 6.62 6.48 6.47 4.87 4.97 5.05 5.09 5.16 5.22 5.24 5.25 5.38 5.41 5.42 5.45 5.47 5.53 5.56 5.65 5.68 5.72 All 54 releases
wpremote / wp_2fa / email_otp.php

email_otp.php in The WP Remote WordPress Plugin 6.76, at wp_2fa/email_otp.php

234 lines 5.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 if (!defined('ABSPATH')) exit;
3 if (!class_exists('WPRWP2FAEmailOTP')) :
4
5 class WPRWP2FAEmailOTP {
6 const DIGITS = 8;
7 const LIFETIME = 600;
8 const TRIES = 3;
9 const SEND_COOLDOWN = 60;
10 const SEND_WINDOW = 900;
11 const SENDS_PER_WINDOW = 5;
12 const SITE_SECRET_CONSTANT = 'AUTH_SALT';
13
14 public static function hasSiteSecret() {
15 return self::siteSecret() !== null;
16 }
17
18 public static function secondsUntilNextSend($user_id) {
19 $rate = self::loadRate($user_id);
20 $now = time();
21
22 if ($now - $rate['window_at'] >= self::SEND_WINDOW) {
23 return 0;
24 }
25
26 if ($rate['sends'] >= self::SENDS_PER_WINDOW) {
27 return $rate['window_at'] + self::SEND_WINDOW - $now;
28 }
29
30 return max(0, $rate['last_sent_at'] + self::SEND_COOLDOWN - $now);
31 }
32
33 public static function hasLiveCode($user_id) {
34 $challenge = self::loadChallenge($user_id);
35
36 return $challenge !== null && intval($challenge['expires_at']) > time();
37 }
38
39 public static function issue($user) {
40 $secret = self::siteSecret();
41 if ($secret === null) {
42 return null;
43 }
44
45 if (self::secondsUntilNextSend($user->ID) > 0) {
46 return null;
47 }
48
49 try {
50 $number = random_int(0, pow(10, self::DIGITS) - 1);
51 } catch (Exception $error) {
52 return null;
53 }
54
55 $code = str_pad(strval($number), self::DIGITS, '0', STR_PAD_LEFT);
56
57 $challenge = array(
58 'code_hash' => self::codeHash($user->ID, $code, $secret),
59 'expires_at' => time() + self::LIFETIME,
60 'tries_left' => self::TRIES
61 );
62
63 if (!self::recordSend($user->ID)) {
64 return null;
65 }
66
67 if (!self::storeChallenge($user->ID, $challenge)) {
68 return null;
69 }
70
71 return $code;
72 }
73
74 public static function redeem($user, $code) {
75 $secret = self::siteSecret();
76 if ($secret === null) {
77 return false;
78 }
79
80 $code = self::parseCode($code);
81 if ($code === null) {
82 return false;
83 }
84
85 $challenge = self::loadChallenge($user->ID);
86 if ($challenge === null) {
87 return false;
88 }
89
90 if (intval($challenge['expires_at']) <= time()) {
91 self::claim($user->ID, $challenge);
92 return false;
93 }
94
95 if (!hash_equals($challenge['code_hash'], self::codeHash($user->ID, $code, $secret))) {
96 self::spendTry($user->ID, $challenge);
97 return false;
98 }
99
100 self::discard($user->ID);
101 self::clearSendCount($user->ID);
102
103 return true;
104 }
105
106 public static function revoke($user_id) {
107 delete_user_meta($user_id, WPRWP2FA::EMAIL_RATE_META_KEY);
108
109 return self::discard($user_id);
110 }
111
112 public static function discard($user_id) {
113 delete_user_meta($user_id, WPRWP2FA::EMAIL_CHALLENGE_META_KEY);
114
115 return !metadata_exists('user', $user_id, WPRWP2FA::EMAIL_CHALLENGE_META_KEY);
116 }
117
118 private static function spendTry($user_id, $challenge) {
119 if (intval($challenge['tries_left']) <= 1) {
120 self::claim($user_id, $challenge);
121 return;
122 }
123
124 $spent = $challenge;
125 $spent['tries_left'] = intval($challenge['tries_left']) - 1;
126
127 if (update_user_meta($user_id, WPRWP2FA::EMAIL_CHALLENGE_META_KEY, $spent, $challenge) === false) {
128 self::claim($user_id, $challenge);
129 }
130 }
131
132 # Proving inbox control clears the window cap, so logging in repeatedly does
133 # not lock someone out. The cooldown is kept so codes still cannot be pulled
134 # back to back, and a failed write only means the budget is not returned.
135 private static function clearSendCount($user_id) {
136 $current = self::loadRate($user_id);
137 $next = array(
138 'sends' => 0,
139 'window_at' => time(),
140 'last_sent_at' => $current['last_sent_at']
141 );
142
143 update_user_meta($user_id, WPRWP2FA::EMAIL_RATE_META_KEY, $next, $current);
144 }
145
146 private static function loadRate($user_id) {
147 $blank = array('sends' => 0, 'window_at' => 0, 'last_sent_at' => 0);
148 $rate = get_user_meta($user_id, WPRWP2FA::EMAIL_RATE_META_KEY, true);
149
150 if (!self::validRate($rate, $blank)) {
151 delete_user_meta($user_id, WPRWP2FA::EMAIL_RATE_META_KEY);
152 return $blank;
153 }
154
155 return $rate;
156 }
157
158 private static function validRate($rate, $blank) {
159 if (!is_array($rate)) {
160 return false;
161 }
162
163 foreach ($blank as $field => $default) {
164 if (!isset($rate[$field]) || !is_int($rate[$field]) || $rate[$field] < 0) {
165 return false;
166 }
167 }
168
169 return true;
170 }
171
172 private static function recordSend($user_id) {
173 $now = time();
174 $current = self::loadRate($user_id);
175
176 $window_at = $current['window_at'];
177 $sends = $current['sends'];
178 if ($now - $window_at >= self::SEND_WINDOW) {
179 $window_at = $now;
180 $sends = 0;
181 }
182
183 $next = array(
184 'sends' => $sends + 1,
185 'window_at' => $window_at,
186 'last_sent_at' => $now
187 );
188
189 # $current is left untouched: it is the value the update guards on.
190 return update_user_meta($user_id, WPRWP2FA::EMAIL_RATE_META_KEY, $next, $current) !== false;
191 }
192
193 private static function claim($user_id, $challenge) {
194 return delete_user_meta($user_id, WPRWP2FA::EMAIL_CHALLENGE_META_KEY, $challenge) === true;
195 }
196
197 private static function loadChallenge($user_id) {
198 $challenge = get_user_meta($user_id, WPRWP2FA::EMAIL_CHALLENGE_META_KEY, true);
199
200 if (!is_array($challenge) || !isset($challenge['code_hash'], $challenge['expires_at'], $challenge['tries_left'])) {
201 return null;
202 }
203
204 if (!is_string($challenge['code_hash']) || !is_int($challenge['expires_at']) || !is_int($challenge['tries_left'])) {
205 return null;
206 }
207
208 return $challenge;
209 }
210
211 private static function storeChallenge($user_id, $challenge) {
212 return update_user_meta($user_id, WPRWP2FA::EMAIL_CHALLENGE_META_KEY, $challenge) !== false;
213 }
214
215 private static function codeHash($user_id, $code, $secret) {
216 return hash_hmac('sha256', intval($user_id) . "\0" . $code, $secret);
217 }
218
219 private static function siteSecret() {
220 return WPRHelper::configSalt(self::SITE_SECRET_CONSTANT);
221 }
222
223 private static function parseCode($code) {
224 if (!is_string($code)) {
225 return null;
226 }
227
228 $code = trim($code);
229
230 return WPRHelper::safePregMatch('/^\d{' . self::DIGITS . '}$/D', $code) === 1 ? $code : null;
231 }
232 }
233 endif;
234