本文へスキップ
2FAAuth.Work
言語: 日本語
9分で読める 2faauth.work 著 編集方針

TOTPの仕組み:6桁コードを生み出す数学

共有シークレットから入力する6桁の数字まで、時間ベースのワンタイムパスワード(TOTP)コードが生成される仕組みを平易な言葉で解説します。

TOTPコードの正体

認証アプリを開くと、30秒の寿命をカウントダウンする6桁の数字が表示されます。あの数字がTOTPコードです。時間ベースのワンタイムパスワードで、以前のHMACベースの方式であるHOTPを拡張したものとしてRFC 6238で標準化されています。

重要なのは、コードはパスワードではないという点です。共有シークレット を保持していることの証明です。共有シークレットとは、初めて2FAを設定した際にデバイスにインストールされる値です。コードはそのシークレットと現在時刻から導出されるため、30秒ごとに変わり、入力後に覗き見た者には無意味です。

この仕組みのために何かが送信されることはありません。SMSも、ログインサーバーへの接続も、ネットワーク自体も不要です。アプリとサーバーが、それぞれの時計に基づき、同じ計算を独立して行います。同じ入力、同じ計算、同じコード。それが仕組みのすべてです。

共有シークレット:すべての始まり

アカウントで二要素認証(2FA)を有効にすると、サーバーはランダムなシークレットキーを生成します。通常は20バイトの暗号学的ランダム値です。この一連の文字列こそが、その後のすべての根幹となります。

サーバーはこれを一度だけ手渡します。通常はQRコードの形式です。ピクセルの模様に見えるあれは、実態は単なるエンコードされたテキストです。Base32(アルファベットA〜Zと数字2〜7)で書かれたシークレットそのものに、アカウント名と発行者を加えたものです。認証アプリはこれをスキャンし、デコードし、シークレットをローカルに保存します。

その瞬間から、同じシークレットのコピーが2つ存在します。1つはサーバー上に、もう1つはアプリの中に。どちらも以後ネットワーク越しに送信されることはありません。今後生成されるすべてのコードは、この共有された値から導出されます。だからこそ、保護が重要になるのです。

QRコードのスクリーンショットは、鍵の恒久的なコピーです。見た者は誰でも、永久に有効なコードを生成できます。スキャンしたら、削除してください。

時間ステップ:動くカウンター

TOTP は生の時計を直接使いません。代わりに、アプリとサーバーの両方が現在の Unix タイムスタンプ(1970年からの秒数)を取得し、30 で割って余りを捨てます:

counter = floor(unix_time / 30)

結果は 30 秒ごとに 1 ずつ増える単純な整数です。執筆時点では、そのカウンターは非常に大きな数になっています — しかしアプリが気にするのは、双方が同じ値を計算することだけです。同じカウンターに同じ秘密鍵を組み合わせれば、同じコードになります。

なぜ時計のずれがコードを壊すのか

この仕組み全体は一致に依存しています。スマホの時計が 40 秒ずれていると、あなたはカウンター N を計算しますが、サーバーはすでに N+1 に進んでいます — コードが異なり、「無効なコード」エラーが出ます。完璧に打ち込んだのにです。

サーバーは時計がずれることを知っているので、多くの場合、前または次の時間ステップのコードを許容期間として受け入れます。それを超えたら、デバイスの時計を再同期してください(自動設定にします)。

HMAC: 秘密鍵とカウンターの混合

すべてのTOTPコードの中心にあるのは一つの関数、HMAC-SHA1です。HMACとはHash-based Message Authentication Code(ハッシュベースメッセージ認証コード)の略で、ここでの役割は単純です — 2つの入力を混ぜ合わせて、予測不可能な1つの出力を作り出すこと。

2つの入力とは:

  • 共有秘密鍵 — あなたのスマートフォンとサーバーだけが知るキー
  • 時間カウンター — 30秒ごとに変わる数値

HMACは「キー付きハッシュ」と考えてください。普通のハッシュは毎回同じ方法でデータをスクランブルします。HMACはキーに応じて異なる方法でスクランブルします。同じ秘密鍵とカウンター48291を与えれば、常に同じ20バイトの結果が得られます。カウンターを1つ変えるだけで、出力はまったく無関係に見えるものへと変わります。

重要なのは、この処理が一方通行だという点です。攻撃者が何百万もの出力を目にしても、秘密鍵について利用可能な情報は何も得られません — この性質こそが、この仕組み全体を安全にしているのです。

