1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
|
<?php
namespace CraftBlue;
use RandomLib\Factory as RandomFactory;
use SecurityLib\Strength as SecurityStrength;
use Base32\Base32;
/**
* A Google Authenticator 2Factor authentication implementation written in PHP which Follows RFC6238.
*
* This work is derived from the GoogleAuthenticator project written by Michael Kliewe, @PHPGangsta.
* This new version focuses on industry best practices in security by utilizing third party libraries
* which specialize in the generation of Base32 strings, cryptographically strong pseudorandom strings,
* and performing safe string comparisons that aren't prone to timing attacks.
*
* @author Michael Kliewe, Corey Ballou
* @copyright 2016 POP! Online LLC
* @license MIT
* @link https://github.com/PHPGangsta/GoogleAuthenticator
* @link http://tools.ietf.org/html/rfc6238
* @link https://pop.co/
*/
class GoogleAuthenticator
{
/**
* The length of the code you wish to have generated for the user to key in.
* @var int
*/
protected $_codeLength = 6;
/**
* GoogleAuthenticator constructor.
*
* @access public
* @param int $codeLength
*/
public function __construct($codeLength = 6)
{
$this->setCodeLength($codeLength);
}
/**
* Create a new Base32 encoded secret.
*
* @param int $length The length of the secret key you wish to generate
* @return string
*/
public function createSecret($length = 16)
{
$factory = new RandomFactory();
$generator = $factory->getGenerator(new SecurityStrength(SecurityStrength::MEDIUM));
return $generator->generateString($length, $this->getBase32Characters());
}
/**
* Given a secret and a point in time reference, handle calculating the Google Authenticator code based on
* RFC6238, entitled "TOTP: Time-Based One-Time Password Algorithm."
*
* @access public
* @param string $encodedSecret
* @param int|null $timeSlice A timeSlice represents seconds since the Unix epoch divided by 30
* @link https://tools.ietf.org/html/rfc6238
* @return string
*/
public function getCode($encodedSecret, $timeSlice = null)
{
// if we have no time, generate one ourselves
if ($timeSlice === null) {
$timeSlice = floor(time() / 30);
}
$secretKey = Base32::decode($encodedSecret);
// Pack time into binary string
$time = chr(0) . chr(0) . chr(0) . chr(0) . pack('N*', $timeSlice);
// Hash it with users secret key
$hm = hash_hmac('SHA1', $time, $secretKey, true);
// Use last nipple of result as index/offset
$offset = ord($this->safeSubstr($hm, -1)) & 0x0F;
// grab 4 bytes of the result
$hashpart = $this->safeSubstr($hm, $offset, 4);
// Unpack binary value
$value = unpack('N', $hashpart);
$value = $value[1];
// Only 32 bits
$value = $value & 0x7FFFFFFF;
$modulo = pow(10, $this->_codeLength);
return str_pad($value % $modulo, $this->_codeLength, '0', STR_PAD_LEFT);
}
/**
* Generate a Google Chart QR code url to be used by the Google Authenticator application for snapping a picture
* of the QR code. Please note that a label is used to identify which account a key is associated with. It should
* be unique to a particular user. It contains an account name, which is a URI-encoded string, optionally prefixed
* by an issuer string identifying the provider or service managing that account. This issuer prefix can be used to
* prevent collisions between different accounts with different providers that might be identified using the same
* account name, e.g. the user's email address.
*
* To adjust your QR code image, you can optionally pass the following keys in $params:
*
* - height: The width of your QR code image (integer)
* - width: The height of your QR code image (integer)
* - level: The error correction level allowed by the QR code. Valid values are:
* - L: [Default] Allows recovery of up to 7% data loss
* - M: Allows recovery of up to 15% data loss
* - Q: Allows recovery of up to 25% data loss
* - H: Allows recovery of up to 30% data loss
*
* @access public
* @param string $label
* @param string $secret
* @param string $issuer
* @param array $params User supplied parameters to adjust the output of the QR code
* @link https://github.com/google/google-authenticator/wiki/Key-Uri-Format
* @return string
*/
public function getQRCodeUrl($label, $secret, $issuer = null, $params = array())
{
$label = $this->validateLabel($label, $issuer);
$url = 'otpauth://totp/' . $label . '?secret=' . $secret;
// add the issuer (company namespace)
if (!empty($issuer) && is_string($issuer)) {
$url .= '&issuer=' . rawurlencode($issuer);
}
$encodedUrl = urlencode($url);
$width = !empty($params['width']) && (int) $params['width'] > 0 ? (int) $params['width'] : 200;
$height = !empty($params['height']) && (int) $params['height'] > 0 ? (int) $params['height'] : 200;
$level = !empty($params['level']) && in_array($params['level'], array('L', 'M', 'Q', 'H')) ? $params['level'] : 'M';
return 'https://chart.googleapis.com/chart?chs='.$width.'x'.$height.'&chld='.$level.'|0&cht=qr&chl='.$encodedUrl.'';
}
/**
* Check if the client supplied code is correct by comparing it to the Google secret version.
*
* Note that any codes generated by Authenticator within the following time period will be accepted:
* (current_timestamp - ($tolerance * 30 sec)) to (current timestamp + ($tolerance * 30 sec))
*
* @access public
* @param string $secret
* @param string $code
* @param int $tolerance The +/- time drift allowed when verifying the authenticator code (in 30sec increments)
* @return bool
*/
public function verifyCode($secret, $code, $tolerance = 1)
{
$timeSlice = floor(time() / 30);
for ($i = -$tolerance; $i <= $tolerance; $i++) {
if ($this->strCompare($this->getCode($secret, $timeSlice + $i), $code)) {
return true;
}
}
return false;
}
/**
* Set the code length to generate for the user.
*
* @access public
* @param int $codeLength
* @return GoogleAuthenticator
*/
public function setCodeLength($codeLength = 6)
{
if (!is_integer($codeLength)) {
throw new Exception('You must set a valid code length.');
}
$this->_codeLength = $codeLength;
return $this;
}
/**
* Validate that the user supplied label for QR code generation is in the proper format.
*
* @access public
* @param string $label
* @param string $issuer
* @link https://github.com/google/google-authenticator/wiki/Key-Uri-Format#label
* @return string
* @throws \Exception
*/
public function validateLabel($label, $issuer)
{
$msg =
'Your label cannot contain more than a single colon separating the issuer from the label, i.e. "issuer:label".' .
' Examples of valid labels are "YourCompany:userlogin@yoursite.com" and "BigCoNamespace:JohnDoe".' .
' Please read: https://github.com/google/google-authenticator/wiki/Key-Uri-Format#label';
// it's possible the label doesn't have an issuer
$hasColon = strpos($label, ':') !== false;
$hasColonEncoded = strpos($label, '%3A') !== false;
if (!$hasColon && !$hasColonEncoded) {
return urlencode($label);
}
// if we have an issuer and the label has a colon, we need to verify the label's issuer matches
$parts = $hasColon ? explode(':', $label) : explode('%3A', $label);
if (count($parts) != 2) {
throw new Exception($msg);
}
if (!empty($issuer)) {
if ($parts[0] !== $issuer) {
throw new Exception($msg);
}
return rawurlencode($parts[0]) . ':' . rawurlencode($parts[1]);
}
return rawurlencode($label);
}
/**
* A timing safe string equality check thanks to Anthony Ferrera.
*
* @access protected
* @param string $safe The internal (safe) value to be checked
* @param string $user The user submitted (unsafe) value
* @link http://blog.ircmaxell.com/2014/11/its-all-about-time.html
* @return boolean True if the two strings are identical.
*/
protected function strCompare($safe, $user)
{
if (version_compare(phpversion(), '5.6.0', '>')) {
return hash_equals($safe, $user);
}
$safeLen = $this->safeStrlen($safe);
$userLen = $this->safeStrlen($user);
if ($userLen != $safeLen) {
return false;
}
$result = 0;
for ($i = 0; $i < $userLen; $i++) {
$result |= (ord($safe[$i]) ^ ord($user[$i]));
}
// They are only identical strings if $result is exactly 0...
return $result === 0;
}
/**
* Return the length of a string, even in the presence of mbstring.func_overload.
*
* @access protected
* @param string $string The string we're measuring
* @return int
*/
protected function safeStrlen($string)
{
if (function_exists('mb_strlen')) {
return mb_strlen($string, '8bit');
}
return strlen($string);
}
/**
* Return a string contained within a string, even in the presence of mbstring.func_overload.
*
* @access protected
* @param string $string The string we're searching
* @param int $start What offset should we begin
* @param int|null $length How long should the substring be? (default: the remainder)
* @return string
*/
protected function safeSubstr($string, $start = 0, $length = null)
{
if (function_exists('mb_substr')) {
return mb_substr($string, $start, $length, '8bit');
} elseif ($length !== null) {
return substr($string, $start, $length);
}
return substr($string, $start);
}
/**
* Return the set of allowable characters in a base32 string.
*
* @access protected
* @return string
*/
protected function getBase32Characters()
{
return 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
}
}
|