05 April, 2021

Pythonで実装するTOTP - Part 2

目次

では、Pythonでの実装についてみていきましょう。 HMAC関数に渡す引数3つを順に準備していきます。

まず、秘密鍵からです。 秘密鍵は16文字の半角英大文字+6種類の半角数字でした。 この文字列をPythonの標準ライブラリでBase32のデコードを行います。 関数はBase32ですが、この種類の関数はbase64モジュールに含まれているので base64をインポートします。

import base64

Python2だとstrもバイト列も同じ(極端に言うと、です)なのでbase64.b32decode関数にそのまま渡すことができます。 しかし、渡される文字列が8文字刻みでないとエラーになってしまいます。 汎用的にするために8文字の倍数でないときにパディングとして8文字の倍数になるように "="を追加します。 今回は16文字とわかっているので不要ですが、デコードで最低限エラーにならないように パディングをします。 式は以下のとおりです。

secret_key += '=' * (-len(secret_key) % 8)

モジュロ(%)の計算での符号の関係はこちらの説明を参照してください。 バイナリに戻した秘密鍵をリターンするコードは下記のようになります。

return base64.b32decode(secret_key)

関数名はdecode_secret_keyとでもしましょう。

次に時刻情報です。 Pythonでtimestampを求めるにはdatetimeモジュールのdatetimeオブジェクトで取得できます。 datetimeモジュールからdatatimeオブジェクトをインポートしておきましょう。

from datetime import datetime

しかし、Python2ではdatetimeオブジェクトにはtimestamp関数はありません。 この関数はPython3のVersion3.3から使えるようになります。 ではどうやって計算することができるのでしょうか。 epochタイムをdatetime(1970, 1, 1, 0, 0)として、基準の日時を求めます。 UTCの現在時刻からこの基準日時を引くことでタイムスタンプに相当する時間間隔を計算します。

utcnow = datetime.utcnow()
epoch = datetime(1970, 1, 1, 0, 0)
utctimedelta = utcnow - epoch

datetimeオブジェクトどおしで引き算するとtimedeltaオブジェクトが得られるのでそこから total_seconds()関数で秒数を算出します。得られる秒数は浮動小数なので整数に変換します。

timestamp = int(utctimedalta.total_seconds())

こうしてUTCのタイムスタンプが得られました。 あとはtime-step sizeで整数除算をすれば、HMACに与えるmessage(任意のデータ)の準備が できます。 HMACに与える時刻情報(time-step sizeで割った値)をtimecodeと呼ぶことにします。

必須ではありませんが、この時割り算をしたあまりはtime-step sizeの中で経過した時間と言えます。 つまり、time-step sizeからこのあまりの秒数を引けば残り時間となります。 整数除算の商とあまりを一度に計算する関数がPythonにはあります。divmodです。 商とあまりはタプルとして返されます。

(quo, rem) = divmod(timestamp, period)

ここで使用するperiodは引数として受け取るようにしますが、デフォルトとして30を指定しておきます。

def get_timecode(period=30):

この結果をタプルとしてtimecodeとremaining(残り時間)を返すようにします。

return (quo, (period - rem))

TOTPを計算する関数に渡す引数としてはデコード前のsecret_keyと、timestampperiodで割った 整数のtimecodeを渡すようにします。

そのため、HMAC関数にこれらの情報を渡す前にもうひと手間かける必要があります。 一つはsecret_keyをデコード関数でデコードすること、もう一つはtimecodeを HMAC関数がメッセージとして受け取れるバイト列にすることです。

TOTPを求める関数をget_totpとしましょう。

def get_totp(secret_key, timecode, digits=6):

secret_keyをデコードします。

    secret = decode_secret_key(secret_key)

整数のtimecodeをバイト列のtimecode_arrayに変換します。timecode_arrayは8バイトです。 変換には標準ライブラリのstructモジュールを使用します。 structモジュールはintfloatといった抽象化されたオブジェクトとメモリ上での配列に 相互変換するパッケージです。TOTPの計算ではtimecodelonglongの8バイトに変換するところと のちに出てくるダイジェストを4バイトのlongに変換するところで使用します。 エンディアンはどちらもビッグエンディアンです。

structモジュールのpack関数は与えられたフォーマットで整数をバイト列に変換します。 フォーマットの'>'はビッグエンディアンを意味し、'Q'longlong(8バイト)を指定します。

timecode_array = struct.pack('>Q', timecode)

最後の引数であるハッシュ関数の種類はhashlibモジュールから取得します。 とりあえずhashlibモジュールをインポートします。

import hashlib

sha1hashlib.sha1で取得できます。

ここまででHMACの準備ができました。 ではHMACの計算をしていきましょう。 HMACモジュールをインポートします。

import hmac

hm = hmac.new(secret, timecode_array, hashlib.sha1)

hmオブジェクトからダイジェストを求めます。

digest = hm.digest()

Python3では下記のように直接ダイジェストを求めることができます。 また、この関数ではhashlibを使わずにアルゴリズムの名称(文字列)で指定することができます。

digest = hmac.digest(secret, timecode_array, 'SHA1')

得られたダイジェストは前述のとおりSHA1なので20バイト(160ビット)のバイナリデータです。

Pythonで実装するTOTP - Part 1 Pythonで実装するTOTP - Part 3


Tags: , , ,