多要素認証 TOTP のデモと Base32 エンコードについて

## TOTP サンプルアプリ

2要素認証の動作を確認したい場面があり、 npm の [totp-generator](https://www.npmjs.com/package/totp-generator) をつかって、[TOTP](https://tex2e.github.io/rfc-translater/html/rfc6238.html) の簡単なサンプルアプリを作りました。
キーを入力すると、前後数分間の数字を表示します。

https://7z1gxd.csb.app/ 
:octocat: https://github.com/koseki/totp-demo/
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2596/8921110b-2c13-c919-bf12-df4f7084b7f1.png)

## TOTP のキーの受け渡し方法

いま普及している2要素認証のクライアントアプリは、TOTP のキーを Base32 でエンコードします。

オープンソース版 [Google Authenticator の GitHub リポジトリ](https://github.com/google/google-authenticator/)に、QR コードを生成するための Key URI Format のドキュメントがあります。

https://github.com/google/google-authenticator/wiki/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](https://tex2e.github.io/rfc-translater/html/rfc3548.html)) でエンコードします。`=` によるパディングは省略します。

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

といった感じの URI で QR コードを生成します。

## Base32 の仕組み

Base32 は入力を 5 バイト (= 40 ビット) ずつに分け、その 40 ビットを 5 ビットずつ 8 文字にエンコードします。

最後に 40 ビットに満たないデータが残った場合は、末尾1文字の余分なビットは `0` で埋めて、残りの文字を `=` でパディングします。

下図は、末尾のデータが 1 〜 5 バイトの場合のパディングの数と、末尾の文字になにがあり得るかを示しています。
![base32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2596/b487c910-5198-bc16-9f39-c7d913bedb0f.png)

例えば最後に 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 個となります。
![base64.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2596/2d905c6f-b92f-05bb-e9f1-2f097ac9d7b3.png)

## 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 の倍数にするのが無難そうです。