はじめに
初めまして、サーバーサイドエンジニアのKidaです!
今回は多くの2要素認証機能に使われる、TOTP(Time-based One-time Password)というワンタムパスワードを生成するコードをGoで実装してみようと思います!
2要素認証とは?
TOTP生成コードを書く前に、2要素認証について軽くおさらいをします。
まず認証のための要素として、知識要素、所有要素、生体要素の主に3つの要素があると言われています。
- 知識要素: 本人のみが記憶、知っていること。ログイン時のパスワードなど
- 所有要素: 本人のみが持っているもの。スマートフォンなど
- 生体要素: 本人のみの身体的な特徴。指紋など
2要素認証とはこのうちの2つの要素を用いて認証することを言います。
Google Authenticator等のアプリを自分のデバイスに入れるのがよくあるパターンですが、ログインパスワードの知識要素に加えて、所有要素を使った2要素認証ということになります。
HOTP(HMAC-based One-time Password)とは?
HOTPとはその名の通り、HMACをベースにしたOTP(One-time Password)生成アルゴリズムです。
HMAC(Hashed Message Authentication Code)とは、共有秘密鍵、ハッシュ関数を用いたメッセージ認証符号の一つです。
簡単に言うと、クライアント、サーバー間でメッセージを送受信する際に改竄検知などに用いられる符号のことです。
HOTPはcounterと呼ばれる生成ごとに変わる値を使いOTPを生成しますが、TOTPは、それに時間によって変わる値を使い拡張したものです。
つまりTOTPを理解するためにはHOTPの実装を知る必要があります。
HOTPは以下のような操作で生成します。
- クライアントとサーバーで共通する16バイト以上の秘密鍵を生成する(本記事では簡単のため適当な値) - 新しいHOTPが作られるたびにインクリメントするカウンターを生成する(本記事では簡単のため適当な値) - SHA-1、SHA-256、SHA-512などのハッシュ関数を用い、秘密鍵とカウンターからHMACを生成する 上記で生成したMACはSHA-1の場合でも20バイトの値になるため、ユーザーにとって入力しやすい桁数になるよう下記の処理でTruncateする。 - HMACの末尾4ビットをoffsetとする - offsetから始まる最上位1ビットをマスクした4バイトを取得する(最上位ビットをマスクする理由としては、符号ありとなしのmod計算の結果はプロセサーごとに変わるので、それを避けるため) - 取得した32ビットの値と10^dの剰余計算を行う(dはHOTPの桁数、多くは6桁)
これをGoで実装してみると下記のようになります。
package main import ( "crypto/hmac" "crypto/sha1" "encoding/binary" "fmt" "math" ) func main() { // Counterを生成 // 8バイトの整数として扱うためuint64型で定義 // server, clientで共通である必要がある。ここでは適当な値 counter := uint64(11111111) //秘密鍵、ここでは適当な値 secretKey := []byte("secretkey") generateHOTP(secretKey, counter) } func generateHOTP(secretKey []byte, counter uint64) { // SHA-1アルゴリズムと秘密鍵を用いて、HMACオブジェクトを初期化 mac := hmac.New(sha1.New, secretKey) // HMACオブジェクトにcounterをBigEndian(上位バイトから下位バイトの順)で書き込み binary.Write(mac, binary.BigEndian, counter) // 書き込んだメッセージのMACを計算、 HMAC-SHA-1 の計算結果は20バイトになる sum := mac.Sum(nil) // 末尾4ビットをオフセットとする offset := sum[len(sum)-1] & 0x0f // オフセットから最上位ビットをマスクした連続する4バイトの値を取得する bin_code := binary.BigEndian.Uint32(sum[offset:offset+4]) & 0x7fffffff // 6桁のHOTPを得るため、10の6乗との剰余を計算する mod := bin_code % uint32(math.Pow10(6)) fmt.Println(mod) }
TOTPとは?
TOTPは上述のHOTPのcounterの代わりに時刻に応じて変わるTを使うようにしたもので、下記のように定義されます。
TOTP = HOTP(K, T) T = (現在のUnix time - T0) / X (通常T0は0秒、Xは30秒) Kは共有秘密鍵のこと
つまり先ほどのHOTPのcounterをTにしてあげるだけで実装できます。
また秘密鍵はbase32でencodeする必要があるので、そちらもしています。
func main() { // 時刻によって変動するTを定義 t := uint64(time.Now().Unix() / 30) // base32で秘密鍵をencode、ここではencodeできる適当なstringを引数に渡している secretBytes, _ := base32.StdEncoding.DecodeString("ONXW2ZJAMRQXIYJAO5UXI2BAAAQGC3TEEDX3XPY=") generateHOTP(secretBytes, t) }
以上でTOTPを生成するコードをGoで実装できました!
あとがき
ちなみに内部的にはこのブログの実装とは異なりますが、Yappliでも最近2要素認証が実装されました!
よければ使ってみてください!
参考
RFC 4226: HOTP: An HMAC-Based One-Time Password Algorithm