二段階認証の導入方法 第1回:メリットとアルゴリズム

2026年9月21日月曜日

プログラミング

t f B! P L

1. 二段階認証を導入するメリット

パスワードに認証アプリの確認を加えることで、パスワード漏えいに備えられます。SMS送信やGoogleへの認証API呼び出しも不要です。

二段階認証って、自分で作ったサービスにも付けられるの? 個人で作っているものでも。
付けられます。今回は、Google 認証システムに表示される「一定時間で変わる6桁の数字」を使う方式です。TOTPという共通規格なので、対応する認証アプリと自分のPHPを組み合わせられます。
それなら、かなり便利じゃない?
そうですね。特に、パスワードだけでは心配なログインに、もう一つ確認を加えられるのがメリットです。

メリットどう役立つか
パスワード漏えいへの備えパスワードを知られただけでは、ログインを完了できなくなる
SMS送信が不要認証コードを送るたびのSMS料金がかからない
Googleへの認証API呼び出しが不要自分のサーバーでコードを照合できる
共通規格を使えるGoogle 認証システム以外のTOTP対応アプリも選べる
コード生成はオフラインで可能スマホが通信できないときでも、登録済みのコードは計算できる
Googleと契約する必要もない?
この構成では、Googleの有料認証APIを契約して呼び出すことはありません。自分のプログラムがTOTPを計算します。もちろん、開発やサーバーの費用は別です。

2. ユーザーごとの秘密鍵と暗号化保存

TOTP秘密鍵はユーザーごとにランダム生成し、DBには暗号化して保存します。認証用の秘密鍵と、保存時の暗号化鍵は役割を分けて管理します。

じゃあ、まず仕組みを知りたいわ。何を比べているの?
スマホとサーバーが「同じ秘密鍵」と「現在時刻」を基に、それぞれ計算したコードです。TOTPの計算方式はRFC 6238に記載されています。
時刻は分かるけど、秘密鍵はどこから出てくる?
最初の登録時に、サーバーがユーザーごとに安全な乱数で生成します。ユーザーIDや誕生日から作るのではありません。今回のPHPでは random_bytes(20) を使い、20バイト、つまり160ビットの乱数を作ります。
その鍵をユーザーテーブルに入れておけばいいんだね。当然、そのまま保存するのではなく、暗号化して。
はい。既存のユーザー情報に、二段階認証用の項目を追加するイメージです。

idusernamepassword_hashtotp_secret_enctotp_enabled
1yamadaパスワードのハッシュ山田さん用秘密鍵の暗号文1
2tanakaパスワードのハッシュ田中さん用秘密鍵の暗号文1
3suzukiパスワードのハッシュ未作成ならNULL0
パスワードの保存と同じ?
そこは違います。パスワードはハッシュ化します。一方、TOTPの秘密鍵は、サーバーが元の鍵を取り出して計算する必要があるので、復号できる暗号化で保存します。
ということは、その暗号化にも鍵がいるよね?
そうです。この話には、役割の違う二つの鍵が登場します。

役割保管場所
ユーザーごとのTOTP秘密鍵スマホとサーバーで同じ認証コードを作るスマホの認証アプリ/DBには暗号化して保存
サーバー側の暗号化鍵DBに保存するTOTP秘密鍵を暗号化・復号するサーバーの秘密設定。DBや公開ソースに一緒に入れない
なるほど。DBが漏れても、暗号化鍵まで一緒に渡さないようにするわけだ。
はい。ただ、実行中のサーバー自体を乗っ取られれば、復号される可能性があります。暗号化保存で守れる範囲と、サーバーの管理は分けて考えます。

3. QRコードで秘密鍵を共有する

初回だけQRコードで秘密鍵を認証アプリへ渡します。アプリの6桁が一致することを確認してから、二段階認証を有効にします。

スマホには、どうやって同じ鍵を渡すの?
初回登録用のQRコードです。DBから復号したTOTP秘密鍵を登録用データに入れ、認証アプリで読み取ってもらいます。サーバー側の暗号化鍵はQRに入れません。
つまり、QRは「秘密鍵の受け渡し」なんだ。
その理解で合っています。登録はこの順番です。

  1. 本人のパスワードを確認する。
  2. そのユーザーのTOTP秘密鍵をランダムに生成する。
  3. 秘密鍵を暗号化してユーザーテーブルへ保存する。この時点では未有効。
  4. 登録用QRを表示し、スマホで読み取ってもらう。
  5. スマホの6桁を入力してもらい、サーバーで一致を確認する。
  6. 一致したら totp_enabled = 1 にして登録完了。
