PluginProbe ʕ •ᴥ•ʔ
JetBackup – Backup, Restore & Migrate / trunk
JetBackup – Backup, Restore & Migrate vtrunk
3.1.22.3 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.4.8 1.4.8.1 1.4.9 1.5.0 1.5.1 1.5.1.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.6.0 1.6.10 1.6.11 1.6.12 1.6.13 1.6.15 1.6.5.1 1.6.8.8 1.6.9 1.6.9.1 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7.5 2.0.8.7 2.0.9.11 2.0.9.14 2.0.9.15 2.0.9.6 2.0.9.7 2.0.9.9 3.1.10.7 3.1.11.1 3.1.12.3 3.1.13.4 3.1.14.17 3.1.15.4 3.1.16.1 3.1.17.5 3.1.18.10 3.1.18.8 3.1.18.9 3.1.19.8 3.1.20.3 3.1.21.3 3.1.7.9 3.1.9.2 trunk 1.1.90 1.1.91 1.2.0 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.6 1.3.7 1.3.8 1.3.9 1.4.0 1.4.1 1.4.2
backup / src / JetBackup / 3rdparty / SimpleThenticator / SimpleAuthenticator.php
backup / src / JetBackup / 3rdparty / SimpleThenticator Last commit date
.htaccess 1 year ago SimpleAuthenticator.php 1 year ago autoload.php 1 year ago index.html 1 year ago web.config 1 year ago
SimpleAuthenticator.php
323 lines
1 <?php
2 declare(strict_types=1);
3
4 namespace SimpleThenticator;
5
6 use Exception;
7 use ValueError;
8
9 class SimpleAuthenticator {
10
11 /**
12 * SimpleAuthenticator is a TOTP based on https://github.com/PHPGangsta/GoogleAuthenticator updated and reworked to php8.2 because of inactivity of the original creator.
13 *
14 * Should be usable for all TOTP-Apps according to https://datatracker.ietf.org/doc/html/rfc6238
15 *
16 * @package sebastian/simplethenticator
17 * @author Sebatian Pötter
18 * @version 1.0
19 * @access public
20 * @see https://github.com/poetter-sebastian/SimpleThenticator
21 */
22
23 private int $codeLength;
24 private string $alg;
25
26 /**
27 * @param int|null $codeLength
28 * @param string|null $usedAlg
29 * @throws Exception
30 */
31 public function __construct(?int $codeLength = 6, ?string $usedAlg = 'SHA256')
32 {
33 $this->codeLength = $codeLength ?? 6;
34 $this->alg = $usedAlg ?? 'SHA256';
35
36 if($this->codeLength < 6)
37 {
38 throw new ValueError("Code is less then 6");
39 }
40
41 if(!in_array(strtolower($this->alg), hash_hmac_algos()))
42 {
43 throw new ValueError("Hash function is not supported by hash_hmac");
44 }
45 }
46
47 /**
48 * Gets the used algorithm
49 * @return int
50 */
51 public function getCodeLength(): int
52 {
53 return $this->codeLength;
54 }
55
56 /**
57 * Gets the set code length
58 * @return string
59 */
60 public function getAlgorithm(): string
61 {
62 return $this->alg;
63 }
64
65 /**
66 * Calculate the code, with given secret and point in time.
67 *
68 * @param string $secret
69 * @param float|null $timeSlice
70 *
71 * @return string
72 */
73 public function getCode(string $secret, ?float $timeSlice = null): string
74 {
75 $timeSlice = $timeSlice ?? floor(time() / 30);
76
77 $secretKey = self::base32Decode($secret);
78
79 // Pack time into binary string
80 $time = chr(0) . chr(0) . chr(0) . chr(0) . pack('N*', $timeSlice);
81 // Hash it with users secret key
82 $hm = hash_hmac($this->alg, $time, $secretKey, true);
83 // Use last nipple of result as index/offset
84 $offset = ord(substr($hm, -1)) & 0x0F;
85 // grab 4 bytes of the result
86 $hashPart = substr($hm, $offset, 4);
87
88 // Unpack binary value
89 $value = unpack('N', $hashPart);
90 $value = $value[1];
91 // Only 32 bits
92 $value = $value & 0x7FFFFFFF;
93
94 $modulo = pow(10, $this->codeLength);
95
96 return str_pad((string)($value % $modulo), $this->codeLength, '0', STR_PAD_LEFT);
97 }
98
99 /**
100 * Get QR-Code URL for image, from Google charts.
101 *
102 * @param string $label
103 * @param string $secret
104 * @param string|null $issuer
105 * @param array $params width, height and ecc
106 *
107 * @return string
108 *@example getQRCodeGoogleUrl('Example code', '123456789')
109 *
110 */
111 public function getQRCodeGoogleUrl(string $secret, string $label, ?string $issuer = null, array $params = []): string
112 {
113 $params += [
114 'width' => 200,
115 'height' => 200,
116 'ecc' => 'M',
117 ];
118
119 $width = !empty($params['width']) && (int)$params['width'] > 0 ? (int)$params['width'] : 200;
120 $height = !empty($params['height']) && (int)$params['height'] > 0 ? (int)$params['height'] : 200;
121 $ecc = !empty($params['ecc']) && in_array($params['ecc'], ['L', 'M', 'Q', 'H']) ? $params['ecc'] : 'M';
122
123 $urlencoded = urlencode('otpauth://totp/' .
124 (!is_null($issuer) ? $issuer . ':' : '') . $label .
125 '?secret=' . $secret .
126 ($this->alg != 'SHA1' ? '&algorithm='.$this->alg : '') .
127 (!is_null($issuer) ? '&issuer=' . $issuer : ''));
128
129 return "https://api.qrserver.com/v1/create-qr-code/?data=$urlencoded&size={$width}x$height&ecc=$ecc";
130 }
131
132 /**
133 * Check if the code is correct. This will accept codes starting from $discrepancy*30sec ago to $discrepancy*30sec from now.
134 *
135 * @param string $secret
136 * @param string $code
137 * @param int $discrepancy This is the allowed time drift in 30 second units (8 means 4 minutes before or after)
138 * @param int|null $currentTimeSlice time slice if we want to use other that time()
139 *
140 * @return bool
141 */
142 public function verifyCode(string $secret, string $code, int $discrepancy = 1, ?int $currentTimeSlice = null): bool
143 {
144 $currentTimeSlice = $currentTimeSlice ?? floor(time() / 30);
145
146 if (strlen($code) != $this->codeLength)
147 {
148 return false;
149 }
150
151 for ($i = -$discrepancy; $i <= $discrepancy; ++$i)
152 {
153 $calculatedCode = $this->getCode($secret, $currentTimeSlice + $i);
154 if (self::timingSafeEquals($calculatedCode, $code))
155 {
156 return true;
157 }
158 }
159
160 return false;
161 }
162
163 /**
164 * Helper class to decode base32.
165 *
166 * @param $secret
167 *
168 * @return string
169 */
170 protected function base32Decode($secret): string
171 {
172 if (empty($secret))
173 {
174 return '';
175 }
176
177 $base32chars = $this->getBase32LookupTable();
178 $base32charsFlipped = array_flip($base32chars);
179
180 $paddingCharCount = substr_count($secret, $base32chars[32]);
181 $allowedValues = array(6, 4, 3, 1, 0);
182 if (!in_array($paddingCharCount, $allowedValues))
183 {
184 return '';
185 }
186 for ($i = 0; $i < 4; ++$i)
187 {
188 if ($paddingCharCount == $allowedValues[$i] &&
189 substr($secret, -($allowedValues[$i])) != str_repeat($base32chars[32], $allowedValues[$i]))
190 {
191 return '';
192 }
193 }
194 $secret = str_replace('=', '', $secret);
195 $secret = str_split($secret);
196 $binaryString = '';
197 for ($i = 0; $i < count($secret); $i = $i + 8)
198 {
199 $x = '';
200 if (!in_array($secret[$i], $base32chars))
201 {
202 return '';
203 }
204 for ($j = 0; $j < 8; ++$j)
205 {
206 $x .= str_pad(base_convert((string)@$base32charsFlipped[@$secret[$i + $j]], 10, 2), 5, '0', STR_PAD_LEFT);
207 }
208 $eightBits = str_split($x, 8);
209 for ($z = 0; $z < count($eightBits); ++$z)
210 {
211 $binaryString .= (($y = chr((int)base_convert($eightBits[$z], 2, 10))) || ord($y) == 48) ? $y : '';
212 }
213 }
214
215 return $binaryString;
216 }
217
218 /**
219 * Create new secret.
220 * 16 characters, randomly chosen from the allowed base32 characters.
221 *
222 * @param int $secretLength
223 *
224 * @return string
225 * @throws Exception
226 */
227 public static function createSecret(int $secretLength = 32): string
228 {
229 $validChars = self::getBase32LookupTable();
230
231 // Valid secret lengths are 80 to 640 bits
232 if ($secretLength < 16)
233 {
234 throw new ValueError('The secret is too short');
235 }
236
237 // Valid secret lengths are 80 to 640 bits
238 if ($secretLength > 128)
239 {
240 throw new ValueError('The secret is too long');
241 }
242
243 $secret = '';
244 $rnd = '';
245
246 if (function_exists('random_bytes'))
247 {
248 $rnd = random_bytes($secretLength);
249 }
250 elseif (function_exists('openssl_random_pseudo_bytes'))
251 {
252 $rnd = openssl_random_pseudo_bytes($secretLength, $cryptoStrong);
253 if (!$cryptoStrong)
254 {
255 $rnd = '';
256 }
257 }
258 if (!empty($rnd))
259 {
260 for ($i = 0; $i < $secretLength; ++$i)
261 {
262 $secret .= $validChars[ord($rnd[$i]) & 31];
263 }
264 }
265 else
266 {
267 throw new Exception('No source of secure random');
268 }
269
270 return $secret;
271 }
272
273 /**
274 * Get array with all 32 characters for decoding from/encoding to base32.
275 *
276 * @return string[]
277 */
278 public static function getBase32LookupTable(): array
279 {
280 return [
281 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', // 7
282 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', // 15
283 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', // 23
284 'Y', 'Z', '2', '3', '4', '5', '6', '7', // 31
285 '=', // padding char
286 ];
287 }
288
289 /**
290 * A timing safe equals comparison
291 * more info here: http://blog.ircmaxell.com/2014/11/its-all-about-time.html.
292 *
293 * @param string $safeString The internal (safe) value to be checked
294 * @param string $userString The user submitted (unsafe) value
295 *
296 * @return bool True if the two strings are identical
297 */
298 public static function timingSafeEquals(string $safeString, string $userString): bool
299 {
300 if (function_exists('hash_equals'))
301 {
302 return hash_equals($safeString, $userString);
303 }
304 $safeLen = strlen($safeString);
305 $userLen = strlen($userString);
306
307 if ($userLen != $safeLen)
308 {
309 return false;
310 }
311
312 $result = 0;
313
314 for ($i = 0; $i < $userLen; ++$i)
315 {
316 $result |= (ord($safeString[$i]) ^ ord($userString[$i]));
317 }
318
319 // They are only identical strings if $result is exactly 0...
320 return $result === 0;
321 }
322
323 }