2段階認証、MFAつくってみた
最近、iCloudのハッキングがあったせいか、いろんなサービスがやたらと2段階認証をすすめてくるので 自分の管理するウェブサイトにも実装すべく、2段階認証のプログラムを作ってみました。
まずは、今あるサービスを調べたところGoogleやAWSなどで採用している2段階認証は「RFC 6238 Time-Based One-Time Password」という規格?で作られているようです。 この規格で作るとOTP発行用のアプリケーションを作らなくても、既にGoogleが開発したものがあり手間が省けます。
Android: AWS Virtual MFA、Google Authenticator iPhone: Google Authenticator Windows: Phonn Authenticator Blackberry: Google Authenticator
Source: アマゾン ウェブ サービス(AWS 日本語)
使用方法
$mfa = new MFA();
// 認証処理、成功すればTRUE、失敗すればFALSE
$mfa->verify({ワンタイムパスワード}, {シークレットキー});
// ワンタイムパスワード返り値に出力
$mfa->getOneTimePass({シークレットキー});
// シークレットキーを作成します。
$mfa->getKeygen();
使用方法はこのような感じで、基本的には上記のメソッドで認証ができます。
ただ、注意点が1つ、シークレットキーに使用できるのはbase32で16文字のものしか使用できません。
Source
/**
* Multi-Factor Authentication
*
* Copyright (C) 2014 Vatis
* This software is released under the Creative Commons: BY-NC-ND.
* http://creativecommons.org/licenses/by-nc-nd/4.0/
*
* @package com.lalcs
* @author Vatis
* @since PHP 5.3
* @version 1.0.0
*/
class MFA {
// base32
private $_base32String = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
// 認証時に許す時間の誤差
private $_allowTimeGap = 2;
/**
* OTP認証
*
* @access public
* @param string $otp 認証するOTP
* @param string $key 秘密鍵
* @param string $time 時間(デバッグ用)
* @return boolean
*/
public function verify($otp = NULL, $key = NULL, $time = NULL)
{
// 時間が指定されていなければ現在の時間をセット
if ($time == NULL) { $time = time(); }
// タイムギャップを考慮して3つのパスワードを作成
$nowKey[] = $this->getOneTimePass($key, $time);
$nowKey[] = $this->getOneTimePass($key, $time + $this->_allowTimeGap);
$nowKey[] = $this->getOneTimePass($key, $time - $this->_allowTimeGap);
// 比較
if (in_array($otp, $nowKey) == TRUE) { return TRUE; }
else { return FALSE; }
}
/**
* OTP取得
*
* @access public
* @param string $key 秘密鍵
* @param string $time 時間(デバッグ用)
* @return String
*/
public function getOneTimePass($key = NULL, $time = NULL)
{
// 時間が指定されていなければ現在の時間をセット
if ($time == NULL) { $time = time(); }
// 時間鍵を取得
$timeKey = $this->getTimeKey($time);
// バイナリに変換
$bainaryKey = $this->getBainaryKey($key);
$bainaryTime = $this->getBainaryTime($timeKey);
// HMAC-SHA1でハッシュ化、バイナリで出力
$bainaryHash = hash_hmac('sha1', $bainaryTime, $bainaryKey, TRUE);
// 20バイト目を0xfマスクしてオフセット作成
$offset = ord($bainaryHash[19]) & 0xf;
// オフセットを元にビット演算
$bit1 = ((ord($bainaryHash[$offset + 0]) & 0x7f) << 24);
$bit2 = ((ord($bainaryHash[$offset + 1]) & 0xff) << 16);
$bit3 = ((ord($bainaryHash[$offset + 2]) & 0xff) << 8);
$bit4 = ord($bainaryHash[$offset + 3]) & 0xff;
$otp = ($bit1 | $bit2 | $bit3 | $bit4) % pow(10, 6);
// 6bitに変換
$otp = str_pad($otp, 6, '0', STR_PAD_LEFT);
return $otp;
}
/**
* 秘密鍵を作成
*
* @access public
* @param string $length 秘密鍵の桁数
* @return String
*/
public function getKeygen($length = 16)
{
$output = NULL;
for ($i=0; $i < $length; $i++)
{
$output .= substr($this->_base32String, mt_rand(0, strlen($this->_base32String) -1), 1);
}
// Output
return $output;
}
/**
* OTPの残り時間を取得
*
* @access public
* @return int
*/
public function getTimeKeyLimit()
{
return (INT) -(time() % 30 - 30);
}
/**
* 時間鍵を取得
*
* @access private
* @param string $stepTime トークンを再生成する時間
* @return Hex
*/
private function getTimeKey($time = NULL)
{
if ($time == NULL) { return FALSE; }
return floor($time / 30);
}
/**
* 秘密鍵をバイナリに変換
*
* @access private
* @param string $key 16文字のbase32で構成された秘密鍵
* @return Hex
*/
private function getBainaryKey($key)
{
$bainary80bit = '';
for($i = 0; $i < strlen($key); $i++)
{
// base32を10進数で取得
$decimalBase32 = strrpos($this->_base32String, $key[$i]);
// 10進数→2進数に変換
$binary = base_convert($decimalBase32, 10, 2);
// 5bitに変換
$binary5bit = str_pad($binary, 5, '0', STR_PAD_LEFT);
// 結合
$bainary80bit .= $binary5bit;
}
// 20bitごとに配列に変換
$bainary20bits = str_split($bainary80bit, 20);
$hex80bit = '';
foreach($bainary20bits as $bit)
{
// 2進数→16進数変換
$hex = base_convert($bit, 2, 16);
// 20bitに変換
$hex20bit = str_pad($hex, 5, '0', STR_PAD_LEFT);
// 結合
$hex80bit .= $hex20bit;
}
// 16進数文字列をバイナリ文字列に変換
$bainaryHex = pack('H*', $hex80bit);
return $bainaryHex;
}
/**
* 時間鍵をバイナリに変換
*
* @access private
* @param string $time 時間鍵
* @return Hex
*/
private function getBainaryTime($time)
{
// 10進数→16進数に変換
$hex = base_convert($time, 10, 16);
// 64bitに変換
$hex64bit = str_pad($hex, 16, '0', STR_PAD_LEFT);
// 16進数文字列をバイナリ文字列に変換
$bainaryHex = pack('H*', $hex64bit);
return $bainaryHex;
}
}
短時間で走り書きしたのでコメントがおかしい箇所が。。。