トランケーション: 20バイトから6桁へ

HMAC-SHA-1は20バイトを生成します。画面に表示できる量をはるかに超えています。TOTPはこれを動的トランケーションで圧縮します:

  1. HMAC出力の最後のニブル(下位4ビット)を取り出します。0から15の数値が得られます。
  2. これをオフセットとして使い、出力のその位置から4バイトを取り出します。
  3. その4バイトの最上位ビットをマスクし、31ビット整数を残します。

この整数は表示するには依然として大きすぎるため、1,000,000で割った余りを求めます。0から999,999の範囲の余りがコードとなり、6桁になるようゼロ埋めされます。余りが4,207なら 042207 と表示されます。

動的と呼ばれるのは、オフセットがデータ自体から導かれるためです。各コードはHMAC出力の異なる部分をサンプリングします。

一部のサービスは7桁または8桁のコードを使い、代わりに10,000,000や100,000,000で剰余を取ります。計算方法は同じで、コード空間が大きくなり、推測が難しくなります。

コードを手入力しても安全な理由

6桁のコードには100万通りの値があります。紙上では控えめな数字です。実際に機能するのは、試行できる回数がごくわずかだからです。サーバーはログイン試行にレート制限をかけ、繰り返される失敗を拒否するため、攻撃者は組み合わせを総当たりできません。真の防御はコードの長さではなく、レート制限なのです。

また、各コードの寿命は短いものです。コードは30秒の時間窓に紐付けられ、その前後のごく短い期間しか受け付けられないため、盗まれたコードは再利用される前に失効します。

最後に、コードを見ても未来については何も分かりません。コードはあなたの秘密鍵を使ったHMAC計算の出力です。HMACは一方向関数であり、出力から入力を再構築する方法はありません。今日のコードを一目見た攻撃者でも、秘密鍵を導き出すことはできず、明日のコードを予測することもできません。その裏にあるカウンターの対となる値を知らないからです。

よくある質問とエッジケース

コードが失敗する理由

最も一般的な原因は時計のずれです。スマートフォンの時計とサーバーの時計は許容範囲内に保たれている必要があるため、時刻がずれたスマートフォンは誤ったコードを生成します。対処法:自動時刻同期を有効にしてください。

誤ったシークレットもサイレントに失敗します。これは通常、QRコードを別のアカウントの設定にスキャンした場合に起こります。さらに、QRコードは期限切れになることがあります。多くのサービスは登録コードを短期間経過後、または画面を先に進めた後に無効化するため、古いスクリーンショットは機能しません。

コードは再利用できるか?

技術的には可能ですが、ほとんどのサーバーは現在のタイムステップ内で既に受け入れられたコードを拒否します。これにより、コードを覗き見て数秒後に再送する攻撃者を防げます。

デバイスを紛失した場合

共有シークレットはデバイス上にのみ存在するため、デバイスを紛失するとアクセスも失います。ただし、2FA設定時にリカバリーコードを保存していれば別です。それらはオフラインで保管してください。通常、各コードは一度しか使用できません。

全体のまとめ:完全なフロー

すべては一つのパイプラインに集約されます:共有シークレット → 時間カウンター → HMAC → 切り詰め → 6桁の数字。

最初から最後までの、ある30秒間のサイクル:

  • セットアップ時にQRコードをスキャンします。アプリが共有シークレットを保存します。
  • 12:04:30になると、アプリは現在のタイムステップを計算します(30秒区切りでUnixエポックから数えると約24,000)。
  • HMAC-SHA1がシークレットとそのカウンターを混ぜ合わせ、20バイトを生成します。
  • 切り詰めでは、そのうちの4バイトを選び、数値に変換し、末尾の6桁を残します。たとえば482913。
  • それを入力します。同じシークレットを保持するサーバーが同一の計算を実行し(時計のずれに備えて1〜2ステップの誤差を許容)、コードが一致します。

全体の連鎖を守る習慣は2つ:QRコードをパスワードのように扱うこと、そしてデバイスの時計を自動設定にしておくこと。この2つを守れば、あとは数学が処理してくれます。

このガイドを共有

9分で読める

2FAの歴史:ハードウェアトークンからパスキーまで

クリップ式ハードウェアトークンやSMSコードから、今日のフィッシング耐性のあるパスキーへ。二要素認証がどのように進化し、なぜ各段階が起きたのかを解説します。

続きを読む