最近公司为了过网站的等保三级,对网站的安全性做了全面升级。后台的安全性要求比较高,所以在公司的要求下,我给后台做了一个动态口令登录模块。
上图就是我们公司后台的登录效果图,除了常规的用户名,还有一个动态口令验证。
这个动态口令用到的第一个是auth算法类:
class Auth_Oath
{
const keyRegeneration = 30; // Interval between key regeneration
const otpLength = 6; // Length of the Token generated
private static $lut = array( // Lookup needed for Base32 encoding
"A" => 0, "B" => 1,
"C" => 2, "D" => 3,
"E" => 4, "F" => 5,
"G" => 6, "H" => 7,
"I" => 8, "J" => 9,
"K" => 10, "L" => 11,
"M" => 12, "N" => 13,
"O" => 14, "P" => 15,
"Q" => 16, "R" => 17,
"S" => 18, "T" => 19,
"U" => 20, "V" => 21,
"W" => 22, "X" => 23,
"Y" => 24, "Z" => 25,
"2" => 26, "3" => 27,
"4" => 28, "5" => 29,
"6" => 30, "7" => 31
);
/**
* Generates a 16 digit secret key in base32 format
* @return string
**/
public static function generate_secret_key($length = 16) {
$b32 = "234567QWERTYUIOPASDFGHJKLZXCVBNM";
$s = "";
for (i = 0;i < length;i++)
s .=b32[rand(0,31)];
return $s;
}
/**
* Returns the current Unix Timestamp devided by the keyRegeneration
* period.
* @return integer
**/
public static function get_timestamp() {
return floor(microtime(true)/self::keyRegeneration);
}
/**
* Decodes a base32 string into a binary string.
**/
public static function base32_decode($b32) {
b32 = strtoupper(b32);
if (!preg_match('/^[ABCDEFGHIJKLMNOPQRSTUVWXYZ234567]+/',b32, $match))
throw new Exception('Invalid characters in the base32 string.');
l = strlen(b32);
$n = 0;
$j = 0;
$binary = "";
for (i = 0;i < l;i++) {
n =n << 5; // Move buffer left by 5 to make room
n =n + self::lut[b32[$i]]; // Add value into buffer
j =j + 5; // Keep track of number of bits in buffer
if ($j >= 8) {
j =j - 8;
binary .= chr((n & (0xFF << j)) >>j);
}
}
return $binary;
}
/*by tang*/
public static function base32_encode(data,length){
$basestr = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
$count = 0;
if ($length > 0) {
buffer =data[0];
$next = 1;
$bitsLeft = 8;
while ((bitsLeft>0 ||next < $length)) {
if ($bitsLeft < 5) {
if (next<length) {
$buffer <<= 8;
buffer |=data[$next++] & 0xFF;
$bitsLeft += 8;
} else {
pad = 5 -bitsLeft;
buffer <<=pad;
bitsLeft +=pad;
}
}
index = 0x1F&(buffer >> ($bitsLeft - 5));
$bitsLeft -= 5;
result .=basestr[$index];
$count++;
}
}
return $result;
}
/**
* Takes the secret key and the timestamp and returns the one time
* password.
*
* @param binary $key - Secret key in binary form.
* @param integer $counter - Timestamp as returned by get_timestamp.
* @return string
**/
public static function oath_hotp(key,counter)
{
if (strlen($key) < 8)
throw new Exception('Secret key is too short. Must be at least 16 base 32 characters');
bin_counter = pack('N*', 0) . pack('N*',counter); // Counter must be 64-bit int
hash = hash_hmac ('sha1',bin_counter, $key, true);
return str_pad(self::oath_truncate($hash), self::otpLength, '0', STR_PAD_LEFT);
}
/**
* Verifys a user inputted key against the current timestamp. Checks $window
* keys either side of the timestamp.
*
* @param string $b32seed
* @param string $key - User specified key
* @param integer $window
* @param boolean $useTimeStamp
* @return boolean
**/
public static function verify_key(b32seed,key, window = 5,useTimeStamp = true) {
$timeStamp = self::get_timestamp();
if (useTimeStamp !== true)timeStamp = (int)$useTimeStamp;
binarySeed = self::base32_decode(b32seed);
for (ts =timeStamp - window;ts <= timeStamp +window; $ts++)
if (self::oath_hotp(binarySeed,ts) == $key)
return true;
return false;
}
/**
* Extracts the OTP from the SHA1 hash.
* @param binary $hash
* @return integer
**/
public static function oath_truncate($hash)
{
offset = ord(hash[19]) & 0xf;
return (
((ord(hash[offset+0]) & 0x7f) << 24 ) |
((ord(hash[offset+1]) & 0xff) << 16 ) |
((ord(hash[offset+2]) & 0xff) << 8 ) |
(ord(hash[offset+3]) & 0xff)
) % pow(10, self::otpLength);
}
}
同时验证的时候用的是base32加密和解密的算法:
class Auth_Base32
{
/**
* Alphabet for encoding and decoding base32
*
* @var array
*/
private static $alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567=';
/**
* Creates an array from a binary string into a given chunk size
*
* @param string $binaryString String to chunk
* @param integer $bits Number of bits per chunk
* @return array
*/
private static function chunk(binaryString,bits)
{
binaryString = chunk_split(binaryString, $bits, ' ');
if (substr(binaryString, (strlen(binaryString)) - 1) == ' ') {
binaryString = substr(binaryString, 0, strlen($binaryString)-1);
}
return explode(' ', $binaryString);
}
/**
* Encodes into base32
*
* @param string $string Clear text string
* @return string Base32 encoded string
*/
public static function encode($string)
{
if (strlen($string) == 0) {
// Gives an empty string
return '';
}
// Convert string to binary
$binaryString = '';
foreach (str_split(string) ass) {
// Return each character as an 8-bit binary string
binaryString .= sprintf('%08b', ord(s));
}
// Break into 5-bit chunks, then break that into an array
binaryArray = self::chunk(binaryString, 5);
// Pad array to be divisible by 8
while (count($binaryArray) % 8 !== 0) {
$binaryArray[] = null;
}
$base32String = '';
// Encode in base32
foreach (binaryArray asbin) {
$char = 32;
if (!is_null($bin)) {
// Pad the binary strings
bin = str_pad(bin, 5, 0, STR_PAD_RIGHT);
char = bindec(bin);
}
// Base32 character
base32String .= self::alphabet[$char];
}
return $base32String;
}
/**
* Decodes base32
*
* @param string $base32String Base32 encoded string
* @return string Clear text string
*/
public static function decode($base32String)
{
// Only work in upper cases
base32String = strtoupper(base32String);
// Remove anything that is not base32 alphabet
$pattern = '/[^A-Z2-7]/';
base32String = preg_replace(pattern, '', $base32String);
if (strlen($base32String) == 0) {
// Gives an empty string
return '';
}
base32Array = str_split(base32String);
$string = '';
foreach (base32Array asstr) {
char = strpos(self::alphabet, $str);
// Ignore the padding character
if ($char !== 32) {
string .= sprintf('%05b',char);
}
}
while (strlen($string) %8 !== 0) {
string = substr(string, 0, strlen($string)-1);
}
binaryArray = self::chunk(string, 8);
$realString = '';
foreach (binaryArray asbin) {
// Pad each value to 8 bits
bin = str_pad(bin, 8, 0, STR_PAD_RIGHT);
// Convert binary strings to ASCII
realString .= chr(bindec(bin));
}
return $realString;
}
}
第一步:
先生成一个16位到30位的种子
/**
* 生成随机的字符串
* @param int $length
* @return string
*/
public function generate_password($length = 8)
{
// 密码字符集,可任意添加你需要的字符
chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#%^&*()-_ []{}<>~`+=,.;:/?|';
$password = '';
for (i = 0;i < length;i++) {
// 这里提供两种字符获取方式
// 第一种是使用 substr 截取$chars中的任意一位字符;
// 第二种是取字符数组 $chars 的任意元素
// password .= substr(chars, mt_rand(0, strlen($chars) – 1), 1);
password .=chars[mt_rand (0, strlen ($chars) - 1)];
}
return $password;
}
对生成的种子字符串进行base32加密
$base32 = myApp::getEvent ('Auth_Base32');
rand =this->generate_password (20);//生成随机种子
rand =base32->encode ($rand);
rand = str_replace ('=', '',rand);//去除填充的‘='
将生成的这个密钥保存到具体的用户表中,再用这个密钥生成一个二维码(这个二维码是为了方便登录谷歌的验证器)
该处用的是php的二维码类(详情可以google)
$this->load->library ('Qrcode/Q');
$q = new Q();
text = sprintf ("otpauth://totp/%s?secret=%s",name, $seed);
img =q->outCode($text);
效果如下:
然后用google Authenticator这个软件扫描一下就可以看到6位数字的密码(安卓的需要手动输入密钥,有的安卓不支持扫描);
谷歌验证器如下图:
最后一步就是验证
$object = myApp::getEvent ('Auth_Oath');
flag =object->verify_key(userData['temp_seed'],code);
if (!$flag){
return array(
'code' => -6,
'msg' => '错误的用户名或者错误的动态口令'
);
}
//userData['temp_seed'] 这个是查询出该用于的保存在数据库中的密钥就是用base32加密的哪个字符串code是六位动态口令数字
一般验证成功需要将这次成功的动态口令保存在对应用户的那条数据中,防止在短时间内重复使用。