注目イベント!
アドベントカレンダー2024開催中!
一年を締めくくる特別なイベント、アドベントカレンダーを今年も開催しています!
初心者からベテランまで楽しめる内容で、毎日新しい技術トピックをお届けします。
詳細はこちらから!
event banner

Javaコードで理解するTOTPの仕組み

| 3 min read
Author: shigeki-shoji shigeki-shojiの画像

庄司です。

システムやサービスへのアクセスでユーザ名とパスワードだけの認証はセキュリティが弱く、多くのサービスでは多要素認証 (Multi-Factor Authentication) の利用が一般的になっています。

例えば AWS アカウントのルートユーザーアクセスでは多要素認証 (MFA) の利用が強く推奨され、企業のポリシー等でも MFA の利用が強制されることも増えています。

多要素認証では、TOTP (Time-Based One-Time Password) を利用するもの以外にも SMS や生体認証を用いるものなどいくつかありますが、この記事では TOTP の仕組みについて解説します。

仕様

#

TOTP の仕様は次のドキュメントがあります。

このドキュメントの Appendix A には Java を使ったサンプルコードがあり、テストデータは Appendix B にあります。

また、さまざまなサービスの MFA でサポートされているアプリケーション Google Authenticator のソースコードは GitHub リポジトリに公開されています。

シークレット (Secret)

#

MFA を有効にするとき、最初にQRコード[1]を読んで初期化することが多いでしょう。読み込むと次のような URI が書かれています。

otpauth://totp/{user}@{servicename}?secret={secret}

TOTP で使用するのは、シークレット (secret) の部分になります。このシークレットは、Base32 フォーマットによりエンコードされているため、Java 標準ライブラリでデコードできません。

かわりに Apache Commons Codec の Base32 が使用可能です。

import org.apache.commons.codec.binary.Base32;

byte[] decodedSecret = new Base32().decode(secret);

時間

#

TOTP で使用する時間は、エポック秒 (1970年1月1日午前0時0分0秒からの経過秒数) を 30 で割って求め、これをバイト配列に変換します。バイト配列のサイズは、long 値であるため 8 バイトです。

import java.time.Instant;
import java.nio.ByteBuffer;

long t = Instant.now.getEpochSecond / 30;
byte[] time = ByteBuffer.allocate(8).putLong(t).array();

HMAC

#

これで生成する準備が整いました。では、シークレットと時間を使って One-Time Password の元となるハッシュ値を求めます。

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

Mac hmac = Mac.getInstance("HmacSHA1");
SecretKeySpec keySpec = new SecretKeySpec(decodedSecret, "RAW");
hmac.init(keySpec);
byte[] hash = hmac.doFinal(time);

ワンタイムパスワードコード

#

ワンタイムパスワードの生成には RFC4226 も関連しています。RFC4226 の「5.3. Generating an HOTP Value」の手順を確認します。

Step 1 の値は HMAC のところで求めました。

Step 2 では、このバイト配列のオフセットを動的に決定します。バイト配列 (HmacSHA1 のハッシュ値のバイト配列サイズは 20 byte) の最後にある下位の 4 bit を取得します。

int offset = hash[19] & 0xf;

このオフセットの値を使って、4 byte のバイト配列を取得して int に変換しますが、マイナス値にならないよう、最上位は 0x7f でマスクします。

int binary =
    ((hash[offset] & 0x7f) << 24) |
    ((hash[offset + 1] & 0xff) << 16) |
    ((hash[offset + 2] & 0xff) << 8) |
    (hash[offset + 3] & 0xff);

必要な桁数 (ここでは 6 桁) を余剰を使って求めます。

int otp = binary % 1000000;

Step 3 に書かれているように文字列に変換します。

System.println(String.format("%06d", otp));

おわりに

#

この記事で、TOTP のコード生成では一方向ハッシュを用いていること、取り出す値の位置はハッシュ値から動的に決定して固定の位置の値ではないことをコードを使って解説しました。

このような仕組みであるためコードから一意のシークレットを求めることは非常に困難です。ただしシークレットの取り扱いには注意が必要です。

参考

#

  1. QRコードは株式会社デンソーウェーブの登録商標です。 ↩︎

豆蔵では共に高め合う仲間を募集しています!

recruit

具体的な採用情報はこちらからご覧いただけます。