N2
NanToo
TOTP の仕組み — 6 桁コードがどう 30 秒同期するのか、RFC 6238 と HMAC-SHA-1 を読む
DDEVELOPER
開発9 分で読める

TOTP の仕組み — 6 桁コードがどう 30 秒同期するのか、RFC 6238 と HMAC-SHA-1 を読む

Google Authenticator や Authy、1Password、Microsoft Authenticator のいずれを使っていても、画面に表示される 6 桁の数字は 30 秒ごとに切り替わり、世界中のサーバと完全に同期しています。サーバとの通信はないのに、なぜ一致するのか。本記事では RFC 4226 (HOTP, 2005)RFC 6238 (TOTP, 2011)、その基盤となる HMAC-SHA-1 の数学を擬似コードで解き、Passkey (FIDO2 / WebAuthn) への移行までを整理します。

#TOTP#二段階認証#RFC 6238#HMAC#セキュリティ

なぜ通信なしで「同じ 6 桁」が出るのか

TOTP の本質は単純です。サーバとクライアントが事前に共有秘密 (shared secret) を持ち、現在時刻を入力にして同じハッシュ計算を行い、結果を 6 桁に切り出す。それだけ。

サーバ側:        TOTP(secret, 現在時刻 / 30) → 例: 482931
ユーザー端末側:  TOTP(secret, 現在時刻 / 30) → 例: 482931
                ↑ 入力が同じなら出力も同じ

その共有秘密は QR コードを 1 度だけ読み取った時に交換され、以降はサーバとも端末とも通信しません。完全オフラインで 6 桁が一致します (Authenticator アプリは飛行機モードでも動く)。

前段: RFC 4226 HOTP — カウンタベースのワンタイムパスワード

2005 年、IETF が RFC 4226 "HOTP: An HMAC-Based One-Time Password Algorithm" を発行。OATH (Open AuTHentication) コンソーシアムが標準化を推進しました。

HOTP の式:

HOTP(K, C) = Truncate(HMAC-SHA-1(K, C))

K: 共有鍵 (Key, 通常 160 bit / 20 bytes)
C: カウンタ (Counter, 8 bytes 整数、認証ごとに +1)
HMAC-SHA-1: 鍵付きハッシュ関数 (RFC 2104)
Truncate: 20 bytes ハッシュから 6-8 桁の数字を切り出す

カウンタ C は認証成功のたびにクライアント側とサーバ側で同期しながら +1 されます。問題はカウンタずれ: ユーザーが認証画面でコードを生成して使わずに閉じると、クライアント側だけが +1 してずれる。RFC 4226 はサーバが ±50 ステップ程度を試行することで吸収する仕様。

OATH ハードトークン (古い銀行の専用デバイスや YubiKey の HOTP モード) はこの方式です。

RFC 6238 TOTP — カウンタを「時刻」に置き換える

2011 年、IETF が RFC 6238 "TOTP: Time-Based One-Time Password Algorithm" を発行。HOTP のカウンタずれ問題を、カウンタを世界の時刻に固定することで解消しました。

TOTP(K, T) = HOTP(K, T)

T = floor((Current Unix Time - T0) / X)
T0: 起点 (デフォルト 0、つまり 1970-01-01 00:00 UTC)
X:  ステップ秒数 (デフォルト 30)
Current Unix Time: 現在の Unix エポック秒

例: 2026-04-30 12:00:00 UTC は Unix Time 1782475200 → 30 で割って T = 59415840。サーバとクライアントが同じ秒に「今が T = 59415840 のステップだ」と認識すれば、出力 6 桁も一致します。

30 秒ステップなので、コードが切り替わるのは「秒数が 30 で割り切れる瞬間」。スマホ画面の 30 秒バーはこの境界をカウントダウンしているだけ。

Dynamic Truncation — 20 byte ハッシュから 6 桁を取り出す

HMAC-SHA-1 の出力は 160 bit = 20 bytes。これを 6 桁の整数 (10⁶ 通り = 約 20 bit) に削るのが Dynamic Truncation。RFC 4226 §5.3 で厳密に定義:

// h = HMAC-SHA-1(K, C) — 20 bytes
offset = h[19] & 0x0F                    // 最下位 4 bit (0-15)
binary = ((h[offset]   & 0x7F) << 24) |  // 4 bytes を取り出して結合
         ((h[offset+1] & 0xFF) << 16) |  // 最上位 bit は 0 にする (符号無し化)
         ((h[offset+2] & 0xFF) <<  8) |
          (h[offset+3] & 0xFF)
otp = binary % 10⁶                       // 6 桁にする (0-999999)

"Dynamic" の由来は、抽出する位置 offset がハッシュ結果から動的に決まること。これにより攻撃者がハッシュの一部だけ予測しても OTP の予測には繋がらない設計。

6 桁にすると同じ値の衝突確率は 1/10⁶ = 10⁻⁶。ブルートフォースを防ぐため、サーバ側は連続失敗時のロックアウトまたはレートリミットを必ず実装します (一般的に 3-5 回ミスでロック)。

時刻ずれの許容 — 30 秒の許容ウィンドウ

クライアント (スマホ) とサーバの時計が完全に一致することはありません。スマホは NTP / OS の時刻同期に依存し、数秒のずれは普通。RFC 6238 §6 では時計ずれ対応として、サーバが 現在ステップ ±1 (合計 3 ステップ = 90 秒) を試行することを推奨しています。

