TOTPの仕組み:30秒コード、共有鍵、時刻ずれとRFC 6238
DDEVELOPER
開発(更新: 2026-10-16)12 分で読める

TOTPの仕組み:30秒コード、共有鍵、時刻ずれとRFC 6238

認証アプリの短いコードは、サーバーと共有した秘密鍵と時刻から計算できます。ただし「世界中で完全同期」「必ず6桁・30秒」という仕組みではありません。計算が一致する条件と、検証側で必要な再利用防止・試行制限を切り分け、最後にパスキーとの違いを整理します。

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

通信なしで同じコードを計算できる条件

TOTPは、共有秘密鍵と時刻から決定的に計算するコードです。同じ鍵・アルゴリズム・桁数・時間ステップ・起点を使い、同じ時刻区間に入れば同じ値になります。時計は自動的に完全同期するわけではなく、6桁・30秒以外の設定もあります。

初期設定にはQRコードや手入力などを使います。登録後のコード生成自体はオフラインで可能ですが、ログイン時の送信、端末の時刻補正、アプリのバックアップ・同期まで通信しないという意味ではありません。QRコードや設定鍵は認証情報として扱い、共有・公開しないでください。

HOTPからTOTPへ:カウンタの代わりに時刻を使う

RFC 4226(2005年)のHOTPは、共有鍵Kと8バイトのカウンタCからHMAC-SHA-1を計算し、短い数字列へ変換します。生成器側は新しいコードを生成するとカウンタが進み、検証側は受理した値に合わせて更新するため、未使用コードの生成でもずれが生じます。先読み幅に一律の「一般値」はありません。

RFC 6238(2011年)のTOTPは、イベントカウンタを時刻由来の整数Tへ置き換えます。HMAC-SHA-256・HMAC-SHA-512も選択できます。両RFCはInformationalであり、認証サービスの運用全体を定めるものではありません。

T = floor((unixSeconds - T0) / X)
T0 = 0   // 既定の起点:Unixエポック
X = 30   // 既定の時間ステップ(秒)
TOTP = HOTP方式の計算(K, T)

2026-04-30 12:00:00 UTCはUnix時刻1777550400、Tは59251680です。同じ「秒」でなく同じステップなら同じ入力です。時刻はタイムゾーン表示ではなくUnix秒として扱います。コードの再計算境界を越えても、短い数値へ縮めるため偶然同じ表示値になる可能性はあります。

動的トランケーションとゼロ埋め

HMACの結果から位置を選んで31ビットの非負整数を読み、10の桁数乗で割った余りを使います。6桁なら000000〜999999です。余りを整数のまま表示せず、先頭の0を補います。SHA-1の出力は20バイトで、SHA-256/512では長さが異なります。

次はNode.jsのCommonJSで実行できる、T0=0・X=30に固定した学習例です。Base32文字列ではなく復号済みの鍵バイト列を渡します。秘密鍵の安全な発行・保管、入力レート制限、再利用拒否、登録や復旧は含まないため、この関数だけでログイン認証を実装しないでください。

const { createHmac } = require("node:crypto");

// 学習・既知ベクトル検証用。認証サーバーの実装ではありません。
function totp(key, unixSeconds, algorithm = "sha1", digits = 6) {
  if (!Buffer.isBuffer(key) || key.length < 16) throw new Error("Invalid key");
  if (!Number.isSafeInteger(unixSeconds) || unixSeconds < 0) throw new Error("Invalid time");
  if (!["sha1", "sha256", "sha512"].includes(algorithm)) throw new Error("Invalid algorithm");
  if (![6, 8].includes(digits)) throw new Error("Invalid digits");
  const step = BigInt(Math.floor(unixSeconds / 30)); // T0 = 0, X = 30
  const message = Buffer.alloc(8);
  message.writeBigUInt64BE(step); // 8バイトのビッグエンディアン
  const digest = createHmac(algorithm, key).update(message).digest();
  const offset = digest[digest.length - 1] & 15;
  const value = digest.readUInt32BE(offset) & 0x7fffffff;
  return String(value % (10 ** digits)).padStart(digits, "0");
}

const testKey = Buffer.from("12345678901234567890", "ascii"); // 公開テスト専用
console.log(totp(testKey, 59, "sha1", 8)); // 94287082

RFCの既知テスト用鍵と時刻59秒では、8桁は94287082、6桁は287082になります。この鍵は公開済みなので実アカウントには使えません。6桁の総当たり成功率は1候補・1回ならおおむね100万分の1ですが、剰余によるわずかな偏りがあり、受理候補と試行数を増やすほど危険が増えます。これは一般の「衝突確率」とは区別します。

時刻ずれ、通信遅延、再利用を別々に扱う

RFC 6238 §5.2は、通信遅延の許容を最大1ステップとすることを推奨しています。「現在±1を常に受理せよ」という規定ではありません。時計のずれは§6の別の扱いで、許容する過去・未来の幅を検証側で決めます。

説明用に現在の前後1ステップを調べる場合、X=30、サーバ時刻60秒ではT=2なので候補はT=1,2,3です。対応する生成時刻は30秒以上120秒未満ですが、これを一律に「時計ずれ±30秒」や「入力後90秒有効」とは呼べません。境界の位置、遅延、記録済みのずれで結果が変わります。

一度認証に成功したOTPは、同じ有効期間内でも再受理しません。複数要求が同時に届く場合も一度だけ消費されるよう、検証と使用済み状態の更新を原子的に扱います。コードの一致だけで認証成功とする説明では不十分です。

短いOTPには試行回数制限が必要です。固定の「3〜5回」を規格共通値とせず、脅威・復旧・利用性を含めて設計します。時刻が大きくずれた場合は、信頼できる別の認証手段で再同期・再登録を行います。

