PluginProbe
DoLogin Security / trunk
DoLogin Security vtrunk
5.0.10 4.8.3 trunk 1.0 1.1 1.1.1 1.2 1.2.1 1.2.2 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.4 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.5 1.6 All 64 releases
dologin / lib / two-fa-lib.cls.php

two-fa-lib.cls.php in DoLogin Security trunk, at lib/two-fa-lib.cls.php

289 lines 7.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace dologin\lib;
3
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 *
11 * @link http://www.phpgangsta.de/
12 */
13 class Two_FA_Lib {
14
15 protected $_codeLength = 6;
16
17 /**
18 * Create new secret.
19 * 16 characters, randomly chosen from the allowed base32 characters.
20 *
21 * @param int $secretLength
22 *
23 * @return string
24 */
25 public function createSecret( $secretLength = 16 ) {
26 $validChars = $this->_getBase32LookupTable();
27
28 // Valid secret lengths are 80 to 640 bits
29 if ( $secretLength < 16 || $secretLength > 128 ) {
30 throw new \Exception( 'Bad secret length' );
31 }
32 $secret = '';
33 $rnd = false;
34 if ( function_exists( 'random_bytes' ) ) {
35 $rnd = random_bytes( $secretLength );
36 } elseif ( function_exists( 'mcrypt_create_iv' ) ) {
37 $rnd = mcrypt_create_iv( $secretLength, MCRYPT_DEV_URANDOM );
38 } elseif ( function_exists( 'openssl_random_pseudo_bytes' ) ) {
39 $rnd = openssl_random_pseudo_bytes( $secretLength, $cryptoStrong );
40 if ( ! $cryptoStrong ) {
41 $rnd = false;
42 }
43 }
44 if ( $rnd !== false ) {
45 for ( $i = 0; $i < $secretLength; ++$i ) {
46 $secret .= $validChars[ ord( $rnd[ $i ] ) & 31 ];
47 }
48 } else {
49 throw new \Exception( 'No source of secure random' );
50 }
51
52 return $secret;
53 }
54
55 /**
56 * Calculate the code, with given secret and point in time.
57 *
58 * @param string $secret
59 * @param int|null $timeSlice
60 *
61 * @return string
62 */
63 public function getCode( $secret, $timeSlice = null ) {
64 if ( $timeSlice === null ) {
65 $timeSlice = floor( time() / 30 );
66 }
67
68 $secretkey = $this->_base32Decode( $secret );
69
70 // Pack time into binary string
71 $time = chr( 0 ) . chr( 0 ) . chr( 0 ) . chr( 0 ) . pack( 'N*', $timeSlice );
72 // Hash it with users secret key
73 $hm = hash_hmac( 'SHA1', $time, $secretkey, true );
74 // Use last nipple of result as index/offset
75 $offset = ord( substr( $hm, -1 ) ) & 0x0F;
76 // grab 4 bytes of the result
77 $hashpart = substr( $hm, $offset, 4 );
78
79 // Unpak binary value
80 $value = unpack( 'N', $hashpart );
81 $value = $value[1];
82 // Only 32 bits
83 $value = $value & 0x7FFFFFFF;
84
85 $modulo = pow( 10, $this->_codeLength );
86
87 return str_pad( $value % $modulo, $this->_codeLength, '0', STR_PAD_LEFT );
88 }
89
90 /**
91 * Get QR-Code URL for image, from google charts.
92 *
93 * @param string $name
94 * @param string $secret
95 * @param string $title
96 * @param array $params
97 *
98 * @return string
99 */
100 public function getQRCodeGoogleUrl( $name, $secret, $title = null, $params = array() ) {
101 $width = ! empty( $params['width'] ) && (int) $params['width'] > 0 ? (int) $params['width'] : 200;
102 $height = ! empty( $params['height'] ) && (int) $params['height'] > 0 ? (int) $params['height'] : 200;
103 $level = ! empty( $params['level'] ) && array_search( $params['level'], array( 'L', 'M', 'Q', 'H' ) ) !== false ? $params['level'] : 'M';
104
105 $urlencoded = 'otpauth://totp/' . $name . '?secret=' . $secret;
106 if ( isset( $title ) ) {
107 $urlencoded .= '&issuer=' . rawurlencode( $title );
108 }
109
110 return $urlencoded;
111 }
112
113 /**
114 * Check if the code is correct. This will accept codes starting from $discrepancy*30sec ago to $discrepancy*30sec from now.
115 *
116 * @param string $secret
117 * @param string $code
118 * @param int $discrepancy This is the allowed time drift in 30 second units (8 means 4 minutes before or after)
119 * @param int|null $currentTimeSlice time slice if we want use other that time()
120 *
121 * @return bool
122 */
123 public function verifyCode( $secret, $code, $discrepancy = 1, $currentTimeSlice = null ) {
124 return false !== $this->findValidTimeSlice( $secret, $code, $discrepancy, $currentTimeSlice );
125 }
126
127 /**
128 * Return the matched time slice so callers can block TOTP replay.
129 *
130 * @param string $secret
131 * @param string $code
132 * @param int $discrepancy
133 * @param int|null $currentTimeSlice
134 *
135 * @return int|false
136 */
137 public function findValidTimeSlice( $secret, $code, $discrepancy = 1, $currentTimeSlice = null ) {
138 if ( $currentTimeSlice === null ) {
139 $currentTimeSlice = (int) floor( time() / 30 );
140 }
141
142 if ( strlen( $code ) != 6 ) {
143 return false;
144 }
145
146 for ( $i = -$discrepancy; $i <= $discrepancy; ++$i ) {
147 $matched_slice = (int) $currentTimeSlice + $i;
148 $calculatedCode = $this->getCode( $secret, $matched_slice );
149 if ( $this->timingSafeEquals( $calculatedCode, $code ) ) {
150 return $matched_slice;
151 }
152 }
153
154 return false;
155 }
156
157 /**
158 * Set the code length, should be >=6.
159 *
160 * @param int $length
161 *
162 * @return PHPGangsta_GoogleAuthenticator
163 */
164 public function setCodeLength( $length ) {
165 $this->_codeLength = $length;
166
167 return $this;
168 }
169
170 /**
171 * Helper class to decode base32.
172 *
173 * @param $secret
174 *
175 * @return bool|string
176 */
177 protected function _base32Decode( $secret ) {
178 if ( empty( $secret ) ) {
179 return '';
180 }
181
182 $base32chars = $this->_getBase32LookupTable();
183 $base32charsFlipped = array_flip( $base32chars );
184
185 $paddingCharCount = substr_count( $secret, $base32chars[32] );
186 $allowedValues = array( 6, 4, 3, 1, 0 );
187 if ( ! in_array( $paddingCharCount, $allowedValues ) ) {
188 return false;
189 }
190 for ( $i = 0; $i < 4; ++$i ) {
191 if ( $paddingCharCount == $allowedValues[ $i ] &&
192 substr( $secret, -( $allowedValues[ $i ] ) ) != str_repeat( $base32chars[32], $allowedValues[ $i ] ) ) {
193 return false;
194 }
195 }
196 $secret = str_replace( '=', '', $secret );
197 $secret = str_split( $secret );
198 $binaryString = '';
199 for ( $i = 0; $i < count( $secret ); $i = $i + 8 ) {
200 $x = '';
201 if ( ! in_array( $secret[ $i ], $base32chars ) ) {
202 return false;
203 }
204 for ( $j = 0; $j < 8; ++$j ) {
205 $x .= str_pad( base_convert( @$base32charsFlipped[ @$secret[ $i + $j ] ], 10, 2 ), 5, '0', STR_PAD_LEFT ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- vendored PHPGangsta TOTP library.
206 }
207 $eightBits = str_split( $x, 8 );
208 for ( $z = 0; $z < count( $eightBits ); ++$z ) {
209 $binaryString .= ( ( $y = chr( base_convert( $eightBits[ $z ], 2, 10 ) ) ) || ord( $y ) == 48 ) ? $y : ''; // phpcs:ignore WordPress.CodeAnalysis.AssignmentInTernaryCondition.FoundInTernaryCondition -- vendored library.
210 }
211 }
212
213 return $binaryString;
214 }
215
216 /**
217 * Get array with all 32 characters for decoding from/encoding to base32.
218 *
219 * @return array
220 */
221 protected function _getBase32LookupTable() {
222 return array(
223 'A',
224 'B',
225 'C',
226 'D',
227 'E',
228 'F',
229 'G',
230 'H', // 7
231 'I',
232 'J',
233 'K',
234 'L',
235 'M',
236 'N',
237 'O',
238 'P', // 15
239 'Q',
240 'R',
241 'S',
242 'T',
243 'U',
244 'V',
245 'W',
246 'X', // 23
247 'Y',
248 'Z',
249 '2',
250 '3',
251 '4',
252 '5',
253 '6',
254 '7', // 31
255 '=', // padding char
256 );
257 }
258
259 /**
260 * A timing safe equals comparison
261 * more info here: http://blog.ircmaxell.com/2014/11/its-all-about-time.html.
262 *
263 * @param string $safeString The internal (safe) value to be checked
264 * @param string $userString The user submitted (unsafe) value
265 *
266 * @return bool True if the two strings are identical
267 */
268 private function timingSafeEquals( $safeString, $userString ) {
269 if ( function_exists( 'hash_equals' ) ) {
270 return hash_equals( $safeString, $userString );
271 }
272 $safeLen = strlen( $safeString );
273 $userLen = strlen( $userString );
274
275 if ( $userLen != $safeLen ) {
276 return false;
277 }
278
279 $result = 0;
280
281 for ( $i = 0; $i < $userLen; ++$i ) {
282 $result |= ( ord( $safeString[ $i ] ) ^ ord( $userString[ $i ] ) );
283 }
284
285 // They are only identical strings if $result is exactly 0...
286 return $result === 0;
287 }
288 }
289