- Python
Python hashlibの使い方|SHA256・MD5とファイルのハッシュ値
hashlibとは、SHA-256やMD5などのハッシュ値を計算するPythonの標準ライブラリで、hashlib.sha256(バイト列).hexdigest() と書くだけで16進数のハッシュ値が得られます。 ハッシュ値は、データから計算される固定長の値です。同じデータからは必ず同じ値になり、1バイトでも違えばまったく別の値になるので、ファイルが同じかどうかの確認や、ダウンロードしたファイルの検証に使われます。
この記事では、文字列・バイト列・ファイルの3パターンをコピペで動く形で示し、大きなファイル向けのチャンク読みと Python 3.11 で追加された hashlib.file_digest を時間とメモリで比べます。アルゴリズムの早見表、base64 への変換、sha256sum コマンドとの突き合わせ、hmac、パスワード用途での注意まで扱います。掲載した出力は Python 3.11.15(OpenSSL 3.0.13、Linux)で実際に動かした結果です。
hashlibの早見表
| やりたいこと | コード | 結果の型 |
|---|---|---|
| バイト列の SHA-256 | hashlib.sha256(b"hello").hexdigest() |
str(16進64文字) |
| 文字列の SHA-256 | hashlib.sha256("こんにちは".encode("utf-8")).hexdigest() |
str |
| MD5 | hashlib.md5(b"hello").hexdigest() |
str(16進32文字) |
| 生のバイト列で受け取る | hashlib.sha256(b"hello").digest() |
bytes(32バイト) |
| 少しずつ足して計算 | h = hashlib.sha256(); h.update(b"...") |
― |
| ファイル(3.11以降) | hashlib.file_digest(f, "sha256").hexdigest()(f は "rb" で開く) |
str |
| ファイル(3.10以前も可) | while chunk := f.read(1024 * 1024): h.update(chunk) |
― |
| base64 で出力 | base64.b64encode(hashlib.sha256(data).digest()).decode() |
str(44文字) |
| 名前でアルゴリズムを選ぶ | hashlib.new("sha256", b"hello") |
― |
| 鍵つきハッシュ(HMAC) | hmac.new(key, msg, hashlib.sha256).hexdigest() |
str |
| ハッシュ値を比べる | hmac.compare_digest(a, b) |
bool |
| パスワードから鍵を作る | hashlib.scrypt(...) / hashlib.pbkdf2_hmac(...) |
bytes |
渡すのは必ず bytes です。str はそのまま渡せないので .encode("utf-8") で変換します。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Python hashlibの使い方|SHA256・MD5とファイルのハッシュ値」
パターン1:バイト列のハッシュ値を求める
import hashlib
print(hashlib.sha256(b"hello").hexdigest())
print(hashlib.md5(b"hello").hexdigest())
2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
5d41402abc4b2a76b9719d911017c592
流れは「ハッシュオブジェクトを作る → 結果を取り出す」の2段階です。取り出し方は2つあります。
| メソッド | 返すもの | 例(SHA-256) |
|---|---|---|
hexdigest() |
16進数の文字列(str) | 64文字 |
digest() |
生のバイト列(bytes) | 32バイト |
データを分けて渡すこともできます。update() を何回かに分けて呼んでも、まとめて渡したときと同じ値になります。 これが、後で出てくるファイルのチャンク読みの土台です。
import hashlib
h = hashlib.sha256()
h.update(b"hel")
h.update(b"lo")
print(h.hexdigest() == hashlib.sha256(b"hello").hexdigest())
True
パターン2:文字列のハッシュ値を求める
文字列は encode() でバイト列に変えてから渡します。
import hashlib
text = "こんにちは"
print(hashlib.sha256(text.encode("utf-8")).hexdigest())
125aeadf27b0459b8760c13a3d80912dfa8a81a68261906f60d87f4a0268646c
strをそのまま渡すとTypeErrorになる
import hashlib
hashlib.sha256("hello")
TypeError: Strings must be encoded before hashing
h.update("hello") でも同じメッセージが出ます。直し方は "hello".encode("utf-8") か、リテラルなら b"hello" にすることです。
| 直す前 | 直した後 |
|---|---|
hashlib.sha256("hello") |
hashlib.sha256("hello".encode("utf-8")) |
hashlib.sha256(text) |
hashlib.sha256(text.encode("utf-8")) |
hashlib.sha256(open(path).read()) |
hashlib.sha256(open(path, "rb").read()) |
文字コードと改行でハッシュ値は変わる
ハッシュ値はバイト列から計算されるので、同じ文字でも文字コードが違えば別の値になります。「日本語」という3文字で確かめました(先頭16文字のみ表示)。
| 文字コード | SHA-256(先頭16文字) |
|---|---|
| utf-8 | 77710aedc74ecfa3 |
| cp932 | 7fa11a31677814a9 |
| utf-16 | 594678acc1de24bd |
改行の有無でも変わります。
| 入力 | SHA-256 |
|---|---|
b"hello" |
2cf24dba5fb0a30e... |
b"hello\n" |
5891b5b522d5df08... |
b"hello\r\n" |
cd2eca3535741f27... |
ほかのツールや他言語と値が合わないときは、まず文字コードと末尾の改行を確認すると早く解決します。
パターン3:ファイルのハッシュ値を求める
ファイルは必ずバイナリモード("rb")で開きます。書き方は3通りあります。
# filehash.py
import hashlib
import sys
import time
import tracemalloc
def hash_read_all(path):
"""1. 全部読んでから計算(小さいファイル向け)"""
with open(path, "rb") as f:
return hashlib.sha256(f.read()).hexdigest()
def hash_chunks(path, chunk_size=1024 * 1024):
"""2. 1MiBずつ読んで update(Python 3.8以降)"""
h = hashlib.sha256()
with open(path, "rb") as f:
while chunk := f.read(chunk_size):
h.update(chunk)
return h.hexdigest()
def hash_file_digest(path):
"""3. hashlib.file_digest(Python 3.11以降)"""
with open(path, "rb") as f:
return hashlib.file_digest(f, "sha256").hexdigest()
if __name__ == "__main__":
path = sys.argv[1]
for func in (hash_read_all, hash_chunks, hash_file_digest):
tracemalloc.start()
start = time.perf_counter()
digest = func(path)
elapsed = time.perf_counter() - start
_, peak = tracemalloc.get_traced_memory()
tracemalloc.stop()
print(f"{func.__name__:17} {digest[:16]}... {elapsed:.3f}s peak={peak / 1024 / 1024:.1f}MiB")
tracemalloc は Python が確保したメモリの最大値を測るための標準ライブラリで、比較のために入れています。普段使うときは3つの関数だけを取り出してください。
小さいファイルで3つの結果が同じことを確かめる
中身が hello と改行だけの hello.txt(6バイト)で実行します。
printf 'hello\n' > hello.txt
python filehash.py hello.txt
hash_read_all 5891b5b522d5df08... 0.000s peak=0.0MiB
hash_chunks 5891b5b522d5df08... 0.000s peak=1.0MiB
hash_file_digest 5891b5b522d5df08... 0.000s peak=0.3MiB
3つとも同じ値です。
200MBのファイルで時間とメモリを比べる
209,715,200 バイト(200MiB)のファイルを作って実行した結果です(2回目の計測)。
hash_read_all bf375859eeb4cfaf... 0.262s peak=200.0MiB
hash_chunks bf375859eeb4cfaf... 0.177s peak=2.0MiB
hash_file_digest bf375859eeb4cfaf... 0.185s peak=0.3MiB
| 方法 | 時間 | メモリの最大値 | 使える版 | 向いている場面 |
|---|---|---|---|---|
f.read() で全部読む |
0.262秒 | 200.0MiB(ファイルと同じ大きさ) | すべて | 数MBまでの小さなファイル |
| チャンク読み(1MiBずつ) | 0.177秒 | 2.0MiB | 3.8以降(:= を使うため) |
3.10以前でも動かしたいとき、進み具合を表示したいとき |
hashlib.file_digest |
0.185秒 | 0.3MiB | 3.11以降 | 3.11以降なら第一候補 |
全部読む方法は、ファイルの大きさと同じだけメモリを使います。 チャンク読みと file_digest は、ファイルがどれだけ大きくてもメモリの使用量がほぼ一定でした。時間は計測のたびに少し変わりますが、この環境ではチャンク読みと file_digest のほうが短くなりました(数値は環境によって変わります)。
file_digestの使い方と注意点
hashlib.file_digest(fileobj, digest, /) は Python 3.11 で追加された関数です(公式ドキュメントの hashlib のページで確認)。2つ目の引数には、アルゴリズム名の文字列か、ハッシュオブジェクトを返す関数を渡せます。
import hashlib
import io
with open("hello.txt", "rb") as f:
print(hashlib.file_digest(f, "sha256").hexdigest())
with open("hello.txt", "rb") as f:
print(hashlib.file_digest(f, lambda: hashlib.blake2b(digest_size=16)).hexdigest())
# ファイルでなくても、バイナリを読めるオブジェクトなら渡せる
print(hashlib.file_digest(io.BytesIO(b"hello\n"), "md5").hexdigest())
5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03
ea41b4de6c03f13a95758b28dd75693e
b1946ac92492d2347c6235b4d2611184
テキストモードで開いたファイルを渡すと、次のエラーになります。
with open("hello.txt") as f: # "rb" を付けていない
hashlib.file_digest(f, "sha256")
ValueError: '<_io.TextIOWrapper name='hello.txt' mode='r' encoding='utf-8'>' is not a file-like object in binary reading mode.
open(path, "rb") に直せば解決します。 テキストモードは改行の変換や文字コードの解釈が入るため、ファイルのハッシュ値には必ずバイナリモードを使います。
Python 3.10以前でも動く書き方
:=(代入式)が使えない 3.7 以前も含めて動かしたい場合は、iter を使った形にします。次のコードは 3.11 で実行して、上と同じ値になることを確かめています。
import hashlib
from functools import partial
def hash_chunks_compat(path, chunk_size=1024 * 1024):
h = hashlib.sha256()
with open(path, "rb") as f:
for chunk in iter(partial(f.read, chunk_size), b""):
h.update(chunk)
return h.hexdigest()
print(hash_chunks_compat("hello.txt"))
5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03
アルゴリズム早見表(出力長・安全性・用途)
b"hello" を各アルゴリズムで計算し、出力の長さを実測しました。
| アルゴリズム | hashlibでの名前 | 出力長(ビット) | digest のバイト数 | hexdigest の文字数 | 安全性の目安 | 主な用途 |
|---|---|---|---|---|---|---|
| MD5 | md5 |
128 | 16 | 32 | 同じ値になる別データを作る方法が知られている | 転送時の破損チェック、重複ファイルの検出、既存の仕組みとの互換 |
| SHA-1 | sha1 |
160 | 20 | 40 | 同じ値になる別データの作成例が公表されている | Git のオブジェクトIDなど既存の仕組みとの互換 |
| SHA-224 | sha224 |
224 | 28 | 56 | 現在広く使われている | 出力を短くしたいとき |
| SHA-256 | sha256 |
256 | 32 | 64 | 現在広く使われている | 配布ファイルの検証、署名、迷ったらこれ |
| SHA-384 | sha384 |
384 | 48 | 96 | 現在広く使われている | より長い出力が指定されているとき |
| SHA-512 | sha512 |
512 | 64 | 128 | 現在広く使われている | 長い出力が必要なとき |
| SHA3-256 | sha3_256 |
256 | 32 | 64 | SHA-2 と別の設計の標準 | SHA-3 が指定されているとき |
| SHA3-512 | sha3_512 |
512 | 64 | 128 | SHA-2 と別の設計の標準 | 同上 |
| BLAKE2b | blake2b |
最大512(可変) | 64 | 128 | 現在広く使われている | 高速なハッシュ、鍵つきハッシュ、出力長の指定 |
| BLAKE2s | blake2s |
最大256(可変) | 32 | 64 | 現在広く使われている | 32ビット環境や小さな機器向け |
| SHAKE128 / 256 | shake_128 / shake_256 |
任意 | 任意 | 任意 | SHA-3 系の可変長 | 長さを自由に決めたいとき(hexdigest(16) のように長さを渡す) |
出力長と文字数は実測値です。安全性の欄は一般に知られた評価を短くまとめたもので、今回あらためて検証したものではありません。
選び方の目安は次のとおりです。
| 目的 | 選ぶもの |
|---|---|
| ダウンロードしたファイルが配布元と同じか確かめる | 配布元が公開している方式(多くは SHA-256)に合わせる |
| 自分で新しく方式を決める | SHA-256 |
| 改ざんの検出が目的ではなく、重複ファイルを素早く見つけたい | MD5 や BLAKE2 でも用は足りる。迷うなら SHA-256 |
| 共有した鍵で「送り主が正しいか」も確かめたい | HMAC-SHA256(hmac モジュール) |
| パスワードを保存する | scrypt や pbkdf2_hmac(後述)。sha256 を1回かけるだけでは足りない |
MD5とSHA-1はどこまで使えるか
MD5 と SHA-1 は、同じハッシュ値になる別のデータを意図的に作る方法が知られています。 そのため、「誰かが中身をすり替えていないか」を確かめる用途や電子署名には、SHA-256 以降を選びます。
一方で、通信やコピーの途中で偶然ビットが変わっていないかを確かめる用途や、手元のフォルダから同じ内容のファイルを探す用途では、今も広く使われています。「意図的なすり替えを防ぎたいかどうか」が判断の分かれ目です。
セキュリティ目的ではないことを明示する引数もあります。
import hashlib
print(hashlib.md5(b"hello", usedforsecurity=False).hexdigest())
5d41402abc4b2a76b9719d911017c592
usedforsecurity は Python 3.9 で追加されたキーワード専用引数で、既定値は True です(公式ドキュメントで確認)。MD5 の利用が制限された環境でも、False を指定するとセキュリティ以外の目的として計算できる場合があります。
使えるアルゴリズムを調べる
import hashlib
print(sorted(hashlib.algorithms_guaranteed))
print(sorted(hashlib.algorithms_available - hashlib.algorithms_guaranteed))
['blake2b', 'blake2s', 'md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha3_224', 'sha3_256', 'sha3_384', 'sha3_512', 'sha512', 'shake_128', 'shake_256']
['md5-sha1', 'ripemd160', 'sha512_224', 'sha512_256', 'sm3']
| 属性 | 意味 |
|---|---|
hashlib.algorithms_guaranteed |
どの環境の Python でも使えると決められているもの |
hashlib.algorithms_available |
今の環境で使えるもの(OpenSSL が提供するものを含む)。この環境では19種類 |
2行目のように OpenSSL 由来のものは、hashlib.new("名前", データ) で使います。存在しない名前を渡すと次のようになります。
hashlib.new("sha999")
ValueError: unsupported hash type sha999
hexdigestとbase64出力を変換する
API の仕様によっては、ハッシュ値を16進数ではなく base64 で渡すよう求められます。ポイントは、hexdigest() の文字列ではなく digest() のバイト列を base64 にすることです。
import base64
import hashlib
d = hashlib.sha256(b"hello").digest()
print(type(d), len(d))
print(d.hex()) # hexdigest() と同じ
print(base64.b64encode(d).decode()) # 標準の base64
print(base64.urlsafe_b64encode(d).decode()) # URL に入れられる形
print(base64.urlsafe_b64encode(d).rstrip(b"=").decode()) # 末尾の = を外した形
<class 'bytes'> 32
2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
LPJNul+wow4m6DsqxbninhsWHlwfp0JecwQzYpOLmCQ=
LPJNul-wow4m6DsqxbninhsWHlwfp0JecwQzYpOLmCQ=
LPJNul-wow4m6DsqxbninhsWHlwfp0JecwQzYpOLmCQ
| 形式 | SHA-256 の長さ | 使われる文字 |
|---|---|---|
| 16進数(hexdigest) | 64文字 | 0〜9、a〜f |
| base64 | 44文字(末尾に = が1つ) | 英数字と + / |
| URLセーフ base64 | 44文字(= を外すと43文字) | 英数字と - _ |
16進数とbase64を相互に変換する
import base64
import hashlib
hex_value = hashlib.sha256(b"hello").hexdigest()
# 16進数 → base64
b64_value = base64.b64encode(bytes.fromhex(hex_value)).decode()
print(b64_value)
# base64 → 16進数
print(base64.b64decode(b64_value).hex())
LPJNul+wow4m6DsqxbninhsWHlwfp0JecwQzYpOLmCQ=
2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
うまくいかない例:16進数の文字列をそのままbase64にする
hex_value = hashlib.sha256(b"hello").hexdigest()
print(base64.b64encode(hex_value.encode()).decode())
MmNmMjRkYmE1ZmIwYTMwZTI2ZTgzYjJhYzViOWUyOWUxYjE2MWU1YzFmYTc0MjVlNzMwNDMzNjI5MzhiOTgyNA==
88文字の別の値になりました。これは「64文字のテキスト」を base64 にした結果です。相手の仕様が「SHA-256 の base64」なら 44文字になるはずなので、長さを見ると取り違えに気づけます。
sha256sum・certutil・Get-FileHashと突き合わせる
Python で計算した値は、OS のコマンドの結果と一致します。hello.txt で確かめました。
Linux・macOS
sha256sum hello.txt
md5sum hello.txt
sha1sum hello.txt
openssl dgst -sha256 hello.txt
openssl dgst -sha256 -binary hello.txt | base64
5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03 hello.txt
b1946ac92492d2347c6235b4d2611184 hello.txt
f572d396fae9206628714fb2ce00f72e94f2258f hello.txt
SHA2-256(hello.txt)= 5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03
WJG1tSLV3whtD/CxEPvZ0hu0/HFjrzTQgoai6Eb2vgM=
Python の hashlib.file_digest(f, "sha256").hexdigest() の結果 5891b5b5...6be03 と同じ値です。macOS では shasum -a 256 hello.txt が同じ役割を持ちます(macOS では未実行)。
Windows
Windows のコマンドは今回の検証環境(Linux)では未実行です。一般に知られた書き方を載せます。
# コマンドプロンプト・PowerShell のどちらでも
certutil -hashfile hello.txt SHA256
# PowerShell
Get-FileHash hello.txt -Algorithm SHA256
| コマンド | 対応OS | 今回の確認 |
|---|---|---|
sha256sum ファイル |
Linux | 実行済み(Python と一致) |
openssl dgst -sha256 ファイル |
OpenSSL のある環境 | 実行済み(Python と一致) |
shasum -a 256 ファイル |
macOS | 未実行 |
certutil -hashfile ファイル SHA256 |
Windows | 未実行 |
Get-FileHash ファイル -Algorithm SHA256 |
Windows(PowerShell) | 未実行 |
Get-FileHash は16進数を大文字で表示することが知られています。大文字と小文字の違いで不一致と判定しないよう、比べる前に .lower() でそろえておくと確実です。
公開されているハッシュ値と照合するコード
import hashlib
import hmac
expected = "5891B5B522D5DF086D0FF0B110FBD9D21BB4FC7163AF34D08286A2E846F6BE03"
with open("hello.txt", "rb") as f:
actual = hashlib.file_digest(f, "sha256").hexdigest()
if hmac.compare_digest(actual, expected.lower()):
print("一致しました")
else:
print("一致しません")
一致しました
同じ内容のファイルを探すコード
import hashlib
from pathlib import Path
seen = {}
for p in sorted(Path(".").glob("*.txt")):
with p.open("rb") as f:
digest = hashlib.file_digest(f, "sha256").hexdigest()
seen.setdefault(digest, []).append(p.name)
for digest, names in seen.items():
print(digest[:12], names)
hello.txt とそのコピー copy.txt、内容の違う other.txt を置いて実行した結果です。
5891b5b522d5 ['copy.txt', 'hello.txt']
abc6fd595fc0 ['other.txt']
hmacで鍵つきハッシュを作り、compare_digestで比べる
ハッシュ値だけでは「誰が作ったか」は分かりません。送り手と受け手だけが知る鍵を混ぜて計算するのが HMAC で、Webhook の署名検証などで使われます。標準ライブラリの hmac モジュールで計算します。
import hashlib
import hmac
key = b"secret-key"
message = b"order_id=1001&amount=3"
signature = hmac.new(key, message, hashlib.sha256).hexdigest()
print(signature)
# 受け取った署名と、自分で計算した署名を比べる
print(hmac.compare_digest(signature, signature))
print(hmac.compare_digest(signature, "0" * 64))
652b9002c59241e37ab133694621636c9f6feddaba6d0c16ddb2b6cc12fb3b48
True
False
| 比べ方 | 特徴 |
|---|---|
a == b |
違う文字が見つかった時点で比較を終えるため、かかる時間から内容を推測される余地がある |
hmac.compare_digest(a, b) |
内容にかかわらず同じ手順で比較する。署名やトークンの比較に向く |
署名・トークン・パスワード由来の値を比べるときは hmac.compare_digest を使う、と覚えておくと安心です。 引数は str 同士か bytes 同士でそろえます。
鍵と sha256(鍵 + メッセージ) のように自分でつなげる方法より、hmac を使うほうが確実です。HMAC は、つなげ方に由来する弱点が出ないように設計された方式だからです。
BLAKE2 は、key 引数だけで鍵つきハッシュを計算できます。
import hashlib
print(hashlib.blake2b(b"hello", digest_size=16).hexdigest())
print(hashlib.blake2b(b"hello", key=b"secret-key", digest_size=16).hexdigest())
46fb7408d4f285228f4af516ea25851b
b1fb20c57a78318a65275be0730592bc
パスワードにはscryptやpbkdf2_hmacを使う理由
SHA-256 は速く計算できることを目指して作られています。この環境で hashlib.sha256(b"correct horse").digest() を100万回繰り返すと、0.511秒で終わりました。ファイルの検証にはうれしい性質ですが、パスワードの保存では、保存した値が外に出たときに、候補を次々に試す総当たりも同じ速さでできてしまいます。
そこで、わざと時間とメモリを使う専用の関数(鍵導出関数)を使います。hashlib には2つ入っています。
import hashlib
import time
salt = bytes.fromhex("00112233445566778899aabbccddeeff") # 説明用の固定値
start = time.perf_counter()
dk = hashlib.pbkdf2_hmac("sha256", b"correct horse", salt, 600_000)
print("pbkdf2", dk.hex(), round(time.perf_counter() - start, 3))
start = time.perf_counter()
dk = hashlib.scrypt(b"correct horse", salt=salt, n=2**14, r=8, p=1, dklen=32)
print("scrypt", dk.hex(), round(time.perf_counter() - start, 3))
pbkdf2 c86dc9e58e15a00450c9f7d3310c51a16b77142b24de7ac79c735ea118c5cf50 0.265
scrypt f5206d570fcd120bd1f23a8cd186bd87c04ac1db00e9ac1efca589774ae6ecb8 0.056
| 方法 | 1回の計算時間(この環境) | 100万件の候補を試す時間の目安 |
|---|---|---|
sha256 を1回 |
約0.0000005秒 | 約0.5秒(実測) |
scrypt(n=2**14, r=8, p=1) |
0.056秒 | 約15時間(1回の時間 × 100万の計算値) |
pbkdf2_hmac(sha256、60万回) |
0.265秒 | 約3日(同上) |
正しい利用者のログインは1回の計算で済むので 0.1〜0.3 秒は気になりませんが、総当たりには大きな差になります。 これが専用の関数を使う理由です。
| 関数 | 特徴 | 主な引数 |
|---|---|---|
hashlib.pbkdf2_hmac(hash_name, password, salt, iterations, dklen=None) |
反復回数で時間を調整する。Python 3.4 で追加 | 公式ドキュメントは、2022年時点で SHA-256 なら数十万回の反復を勧めている |
hashlib.scrypt(password, *, salt, n, r, p, maxmem=0, dklen=64) |
時間に加えてメモリも使わせる。Python 3.6 で追加 | n は CPU とメモリのコスト、r はブロックサイズ、p は並列度 |
シグネチャと推奨の記述は、公式ドキュメントの hashlib のページで確認しました。
ソルトつきで保存し、照合するコード
ソルトは、パスワードごとに付ける乱数です。同じパスワードでも保存される値が毎回変わるので、あらかじめ計算した一覧表との突き合わせを防げます。
import hashlib
import hmac
import os
def hash_password(password: str) -> str:
salt = os.urandom(16)
dk = hashlib.scrypt(password.encode("utf-8"), salt=salt, n=2**14, r=8, p=1, dklen=32)
return f"scrypt$16384$8$1${salt.hex()}${dk.hex()}"
def verify_password(password: str, stored: str) -> bool:
_, n, r, p, salt_hex, dk_hex = stored.split("$")
dk = hashlib.scrypt(
password.encode("utf-8"),
salt=bytes.fromhex(salt_hex),
n=int(n), r=int(r), p=int(p), dklen=32,
)
return hmac.compare_digest(dk, bytes.fromhex(dk_hex))
stored = hash_password("correct horse")
print(len(stored))
print(verify_password("correct horse", stored), verify_password("wrong", stored))
print(hash_password("correct horse") == stored)
114
True False
False
最後の False は、同じパスワードでもソルトが違うので保存値が変わることを示しています。パラメータ(n, r, p)を一緒に保存しておくと、後から強度を上げるときに以前の値も照合できます。
scrypt の n を大きくしすぎると、既定のメモリ上限に当たります。
hashlib.scrypt(b"p", salt=salt, n=2**20, r=8, p=1)
ValueError: [digital envelope routines] memory limit exceeded
この場合は maxmem 引数で上限を広げるか、n を見直します。
Web アプリケーションのログイン機能では、bcrypt や Argon2 を扱う専用ライブラリを使うことも多くあります。ここでは「標準ライブラリだけで作るなら scrypt か pbkdf2_hmac」と押さえておけば十分です。
つまずきやすい点と直し方
| 起きること | 理由 | 直し方 |
|---|---|---|
TypeError: Strings must be encoded before hashing |
str を渡している | .encode("utf-8") を付ける。ファイルは "rb" で開く |
ValueError: ... is not a file-like object in binary reading mode. |
file_digest にテキストモードのファイルを渡している | open(path, "rb") にする |
AttributeError: module 'hashlib' has no attribute 'file_digest' |
Python 3.10 以前 | チャンク読みに切り替える(このエラーは 3.11 の環境では未再現) |
| ほかのツールと値が合わない | 文字コード、末尾の改行、大文字と小文字 | バイト列をそろえ、.lower() で比べる |
| base64 の値が相手と合わない | hexdigest の文字列を base64 にしている | digest() を base64 にする |
hexdigest() を2回呼んだら同じ値、update() 後は別の値 |
ハッシュオブジェクトは足した分をすべて覚えている | 別のデータを計算するときは新しいオブジェクトを作る |
| 大きなファイルでメモリが足りなくなる | f.read() で全部読んでいる |
file_digest かチャンク読みにする |
よくある質問
Q. hexdigest() と digest() はどちらを使えばよいですか?
A. 画面表示やログ、テキストでの受け渡しには hexdigest() が向いています。base64 にしたい、バイナリのまま保存したい、HMAC の比較に使いたい、という場合は digest() を使います。
Q. ハッシュ値から元のデータに戻せますか? A. 戻せません。ハッシュは一方向の計算です。元に戻せる必要があるなら、暗号化という別の技術を使います。base64 は見た目を変えるだけの変換なので、ハッシュとも暗号化とも役割が違います。
Q. Python 組み込みの hash() 関数との違いは何ですか?
A. hash() は辞書や集合の内部で使うための値で、文字列に対してはプロセスを起動するたびに結果が変わります。ファイルの検証や保存には、毎回同じ値になる hashlib を使います。
Q. チャンクの大きさはいくつにすればよいですか? A. 64KiB〜1MiB 程度がよく使われます。この記事では 1MiB で、200MiB のファイルを 0.177秒・最大 2.0MiB のメモリで計算できました。3.11 以降なら file_digest に任せると、大きさを考えずに済みます。
まとめ
- hashlib に渡すのは bytes。文字列は
.encode("utf-8")、ファイルは"rb"で開く - ファイルは 3.11 以降なら
hashlib.file_digest、それ以前はチャンク読み。200MiB のファイルでもメモリは数MiB以内に収まった - 迷ったら SHA-256。MD5 と SHA-1 は破損チェックや重複検出、既存の仕組みとの互換に使う
- base64 にするのは
digest()のバイト列。SHA-256 なら 44文字になる sha256sumやopenssl dgstの結果と一致することを実行して確かめた(Windows のコマンドは未実行)- 署名やトークンの比較は
hmac.compare_digest、鍵つきハッシュはhmacモジュール - パスワードには scrypt や pbkdf2_hmac。わざと時間をかけることで総当たりを難しくする
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Python hashlibの使い方|SHA256・MD5とファイルのハッシュ値」
株式会社RJCでは、Pythonのハッシュ計算の使い方を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。