TOTP サンプルアプリ
2要素認証の動作を確認したい場面があり、 npm の totp-generator をつかって、TOTP の簡単なサンプルアプリを作りました。
キーを入力すると、前後数分間の数字を表示します。
https://7z1gxd.csb.app/
https://github.com/koseki/totp-demo/
TOTP のキーの受け渡し方法
いま普及している2要素認証のクライアントアプリは、TOTP のキーを Base32 でエンコードします。
オープンソース版 Google Authenticator の GitHub リポジトリに、QR コードを生成するための Key URI Format のドキュメントがあります。
REQUIRED: The
secret
parameter is an arbitrary key value encoded in Base32 according to RFC 3548. The padding specified in RFC 3548 section 2.2 is not required and should be omitted.
任意の長さのキーを Base32 (RFC3548) でエンコードします。=
によるパディングは省略します。
otpauth://totp/Example:alice@example.com?secret=JBSWY3DPEHPK3PXP&issuer=Example
といった感じの URI で QR コードを生成します。
Base32 の仕組み
Base32 は入力を 5 バイト (= 40 ビット) ずつに分け、その 40 ビットを 5 ビットずつ 8 文字にエンコードします。
最後に 40 ビットに満たないデータが残った場合は、末尾1文字の余分なビットは 0
で埋めて、残りの文字を =
でパディングします。
下図は、末尾のデータが 1 〜 5 バイトの場合のパディングの数と、末尾の文字になにがあり得るかを示しています。
例えば最後に 2 バイト残ったら、16ビットを表現するには 4文字 20 ビット必要になります。4文字目は先頭の 1 ビットだけが有効で残りの 4ビットは 0
にします。残りの 4 文字は =
でパディングします。
- 末尾データが 1 バイトの場合 …… 2 文字必要なので、
=
は 6 個。最終文字の末尾 2 ビットが 0 - 末尾データが 2 バイトの場合 …… 4 文字必要なので、
=
は 4 個。最終文字の末尾 4 ビットが 0 - 末尾データが 3 バイトの場合 …… 5 文字必要なので、
=
は 3 個。最終文字の末尾 1 ビットが 0 - 末尾データが 4 バイトの場合 …… 7 文字必要なので、
=
は 1 個。最終文字の末尾 3 ビットが 0
となり、最終文字が表す値は、それぞれ、4, 16, 8, 2 の倍数になります。
正規表現で書くと、以下のようになります。(https://stackoverflow.com/a/27362880 を改変)
^(?:[A-Z2-7]{8})*(?:[A-Z2-7][AEIMQUY4]={6}|[A-Z2-7]{3}[AQ]={4}|[A-Z2-7]{4}[ACEGIKMOQSUWY246]={3}|[A-Z2-7]{6}[AIQY]=)?$
ちなみに、Base64 は全く同じことを 24 ビット単位、6 ビット 1 文字で行います。3 バイト(8 * 3
)を 4 文字(6 * 4
)にエンコードします。末尾が 1 バイトなら =
は 2 個、2バイトなら =
1 個となります。
2要素認証クライアントの挙動の違いについて
いくつかの TOTP クライアントで挙動を確認しました。
- Authy
- iPhone の Google Authenticator
- MS Authenticator
- iPhone 標準のパスワードアプリ
パディングの有無
iPhone の Google Authenticator は、キーが =
でパディングされていると、エラーで QR コードが読めません。=
を消す必要があります。他はパディングがあっても読み込めていました。
末尾の不正文字
キーの末尾の文字が正しくない場合、Google Authenticator はエラーにします。
例えば、キーが4文字だった場合、上で説明したように末尾の文字は A
か Q
のどちらかです。A
か Q
でなければ、Google Authenticator は不正なキーとして QR コードを読み取りません。
Authy や MS Authenticator、npm の totp-generator
は、余分なビットは切り捨てます。AAAR
は AAAQ
と同じキーになるようでした。
iPhone 標準のパスワードアプリは、余分なビットがあったら残りを 0 で埋めて桁を増やす動作になっていました。AAAR
は AAARA
と同じキーになります。
以下は Python の出力ですが (Python の Base32 は切り捨てる動作をするようです)、
>>> base64.b32decode('AAAQ====')
b'\x00\x01'
>>> base64.b32decode('AAAR====')
b'\x00\x01'
>>> base64.b32decode('AAARA===')
b'\x00\x01\x10'
Authy は、最初の2つが同じキーになります。iOS 標準パスワードアプリは、下の2つが同じキーになります。
不正なキーを生成する意味がないので実際には問題ないのでしょうが、動作に微妙な違いがあって面白かったので、まとめてみました。TOTP の QR コードを生成するときは、キーの長さは 40 bit の倍数にするのが無難そうです。