
TOTPの仕組み:30秒コード、共有鍵、時刻ずれとRFC 6238
認証アプリの短いコードは、サーバーと共有した秘密鍵と時刻から計算できます。ただし「世界中で完全同期」「必ず6桁・30秒」という仕組みではありません。計算が一致する条件と、検証側で必要な再利用防止・試行制限を切り分け、最後にパスキーとの違いを整理します。
通信なしで同じコードを計算できる条件
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)); // 94287082RFCの既知テスト用鍵と時刻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=30secretは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秒 | T | 8桁コード |
|---|---|---|
| 59 | 1 | 94287082 |
| 1111111109 | 37037036 | 07081804 |
| 20000000000 | 666666666 | 65353130 |
表示値の一致は計算の検証であり、実運用の安全性全体の証明ではありません。SHA-256/512を試す場合は、それぞれのRFCテスト鍵も使い分けます。
まとめ:計算条件と検証方針を分ける
- 共通の鍵と計算条件、同じ時刻ステップがあって初めて同じOTPになる
- 6桁・30秒は一般的な設定で、アルゴリズムの唯一の設定ではない
- コード生成がオフラインでも、ログインや同期までオフラインとは限らない
- 鍵の安全な発行・保管、試行制限、再利用拒否を計算関数とは別に扱う
- TOTPとパスキーの違いは、短いコードかどうかより、認証先との結び付きと鍵の持ち方にある
参考文献・ソース
記事作成に関する注記
本記事は AI(大規模言語モデル)を編集補助として活用して作成しています。 公開前に編集者が内容を確認していますが、事実誤認・仕様の解釈ミス・最新情報との齟齬が含まれる可能性があります。 重要な判断を行う際は、本文中の一次ソースや公式ドキュメントを必ずご自身でご確認ください。 誤りにお気づきの場合は、お問い合わせフォームよりご連絡いただけると助かります。




