panco’s blog

興味が沸いたことを書く

werkzeug.security.generate_password_hash() について調べた

趣味の個人開発でとあるログイン機能を設計・実装している。

「テーブルに平文パスワードを入れるのはだめだよな。普通どうするのかな」という疑問から調べてみると、generate_password_hash() と check_password_hash() に出会った。この記事では、generate_password_hash() について理解したことを書く。

事前知識は皆無であるため公式ドキュメントを参照しつつ、理解できなかった点は ChatGPT の力を借りた。

実行環境

  • python 3.10.13
  • werkzeug 2.2.3

generate_password_hash() とは

公式ドキュメント
Utilities — Werkzeug Documentation (2.2.x)

簡潔に言うと、平文のパスワードを何らかのハッシュアルゴリズム(+ソルト)を使ってハッシュ値にしてくれるもの。

This method can not generate unsalted passwords but it is possible to set param method=’plain’ in order to enforce plaintext passwords.

上記の通り、ソルトを生成してハッシュ値を出力する仕様となっている。"method=’plain’" については method='plain' で記載する。

パラメータ、戻り値は以下の通り。(公式ドキュメントからの抜粋)

Parameters: password (str) – the password to hash.

method (str) – the hash method to use (one that hashlib supports). Can optionally be in the format pbkdf2:method:iterations to enable PBKDF2.

salt_length (int) – the length of the salt in letters.

Return type: str

戻り値の形式は以下の通り。

method$salt$hash

とりあえず適当にコードを書いて実行してみる。

実行コード

from werkzeug.security import generate_password_hash

# 平文のパスワード
pw = 'panco2355'
# generate_password_hash() デフォルト
pw_hash = generate_password_hash(pw)

print('pw =' + pw)
print('pw_hash = ' + pw_hash)

実行結果

pw = panco2355
pw_hash = pbkdf2:sha256:260000$O0B6LXDYDeichV3Q$00427a87a7223395a3c2a407df790ff3c17571b2ef4eb20dccd57a1204ef9407

平文の panco2355 はいいとして、pw_hash の中身を見ていく。

method$salt$hash

の形式に当てはめると、

method salt hash
pbkdf2:sha256:260000 O0B6LXDYDeichV3Q 00427a87a7223395a3c2a407df790ff3c17571b2ef4eb20dccd57a1204ef9407

となる。第一引数(つまり password)しか設定しない場合、method='pbkdf2:sha256'、salt_length=16 となる。

method で設定できるハッシュアルゴリズム

werkzeug 2.2.x で使用できる method は hashlib がサポートしているものとのこと。 Python 3.10.x だと、少なくとも以下のハッシュアルゴリズムが使用できるらしい。

Constructors for hash algorithms that are always present in this module are sha1(), sha224(), sha256(), sha384(), sha512(), blake2b(), and blake2s().

hashlib — Secure hashes and message digests — Python 3.10.13 documentation

method "the format pbkdf2:method:iterations" って何?

公式ドキュメントを読んでいて理解できなかった点がある。以下の一文。

If PBKDF2 is wanted it can be enabled by setting the method to pbkdf2:method:iterations where iterations is optional:

" by setting the method to pbkdf2:method:iterations" が何を言っているのか全然わからない。どう実装するのか、それによって何が変わるのかが理解できない。
ということで、ChatGPT にこの一文をまるっと投げて解説してもらった。結果はこちら。

ChatGPT-3.5 の解説

ひとまず、pbkdf2、method、iterations の概念が理解できた。実装例まである。(pbkdf2 の原理も理解したいところだが、夜が明けてしまいそうなので一旦スルーする)

Chat GPT の解説を踏まえ、以下のコードを実行した。

実行コード

from werkzeug.security import generate_password_hash

# 平文のパスワード
pw = 'panco2355'
# generate_password_hash() デフォルト
pw_hash = generate_password_hash(pw)
# method='pbkdf2:sha256:1000'
pw_hash_pbkdf2 = generate_password_hash(pw, method='pbkdf2:sha256:1000')

