
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) への移行までを整理します。
なぜ通信なしで「同じ 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
参考文献・ソース
- RFC 6238 — TOTP: Time-Based One-Time Password Algorithm (2011) ↗
- RFC 4226 — HOTP: An HMAC-Based One-Time Password Algorithm (2005) ↗
- RFC 2104 — HMAC: Keyed-Hashing for Message Authentication (1997) ↗
- RFC 4648 — Base32 Data Encoding ↗
- NIST SP 800-63-4 — Digital Identity Guidelines (2024) ↗
- FIDO Alliance — Passkeys / WebAuthn ↗
- W3C WebAuthn Level 3 ↗
- Google KeyURI Format (otpauth:// URI scheme) ↗
記事作成に関する注記
本記事は AI(大規模言語モデル)を編集補助として活用して作成しています。 公開前に編集者が内容を確認していますが、事実誤認・仕様の解釈ミス・最新情報との齟齬が含まれる可能性があります。 重要な判断を行う際は、本文中の一次ソースや公式ドキュメントを必ずご自身でご確認ください。 誤りにお気づきの場合は、お問い合わせフォームよりご連絡いただけると助かります。


