/** * 基于时间的一次性密钥生成算法,规则: * 1. 从T0开始已经过的时间,每个TI为一个单位,总数记为C。实践当中用时间戳除以间隔秒数(30S)得到 C. * 2. 使用C作为消息,K作为密钥,计算HMAC哈希值H(定义来自之前的HMAC算法,但是大部分加密算法库都有支持)。 K应当保持原样继续传递,C应当以64位的原始无符号的整形数值传递。 * 3. 取H中有意义最后4位数的作为弥补,记为O。 * 4. 取H当中的4位,从O字节MSB开始,丢弃最高有效位,将剩余位储存为(无符号)的32位整形数值I。 * 5. 密码即为基数为10的最低N位数。如果结果位数少于N,从左边开始用0补全。 */ class TOTP{ protected $secretKey; //密钥 public $interval = 30; //30S public $length = 6; //Number of digits public function __construct($secretKey){ $this->secretKey = $secretKey; } public function makeHash($input){ return hash_hmac("sha1", $input, $this->secretKey, $raw_output = true); } public function getTimeCounter($timestamp = null){ $timestamp = $timestamp ?: time(); //将时间计数器转为64位无符号整型。 return pack('N*', '0') . pack('N*', floor($timestamp / $this->interval)); } /* private function rawHash2Hex($rawHash){ $chars =str_split($rawHash); $hex = []; foreach ($chars as $char) { $hex[] = str_pad(base_convert(ord($char), 10, 16), 2, '0', STR_PAD_LEFT); // 位操作也可达到同样的目的 //$byte = ord($char); //$high = (($byte & 0xf0) >> 4) & 0xf; //$low = $byte & 0xf; //$hex[] = sprintf('%x', $high); //$hex[] = sprintf('%x', $low); } return implode('', $hex); }*/ public function truncate($rawHash){ $byteArray = $this->convertToByteArray($rawHash); $lastByte = end($byteArray); $offset = $lastByte & 0xf; //取最后四个bit作为索引偏移量 //从$byteArray的$offset处开始向右取连续4个bytes合并成一个整数。 $binary = (($byteArray[$offset] & 0x7f /*丢弃符号位*/) << 24) | (($byteArray[$offset + 1] & 0xff) << 16) | (($byteArray[$offset + 2] & 0xff) << 8) | ($byteArray[$offset + 3] & 0xff); /* 取4个字节是因为,最大的offset=15, 加上4正好是19,也即是bytesArray的最大索引号。*/ return $binary % pow(10, $this->length); } public function convertToByteArray($rawHash){ return array_map('ord', str_split($rawHash)); } public function now() { return $this->at(); } public function at($timestamp = null){ $tc = $this->getTimeCounter($timestamp); $rawHash = $this->makeHash($tc); $code = $this->zeroPadded($this->truncate($rawHash)); return $code; } private function zeroPadded($number){ return str_pad(strval($number), $this->length, '0', STR_PAD_LEFT); } }Tips: 该实现并未对base32编码之后的secret进行解码,需要自行解码之后再传递。
下面从Github上扒下来的一个函数进行算法验证:
function getTOTPCode($secret, $timeSlice = null) { //https://github.com/PHPGangsta/GoogleAuthenticator if ($timeSlice === null) { $timeSlice = floor(time() / 30); } $secretkey = $secret; // 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(substr($hm, -1)) & 0x0F; // grab 4 bytes of the result $hashpart = substr($hm, $offset, 4); // Unpak binary value $value = unpack('N', $hashpart); $value = $value[1]; // Only 32 bits $value = $value & 0x7FFFFFFF; $modulo = pow(10, 6); return str_pad($value % $modulo,6, '0', STR_PAD_LEFT); }* Google 验证:http://blog.tinisles.com/2011/10/google-authenticator-one-time-password-algorithm-in-javascript/