1<?php 2 3// The MIT License (MIT) 4 5// Copyright (c) 2013 Chris Cornutt 6 7// Permission is hereby granted, free of charge, to any person obtaining a copy 8// of this software and associated documentation files (the "Software"), to deal 9// in the Software without restriction, including without limitation the rights 10// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11// copies of the Software, and to permit persons to whom the Software is 12// furnished to do so, subject to the following conditions: 13 14// The above copyright notice and this permission notice shall be included in 15// all copies or substantial portions of the Software. 16 17// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23// THE SOFTWARE. 24 25namespace GAuth; 26 27/** 28 * A class for generating the codes compatible with the Google Authenticator 29 * clients. 30 * 31 * NOTE: A lot of the logic from this class has been borrowed from this class: 32 * http://www.idontplaydarts.com/wp-content/uploads/2011/07/ga.php_.txt 33 * 34 * @author Chris Cornutt <ccornutt@phpdeveloper.org> 35 * @package GAuth 36 * @license MIT 37 */ 38 39class Auth 40{ 41 /** 42 * Internal lookup table 43 * @var array 44 */ 45 private $lookup = array(); 46 47 /** 48 * Initialization key 49 * @var string 50 */ 51 private $initKey = null; 52 53 /** 54 * Seconds between key refreshes 55 * @var integer 56 */ 57 private $refreshSeconds = 30; 58 59 /** 60 * Length of codes to generate 61 * @var integer 62 */ 63 private $codeLength = 6; 64 65 /** 66 * Range plus/minus for "window of opportunity" on allowed codes 67 * @var integer 68 */ 69 private $range = 2; 70 71 /** 72 * Initialize the object and set up the lookup table 73 * Optionally the Initialization key 74 * 75 * @param string $initKey Initialization key 76 */ 77 public function __construct($initKey = null) 78 { 79 $this->buildLookup(); 80 81 if ($initKey !== null) { 82 $this->setInitKey($initKey); 83 } 84 } 85 86 /** 87 * Build the base32 lookup table 88 * 89 * @return null 90 */ 91 public function buildLookup() 92 { 93 $lookup = array_combine( 94 array_merge(range('A', 'Z'), range(2, 7)), 95 range(0, 31) 96 ); 97 $this->setLookup($lookup); 98 } 99 100 /** 101 * Get the current "range" value 102 * @return integer Range value 103 */ 104 public function getRange() 105 { 106 return $this->range; 107 } 108 109 /** 110 * Set the "range" value 111 * 112 * @param integer $range Range value 113 * @return \GAuth\Auth instance 114 */ 115 public function setRange($range) 116 { 117 if (!is_numeric($range)) { 118 throw new \InvalidArgumentException('Invalid window range'); 119 } 120 $this->range = $range; 121 return $this; 122 } 123 124 /** 125 * Set the initialization key for the object 126 * 127 * @param string $key Initialization key 128 * @throws \InvalidArgumentException If hash is not valid base32 129 * @return \GAuth\Auth instance 130 */ 131 public function setInitKey($key) 132 { 133 if (preg_match('/^['.implode('', array_keys($this->getLookup())).']+$/', $key) == false) { 134 throw new \InvalidArgumentException('Invalid base32 hash!'); 135 } 136 $this->initKey = $key; 137 return $this; 138 } 139 140 /** 141 * Get the current Initialization key 142 * 143 * @return string Initialization key 144 */ 145 public function getInitKey() 146 { 147 return $this->initKey; 148 } 149 150 /** 151 * Set the contents of the internal lookup table 152 * 153 * @param array $lookup Lookup data set 154 * @throws \InvalidArgumentException If lookup given is not an array 155 * @return \GAuth\Auth instance 156 */ 157 public function setLookup($lookup) 158 { 159 if (!is_array($lookup)) { 160 throw new \InvalidArgumentException('Lookup value must be an array'); 161 } 162 $this->lookup = $lookup; 163 return $this; 164 } 165 166 /** 167 * Get the current lookup data set 168 * 169 * @return array Lookup data 170 */ 171 public function getLookup() 172 { 173 return $this->lookup; 174 } 175 176 /** 177 * Get the number of seconds for code refresh currently set 178 * 179 * @return integer Refresh in seconds 180 */ 181 public function getRefresh() 182 { 183 return $this->refreshSeconds; 184 } 185 186 /** 187 * Set the number of seconds to refresh codes 188 * 189 * @param integer $seconds Seconds to refresh 190 * @throws \InvalidArgumentException If seconds value is not numeric 191 * @return \GAuth\Auth instance 192 */ 193 public function setRefresh($seconds) 194 { 195 if (!is_numeric($seconds)) { 196 throw \InvalidArgumentException('Seconds must be numeric'); 197 } 198 $this->refreshSeconds = $seconds; 199 return $this; 200 } 201 202 /** 203 * Get the current length for generated codes 204 * 205 * @return integer Code length 206 */ 207 public function getCodeLength() 208 { 209 return $this->codeLength; 210 } 211 212 /** 213 * Set the length of the generated codes 214 * 215 * @param integer $length Code length 216 * @return \GAuth\Auth instance 217 */ 218 public function setCodeLength($length) 219 { 220 $this->codeLength = $length; 221 return $this; 222 } 223 224 /** 225 * Validate the given code 226 * 227 * @param string $code Code entered by user 228 * @param string $initKey Initialization key 229 * @param string $timestamp Timestamp for calculation 230 * @param integer $range Seconds before/after to validate hash against 231 * @throws \InvalidArgumentException If incorrect code length 232 * @return boolean Pass/fail of validation 233 */ 234 public function validateCode($code, $initKey = null, $timestamp = null, $range = null) 235 { 236 if (strlen($code) !== $this->getCodeLength()) { 237 throw new \InvalidArgumentException('Incorrect code length'); 238 } 239 240 $range = ($range == null) ? $this->getRange() : $range; 241 $timestamp = ($timestamp == null) ? $this->generateTimestamp() : $timestamp; 242 $initKey = ($initKey == null) ? $this->getInitKey() : $initKey; 243 244 $binary = $this->base32_decode($initKey); 245 246 for ($time = ($timestamp - $range); $time <= ($timestamp + $range); $time++) { 247 if ($this->generateOneTime($binary, $time) == $code) { 248 return true; 249 } 250 } 251 return false; 252 } 253 254 /** 255 * Generate a one-time code 256 * 257 * @param string $initKey Initialization key [optional] 258 * @param string $timestamp Timestamp for calculation [optional] 259 * @return string Geneerated code/hash 260 */ 261 public function generateOneTime($initKey = null, $timestamp = null) 262 { 263 $initKey = ($initKey == null) ? $this->getInitKey() : $initKey; 264 $timestamp = ($timestamp == null) ? $this->generateTimestamp() : $timestamp; 265 266 $hash = hash_hmac ( 267 'sha1', 268 pack('N*', 0) . pack('N*', $timestamp), 269 $initKey, 270 true 271 ); 272 273 return str_pad($this->truncateHash($hash), $this->getCodeLength(), '0', STR_PAD_LEFT); 274 } 275 276 /** 277 * Generate a code/hash 278 * Useful for making Initialization codes 279 * 280 * @param integer $length Length for the generated code 281 * @return string Generated code 282 */ 283 public function generateCode($length = 16) 284 { 285 $lookup = implode('', array_keys($this->getLookup())); 286 $code = ''; 287 288 for ($i = 0; $i < $length; $i++) { 289 $code .= $lookup[mt_rand(0, strlen($lookup)-1)]; 290 } 291 292 return $code; 293 } 294 295 /** 296 * Geenrate the timestamp for the calculation 297 * 298 * @return integer Timestamp 299 */ 300 public function generateTimestamp() 301 { 302 return floor(microtime(true)/$this->getRefresh()); 303 } 304 305 /** 306 * Truncate the given hash down to just what we need 307 * 308 * @param string $hash Hash to truncate 309 * @return string Truncated hash value 310 */ 311 public function truncateHash($hash) 312 { 313 $offset = ord($hash[19]) & 0xf; 314 315 return ( 316 ((ord($hash[$offset+0]) & 0x7f) << 24 ) | 317 ((ord($hash[$offset+1]) & 0xff) << 16 ) | 318 ((ord($hash[$offset+2]) & 0xff) << 8 ) | 319 (ord($hash[$offset+3]) & 0xff) 320 ) % pow(10, $this->getCodeLength()); 321 } 322 323 /** 324 * Base32 decoding function 325 * 326 * @param string base32 encoded hash 327 * @throws \InvalidArgumentException When hash is not valid 328 * @return string Binary value of hash 329 */ 330 public function base32_decode($hash) 331 { 332 $lookup = $this->getLookup(); 333 334 if (preg_match('/^['.implode('', array_keys($lookup)).']+$/', $hash) == false) { 335 throw new \InvalidArgumentException('Invalid base32 hash!'); 336 } 337 338 $hash = strtoupper($hash); 339 $buffer = 0; 340 $length = 0; 341 $binary = ''; 342 343 for ($i = 0; $i < strlen($hash); $i++) { 344 $buffer = $buffer << 5; 345 $buffer += $lookup[$hash[$i]]; 346 $length += 5; 347 348 if ($length >= 8) { 349 $length -= 8; 350 $binary .= chr(($buffer & (0xFF << $length)) >> $length); 351 } 352 } 353 354 return $binary; 355 } 356}