ここから、この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^6
や10^8
で割った時に6桁や8桁未満になったときのために0埋めできるように
フォーマット文字列を用意しておきます。
'{:06d}'
もしくは'{:08d}'
です。
この6
や8
をdigits
引数で決めたいですね。
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
と書くとx
のy
乗です。
さあ、完成しました。
Google Authenticatorに適当に同じ値のsecret_key
を指定して、同じ数値が得られるか確認してみましょう。
Base32
でエンコードした値は[A-Z2-7]{16}
でした。
例として'ABCDEFGHIJKLMNOP'
などを入れてみてください。
動作確認済みのソースをPython2
用とPython3
用で作成し、同じフォルダに置いてあります。