/**
* 基于时间的一次性密钥生成算法,规则:
* 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/