05 April, 2021

Pythonで実装するTOTP - Part 3

目次

ここから、この20バイトを6文字の数字に変換していく方法を説明します。

この20バイトの連続する4バイトを使用してlong整数を取り出します。 取り出す位置は最後のバイト(オフセット19)の下位4ビットで決めます。 一般的な言語ではdigest[19]と書いて指定します。 Pythonでは最後のバイトなのでdigest[-1]と書けます。 アルゴリズムはSHA-1が一般的ですが、RFC6238ではSHA-256などほかのアルゴリズムも オプションとして使用できるとしています。 その場合は32バイトになるのでdigest[31]としなければなりませんが、digest[-1]と しておけばダイジェスト長にあわせて変更する必要はなくなります。

この最後の1バイトを数値として扱うためにord関数で変換します。 そして、下記の式で下位4ビットを取り出します。

offset = ord(digest[-1]) & 0x0f

バイト列から任意のスライスを取得するので今計算したoffsetでスライスを作ります。

value_array = digest[offset:offset+4]

バイト列からlong整数に変換するのに既出のstructモジュールのunpack_from関数を使用します。 この関数は第3引数にバイト列のどこから使用するかというオフセットを取ることができるので 先ほどのスライスはあらかじめ計算せず、この関数にオフセットを渡すことにします。

log_value = struct.unpack_from('>l', digest, offset)

フォーマットはpack関数と同様'>'でビッグエンディアンを、'l'(エル)でlongを指定しています。

unpack_fromは複数のフォーマット文字で複数の値をリストで返す仕様になっているので、 long_valueはリストになっています。値は一つしかないのでインデックス0の値を使用します。 この時、正のlong整数にするために0x7fffffffでマスクします。

value = long_value[0] & 0x7fffffff

valueは31ビットなので6桁の整数より大きくなることがあります。 6桁にするために10^6で割ったあまりを使用します。

RFC6238では6桁と8桁のオプションが許されているようです。 引数でdigit=6として呼び出し時に8が指定できるようにしているので、このオプションにも 対応しておきましょう。

10^610^8で割った時に6桁や8桁未満になったときのために0埋めできるように フォーマット文字列を用意しておきます。 '{:06d}'もしくは'{:08d}'です。 この68digits引数で決めたいですね。 totp_format文字列を作っていったんフォーマット文字列を変数に入れておきます。

totp_format = '{{:0{digits:}d}}'.format(digits=digits)

最初と最後の2重{{,}}は1重の{,}に変換されます。 内側の{digits:}は引数のdigitsの値で置き換えられます。 結果として'{:06d}''{:08d}'となります。

最後にtotpを計算します。

totp = totp_format.format(value % 10**digits)

**演算子はx**yと書くとxy乗です。

さあ、完成しました。 Google Authenticatorに適当に同じ値のsecret_keyを指定して、同じ数値が得られるか確認してみましょう。 Base32でエンコードした値は[A-Z2-7]{16}でした。 例として'ABCDEFGHIJKLMNOP'などを入れてみてください。

動作確認済みのソースをPython2用とPython3用で作成し、同じフォルダに置いてあります。

  • py2totp.py
  • py3totp.py

Pythonで実装するTOTP - Part 2


Tags: , , ,