Yappli Tech Blog

株式会社ヤプリの開発メンバーによるブログです。最新の技術情報からチーム・働き方に関するテーマまで、日々の熱い想いを持って発信していきます。

Goによる2要素認証の実装について

はじめに

初めまして、サーバーサイドエンジニアの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で実装してみると下記のようになります。

The Go Playground

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要素認証が実装されました!

よければ使ってみてください!

2要素認証機能を実装しました|Yappli

参考

RFC 4226: HOTP: An HMAC-Based One-Time Password Algorithm

RFC 6238: TOTP: Time-Based One-Time Password Algorithm

今さらTOTPクライアントを実装する|murakmii|note