Python で HOTP および TOTP を計算する

Google Authenticator 等で利用されている HOTP (HMAC-Based One-Time Password) や TOTP (Time-Based One-Time Password) を Python で計算する方法を書きます。

"HMAC-Based" は「カウンタベース」、"Time-Based" は「時間ベース」や「タイムベース」と訳されます。

参考「[RFC 4226 - HOTP: An HMAC-Based One-Time Password Algorithm 日本語訳](https://tex2e.github.io/rfc-translater/html/rfc4226.html)」
参考「[RFC 6238 - TOTP: Time-Based One-Time Password Algorithm 日本語訳](https://tex2e.github.io/rfc-translater/html/rfc6238.html)」

# 1. 鍵を生成する

後述しますが、本記事では HOTP も TOTP も `HMAC-SHA-1` ハッシュ値を用いるため、鍵の長さは 20 バイトにします。

HOTP や TOTP の仕様として決められているわけではないですが、多くの場合、鍵を Base32 エンコードしてやり取りします。

```python
import secrets
import base64

# 鍵を生成
seed = secrets.token_bytes(20)

print(seed)

seed_string = base64.b32encode(seed)
print(seed_string)
```

```text:実行結果の例
b'\xdd\x1e&%\xa8\xda\x7f\xa3\xb8k\x80\x9e\xd9 \xc8h\x8fS\x83F'
b'3UPCMJNI3J72HODLQCPNSIGINCHVHA2G'
```

鍵は乱数で生成しますが、暗号強度の弱い `random` モジュールでなく暗号強度の強い `secrets` モジュールを用います。

参考「[secrets.token_bytes - secrets --- 機密を扱うために安全な乱数を生成する — Python 3.11.5 ドキュメント](https://docs.python.org/ja/3/library/secrets.html#secrets.token_bytes)」

鍵をやり取りするために鍵を Base32 エンコードした文字列を得ます。

# 2. HOTP を計算する

HOTP (HMAC-Based One-Time Password) は名前の通り HMAC を利用します。

「鍵」と「カウンタ」と呼ばれる値から HMAC ハッシュ値を求めます。

仕様上はダイジェスト関数に `SHA-256` や `SHA-512` を用いることができますが、多くの場合 `SHA-1` が使われるため、ここでは `HMAC-SHA-1` ハッシュ値を使用します。

`HMAC-SHA-1` の場合は鍵の長さを 20 バイトにします。

「カウンタ」はビッグエンディアン形式で長さは 8 バイトとします。

カウンタの初期値は自由ですが、0 または 1 に設定される場合が多いようです (Google Authenticator で鍵を手動入力した場合は 1 に設定されるようです) 。

ここでは HOTP の桁数は良く使われている 6 とします。

```python
import hmac
import hashlib

# 
def dynamic_truncate(digest_bytes: bytes) -> int:

    """動的切り捨てする"""

    # 下位 4 ビットを offset とする
    # len(digest_bytes) == 20
    offset = digest_bytes[19] & 0xf

    # offset から 4 バイトの数値を得る
    binary = int.from_bytes(digest_bytes[offset : offset + 4], byteorder='big')

    # 符号有無の混乱を防ぐために最上位ビットを除外する
    binary_masked = binary & 0x7fffffff

    return binary_masked

def generate_hotp(seed: bytes, counter: bytes) -> str:

    """HOTP を計算する

    カウンタはビッグエンディアンで 8 バイト

    """

    digest_bytes = hmac.new(seed, counter, hashlib.sha1).digest()

    otp = dynamic_truncate(digest_bytes) % 1000000

    otp_string = str(otp).zfill(6)

    return otp_string
```

Python では `hmac` モジュールを用いて `HMAC-SHA-1` ハッシュ値を計算できます。

参考「[hmac --- メッセージ認証のための鍵付きハッシュ化 — Python 3.11.5 ドキュメント](https://docs.python.org/ja/3/library/hmac.html)」

`HMAC-SHA-1` ハッシュ値の長さは 20 バイトですが、HOTP の長さをそこまで長くしないため、RFC 4226 で定義されている「動的切り捨て (dynamic truncation)」を行って 4 バイトの長さの値を得ます (下位 4 ビットをオフセットとみなし、そのオフセットから長さ 4 バイトを切り出す) 。

また、32 ビットの値の符号有無により後の計算結果が異なることを防ぐため、最上位ビットを除外して 31 ビットの値にします。

HOTP の桁数を 6 にするため、$10^6 = 1000000$ で割った余りを求めます。

参考「[5.3. Generating an HOTP Value - RFC 4226 - HOTP: An HMAC-Based One-Time Password Algorithm 日本語訳](https://tex2e.github.io/rfc-translater/html/rfc4226.html#5-3--Generating-an-HOTP-Value)」

```python:例
# HOTP を計算する
seed = b'\xdd\x1e&%\xa8\xda\x7f\xa3\xb8k\x80\x9e\xd9 \xc8h\x8fS\x83F'

counter = (1).to_bytes(8, byteorder='big')

hotp = generate_hotp(seed, counter)
print(hotp)
```

```text:例の実行結果
836609
```

※ HOTP を実用的に使うは別途「ブルートフォース攻撃対策」や「カウンタ再同期」の機能を実装する必要があります。

参考「[7. Security Requirements - RFC 4226 - HOTP: An HMAC-Based One-Time Password Algorithm 日本語訳](https://tex2e.github.io/rfc-translater/html/rfc4226.html#7--Security-Requirements)」

# 3. TOTP を計算する

TOTP は、秒単位の UNIX 時間から「ステップ数」と呼ばれる値を計算し、その値をカウンタの値として HOTP を計算します。

「ステップ数」は UNIX 時間を「時間ステップ」と呼ばれる時間で割った商です。

「時間ステップ」は多くの場合 30 秒とされるため、ここでも 30 秒とします。

```python
import time

# 
time_step = 30

# 
def get_current_unix_time() -> int:
    return int(time.time())

def get_current_steps() -> int:
    return get_current_unix_time() // time_step

def generate_totp(seed: bytes, steps: int) -> str:

    """TOTP を計算する"""

    steps_bytes = steps.to_bytes(8, byteorder='big')

    otp_string = generate_hotp(seed, steps_bytes)

    return otp_string
```

Python では `time.time()` を用いて秒単位の UNIX 時間を取得できます。

参考「[time.time - time --- 時刻データへのアクセスと変換 — Python 3.11.5 ドキュメント](https://docs.python.org/ja/3/library/time.html#time.time)」

前述の通り HOTP のカウンタは 8 バイトの長さで扱うため、ステップ数を 8 バイトにします。

```python:例
# TOTP を計算する
seed = b'\xdd\x1e&%\xa8\xda\x7f\xa3\xb8k\x80\x9e\xd9 \xc8h\x8fS\x83F'

totp = generate_totp(seed, get_current_steps())
print(totp)
```

```text:例の実行結果の例
524240
```

# 4. Google Authenticator で HOTP または TOTP を利用する

Google Authenticator の「セットアップキーを入力 (Enter a setup key)」から、前述の `seed_string` の値を入力し、「カウンタベース (Counter based)」または「時間ベース (Time based)」を選択することで、Google Authenticator 上にコードが表示されるようになります。

HOTP の場合、カウンタが同期されていれば前述のコードの `hotp` の値と一致します。

TOTP の場合、時刻がしっかり同期されていれば前述のコードの `totp` の値と一致します。

※ HOTP または TOTP 用の QR コードの生成方法は後述。

# 5. TOTP で時間のズレを許容する

本記事では深く触れませんが、前回や次回の TOTP を許容することで多少の時間のズレがあってもコードを受け入れることができます。

```python:例
# TOTP を計算する
seed = b'\xdd\x1e&%\xa8\xda\x7f\xa3\xb8k\x80\x9e\xd9 \xc8h\x8fS\x83F'

steps = get_current_steps()

totp_prev = generate_totp(seed, steps - 1)
print(totp_prev)

totp = generate_totp(seed, steps)
print(totp)

totp_next = generate_totp(seed, steps + 1)
print(totp_next)
```

```text:例の実行結果の例
262912
147845
014180
```

# 6. HOTP または TOTP 用の QR コードを生成する

鍵等をやり取りするために「otpauth URI」が用いられます。

otpauth URI を QR コードにすることで Google Authenticator 等で読み込めるようになります。

TOTP の場合は以下のようにして otpauth URI を生成できます。

```python:例
import urllib.parse

# 
time_step = 30

# 
seed_string = '3UPCMJNI3J72HODLQCPNSIGINCHVHA2G'

# otpauth URI を生成する
issuer = urllib.parse.quote('Test Issuer')
accountname = urllib.parse.quote('Test Account Name')

type = 'totp'
label = f'{issuer}:{accountname}'
parameters = f'secret={seed_string}&issuer={issuer}&algorithm=SHA1&digits=6&period={time_step}'

otpauth_uri = f'otpauth://{type}/{label}?{parameters}'

print(otpauth_uri)
```

```text:例の実行結果
otpauth://totp/Test%20Issuer:Test%20Account%20Name?secret=3UPCMJNI3J72HODLQCPNSIGINCHVHA2G&issuer=Test%20Issuer&algorithm=SHA1&digits=6&period=30
```

参考「[Key Uri Format · google/google-authenticator Wiki · GitHub](https://github.com/google/google-authenticator/wiki/Key-Uri-Format)」

---

※QRコードは株式会社デンソーウェーブの登録商標です。