公開:2025.09.10 11:00 | 更新: 2025.09.10 01:01
タイトルにある TOTP(Time-based One-Time Password) は、時間を基準にワンタイムパスワードを生成する仕組みです。
もともとカウンタを基準にした HOTP(HMAC-based One-Time Password) が存在し、それを時間ベースに拡張したものがTOTPになります。
TOTPはGoogle AuthenticatorやMicrosoft Authenticatorといった認証アプリに実装され、2段階認証(2FA/MFA)で広く利用されています。
筆者自身もAWSへのログインにGoogle Authenticatorを使用しています。
この仕組みの大きなメリットは、パスワードが「使い捨て」であることです。
仮に漏洩しても一定時間で無効化されるため、攻撃者が利用できる時間的な余地が限定されます。
TOTPの仕組みを調べる
┗RFC 6238とは?
開発環境・実装と検証
┗開発環境
┗実装と検証
┗生成コード
アプリとサーバーが同じ数字を生成できる仕組み
「同じ秘密鍵(共通鍵)」と「同じ計算方法(アルゴリズム)」を共有
なぜ認証に使用できるのか
まとめ
┗参考資料
日常的に使っているTOTPですが、以下の3点が気になったため実際に実装して挙動を確認してみました。
まずTOTPの作り方を調べると、RFC 6238というワードが出てきたため調べてみました。
RFCとは、IETFという組織が定めるインターネットの標準仕様をまとめた文書のことです。
この標準があるおかげで、Google AuthenticatorやMicrosoft Authenticatorなど、異なるアプリでも同じ仕組みで動作させることが可能です。
RFC 6238には、下記のように要件の記載があります。
TOTPは TOTP = HOTP(K, T) という式で定義されていて、Tは「30秒ごとに区切ったUnix時間から計算される整数」を意味します(RFC 6238より)。
標準ではHMAC-SHA1を使いますが、SHA-256やSHA-512も利用可能とされています。
上記のことから「1.なぜ30秒ごとに変わるのか」は、RFC 6238で定められていたから、が答えとなります。
※以下引用の画像等、本記事内容に関する出典・参考:
RFC 6238(原文)
日本語訳(参考)
それでは、実際に実装して挙動を確認してみます。
エディター:VSCode
言語:Go
下記がTOTPを生成するコードです。
package main
import (
"crypto/hmac"
"crypto/sha1"
"encoding/base32"
"encoding/binary"
"fmt"
"time"
)
// TOTP = HOTP(K, T)
func generateTOTP(secret string, step int64, digits int) string {
key, _ := base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(secret)
counter := time.Now().Unix() / step
return hotp(key, counter, digits)
}
func hotp(key []byte, counter int64, digits int) string {
var buf [8]byte
binary.BigEndian.PutUint64(buf[:], uint64(counter))
h := hmac.New(sha1.New, key)
h.Write(buf[:])
hash := h.Sum(nil)
offset := hash[len(hash)-1] & 0x0F
bin := (int(hash[offset]&0x7F) << 24) |
(int(hash[offset+1]) << 16) |
(int(hash[offset+2]) << 8) |
int(hash[offset+3])
mod := 1
for i := 0; i < digits; i++ {
mod *= 10
}
return fmt.Sprintf("%0*d", digits, bin%mod)
}
func main() {
secret := "JBSWY3DPEHPK3PXP" // 共有する秘密鍵(Base32)
step := int64(30)
digits := 6
totp := generateTOTP(secret, step, digits)
now := time.Now().Unix()
remain := step - (now % step)
fmt.Printf("TOTP: %s 残り秒数: %d\n", totp, remain)
}
上記の生成コードを実行すると下記の結果が返ってきます。
TOTP: 439211 残り秒数: 18
...(時間経過)
TOTP: 850355 残り秒数: 30
h := hmac.New(sha1.New, key)
h.Write(buf[:])
hash := h.Sum(nil)
アプリとサーバーは「同じ秘密鍵(共通鍵)」と「同じ計算方法(アルゴリズム)」を共有しています。
さらに「現在時刻を30秒ごとに区切って得られる整数(T)」も両者が同じように計算します。
つまり、秘密鍵さえ同じなら、アプリとサーバーが別々に計算しても同じ数字が出てくるというわけです。
fmt.Printf("TOTP: %s 残り秒数: %d\n", totp, remain)
ここで作られた数字は、ユーザーに表示されるTOTPです。
サーバー側も同じ秘密鍵と同じアルゴリズムを使って同じ数字を生成できます。
そしてユーザーが入力した数字とサーバーの計算結果を比較し、もし一致すれば「この人は秘密鍵を持っている=正しいユーザー」と確認できます。
今回、TOTPの仕組みを実際にコードで実装しながら検証することで、普段は当たり前のように使っている二段階認証の裏側を理解できました。
3つの気になった点をまとめると以下になります。
1. なぜ30秒ごとに変わるのか →RFCで標準として定められているから
2. なぜアプリとサーバーが同じ数字を出せるのか →共通の秘密鍵と同じアルゴリズムを使っているから
3. なぜ認証に使用できるのか →サーバーでも同じ数字を計算でき、一致すれば正しいユーザーと確認できるから
実際に手を動かすことで、TOTPの有効性とセキュリティ技術の奥深さを改めて実感しました。仕様書を読むだけでは得られない納得感もあり、今後もより安全で利便性の高い認証基盤の理解につながるよう、「どういう仕組みなのか」を意識して業務に取り組みます。
RFC 6238(原文):https://www.rfc-editor.org/rfc/rfc6238
日本語訳(参考):https://tex2e.github.io/rfc-translater/html/rfc6238.html
LOADING...