サーバ受信時刻 T_s に対して:
1. TOTP(K, T_s)   を計算 → クライアント送信値と比較
2. TOTP(K, T_s-1) を計算 → 比較 (クライアント時計が遅れている場合)
3. TOTP(K, T_s+1) を計算 → 比較 (クライアント時計が進んでいる場合)
いずれか一致なら認証成功

これにより最大 ±30 秒 (実質 60 秒幅) の時計ずれを許容。これ以上ずれた場合は認証失敗となり、ユーザーは時計同期を促されます。Authy のような一部実装ではサーバとの時刻ずれを検出して自動補正するモードも持ちます。

QR コードの中身 — otpauth:// URI スキーム

初回登録で表示される QR コードの中身は、otpauth:// 形式の URI です。Google が事実上の業界標準として広めた仕様で、後に Microsoft や 1Password も採用。

otpauth://totp/Example:alice@example.com
  ?secret=JBSWY3DPEHPK3PXP   // Base32 エンコードされた共有鍵
  &issuer=Example
  &algorithm=SHA1            // SHA1 / SHA256 / SHA512
  &digits=6                  // 6 or 8
  &period=30                 // 秒

ほとんどのアプリは SHA-1 / 6 桁 / 30 秒のデフォルト前提で動きます。Google Authenticator は長らく非デフォルトパラメータを無視していました (現在は対応済み)。

secret は Base32 (RFC 4648) でエンコードされた 80-160 bit の鍵。短すぎるとブルートフォース攻撃のリスクが上がります。NIST SP 800-63B は 最小 128 bitを推奨。

TOTP の弱点 — フィッシングには弱い

TOTP は「秘密が漏れない限り」安全ですが、いくつかの弱点があります。

  • リアルタイムフィッシング: 偽サイトに ID/PW を入れさせ、即座に本物のサイトに転送。被害者が 6 桁を入れた瞬間、攻撃者がリアルタイムで本物にログイン。30 秒以内なら完全に有効
  • 共有秘密の保管リスク: スマホを乗っ取られると secret も漏れる (Google Authenticator が暗号化バックアップを実装したのは 2023 年)
  • SIM スワッピングは関係ない: TOTP は SMS と違い、SIM 移行で乗っ取られない (これは TOTP の利点)
  • 共有秘密のサーバ側保管: サーバ側で secret を平文または可逆暗号化で保持する必要があり、サーバ侵害時に大量に流出する

一方、TOTP の大きな利点は実装の単純さとオフライン動作。導入コストが極めて低く、大半のサービスで「ないよりは大幅にマシ」なセキュリティ向上を提供します。

次世代: Passkey / FIDO2 への移行

TOTP の弱点 (リアルタイムフィッシング、共有秘密管理) を根本的に解決するのが Passkey (パスキー)。FIDO Alliance が標準化した WebAuthn (W3C) + CTAP2 (FIDO2) をベースにした認証方式。

  • 公開鍵暗号方式: クライアント側で秘密鍵生成、公開鍵だけサーバに送信 → サーバ侵害でも秘密鍵は漏れない
  • ドメインバインディング: 認証時にブラウザがドメインを検証 → フィッシングサイトでは絶対に認証成功しない
  • 生体認証 / PIN: ユーザー認証は端末側 (Face ID / 指紋 / Windows Hello)
  • Cross-device 同期: iCloud Keychain / Google Password Manager で複数端末同期

2022 年から Apple / Google / Microsoft が本格対応開始、2024 年以降は Amazon / GitHub / X / Adobe など主要サービスが Passkey ログインを正式提供。NIST SP 800-63-4 (2024) では TOTP を「Authentication Assurance Level 2 (AAL2)」、Passkey を「AAL3」と区別しています。

とはいえ TOTP は当面なくならず、両者併存が続きそうです。

実装ライブラリと検証ツール

言語別の代表的ライブラリ:

  • JavaScript/TypeScript: otpauth (npm)、speakeasy (npm)
  • Python: pyotp
  • Go: github.com/pquerna/otp
  • Rust: totp-rs
  • PHP: spomky-labs/otphp

すべて RFC 6238 完全準拠。実装の差は時刻ずれ許容ウィンドウのデフォルト値や、SHA-256/SHA-512 サポート程度。

本サイトの パスワード生成ツール は TOTP の secret 候補生成にも応用可能 (Base32 出力モードを追加すれば即座に対応)。秘密鍵長は NIST 推奨の 128 bit (16 bytes) 以上を使ってください。

まとめ

  • TOTP = HOTP (RFC 4226) のカウンタを「時刻 / 30」に置き換えたもの (RFC 6238)
  • 共有秘密 + 時刻 → HMAC-SHA-1 → Dynamic Truncation で 6 桁を切り出す
  • サーバとクライアントは時刻同期 (NTP) ありきで動作
  • 時計ずれは ±1 ステップ (合計 90 秒) を試行して吸収
  • QR コードの中身は otpauth:// URI スキーム (Google de facto)
  • 弱点: リアルタイムフィッシング・共有秘密のサーバ保管リスク
  • 次世代: Passkey (FIDO2 / WebAuthn) — 公開鍵暗号 + ドメインバインディング
  • NIST SP 800-63-4: TOTP=AAL2, Passkey=AAL3

参考文献・ソース

記事作成に関する注記

本記事は AI(大規模言語モデル)を編集補助として活用して作成しています。 公開前に編集者が内容を確認していますが、事実誤認・仕様の解釈ミス・最新情報との齟齬が含まれる可能性があります。 重要な判断を行う際は、本文中の一次ソースや公式ドキュメントを必ずご自身でご確認ください。 誤りにお気づきの場合は、お問い合わせフォームよりご連絡いただけると助かります。

🔧 関連ツール

📚 関連記事