QRを出した時点では、まだ登録完了にしないんだね。
はい。読み取りに失敗したまま有効にすると、本人がログインできなくなるからです。

4. 6桁のコードを作るアルゴリズム

スマホとサーバーは、同じ秘密鍵と30秒単位の時刻からコードを計算します。同じ条件で計算するため、通信しなくても同じ6桁が得られます。

で、肝心の6桁はどう計算する?
まず、現在のUnix時刻を30で割り、小数部分を切り捨てます。それが「今は何番目の30秒区間か」という番号です。
時間枠の番号 = floor(Unix時刻 ÷ 30)
その番号と秘密鍵を組み合わせる?
はい。今回の設定では、時間枠の番号を8バイトのデータにし、秘密鍵を使ってHMAC-SHA1を計算します。結果から規定の方法で31ビットの整数を取り出し、100万で割った余りを6桁に整えます。
時間枠の番号
  → 秘密鍵を使ってHMAC-SHA1を計算
  → 計算結果から31ビットの整数を取り出す
  → 1,000,000で割った余り
  → 先頭の0を含めて6桁にする
「暗号化して比較する」という理解とは、少し違うのか。
そうですね。DB保存時には暗号化しますが、6桁を作る処理はHMACによる計算です。認証コードを復号して秘密鍵を取り出す仕組みではありません。
スマホとサーバーは同じものを持っているから、同じ数字になる。
そのとおりです。下は説明用の例で、実際の計算値ではありません。

計算する側入力結果の例
スマホユーザーAの秘密鍵+時間枠100482915
サーバーユーザーAの秘密鍵+時間枠100482915
別ユーザーのスマホユーザーBの秘密鍵+時間枠100705138
同じ時刻でも、ユーザーごとに鍵が違うからコードも変わるわけか。
はい。ただし6桁なので、別ユーザーと偶然同じ数字になることはあります。コードだけでユーザーを探すのではなく、パスワード認証を通過したユーザーの鍵で照合します。

5. ログインを完了させるタイミング

パスワードが合った段階では仮認証にとどめます。認証コードの確認にも成功してから、会員ページへ進めるようにします。

あとは、パスワードの後にこれを確認すれば二段階認証になるね。
はい。パスワードが合った段階は「仮認証」にとどめ、6桁の確認に成功してからログインを完了させます。先に会員ページへ入れてしまうと、二段階目を飛ばせてしまいます。

6. 必要な環境とライブラリ

PHPとMySQLを使い、TOTP計算はOTPHP、QR生成はendroid/qr-codeに任せます。秘密鍵の暗号化にはPHPのSodium拡張を使います。

必要なライブラリは? PHPだけでできる?
PHPでできます。この連載では計算とQR生成にライブラリを使い、DB保存とのつなぎ方に集中します。

用途使用するもの
実行環境PHP 8.4以上の8系、MySQL 8系、Composer
TOTP計算spomky-labs/otphp 11系
QR画像生成endroid/qr-code 6系とPHPのXML拡張。SVG形式で生成
秘密鍵の暗号化PHPのSodium拡張
DB接続PHPのPDO・pdo_mysql拡張
乱数・パスワードPHPの random_bytes()password_hash()password_verify()
サンプル一式の sample フォルダーで composer install を実行すれば、付属の composer.json に指定したライブラリが入ります。空のプロジェクトに導入する場合の指定は次のとおりです。
composer require 'spomky-labs/otphp:^11.0' 'endroid/qr-code:^6.0'
OTPHPはTOTP対応ライブラリで、QR生成にはendroid/qr-codeを使います。この連載はQRライブラリ側のPHP要件に合わせてPHP 8.4以上にしています。
仕組みが分かると、思ったより身近だね。
次回は、この流れをユーザーテーブルとPHPにつなげます。秘密鍵を生成して暗号化保存し、必要なときだけ復号してコードを確認するところを作りましょう。

検索

年月ごとアーカイブ

フレンドブログ

Translate

自己紹介

自分の写真
映画が好き! 音楽好き! 演奏も好き! ミキシングが一番好き!

QooQ