Idiotproof

二段階認証 (TOTP)

TOTPは二段階認証によく使われるアルゴリズム。 Google Authenticatorなどで使用できる。

TOTPはTime-Based One-Time Passwordの略で、RFC-6238で定義されている。 実際のパスワード生成部分には、ワンタイムパスワード生成アルゴリズムであるHOTPを使用している。 HOTPはRFC-4226に定義されている。

ソース

HOTP

public static String hotp( byte[] secret, long count, int digit ) {

    byte[] hash = Hashing.hmacSha1( secret )
                         .hashLong( Long.reverseBytes( count ) )
                         .asBytes();

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

    var codeAsStr = Integer.toString( code );
    return codeAsStr.substring( codeAsStr.length() - digit );
}

シークレットは、認証相手と共有する鍵。 Javaはビッグエンディアンだが、ハッシュに使うカウント値はリトルエンディアン。そのためLong.reverseBytes()を呼ぶ必要がある。 digitは桁数。大抵6固定。

TOTP

public static String totp( byte[] secret, long time, int step, int digit ) {
    long timeCounter = time / step;
    return hotp( secret, timeCounter, digit );
}

TOTPはUnix Timeに基づき、countを決定する。単位は秒なので、System.currentTimeMillis() / 1000を使えばいい。 stepはトークン更新のタイミング(認証アプリ等で数字が切り替わる間隔)を指定する。実際には30秒固定らしい。

URL

QRコード読み取り等で使われるURLの形式は、Googleが定めている。

https://github.com/google/google-authenticator/wiki/Key-Uri-Format

public static URI toUri( String label, String secret, String issuer ) {

    var query = new StringJoiner( "&" );
    query.add( "secret=" + BaseEncoding.base32().omitPadding().encode( secret.getBytes( StandardCharsets.UTF_8 ) ) );
    query.add( "issuer=" + issuer );

    try {
        return new URI( "otpauth", "totp", "/" + label, "secret=foo&issuer=bar", null );
    } catch ( URISyntaxException e ) {
        throw new RuntimeException( e );
    }
}
otpauth://totp/foo?secret=bar&issuer=buz

シークレットはBase32でエンコードされ、パディングは含んではいけない。 digitsperiodパラメーターもあるが、それぞれ630固定なので、指定しなくてよい。