2段階認証、MFAつくってみた

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

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

Profile

Kazuki Hayashi

I'm a full stack engineer.
I love programming and alcohol.

TOC