hashlib

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

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

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

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

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

このように hashlib によるハッシュ化を使用することで、ハッシュ値を用いた様々制御を実装することが可能です。

まとめ

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

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

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

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

ソースコード

上記で紹介しているソースコードについては GitHub にて公開しています。参考にしていただければと思います。

あわせて読みたい
【Python Tech】プログラミングガイド
【Python Tech】プログラミングガイド
ABOUT ME
ホッシー
ホッシー
システムエンジニア
はじめまして。当サイトをご覧いただきありがとうございます。 私は製造業のメーカーで、DX推進や業務システムの設計・開発・導入を担当しているシステムエンジニアです。これまでに転職も経験しており、以前は大手電機メーカーでシステム開発に携わっていました。

プログラミング言語はこれまでC、C++、JAVA等を扱ってきましたが、最近では特に機械学習等の分析でも注目されているPythonについてとても興味をもって取り組んでいます。これまでの経験をもとに、Pythonに興味を持つ方のお役に立てるような情報を発信していきたいと思います。どうぞよろしくお願いいたします。

※キャラクターデザイン:ゼイルン様
記事URLをコピーしました