API キーの検証ロジックやらを実装しようとするとたまに登場するであろう hmac モジュールです。
RFC レベルまで理解しようとすると苦行だと思いますが、このモジュール自体の基本的なところはあっさりしているのでまとめました。
公式ドキュメントがしっかりしているから特に存在意義はないと思います。
new メソッドでオブジェクト生成
new
メソッドで HMAC オブジェクトを生成します。
第一引数はハッシュ化するための key 、第二引数はハッシュ化する文字列、 第三引数がハッシュ化するためのアルゴリズムです。
>>> import hashlib, hmac >>> key = b'foo' >>> msg = b'test' >>> a = hmac.new(key, msg, hashlib.sha256)
ダイジェスト値の取得
digest
メソッドでバイト列のダイジェスト値が取得できます。ダイジェスト値ってなんだ?
>>> a.digest() b':\\\x147aB\x83\xa4\xc5Wg\x0f1\x96\xc5m4\xdb\xc5\x10\x9f+\xf3\xdbs\xe61\xcb\x13p\xa4\xe2'
hexdigest
メソッドを使うと16進数の形式で取得できます。API キーなどで使いたい場合はこちらの方が便利かもしれません。
>>> a.hexdigest()
'3a5c1437614283a4c557670f3196c56d34dbc5109f2bf3db73e631cb1370a4e2'
ちなみに binascii モジュール を使うと digest
の結果と hexdigest
の結果を相互変換できます。
- digest → hexdigest
>>> import binascii >>> d = a.digest() >>> binascii.hexlify(d).decode('utf-8') '3a5c1437614283a4c557670f3196c56d34dbc5109f2bf3db73e631cb1370a4e2'
- hexdigest → digest
>>> h = a.hexdigest() >>> binascii.unhexlify(h) b':\\\x147aB\x83\xa4\xc5Wg\x0f1\x96\xc5m4\xdb\xc5\x10\x9f+\xf3\xdbs\xe61\xcb\x13p\xa4\xe2'
ダイジェスト値の検証
このハッシュ(のダイジェスト値)ですが、クライアントから送信されてきた値とサーバー側で計算した値が等しいかどうかを検証する処理がセットになることが多いと思います。
そんなときは compare_digest
メソッドを使います。
>>> a_byte = hmac.new(key, b'hoge', hashlib.sha256).digest() >>> b_byte = hmac.new(key, b'hoge', hashlib.sha256).digest() >>> hmac.compare_digest(a_byte, b_byte) True >>> c_byte = hmac.new(key, b'fuga', hashlib.sha256).digest() >>> hmac.compare_digest(a_byte, c_byte) False
digest の戻り値( byte 型)でも hexdigest の戻り値( str 型)でも OK です。
>>> a_hex = hmac.new(key, b'hoge', hashlib.sha256).hexdigest() >>> b_hex = hmac.new(key, b'hoge', hashlib.sha256).hexdigest() >>> hmac.compare_digest(a_hex, b_hex) True
ただしどちらかに統一されている必要があり、digest の戻り値と hexdigest の戻り値を直接比較しようとしても TypeError になります。
>>> hmac.compare_digest(a_byte, a_hex) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: a bytes-like object is required, not 'str'
ダイジェスト値の取得の他の方法
new メソッドを経由せずに直接ダイジェスト値(バイト列)を得ることができます。
先ほど何回か登場した digest
メソッドは new
メソッドで生成した HMAC オブジェクトに対するインスタンスメソッドですが、こちらはクラスメソッドです。
>>> hmac.digest(key, msg, hashlib.sha256) b':\\\x147aB\x83\xa4\xc5Wg\x0f1\x96\xc5m4\xdb\xc5\x10\x9f+\xf3\xdbs\xe61\xcb\x13p\xa4\xe2'
ちなみに、クラスメソッドとしての hexdigest はないです。
>>> hmac.hexdigest(key, msg, hashlib.sha256) Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: module 'hmac' has no attribute 'hexdigest'
update メソッドの話
冒頭紹介したnew
メソッドですが、第二引数の「ハッシュ化する文字列 msg 」は必須ではありません。
>>> a = hmac.new(key, None, hashlib.sha256) # あるいはこう >>> a = hmac.new(key, digestmod=hashlib.sha256)
そして、この HMAC オブジェクトに対して、update
メソッドを使うと msg を後から追加することができます。
>>> a.update(b'hoge')
使い方はこんな感じです。
>>> a = hmac.new(key, None, hashlib.sha256) >>> b_digest = hmac.digest(key, b'hoge', hashlib.sha256) # 比較用 >>> hmac.compare_digest(a.digest(), b_digest) False # 当然違う値 >>> a.update(b'hoge') >>> hmac.compare_digest(a.digest(), b_digest) True # 同じ
オブジェクトを直接更新する点に注意が必要です。
>>> a.update(b'hoge') >>> hmac.compare_digest(a.digest(), b_digest) False >>> c = hmac.digest(key, b'hogehoge', hashlib.sha256) # 比較用 >>> hmac.compare_digest(a.digest(), c) True
ちなみに new メソッドではなく digest メソッド HMAC オブジェクトを生成する場合は msg を None にすることはできません。
>>> hmac.digest(key, None, hashlib.sha256) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "/Users/kazuma/.anyenv/envs/pyenv/versions/3.9.5/lib/python3.9/hmac.py", line 201, in digest inner.update(msg) TypeError: object supporting the buffer API required
new メソッドで生成したあとに digest メソッドを使った場合は何の問題もないんですけどね。まぁ困るわけではないので別に良いのですが…
>>> hmac.new(key, None, hashlib.sha256).digest() b'h7\x16\xd9\xd7\xf8.\xed\x17Ll\xae\xbe\x08n\xe93v\xc7\x9d|a\xddg\x0e\xa0\x0f\x7f\x8dn\xb0\xa8'
あとがき
Python の言語仕様の説明に終始してしまいましたが、仕組み自体をもう少し深いところまで理解したいのでそのための準備運動ということで一旦この辺で…