趣味の個人開発でとあるログイン機能を設計・実装している。
「テーブルに平文パスワードを入れるのはだめだよな。普通どうするのかな」という疑問から調べてみると、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 にこの一文をまるっと投げて解説してもらった。結果はこちら。
ひとまず、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は以下のように動作します:
- パスワードとソルト(ランダムなデータ)を組み合わせます。
- ハッシュ関数(通常はSHA-1やSHA-256など)を使用して、この組み合わせをハッシュ化します。
- 1および2のステップを
iterations
回繰り返します。
iterations
の値を高く設定することで、ハッシュ化プロセスが遅くなり、総当たり攻撃に対する耐性が向上します。したがって、高いiterations
値はセキュリティを向上させるために重要です。ただし、iterations
の増加にはコンピュータの処理負荷が増加し、認証プロセスが遅くなるというコストが伴います。適切なiterations
の値は、セキュリティ要件とシステムパフォーマ>ンスを考慮して選択されます。