print('pw = ' + pw)
print('pw_hash = ' + pw_hash)
print('pw_hash_pbkdf2 = ' + pw_hash_pbkdf2)

実行結果

pw = panco2355
pw_hash = pbkdf2:sha256:260000$vDyoTiqgkmBSezIs$5988047da6b68cffb115da078f278850608c17ca6de53a0ee178727111db97f0
pw_hash_pbkdf2 = pbkdf2:sha256:1000$i0zIckuEmddoEWNf$15da30ba7f19a3103b421b08da9a2b6e394b8c799c6ca8fb8ec80c9f644f61a0

変数 pw_hash_pbkdf2 の値から、第二引数に設定した「pbkdf2:sha256:1000」が使われていることがわかる。デフォルトだと 260,000 回繰り返していたのか。なるほど。

method='sha256'

とあるサンプルコードにて、method='sha256' で設定しているものがあった。その場合、戻り値がどうなるかも見てみる。

実行コード

from werkzeug.security import generate_password_hash, check_password_hash

# 平文のパスワード
pw = 'panco2355'
# generate_password_hash() デフォルト
pw_hash = generate_password_hash(pw)
# method='pbkdf2'
pw_hash_pbkdf2 = generate_password_hash(pw, method='pbkdf2:sha256:1000')
# method='sha256'
pw_hash_sha256 = generate_password_hash(pw, method='sha256')

print('pw = ' + pw)
print('pw_hash = ' + pw_hash)
print('pw_hash_pbkdf2 = ' + pw_hash_pbkdf2)
print('pw_hash_sha256' + pw_hash_sha256)

実行結果

pw = panco2355
pw_hash = pbkdf2:sha256:260000$N7aRYni2ZDia67b5$7b28748685ab69f6bdc18d49b01970949d31bfa08727ca00d986194c953e40a6
pw_hash_pbkdf2 = pbkdf2:sha256:1000$HzAAy6RJqZFbYsgJ$ba6ab5aa7e8c6ffec9ad8cac342008e303b8459c1c1a59eccb17a1091b8bd200
pw_hash_sha256 = sha256$i3NHoX8sEWipcAub$36e7481117406ca0c10537c02b6e99fa59fb626f7573551304528159be662efd

ソルトを生成してハッシュ値を出力することはわかった。そして、PBKDF2 を有効化すると、iterations に設定した任意の回数を繰り返して何かしらを行い、ハッシュ値を出力しているということもわかってきた。(おそらく自分の基礎的な知識がなさすぎる)

method='plain'

使い道がわからないが、method='plain' とすることで平文のパスワードを出力することもできた。

実行コード

from werkzeug.security import generate_password_hash, check_password_hash

# 平文のパスワード
pw = 'panco2355'
# generate_password_hash() デフォルト
pw_hash = generate_password_hash(pw)
# method='pbkdf2'
pw_hash_pbkdf2 = generate_password_hash(pw, method='pbkdf2:sha256:1000')
# method='sha256'
pw_hash_sha256 = generate_password_hash(pw, method='sha256')
# method='plain'
pw_hash_plain = generate_password_hash(pw, method='plain')

print('pw = ' + pw)
print('pw_hash = ' + pw_hash)
print('pw_hash_pbkdf2 = ' + pw_hash_pbkdf2)
print('pw_hash_sha256 = ' + pw_hash_sha256)
print('pw_hash_plain = ' + pw_hash_plain)

実行結果

pw = panco2355
pw_hash = pbkdf2:sha256:260000$r6KekC7Nk7XqDbUi$1ed2202960fe81a88796d574162669dab8cc6ab12a3eb858645bbd4d8bdf9851
pw_hash_pbkdf2 = pbkdf2:sha256:1000$MXAZ6floiejlxW4Y$c406a48ea1dd8312460cd1bbf5088b6c0bc1e8446271fba67608d4a011772040
pw_hash_sha256 = sha256$RkpCXxhoBEVTEdQ5$921b331a536aa5f806a2d7a78977c7804614f8cb6f1766144ddf5265a910bb6a
pw_hash_plain = plain$$panco2355

