RFC 6238 TOTP 使い捨てパスワード

google-authenticator の時刻ベースだけに対応する、 二段階認証の使い捨てパスワード生成コマンドを描いてみました。 コマンドラインで動きます。

Time-Based One-Time Password generator command

使うには google-authenticator の Key-URI 形式をキー・ファイルへ格納しておき、 コマンドラインにそのファイル名を指定して実行します。 使い捨てパスワードを行先頭に表示し、 有効期間を示すためにアスタリスクを 1 秒ごとに減らして上書き表示します。 使い捨てパスワードは、 実行例の 483668 の部分で 30 秒間有効です。 30 秒の期限を過ぎたら、 新しい使い捨てパスワードを作って、 再表示します。

$ cat totpkey
otpauth://totp/Example:alice@example.net?secret=PBHWM6TSGJMEGZRU&issuer=Example
$ perl totpauth.pl totpkey
Example:alice@example.net
483668  |   ***************************|

使い捨てパスワードの計算は、 RFC 6238 の通りです。 引数は 5 つです。 UNIX タイムとキーに加えて、 3 つのパラメータを指定します。 Perl が 32 ビットでビルトしてあっても動作するように浮動小数点数を使って計算をおこなっています。

use POSIX ();
use Digest::SHA qw(hmac_sha1_hex hmac_sha256_hex hmac_sha512_hex);

my %HASH_FUNC = (
    'sha1'   => \&hmac_sha1_hex,
    'sha256' => \&hmac_sha256_hex,
    'sha512' => \&hmac_sha512_hex,
);

# RFC 6238 TOTP: Time-Based One-Time Password Algorithm
sub totp {
    my($unix_time, $key, $algorithm, $digits, $period) = @_;
    $algorithm ||= 'sha1';
    $algorithm = ($algorithm eq 'sha256') || ($algorithm eq 'sha512') ? $algorithm : 'sha1';
    $digits ||= 6;
    $digits = $digits == 6 ? 6 : 8;
    $period ||= 30;

    my $t = POSIX::floor($unix_time / $period);
    my $message = pack "NN", int($t / 4294967296.0), int($t % 4294967296.0);
    my $hash = $HASH_FUNC{$algorithm}->($message, $key);
    my $off = hex substr $hash, -1;
    my $bin0 = (hex substr $hash, $off * 2, 1) & 7;
    my $bin = $bin0 . (substr $hash, $off * 2 + 1, 7);
    my $mask = $digits == 6 ? 1000000 : 100000000;
    return sprintf "%0${digits}d", (hex $bin) % $mask;
}

表示部分は、 アカウントを表示した後、 使い捨てパスワードを計算しては、 行を上書きして書き直すようにしています。

use IO::Handle;

if ($issure_path || $param->{'issure'}) {
    print q(), $param->{'issure'} || $issure_path, ":";
}
print $account, "\n";
STDOUT->autoflush(1);
while (1) {
    my $time = time;
    my $otp = totp($time, $key, $algorithm, $digits, $period);
    print "\r$otp  |";
    my $sec = $time % $period;
    print " " x $sec, "*" x ($period - $sec), "|";
    sleep 1;
}