会社でGH:EやSlackの二要素認証(2FA)有効化をしたら、 どういう仕組みで認証しているのか気になったので調べて作ってみた。
というのは建前で、GW前にこんなことをつぶやいたらhsbtさんに ふぁぼられてしまったのでやらざるを得ない状態になったのであった。 ふぁぼられドリブン開発
GWに暇があったら2FAのワンタイムパスワードを発行する方を実装したい
— ぶてい (@buty4649) 2015, 4月 27
Google Authenticator
何はともあれ、手探りでしらべるのはさすがに骨が折れる…。 しかし、幸いなことにGoogle Authenticatorはギッハブにコードが上がっているのでこれを参考にした。
この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桁の数値を返すことをしている。
実際の動きはどうなっているかというと
こんな感じのことをやっている。
これを踏まえて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=****************
このURIは Key URI
と呼ばれるものでRFCで規定されているわけではなく、
Googleが提唱している?ものらしい。
Google AuthenticatorのWikiに詳しいフォーマットが載っている。
Key Uri Format · google/google-authenticator Wiki
上のURIは見たとおりなのだが、OTPにはTOTPを使って、secretの部分が共通鍵になる。 ただし、このsecretは Base32エンコードされている 。 C#にはBase32をデコードしてくれるライブラリが標準にはないので今回はNugetから持ってきた。
アプリ完成
こんな感じのアプリを作った。 実際にGithubで試した限りではうまく動いたので満足。 なお、QRコードリーダ機能はつけていないので、スマフォなりでQRコードを読んで 読み取ったURIを手入力している(実際はPushbulletを使ってコピペしてるけど)。
おわりに
案外簡単にOTPジェネレータが実装できてよかった。 そこまで難しい仕様でもないし、動作の理解することができた。
2FA自体も、当初はめんどくさそうというイメージだったが、アプリを入れてしまえば そこまで手間ではないのでどんどん設定していったほうが良いと思う。
なお、今回作ったアプリはURIの保存機能がないので、このアプリを使って 2FAを有効化するとしねるので注意。 まぁ、PCをOTPジェネレータにしても2FAにならないので意味ないけどね。。。
VS2013 Communityを使ったのだが、gitやNugetが統合されていてかなり驚いた。 しかし、gitについては使い勝手がいいとはおもえずCLIやSourceTreeを使った方がよいと思った。 (もしかしたら、VSSを使っていた人には分かりやすいのかもしれない)