ぶていのログでぶログ

思い出したが吉日

ワンタイムパスワードジェネレータを作った

会社でGH:EやSlackの二要素認証(2FA)有効化をしたら、 どういう仕組みで認証しているのか気になったので調べて作ってみた。

というのは建前で、GW前にこんなことをつぶやいたらhsbtさんに ふぁぼられてしまったのでやらざるを得ない状態になったのであった。 ふぁぼられドリブン開発

Google Authenticator

何はともあれ、手探りでしらべるのはさすがに骨が折れる…。 しかし、幸いなことにGoogle Authenticatorはギッハブにコードが上がっているのでこれを参考にした。

github.com

このREADMEに書いてある通り RFC4226 と RFC6238 がキモみたいだ

最近はRubyとかLLを主に書いていたので、久しぶりにVSも触りたくなったのでC#で作ってみた。

HOTPとTOTP

RFC4226には HMAC-Basedワンタイムパスワードアルゴリズム(HOTP) が RFC6238には Time-Basedワンタイムパスワードアルゴリズム(TOTP) が それぞれ規定されている。

HOTPとTOTPは無関係ではなく、先にネタバレしてしまうとTOTPはHOTPを内部的に使用している。

HOTPの仕様

HOTPは、HMACを使用した回数ベースのワンタイムパスワードを生成するアルゴリズムになっている。 生成式は以下のとおり

HOTP(Key, Counter) = Truncate(HMAC-SHA1(Key, Counter))

Keyが共有鍵で、Counterが8バイトとのカウンター値となる。

Truncate関数は、何をしているかというとHMAC-SHA1は20バイトのデータを返すのを、 切り詰めて6~8桁の数値を返すことをしている。

実際の動きはどうなっているかというと

  1. ハッシュ値の20バイト目の下位4ビットを取り出しオフセット値とする
  2. 1のオフセット値をハッシュ値のバイト列に当てはめ、そこから31ビット取り出す
  3. 出力する桁数に合わせて切り詰める

こんな感じのことをやっている。

これを踏まえてHOTPをC#でコードを書いたらこんなかんじになる。

private string GenerateHotp(byte[] secret, ulong c, int digit)
{
    // ビッグエンディアンなので注意…
    // これでハマった…
    byte[] counter = BitConverter.GetBytes(c);
    if (BitConverter.IsLittleEndian)
    {
        counter = counter.Reverse().ToArray();
    }

    // HMAC-SHA1(Key, Counter)の部分
    HMACSHA1 hmac = new HMACSHA1(secret);
    byte[] hs = hmac.ComputeHash(counter);

    // Truncateする部分
    int offset = hs[19] & 0x0f;
    uint bin_code = ((uint)hs[offset] & 0x7f) << 24
        | ((uint)hs[offset + 1] &[f:id:buty4649:20150506014217p:plain] 0xff) << 16
        | ((uint)hs[offset + 2] & 0xff) << 8
        | (uint)hs[offset + 3] & 0xff;

    string d = bin_code.ToString();
    return d.Substring(d.Length - digit);
}

HOTPの動作確認用データは ここ が参考になる。

TOTPの仕様

TOTPの生成式は以下のとおり

TOTP(Key, T) = HOTP(Key, T)
T = (Current Unix Time - T0) / X

TOTPは、HOTPのカウンター値を現在のエポックタイムにしただけなのだった。 T0は基準となる時刻、XはTOTPを再生成する基準となる秒数。30秒がデフォルトのようだ。 (ここでTやT0は UTCなので注意 ここでもハマった) なお、HOTPではHMAC-SHA1を仕様するように策定されているが、TOTPではHMAC-SHA256や HMAC-SHA512も使えるようになっている。

HOTP同様にTOTPをC#のコードにすると以下のとおり

private string GenerateTotp(byte[] key, ulong t0, int period)
{
    DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
    ulong t = ((ulong)(DateTime.UtcNow - epoch).TotalSeconds - t0) / (ulong)period;
    return GenerateHotp(key, t);
}

HOTPが完成していればTを求めるだけなので難しくはない。 また、テストデータはRFC6238に記載されているので動作確認も簡単にできる。

QRコードには何が入っているのか?

これでジェネレータ部分ができたのだが、実際に各アプリケーションで2FAを 有効化しようとするとQRコードが発行される。 このQRコードQRコードリーダで読み取ってみると以下のようなURIが表示される。

otpauth://totp/github.com/buty4649?issuer=GitHub&secret=****************

このURIKey URI と呼ばれるものでRFCで規定されているわけではなく、 Googleが提唱している?ものらしい。 Google AuthenticatorのWikiに詳しいフォーマットが載っている。 Key Uri Format · google/google-authenticator Wiki

上のURIは見たとおりなのだが、OTPにはTOTPを使って、secretの部分が共通鍵になる。 ただし、このsecretは Base32エンコードされているC#にはBase32をデコードしてくれるライブラリが標準にはないので今回はNugetから持ってきた。

アプリ完成

f:id:buty4649:20150506014217p:plain

こんな感じのアプリを作った。 実際にGithubで試した限りではうまく動いたので満足。 なお、QRコードリーダ機能はつけていないので、スマフォなりでQRコードを読んで 読み取ったURIを手入力している(実際はPushbulletを使ってコピペしてるけど)。

おわりに

案外簡単にOTPジェネレータが実装できてよかった。 そこまで難しい仕様でもないし、動作の理解することができた。

2FA自体も、当初はめんどくさそうというイメージだったが、アプリを入れてしまえば そこまで手間ではないのでどんどん設定していったほうが良いと思う。

なお、今回作ったアプリはURIの保存機能がないので、このアプリを使って 2FAを有効化するとしねるので注意。 まぁ、PCをOTPジェネレータにしても2FAにならないので意味ないけどね。。。

VS2013 Communityを使ったのだが、gitやNugetが統合されていてかなり驚いた。 しかし、gitについては使い勝手がいいとはおもえずCLIやSourceTreeを使った方がよいと思った。 (もしかしたら、VSSを使っていた人には分かりやすいのかもしれない)