Skip to content

Instantly share code, notes, and snippets.

@yusukemurayama
Last active January 13, 2016 06:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save yusukemurayama/0b1f75abf237f9842ce1 to your computer and use it in GitHub Desktop.
Save yusukemurayama/0b1f75abf237f9842ce1 to your computer and use it in GitHub Desktop.
<?php
// https://github.com/ChristianRiesen/base32 を利用しました。
require_once 'Base32.php';
use Base32\Base32;
class Totp {
/**
* コンストラクタ
*
* @param string $secret_key 秘密鍵
* @param int $token_length 生成するトークンの長さ
* @param int $period トークン生成間隔
* @param string $algorithm アルゴリズム(sha1, sha256, sha512)
*/
function __construct($secret_key, $token_length = 6, $period = 30, $algorithm = 'sha1') {
$this->secret_key = $secret_key;
$this->token_length = $token_length;
$this->period = $period;
$this->algorithm = $algorithm;
}
/**
* トークンを生成します。
*
* @param int clock TOTPトークン生成に使うタイムスタンプ
* @return string 生成したトークン
*/
public function generate($clock = null) {
if ($clock === null) {
// clockの指定がない場合は、現在のタイムスタンプを取得します。
// ※clockを指定するケースは、現在時刻より30秒前や、30秒後のトークンを取得したい場合です。
$clock = time();
}
// 秘密鍵をBase32でデコードします。
$key = Base32::decode($this->secret_key);
# 以下、TOTPの仕様(RFC6238)を参考にトークンを生成します。
# 参考: https://tools.ietf.org/html/rfc6238
$moving_factor = floor($clock / $this->period);
$b = [];
while ($moving_factor > 0) {
$b[] = chr($moving_factor & 0xff);
$moving_factor >>= 8;
}
$text = str_pad(implode('', array_reverse($b)), 8, "\0", STR_PAD_LEFT);
$hash = hash_hmac($this->algorithm, $text, $key, true);
$offset = ord($hash[19]) & 0xf;
$token_base = (ord($hash[$offset]) & 0x7f) << 24
| (ord($hash[$offset + 1]) & 0xff) << 16
| (ord($hash[$offset + 2]) & 0xff) << 8
| (ord($hash[$offset + 3]) & 0xff);
// 規定の長さを取り出します。
$token = $token_base % pow(10, $this->token_length);
// 桁数が足りない場合は0をつめて返します。
return str_pad($token, $this->token_length, 0, STR_PAD_LEFT);
}
/**
* トークンが正しいかをチェックします。
*
* @param string $token チェック対象のトークン
* @return boolean 引数のトークンと生成したトークンが一致すればTRUE、しない場合はFALSE
*/
public function is_valid_old($token) {
if (gettype($token) !== 'string') {
// string型でない場合はFALSEを返します。
return false;
}
if (!preg_match(sprintf('/[0-9]{%d}/', $this->token_length), $token)) {
// 0から9がN個並んでいない場合はFALSEを返します。
return false;
}
return $token === $this->generate();
}
/**
* トークンが正しいかをチェックします。
*
* @param string $token チェック対象のトークン
* @return boolean 引数のトークンと生成したトークンが一致すればTRUE、しない場合はFALSE
*/
public function is_valid($token) {
if (gettype($token) !== 'string') {
// string型でない場合はFALSEを返します。
return false;
}
if (!preg_match(sprintf('/[0-9]{%d}/', $this->token_length), $token)) {
// 0から9がN個並んでいない場合はFALSEを返します。
return false;
}
$clock = time();
foreach ([0, -1, 1] as $idx) {
// 時計のズレなどを考慮して、前後1つもチェックし、一致していればOKとします。
if ($token === $this->generate($clock + $idx * $this->period)) {
return true;
}
}
return false;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment