PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / trunk
Matomo Analytics – Powerful, Privacy-First Insights for WordPress vtrunk
5.11.1 5.11.0 5.10.2 5.10.1 trunk 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.3.2 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.1.0 4.1.1 4.1.2 4.1.3 4.10.0 4.11.0 4.12.0 4.13.0 4.13.2 4.13.3 4.13.4 4.13.5 4.14.0 4.14.1 4.14.2 4.15.0 4.15.1 4.15.2 4.15.3 4.2.0 4.3.0 4.3.1 4.4.1 4.4.2 4.5.0 4.6.0 5.0.1 5.0.2 5.0.3 5.0.4 5.0.5 5.0.6 5.0.7 5.0.8 5.1.0 5.1.1 5.1.2 5.1.3 5.1.4 5.1.5 5.1.6 5.1.7 5.10.0 5.2.0 5.2.1 5.2.2 5.3.0 5.3.1 5.3.2 5.3.3 5.6.0 5.6.1 5.7.0 5.7.1 5.8.0 5.8.1 5.8.2
matomo / app / libs / Authenticator / TwoFactorAuthenticator.php
matomo / app / libs / Authenticator Last commit date
LICENSE.md 6 years ago README.md 6 years ago TwoFactorAuthenticator.php 3 months ago
TwoFactorAuthenticator.php
251 lines
1 <?php
2
3 namespace {
4 /**
5 * PHP Class for handling Google Authenticator 2-factor authentication
6 *
7 * @author Michael Kliewe
8 * @copyright 2012 Michael Kliewe
9 * @license http://www.opensource.org/licenses/bsd-license.php BSD License
10 * @link http://www.phpgangsta.de/
11 *
12 * small adjustments by matomo.org
13 * - renamed class
14 * - removed method getQRCodeGoogleUrl
15 * - use better random secret generator
16 * - apply proper type hints
17 */
18 class TwoFactorAuthenticator
19 {
20 protected $_codeLength = 6;
21 /**
22 * Create new secret.
23 * 16 characters, randomly chosen from the allowed base32 characters.
24 *
25 * @param int $secretLength
26 * @return string
27 */
28 public function createSecret(int $secretLength = 16) : string
29 {
30 $validChars = $this->_getBase32LookupTable();
31 unset($validChars[32]);
32 // modified by matomo.org
33 return \Piwik\Common::getRandomString($secretLength, \implode('', $validChars));
34 }
35 /**
36 * Calculate the code, with given secret and point in time
37 *
38 * @param string $secret
39 * @param int|null $timeSlice
40 * @return string
41 */
42 public function getCode(string $secret, ?int $timeSlice = null)
43 {
44 if ($timeSlice === null) {
45 $timeSlice = \floor(\time() / 30);
46 }
47 $secretkey = $this->_base32Decode($secret);
48 // Pack time into binary string
49 $time = \chr(0) . \chr(0) . \chr(0) . \chr(0) . \pack('N*', $timeSlice);
50 // Hash it with users secret key
51 $hm = \hash_hmac('SHA1', $time, $secretkey, \true);
52 // Use last nipple of result as index/offset
53 $offset = \ord(\substr($hm, -1)) & 0xf;
54 // grab 4 bytes of the result
55 $hashpart = \substr($hm, $offset, 4);
56 // Unpak binary value
57 $value = \unpack('N', $hashpart);
58 $value = $value[1];
59 // Only 32 bits
60 $value = $value & 0x7fffffff;
61 $modulo = \pow(10, $this->_codeLength);
62 return \str_pad($value % $modulo, $this->_codeLength, '0', \STR_PAD_LEFT);
63 }
64 /**
65 * Check if the code is correct. This will accept codes starting from $discrepancy*30sec ago to $discrepancy*30sec from now
66 *
67 * @param string $secret
68 * @param string $code
69 * @param int $discrepancy This is the allowed time drift in 30 second units (8 means 4 minutes before or after)
70 * @param int|null $currentTimeSlice time slice if we want use other that time()
71 * @return bool
72 */
73 public function verifyCode(string $secret, string $code, int $discrepancy = 1, ?int $currentTimeSlice = null) : bool
74 {
75 if ($currentTimeSlice === null) {
76 $currentTimeSlice = \floor(\time() / 30);
77 }
78 for ($i = -$discrepancy; $i <= $discrepancy; $i++) {
79 $calculatedCode = $this->getCode($secret, $currentTimeSlice + $i);
80 if ($this->timingSafeEquals($calculatedCode, $code)) {
81 return \true;
82 }
83 }
84 return \false;
85 }
86 /**
87 * Set the code length, should be >=6
88 *
89 * @param int $length
90 * @return self
91 */
92 public function setCodeLength(int $length)
93 {
94 $this->_codeLength = $length;
95 return $this;
96 }
97 /**
98 * Helper class to decode base32
99 *
100 * @param string $secret
101 * @return string
102 */
103 protected function _base32Decode(string $secret) : string
104 {
105 if (empty($secret)) {
106 return '';
107 }
108 $base32chars = $this->_getBase32LookupTable();
109 $base32charsFlipped = \array_flip($base32chars);
110 $paddingCharCount = \substr_count($secret, $base32chars[32]);
111 $allowedValues = array(6, 4, 3, 1, 0);
112 if (!\in_array($paddingCharCount, $allowedValues)) {
113 return \false;
114 }
115 for ($i = 0; $i < 4; $i++) {
116 if ($paddingCharCount == $allowedValues[$i] && \substr($secret, -$allowedValues[$i]) != \str_repeat($base32chars[32], $allowedValues[$i])) {
117 return \false;
118 }
119 }
120 $secret = \str_replace('=', '', $secret);
121 $secret = \str_split($secret);
122 $binaryString = "";
123 for ($i = 0; $i < \count($secret); $i = $i + 8) {
124 $x = "";
125 if (!\in_array($secret[$i], $base32chars)) {
126 return \false;
127 }
128 for ($j = 0; $j < 8; $j++) {
129 $x .= \str_pad(\base_convert($base32charsFlipped[@$secret[$i + $j]] ?? '', 10, 2), 5, '0', \STR_PAD_LEFT);
130 }
131 $eightBits = \str_split($x, 8);
132 for ($z = 0; $z < \count($eightBits); $z++) {
133 $binaryString .= ($y = \chr(\base_convert($eightBits[$z], 2, 10))) || \ord($y) == 48 ? $y : "";
134 }
135 }
136 return $binaryString;
137 }
138 /**
139 * Helper class to encode base32
140 *
141 * @param string $secret
142 * @param bool $padding
143 * @return string
144 */
145 protected function _base32Encode(string $secret, bool $padding = \true) : string
146 {
147 if (empty($secret)) {
148 return '';
149 }
150 $base32chars = $this->_getBase32LookupTable();
151 $secret = \str_split($secret);
152 $binaryString = "";
153 for ($i = 0; $i < \count($secret); $i++) {
154 $binaryString .= \str_pad(\base_convert(\ord($secret[$i]), 10, 2), 8, '0', \STR_PAD_LEFT);
155 }
156 $fiveBitBinaryArray = \str_split($binaryString, 5);
157 $base32 = "";
158 $i = 0;
159 while ($i < \count($fiveBitBinaryArray)) {
160 $base32 .= $base32chars[\base_convert(\str_pad($fiveBitBinaryArray[$i], 5, '0'), 2, 10)];
161 $i++;
162 }
163 if ($padding && ($x = \strlen($binaryString) % 40) != 0) {
164 if ($x == 8) {
165 $base32 .= \str_repeat($base32chars[32], 6);
166 } elseif ($x == 16) {
167 $base32 .= \str_repeat($base32chars[32], 4);
168 } elseif ($x == 24) {
169 $base32 .= \str_repeat($base32chars[32], 3);
170 } elseif ($x == 32) {
171 $base32 .= $base32chars[32];
172 }
173 }
174 return $base32;
175 }
176 /**
177 * Get array with all 32 characters for decoding from/encoding to base32
178 *
179 * @return array<string>
180 */
181 protected function _getBase32LookupTable() : array
182 {
183 return array(
184 'A',
185 'B',
186 'C',
187 'D',
188 'E',
189 'F',
190 'G',
191 'H',
192 // 7
193 'I',
194 'J',
195 'K',
196 'L',
197 'M',
198 'N',
199 'O',
200 'P',
201 // 15
202 'Q',
203 'R',
204 'S',
205 'T',
206 'U',
207 'V',
208 'W',
209 'X',
210 // 23
211 'Y',
212 'Z',
213 '2',
214 '3',
215 '4',
216 '5',
217 '6',
218 '7',
219 // 31
220 '=',
221 );
222 }
223 /**
224 * A timing safe equals comparison
225 * more info here: https://blog.ircmaxell.com/2014/11/its-all-about-time.html.
226 *
227 * @param string $safeString The internal (safe) value to be checked
228 * @param string $userString The user submitted (unsafe) value
229 *
230 * @return bool True if the two strings are identical
231 */
232 private function timingSafeEquals(string $safeString, string $userString) : bool
233 {
234 if (\function_exists('hash_equals')) {
235 return \hash_equals($safeString, $userString);
236 }
237 $safeLen = \strlen($safeString);
238 $userLen = \strlen($userString);
239 if ($userLen != $safeLen) {
240 return \false;
241 }
242 $result = 0;
243 for ($i = 0; $i < $userLen; ++$i) {
244 $result |= \ord($safeString[$i]) ^ \ord($userString[$i]);
245 }
246 // They are only identical strings if $result is exactly 0...
247 return $result === 0;
248 }
249 }
250 }
251