hashlib

【Python】hashlibによるハッシュ化

【Python】hashlibによるハッシュ化

Pythonでハッシュ化するためのhashlibモジュールについて解説します。

ハッシュ化

ハッシュ関数(一方向関数)

パスワードなどを解読できないように変換するによく使われる関数がハッシュ関数です。ハッシュ関数は、任意の長さのデータを通すと特定の長さのデータ(ハッシュ値)に変換します。

ハッシュ関数の特徴は、ハッシュ値を生成するのは簡単である一方、ハッシュ値から元のデータを生成することが不可能な点です。このような性質からハッシュ関数は一方向関数とも呼ばれます。

よくハッシュ関数で使用されるSHA-256は、長さが256ビットのハッシュ値に変換されます。暗号資産で有名なビットコインのハッシュ値計算でもSHA-256が使用されています。

本記事では、Pythonでハッシュ化するためのモジュールであるhashlibについて紹介します。

ハッシュ化と暗号化の違い

ハッシュ化の他に暗号化という技術もあります。ハッシュ関数は、上記でも説明したように元に戻すことができない一方向関数です。暗号化は、鍵となる情報を用いることでもとのデータに戻すこと(復号化)ができます。ハッシュ化と暗号化の大きな違いはデータに戻すことができるかできないかという点です。

ハッシュが使われる代表的な例がパスワードの保管です。システムのデータベースには、パスワードをハッシュ化した値を保存しておきます。このようにしておくことで、クラッカーが悪意を持ってパスワードを盗んでも元の文字列を復元することはできません。

システムの利用者がパスワードを入力した際は、ハッシュ関数でハッシュ化した値がシステムのデータベースに格納されている値と一致しているかでログイン制御をすることができます。

Pythonでの暗号化・復号化については「PyCryptodomeによる暗号化・復号化」でまとめていますので興味があれば参考にしてください。

hashlibによるハッシュ化

Pythonではハッシュ化のためのモジュールとしてhashlibが提供されています。以降では簡単な例を使ってハッシュ化の方法について説明していきます。

hashlibで提供されているアルゴリズム

hashlibでは、複数のハッシュ化アルゴリズムが提供されています。対応可能なハッシュについては以下のようにalgorithms_guaranteedで確認することができます。

import hashlib

print(hashlib.algorithms_guaranteed)
【実行結果】
{'sha1', 'blake2s', 'shake_128', 'md5', 'shake_256', 'sha3_384', 'sha256', 'sha224', 'sha512', 'sha3_224', 'blake2b', 'sha384', 'sha3_512', 'sha3_256'}

hashlibでは上記で表示しているような多くのハッシュ化アルゴリズムが提供されています。

hashlibを用いたハッシュ化方法

以降では、一般的によく使用されるSHA-256でのハッシュ化を例にhashlibの使い方を説明します。SHA-256を例に使い方を記載しますが、SHA-256以外のアルゴリズムを使いたい場合は、関数名や引数に指定する文字列を変更するだけで簡単にアルゴリズムの変更が可能です。

基本的な使い方

hashlibでSHA-256でハッシュ化する場合には、SHA-256に対応するsha256を以下のように使用します。

import hashlib


def main():
    password = "P@ssw0rd"
    print(password)

    # SHA-256でハッシュ化
    digest = hashlib.sha256(password.encode("utf-8")).hexdigest()
    print(digest)


if __name__ == "__main__":
    main()
【実行結果】
P@ssw0rd
b03ddf3ca2e714a6548e7495e2a03f5e824eaac9837cd7f159c67b90fb4b7342

引数としては、上記のようにハッシュ化文字列をエンコードした値を渡します。上記では返却値にhexdigest()を適用して16進数表記のハッシュ値にしています。

例えば、SHA-512を使いたい場合には、sha256の部分をsha512とするだけでアルゴリズムの変更が可能です。

updateで複数対象を結合してハッシュ化

複数の対象を結合してハッシュしたい場合には、以下のようにupdateで対象を順番に追加してからハッシュ化を実行します。

import hashlib


def main():
    target_str1 = "sample01"
    target_str2 = "sample02"
    joined_str = target_str1 + target_str2
    print(target_str1)
    print(target_str2)
    print(joined_str, "\n")

    # updateで複数対象をつなげてハッシュ化
    h = hashlib.sha256()
    h.update(target_str1.encode("utf-8"))
    h.update(target_str2.encode("utf-8"))
    digest = h.hexdigest()
    print(digest)

    # 文字列を結合したもののハッシュ化と同じ
    digest = hashlib.sha256(joined_str.encode("utf-8")).hexdigest()
    print(digest)


if __name__ == "__main__":
    main()
【実行結果】
sample01
sample02
sample01sample02 

af239deb316d2daad434f8ec780f2e89184d81ae60f3d98c76ca3de4dcca5e45
af239deb316d2daad434f8ec780f2e89184d81ae60f3d98c76ca3de4dcca5e45

updateを使用する場合には、まず引数なしでsha256を呼び出します。その後、ハッシュ化対象の文字列を順にupdateに渡していきます。

上記では、updateで文字を順に追加してからハッシュ化したものと、文字列をあらかじめ結合してからハッシュ化したものを表示していますが、ハッシュ値は同じになっていることが分かるかと思います。

ファイルのデータをハッシュ化 (Python 3.11で追加)

ファイル内のデータに対してハッシュ化したい場合には、以下のようにfile_digestを使用することができます。file_digestは、Python 3.11で追加されていますので、それ以前のバージョンの場合は使えませんのでご注意ください。

import hashlib


