Skip to main content

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;
	}
}

短時間で走り書きしたのでコメントがおかしい箇所が。。。