QRコードとotpauth URI:Base32は暗号化ではない

GoogleのKey URI Formatは、TOTP設定を渡すために広く使われる形式です。RFC 6238自身の登録方式ではありません。次の1行は公開テスト鍵の例で、実アカウントに登録しないでください。実際のURIに注釈や改行を混ぜないことも重要です。

otpauth://totp/Example:alice%40example.com?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ&issuer=Example&algorithm=SHA1&digits=6&period=30

secretはBase32表現の鍵、issuerは発行者、algorithm・digits・periodは計算条件です。Base32は暗号化ではなく、文字数を鍵のビット長と混同しません。この例の32文字は20バイト(160ビット)に復号できますが、公開テスト鍵なので秘密性はありません。

RFC 4226は共有秘密を最低128ビットとすることを必須にし、160ビットを推奨しています。RFC 6238はHMAC出力長に合わせることを推奨します。生成時は暗号学的乱数を使い、端末ごとに固有の鍵を発行します。NIST SP 800-63B-4の鍵とアルゴリズムの強度要件を、単なる文字列の長さやRFCの鍵長と同一視しないでください。

Key URI文書には一部パラメータを無視する実装について古い注意があります。これを現行アプリ全版の対応表とは扱わず、対象アプリ・版で取り込み後の桁数、周期、アルゴリズムを既知値で照合します。

TOTPの限界:フィッシングと鍵の管理

正しいコードでも、偽サイトに入力して中継されれば本物のサービスで使われ得ます。TOTPは手入力されたコードを対象ドメインやログインセッションへ結び付けないため、フィッシング耐性のある認証方式には分類されません。悪用可能時間は検証側の受理方針で決まり、一律30秒ではありません。

  • 登録QR、端末、バックアップ、検証サーバーなど、共有鍵へアクセスできる経路を保護します。サーバーは検証用の鍵または安全に導出できる材料を必要とし、通常のパスワードハッシュだけでは再計算できません。
  • 保存時の暗号化、鍵管理、アクセス制御と監査を組み合わせます。侵害で必ず全鍵が漏れるとも、暗号化していれば漏れないとも断定できません。
  • TOTP生成自体はSIMに依存しませんが、SMSでアカウント復旧や同期サービスへのアクセスを許す構成なら、SIM移行による攻撃が間接的に影響し得ます。

パスワードだけの認証にOTPを加えることは保護を強める一方、登録・復旧・バックアップを含む全体設計が必要です。

パスキーとの違いと認証保証レベル

パスキーは公開鍵方式の認証資格情報です。WebAuthnはW3C、CTAPはFIDO Allianceの仕様で、サーバーは共有秘密ではなく公開鍵で署名を検証します。RP IDとoriginの検査によって別サイトへの資格情報利用を防ぐ設計であり、単に数字を手入力するTOTPとは異なります。

端末側のPIN・生体認証などで利用を承認する構成があり、同期型と端末固定型があります。すべてのパスキーが同期するわけではありません。フィッシング耐性があっても、端末侵害、セッション窃取、弱い復旧経路まで絶対に防ぐという保証ではありません。

NISTのAALはアルゴリズム名だけで決まりません。OTPの要素構成や検証側の条件でAAL1/AAL2の扱いが変わり、AAL2ではフィッシング耐性のある選択肢も提供する必要があります。同期可能な認証器はAAL3に使えません。端末固定鍵でも、非エクスポート性、ハードウェア保護、検証要件などを満たさなければAAL3とは言えません。

実装ライブラリと確認すべき契約

otpauth、speakeasy、PyOTP、pquerna/otp、totp-rs、OTPHPのようなライブラリを使う場合も、採用する版の保守状況と仕様を確認します。一括して「完全準拠」「差は許容幅だけ」と保証できません。

  • RFCの既知テスト値、先頭ゼロ、30秒境界、2038年より後の時刻を検証する
  • 鍵の形式・長さ、桁数・周期・ハッシュの互換性を確認する
  • レート制限、使用済みコードの拒否、同時要求、秘密の保管、復旧を別途設計する

このページのコードは学習用です。本サイトのパスワード生成ツールは文字列用であり、TOTP秘密鍵のbyte長やBase32形式を生成・検証する機能ではありません。Base64とBase32も別形式です。実際の登録QRや鍵を公開の検証フォーム、共有チャット、ログへ貼り付けないでください。

既知の値で計算を確かめる

以下はRFC 6238 Appendix BのSHA-1用公開テスト鍵(ASCIIの12345678901234567890)、T0=0、X=30、8桁を使う検算です。先頭0を含む出力も8文字で保持します。

表が収まらない場合は、左右にスクロールできます。

Unix秒T8桁コード
59194287082
11111111093703703607081804
2000000000066666666665353130

表示値の一致は計算の検証であり、実運用の安全性全体の証明ではありません。SHA-256/512を試す場合は、それぞれのRFCテスト鍵も使い分けます。

まとめ:計算条件と検証方針を分ける

  • 共通の鍵と計算条件、同じ時刻ステップがあって初めて同じOTPになる
  • 6桁・30秒は一般的な設定で、アルゴリズムの唯一の設定ではない
  • コード生成がオフラインでも、ログインや同期までオフラインとは限らない
  • 鍵の安全な発行・保管、試行制限、再利用拒否を計算関数とは別に扱う
  • TOTPとパスキーの違いは、短いコードかどうかより、認証先との結び付きと鍵の持ち方にある

参考文献・ソース

記事作成に関する注記

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

🔧 関連ツール

📚 関連記事