def main():
    filepath = "targetfile.txt"

    # ファイルを読み込んでハッシュ化
    with open(filepath, "rb") as f:
        h = hashlib.file_digest(f, "sha256")
    digest = h.hexdigest()
    print(digest)


if __name__ == "__main__":
    main()
【実行結果】
b03ddf3ca2e714a6548e7495e2a03f5e824eaac9837cd7f159c67b90fb4b7342

上記の結果は「P@ssw0rd」と書かれているだけのtargetfile.txtというテキストファイルを用意しておいて実行した結果になっています。

対象パスのファイルをバイナリ読み取り専用(“rb”)で開き、file_digestにファイルオブジェクトとハッシュアルゴリズム文字列を指定します。

このようにファイルの内容をハッシュ化する際にfile_digestは便利です。

解読耐性のあるハッシュ値を取得する pdkbf2_hmac

上記で見てきたような単純なハッシュ化は、ハッシュ値の対応表がある場合にクラッキングされる恐れがあります。解読対策として、以下のような方法が有効と言われています。

  1. ハッシュ化するデータにSalt(ソルト)と呼ばれるデータをユーザー毎に付け足してからハッシュ化する
  2. 得られたハッシュ値に対して、一定回数以上のハッシュ化を繰り返す。この処理をStretching(ストレッチング)という

hashlibには、Salt(ソルト)、Stretching(ストレッチング)に対応したpdkbf2_hmacが用意されています。pdkbf2_hmacを使用する場合には以下のように使用します。

import base64
import hashlib
import os

# saltの生成
salt = base64.b64encode(os.urandom(32))
# hash化の回数
iter_num = 100000


def main():
    password = "P@ssw0rd"
    print(password)

    # ハッシュ化
    digest = hashlib.pbkdf2_hmac(
        "sha256", password.encode("utf-8"), salt, iter_num
    ).hex()
    print(digest)


if __name__ == "__main__":
    main()
【実行結果】
P@ssw0rd
3e4e252b3d998a58b28cdce579264439d458c1c1228831b239c86458b244d7f3

Salt(ソルト)値は、os.urandomで32バイトのランダムな文字列を生成し、base64モジュールでエンコードして生成しています。Salt(ソルト)値は、os.urandomで16バイトかそれ以上のバイト列にするべきともいわれています。ここの部分はこういった書き方ぐらいに覚えてしまってもよいかなと思います。

pdkdf2_hmacには、ハッシュ化方法である”sha256″とSalt(ソルト)値、Stretching(ストレッチング)の繰り返し回数を指定することでハッシュ値を計算できます。

Saltや繰り返し回数が分からないとハッシュ値の計算は非常に困難になるため、クラッカーによる解読に対する耐性を持たせることができます。このようにpdkdf2_hmacを使うことで解読耐性のあるハッシュ値を取得して利用することが可能です。

ログインを想定したハッシュ化の使用例

ハッシュ化の使用例として、ログインパスワードをハッシュ化して使用する例を紹介します。

import base64
import hashlib
import os

# ユーザーとパスワードのDB
db = {}
# saltの生成
salt = base64.b64encode(os.urandom(32))
# hash化の回数
iter_num = 100000


def get_digest(password: str):
    """パスワードのハッシュ値計算

    Args:
        password: パスワード文字列

    Returns:
        ハッシュ値
    """
    digest = hashlib.pbkdf2_hmac(
        "sha256", password.encode("utf-8"), salt, iter_num
    ).hex()

    return digest


def is_login(user_name: str, password: str):
    """ログイン処理

    Args:
        user_name: ユーザー名
        password: パスワード

    Returns:
        ログイン結果
    """
    return get_digest(password) == db[user_name]


def main():
    user_name = "user01"
    user_password = "P@ssw0rd"

    digest = get_digest(user_password)
    db[user_name] = digest
    print(f"db: {db}\n")

    print(f"正常なログインの場合: {user_name} {user_password}")
    print(is_login(user_name, user_password))

    user_password_ng = "password"
    print(f"パスワード不正の場合: {user_name} {user_password_ng}")
    print(is_login(user_name, user_password_ng))


if __name__ == "__main__":
    main()
db: {'user01': '4320c4509b9c068312131f719cac5380572eb5062000a95d9ae1ce5fca139ecd'}

正常なログインの場合: user01 P@ssw0rd
True
パスワード不正の場合: user01 password
False

上記において、get_digest関数は、入力されたパスワードに対してhashlibのsha256でハッシュ値を求めるものです。また、is_login関数はログイン時の認証のための関数で、与えられたパスワードに対してハッシュ化を行い、データベースに登録されているユーザー名に対応するパスワードと一致すればTrueを返す関数です。

main関数では、与えられたパスワードに対してハッシュ値を求めて、DBを想定した辞書にユーザー名と合わせて記録しています。ここが、ユーザー名とパスワードをシステムのデータベースへ登録する部分をイメージしています。

main関数内でのログイン認証に該当する部分では、ユーザー名とパスワードをis_login関数へ渡しています。パスワードが一致している場合には、Trueが返却されていることが分かります。一方で、パスワードが誤っている場合には、Falseとなっていることが分かります。

まとめ

Pythonでハッシュ化するためのモジュールであるhashlibモジュールについて解説しました。

基本的なハッシュ化の使い方やpdkdf2_hmacによる解読耐性のあるハッシュ値を取得について説明しました。また、ログインを想定した時のパスワードのハッシュ化例についても紹介しています。

ハッシュは、一方向関数である特徴から色々な場面で使用されます。是非hashlibの使い方を覚えてもらえたらなと思います。

Note

hashlibの公式ドキュメントはこちらを参照してください。