Pythonにおけるハッシュ計算

エキスパートPythonプログラミング改訂2版

エキスパートPythonプログラミング改訂2版

はじめに

ADVENTARのPythonアドベントカレンダー 4日目 です。 今作っているもの で後々必要になるかなと思い調べていたハッシュ関数について書きます。


ハッシュ関数について

ハッシュ関数は、任意長のデータ x を与えると固定長のビット列 y を返す関数です。 イメージとして↓のような感じになると思います。

H(x) -> y  # Hはハッシュ関数、xは任意長のデータ、yは固定長のビット列

この時の特性として y から x を特性するのが困難な一方向性関数である必要があり、データの正当性を検証する際などに使われます。

代表的なハッシュ関数アルゴリズムとして、 crc32md5sha-1sha-256 などがありますが、これらは目的に応じて使い分けます。

例えば crc32チェックサムのために設計されたもので、高速に動作し大きなファイルに有効です。そのためファイルが破損してないかどうかの確認によく利用されます。 ただし衝突が多くセキュリティの面では望ましくないようです。

一方、SHA-256は衝突が少なく非常に安全ですが動作は比較的遅く、速さと安全性はトレードオフとなっています。


Pythonのhashlibを触ってみる

Pythonの標準モジュールに含まれているhashlibの中にはMD5SHA-1、SHA-256などのハッシュ関数が入っています。

>>> import hashlib
>>> hashlib.md5(b'hello hello').hexdigest()
'f52d885484f1215ea500a805a86ff443'

>>> hashlib.md5(b'hello hellO').hexdigest()
'263ac69e8be54cdc5baeb3638a63709e'

文字列を少し変更するだけで全く違うハッシュ値が返ってきました。この特徴は y から x を特定されないために重要な特徴です。 ちなみにSHA-256を使いたければ hashlib.sha256 を使えば同じことが簡単にできます(sha-256なので256ビットのビット列が表示されるはずです)。

Cookieのハッシング

もう少し具体的な例を考えてみます。ここではHTTPのリクエストヘッダに↓を加えWebサイトに訪れた回数をクッキーに格納する例を考えます。

set-cookie: visits = s, [hash]

この時、ハッシュ関数s を与えた時の値が hash と一致すれば値は改ざんされていないとみなします。 コードにすると↓のようになります。

import hashlib

def hash_str(s):
    return hashlib.md5(s).hexdigest()

def make_secure_value(s):
    return "%s,%s" % (s, hash_str(s))

def check_secure_value(h):
    value = h.split(',')[0]
    if h == make_secure_value(value):
        return value

しかしこの例では、使っているアルゴリズムを推定して簡単に偽造が出来てしまうという大きな問題があります。

hmacを使ってみる

ハッシュ関数への入力 x に文字列を加えると当然ですがハッシュ値は大きく変化します。さっきの方法ではアルゴリズムがバレれば容易に偽造できますが、入力 x に文字列を足す場合、アルゴリズムが推測されてもこの文字列がばれない限り改ざんが出来ません。

さっきはhashlibを使ってましたが、pythonには標準でhmac(Hash-based Message Authentication Code)モジュールがあるので、そちらを利用します。

>>> import hmac
>>> hmac.new(b'secret', b'shibata').hexdigest()
'5ad21c9df4140462850c3c8cdbf37358'

hmacを使って書き換えると↓のようになります。

import hmac

SECRET = 'secretstring'

def hash_str(s):
    return hmac.new(SECRET, s).hexdigest()

def make_secure_value(s):
    return "%s|%s" % (s, hash_str(s))

def check_secure_value(h):
    value = h.split('|')[0]
    if h == make_secure_value(value):
        return value

これで、SECRETの文字列とアルゴリズムがバレないかぎり、偽造出来ないようになりました。

もう少し考えるとすればSECRETを工夫する必要がありそうです。 具体的にはユーザIDなどから、攻撃者に推測されないユーザ固有の文字列を生成すると良さそうです。 これについて興味のある方は「Salt 作り方」とかで調べれば出てくると思います。 今度それについても書きたい

おわりに

この記事が今年初のAdvent Calender。ためてる内容がいくつかあるのでこの調子で減らしていきたい。