28
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

いまさらTOTP

Last updated at Posted at 2020-09-23

はじめに(Introduction)

最近、某Web口座がやらかしたので、認証を強化せよ!という風潮が高まってきました。
要するに、IDとパスワード方式に加えて認証を追加する必要が出てきました。

最近では、スマホを利用した「Google Authenticator」や「Microsoft Authenticator」を利用して認証を行うサイトなんかも出てきています。
とりあえずそこで使われているTOTP(Time-Based One-Time Password)を見てみます。

多要素認証(Multi-factor authentication)

認証に用いる要素には大きく4つあるそうです。(いままで3つでしたが、最近4つめが出てきたらしい)

  • 知識要素(Knowledge factors)

    知っているもの、要するにパスワードです。
  • 所持要素(Possession factors)

    持っているもの
  • 切断されたトークン(Disconnected tokens)
    対象に接続せずにトークンを発行します。
  • 接続されたトークン(Connected tokens)
    カードリーダー、USBキーなどで、対象に接続する事で認証します。
  • ソフトウェアトークン(Software tokens)
    ソフトウェアでトークンを発行します。スマホ上で追加できます。
  • 固有の要素(Inherent factors)

    生体認証と呼ばれるものです。
  • ロケーションベースの要素(Location based factors)
    近くにいるときは、PINの入力で認証、離れると他の要素も必要となります。

この4つ(3つ?)から、複数(2つ以上)の要素を利用することでセキュリティを高めること出来きます。
ちなみに、TOTPは、所持要素のソフトウェアトークンとなります。

※:__二段階認証__とは異なります。

OTP(One-Time Password)

RFC6238とRFC4226を見ます。
HTOP(HMAC-Based One-Time Password)のパラメータの一つに、時間を設定する事でTOTP(Time-Based One-Time Password)を実現しているようです。

HOTP(HMAC-Based One-Time Password)

Cは、8バイトのカウンターです。クライアントとサーバーで同期をとる必要があります。
Kは、クライアントとサーバー間で共有された秘密です。
Digitは、桁数です。

Step 1:
まず、KCからHMAC-SHA1を求めます。
HS = HMAC-SHA-1(K,C)
HSは20バイトのハッシュ値です。

Step 2:
HSから31ビット取得します。
まず、HSの最後から4ビットから数値を求めます。
この値をOffsetと呼び、0~15までの数値が得られます。
HSOffsetから4バイト取得します。
この4バイトから、後方31ビットを取得します。
これを、Sbitsとします。

Step 3:
Sbitsを数値にします。
数値の下Digit桁が、ワンタイムパスワードとなります。

TOTP(Time-Based One-Time Password)

HOTPのCを時間としたものです。

Xは、タイムステップを秒単位で表したものです。(デフォルト値は30です。)
T0は、タイムステップのカウントを開始するUnix時間です。(デフォルト値は0です。)

時間を計算します。

T = (Unix時間 - T0) / X(小数点以下切り捨て)
Tを8バイトのデータにして、HOTPを行います。

実装例(Sample)

import hashlib
import hmac


def hotp(K, C, digestmod=hashlib.sha1, Digit=6):
    hmacsha1 = hmac.new(K, C.to_bytes(8, 'big'), digestmod)
    HS = hmacsha1.digest()
    Offset = HS[len(HS)-1] & 0x0f
    Sbits = bytes([HS[Offset] & 0x7f]) + HS[Offset+1:Offset+4]
    Snum = int.from_bytes(Sbits, 'big')
    D = Snum % 10**Digit
    return ("{{:0{:d}d}}".format(Digit)).format(D)


def totp(K, C, digestmod=hashlib.sha1, Digit=6):
    T0 = 0
    X = 30
    T = int((C - T0) / X)
    return hotp(K, T, digestmod, Digit)

Key Uri Format

「Google Authenticator」や「Microsoft Authenticator」に読み込ませるQRコードを作ります。

otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example

とりあえず、アカウント名(accountname)と発行者(issuer)と共有の秘密をBase32エンコードしたものがあればできそうです。

otpauth://totp/<発行者>:<アカウント名>?secret=<秘密のBase32エンコード>&issuer=<発行者>

一応、パラメータにアルゴリズム(algorithm)とか桁数(digits)など設定できるらしいんですが、こんな記述が・・・使えないらしい。

Currently, the algorithm parameter is ignored by the Google Authenticator implementations.
現在、アルゴリズム パラメータは Google Authenticator の実装では無視されています。

参考例

とりあえず、Gistにサンプルコードを作成しました。
https://gist.github.com/tnakagawa/829c1cf098b54240116dc103eaa72f4c

まとめ(Summary)

使い方としては、ユーザー側はスマホで出ている6桁の数値をサーバー側に送ります。
サーバー側は、ユーザーの秘密と現在時刻(Unix時間)で6桁の数値を作成し、
送られてきた6桁の数値と同じであるかを判定します。

サーバー側で秘密を持つ必要がある、ハッシュ関数がSHA1であるなど、
いくつか懸念点がありますが、要素としては簡単に追加できそうです。

参照(References)

28
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
28
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?