05 April, 2021

Pythonで実装するTOTP - Part 1

目次

TOTPとは

TOTPとはTime-Based One Time Passwordと呼ばれる、時間によって変わるパスワードです。 二要素認証の一要素としてパスワードの次に入力する情報として使われるようになってきました。

大きく3種類ある認証情報のうちの所有情報にあたります。

本人が知っていること(知識情報)
ID・パスワード、秘密の質問
本人が持っているもの(所有情報)
電話番号(SMS)・ワンタイムパスワード
本人自身の特徴(生体情報)
指紋認証、顔認証

このTOTPは、いったいどのように実現しているのでしょうか。 TOTPなので時刻ベース、となるので時間情報が6桁の数字を決める要素に含まれていることは 間違いありませんが、時間だけであればみな同じになってしまいます。 そのため、個人ごとに作成した秘密鍵を使って、時刻と一緒に計算して6桁を作り出します。 時間の解像度として毎秒だと、入力している間に値が変わってしまいますので、ある程度長い スパンが使われます。一般的には30秒毎になります。Google Authenticatorは30秒固定の ようです。

では、秘密鍵と30秒単位の時刻情報等入力で6桁の数字を作ってみましょう。

秘密鍵は特定の長さのビット列です。便宜上8ビットずつに区切ったバイナリのバイト列、 として扱います。 TOTPでは80ビットの鍵を使用しているようです。詳細はRFC6238を読んでもらうとして、 10バイトのバイナリ値は扱いにくいのでAuthenticatorに設定する鍵はバイナリ値を Base32でエンコードした文字列になっています。
10バイトのバイナリをBase32でエンコードすると16文字の文字列になります。

Google Authenticatorに入力するSecretはこの16文字の文字列です。

秘密鍵が80ビットのバイナリであることはわかりました。 では時刻情報はどのような値をとるのでしょうか。 正解は1970年1月1日の0時0分からの秒数です。通称タイムスタンプ(timestamp)です。 1970年1月1日の0時0分はUNIX Epochタイムと呼ばれていてコンピュータの世界の時間の起点と なっています。 日常扱う時刻とは地域毎に時差のあるローカルタイムを使用しています。 世界中のある一瞬を表すのに場所情報が含まれると面倒なことになります。 なので、協定世界時(UTC)での1970年1月1日0時0分からの秒数を使います。 ちなみに時間にはたくさんの種類があります。 既出のローカルタイム(日本では日本標準時[JST])や協定世界時(UTC)、国際原子時(TAI) などです。かつてはGMTという言い方もよく使われていました。

さて、このタイムスタンプですが、システム、特に使用言語のライブラリによって 整数の秒だったり浮動小数点の秒だったり、ですが、ここでは整数の秒として扱います。 すでに説明した時間の解像度が通常30秒だと言いましたが、この時間をRFC6238では 「time-step size」と呼んでいます。また、通常30秒であることについてRFC6238では デフォルトとして30秒を推奨しています。理由はセキュリティと使い勝手の兼ね合いだと 説明しています。長すぎると攻撃に弱くなり、短すぎると表示されてから入力するまでの 時間が足りなくなってしまいます。

ではプログラム上で使う変数名を決めましょう。後から変わるかもしれませんが。 time-step sizeは長いので「期間」の意味でperiodにします。 タイムスタンプはそのままtimestamp、Base32でエンコードされた秘密鍵はsecret_keyとします。

では実際の計算について説明します。
大きく前半と後半に分かれます。
前半と後半に分けるポイントはHMACをとるところです。
前半はHMAC関数に与える引数を準備するところ、後半はHMACのダイジェストから6桁の数字に 変換するところです。

現状の多くのTOTPはSHA1アルゴリズムを使用しておりGoogle AuthenticatorはSHA1のみを サポートしているようです。
SHA1アルゴリズムを使うのでダイジェストは160ビット(20バイトのバイナリ列)になります。

Pythonで実装するTOTP - Part 2


Tags: , , ,