1<?php 2 3/** 4 * PHP Class for handling Google Authenticator 2-factor authentication 5 * 6 * @author Michael Kliewe 7 * @copyright 2012 Michael Kliewe 8 * @license http://www.opensource.org/licenses/bsd-license.php BSD License 9 * @link http://www.phpgangsta.de/ 10 */ 11 12class PHPGangsta_GoogleAuthenticator 13{ 14 protected $_codeLength = 6; 15 16 /** 17 * Create new secret. 18 * 16 characters, randomly chosen from the allowed base32 characters. 19 * 20 * @param int $secretLength 21 * @return string 22 */ 23 public function createSecret($secretLength = 16) 24 { 25 $validChars = $this->_getBase32LookupTable(); 26 unset($validChars[32]); 27 28 $secret = ''; 29 for ($i = 0; $i < $secretLength; $i++) { 30 $secret .= $validChars[array_rand($validChars)]; 31 } 32 return $secret; 33 } 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($secret, $timeSlice = null) 43 { 44 if ($timeSlice === null) { 45 $timeSlice = floor(time() / 30); 46 } 47 48 $secretkey = $this->_base32Decode($secret); 49 50 // Pack time into binary string 51 $time = chr(0).chr(0).chr(0).chr(0).pack('N*', $timeSlice); 52 // Hash it with users secret key 53 $hm = hash_hmac('SHA1', $time, $secretkey, true); 54 // Use last nipple of result as index/offset 55 $offset = ord(substr($hm, -1)) & 0x0F; 56 // grab 4 bytes of the result 57 $hashpart = substr($hm, $offset, 4); 58 59 // Unpak binary value 60 $value = unpack('N', $hashpart); 61 $value = $value[1]; 62 // Only 32 bits 63 $value = $value & 0x7FFFFFFF; 64 65 $modulo = pow(10, $this->_codeLength); 66 return str_pad($value % $modulo, $this->_codeLength, '0', STR_PAD_LEFT); 67 } 68 69 /** 70 * Get QR-Code URL for image, from google charts 71 * 72 * @param string $name 73 * @param string $secret 74 * @return string 75 */ 76 public function getQRCodeGoogleUrl($name, $secret) { 77 $urlencoded = urlencode('otpauth://totp/'.$name.'?secret='.$secret.''); 78 return 'https://chart.googleapis.com/chart?chs=200x200&chld=M|0&cht=qr&chl='.$urlencoded.''; 79 } 80 81 /** 82 * Check if the code is correct. This will accept codes starting from $discrepancy*30sec ago to $discrepancy*30sec from now 83 * 84 * @param string $secret 85 * @param string $code 86 * @param int $discrepancy This is the allowed time drift in 30 second units (8 means 4 minutes before or after) 87 * @return bool 88 */ 89 public function verifyCode($secret, $code, $discrepancy = 1) 90 { 91 $currentTimeSlice = floor(time() / 30); 92 93 for ($i = -$discrepancy; $i <= $discrepancy; $i++) { 94 $calculatedCode = $this->getCode($secret, $currentTimeSlice + $i); 95 if ($calculatedCode == $code ) { 96 return true; 97 } 98 } 99 100 return false; 101 } 102 103 /** 104 * Set the code length, should be >=6 105 * 106 * @param int $length 107 * @return PHPGangsta_GoogleAuthenticator 108 */ 109 public function setCodeLength($length) 110 { 111 $this->_codeLength = $length; 112 return $this; 113 } 114 115 /** 116 * Helper class to decode base32 117 * 118 * @param $secret 119 * @return bool|string 120 */ 121 protected function _base32Decode($secret) 122 { 123 if (empty($secret)) return ''; 124 125 $base32chars = $this->_getBase32LookupTable(); 126 $base32charsFlipped = array_flip($base32chars); 127 128 $paddingCharCount = substr_count($secret, $base32chars[32]); 129 $allowedValues = array(6, 4, 3, 1, 0); 130 if (!in_array($paddingCharCount, $allowedValues)) return false; 131 for ($i = 0; $i < 4; $i++){ 132 if ($paddingCharCount == $allowedValues[$i] && 133 substr($secret, -($allowedValues[$i])) != str_repeat($base32chars[32], $allowedValues[$i])) return false; 134 } 135 $secret = str_replace('=','', $secret); 136 $secret = str_split($secret); 137 $binaryString = ""; 138 for ($i = 0; $i < count($secret); $i = $i+8) { 139 $x = ""; 140 if (!in_array($secret[$i], $base32chars)) return false; 141 for ($j = 0; $j < 8; $j++) { 142 $x .= str_pad(base_convert(@$base32charsFlipped[@$secret[$i + $j]], 10, 2), 5, '0', STR_PAD_LEFT); 143 } 144 $eightBits = str_split($x, 8); 145 for ($z = 0; $z < count($eightBits); $z++) { 146 $binaryString .= ( ($y = chr(base_convert($eightBits[$z], 2, 10))) || ord($y) == 48 ) ? $y:""; 147 } 148 } 149 return $binaryString; 150 } 151 152 /** 153 * Helper class to encode base32 154 * 155 * @param string $secret 156 * @param bool $padding 157 * @return string 158 */ 159 protected function _base32Encode($secret, $padding = true) 160 { 161 if (empty($secret)) return ''; 162 163 $base32chars = $this->_getBase32LookupTable(); 164 165 $secret = str_split($secret); 166 $binaryString = ""; 167 for ($i = 0; $i < count($secret); $i++) { 168 $binaryString .= str_pad(base_convert(ord($secret[$i]), 10, 2), 8, '0', STR_PAD_LEFT); 169 } 170 $fiveBitBinaryArray = str_split($binaryString, 5); 171 $base32 = ""; 172 $i = 0; 173 while ($i < count($fiveBitBinaryArray)) { 174 $base32 .= $base32chars[base_convert(str_pad($fiveBitBinaryArray[$i], 5, '0'), 2, 10)]; 175 $i++; 176 } 177 if ($padding && ($x = strlen($binaryString) % 40) != 0) { 178 if ($x == 8) $base32 .= str_repeat($base32chars[32], 6); 179 elseif ($x == 16) $base32 .= str_repeat($base32chars[32], 4); 180 elseif ($x == 24) $base32 .= str_repeat($base32chars[32], 3); 181 elseif ($x == 32) $base32 .= $base32chars[32]; 182 } 183 return $base32; 184 } 185 186 /** 187 * Get array with all 32 characters for decoding from/encoding to base32 188 * 189 * @return array 190 */ 191 protected function _getBase32LookupTable() 192 { 193 return array( 194 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', // 7 195 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', // 15 196 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', // 23 197 'Y', 'Z', '2', '3', '4', '5', '6', '7', // 31 198 '=' // padding char 199 ); 200 } 201} 202