首页 PHP 正文
754

兼容google双因子认证的TOTP算法

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

正在加载评论...