method='plain' の場合、ハッシュの概念がないためソルト生成もない。だから「plain$$panco2355」の「$」で挟まれた部分(ソルトの値が入る位置)が空になっている。

salt_length=0 にしたらどうなるのか

何度実行しても同じ平文からは同じハッシュ値が出るはず、と思ったが ValueError で怒られた。

実行コード

from werkzeug.security import generate_password_hash, check_password_hash

# 平文のパスワード
pw = 'panco2355'
# method='salt0'
for i in range(5):
    pw_hash_salt0 = generate_password_hash(pw, method='sha256',salt_length=0)
    print(str(i+1) + '回目' + pw_hash_salt0)

実行結果

ValueError: Salt length must be positive

werkzeug > security.py のここではじかれていた。

def gen_salt(length: int) -> str:
    """Generate a random string of SALT_CHARS with specified ``length``."""
    if length <= 0:
        raise ValueError("Salt length must be positive")

    return "".join(secrets.choice(SALT_CHARS) for _ in range(length))

余談だが、security.py で以下のコードが書かれているため、method='pbkdf2:sha256' のように method に iterations を設定しない場合、デフォルト値の 260000 が採用されることがわかった。

SALT_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
DEFAULT_PBKDF2_ITERATIONS = 260000

werkzeug/src/werkzeug/security.py at 2.2.x · pallets/werkzeug · GitHub

ソルトを使わないハッシュアルゴリズム

技術の進歩に逆行していることは認識しつつ、気になってしまったので軽く ChatGPT に聞いてみた。

たしかに MD5 は非推奨というのはどこかのサイトで見た。やってみよう。
generate_password_hash() では、ソルトを使わないハッシュ値生成はできない(method='plain'と指定した場合を除く)ため、hashlib.md5() を使った。

実行コード

import hashlib

# 平文のパスワード
pw = 'panco2355'

for i in range(5):
    pw_hash_md5 = hashlib.md5(pw.encode()).hexdigest()
    print(str(i+1) + '回目' + str(pw_hash_md5))

実行結果

1回目1dd93192408bd95cae7c7af78de8b6c4
2回目1dd93192408bd95cae7c7af78de8b6c4
3回目1dd93192408bd95cae7c7af78de8b6c4
4回目1dd93192408bd95cae7c7af78de8b6c4
5回目1dd93192408bd95cae7c7af78de8b6c4

当たり前だが本当に同じハッシュ値になった。

PBKDF2 とは

夜明けが近づいてきたため、PBKDF2 についてはまた今度、と思ったがChatGPTに聞いてしまった。

PBKDF2(Password-Based Key Derivation Function 2)は、パスワードから導出されたキーを生成するためのハッシュ関数です。iterations(反復回数)パラメータは、PBKDF2がパスワードから導出されたキーの強度を調整するために使用されます。高いiterations値を使用することで、生成されたキーの強度が向上し、攻撃者による総当たり攻撃からの保護が強化されます。

PBKDF2は、暗号学的に安全なハッシュ関数の特性を持つため、総当たり攻撃に対して強力な保護を提供します。しかし、PBKDF2自体は高速に動作するため、単純なパスワードから生成されたキーは総当たり攻撃に対して脆弱です。iterationsパラメータは、PBKDF2を複数回繰り返すことで、攻撃者に対する防御を向上させるものです。

具体的には、PBKDF2は以下のように動作します:

  1. パスワードとソルト(ランダムなデータ)を組み合わせます。
  2. ハッシュ関数(通常はSHA-1やSHA-256など)を使用して、この組み合わせをハッシュ化します。
  3. 1および2のステップをiterations回繰り返します。

iterationsの値を高く設定することで、ハッシュ化プロセスが遅くなり、総当たり攻撃に対する耐性が向上します。したがって、高いiterations値はセキュリティを向上させるために重要です。ただし、iterationsの増加にはコンピュータの処理負荷が増加し、認証プロセスが遅くなるというコストが伴います。適切なiterationsの値は、セキュリティ要件とシステムパフォーマ>ンスを考慮して選択されます。