
bcryptパスワードハッシング:開発者向け実践ガイド
📷 Pixabay / Pexelsbcryptパスワードハッシング:開発者向け実践ガイド
パスワードセキュリティのためのbcryptに関する必要なすべて — コストファクター、Node.js/Python/PHPでの実装、そして避けるべき一般的なミス。
ユーザーパスワードを保存しなければならない場合、「bcryptを使え」と言われたことがあるでしょう。20年以上にわたって標準的な推奨事項となっており、それには十分な理由があります。しかし、なぜそれが機能するのかを理解し、正しく使用する方法を知ることが、安全な実装とただ安全そうに見える実装の違いを生みます。
このガイドでは、bcryptが実際にどのように機能するか、どのコストファクターを選ぶか、複数の言語での実装方法、そしてパスワードセキュリティを密かに損なうミスについて実践的な観点から解説します。
bcryptとは何で、なぜ存在するのか
bcryptは1999年にNiels ProvosとDavid Mazièresがパスワードハッシング専用に設計しました。その文脈が重要です。速くなるよう構築されたのではなく — 制御され、調整可能な方法で遅くなるよう構築されました。
bcryptの背後にある核心的な洞察は、高速ハッシングがパスワードに関しては脆弱性だということです。SHA-256のような汎用ハッシュは現代のハードウェアで毎秒数十億のハッシュを計算できます。これはチェックサムとデータ整合性には優れています。しかしパスワードには最悪で、攻撃者がパスワードデータベースを盗んだ場合、コモディティGPUで毎秒数十億回の推測を試みることができます。
bcryptはコストファクターを導入することでこれを解決します — アルゴリズムがどれだけの作業をするかを制御するパラメーター。ハッシングの遅さをあなたが決めます。ハードウェアは毎年速くなるので、コストファクターを時間とともに上げて追いつくことができます。
bcryptが実際にどのように機能するか
bcrypt.hash("mypassword", 10)を呼び出すと、内部でいくつかのことが起きます:
- ランダムな16バイトのソルトが生成されます。 このソルトは各ハッシュ操作に固有です。
- パスワードとソルトがEksblowfishキーセットアップに入力されます。 これがbcryptのコアアルゴリズム — コストのかかるキー拡張フェーズを持つBlowfish暗号の修正版です。
- キーセットアップが2^コスト回繰り返されます。 コストファクター10では1,024回です。コスト12では4,096回です。各増分で作業量が2倍になります。
- 結果のハッシュはアルゴリズムバージョン、コストファクター、ソルト、ハッシュをすべて含む60文字の文字列にエンコードされます。
出力は次のようになります:
$2b$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
分解すると:
$2b$— bcryptバージョン10— コストファクター- 次の22文字 — base64エンコードされたソルト
- 残りの文字 — ハッシュ
ソルトはハッシュ文字列に埋め込まれているため、別途保存する必要はありません。bcrypt.compare()関数は保存されたハッシュからソルトを抽出し、入力パスワードをハッシュするために使用します — 手動ソルト管理は不要です。
高速ハッシュ関数が間違ったツールである理由
開発者は時々MD5、SHA-1、SHA-256でパスワードを保存します。これらのアルゴリズムは広く利用可能でよく理解されているため、選択が合理的に見えます。そうではありません。
問題は、これらのアルゴリズムが意図された目的のために破れているわけではありません。問題は速くなるよう設計されており、速さがパスワード保存に対してまさに間違っているということです。
2012年、セキュリティ研究者のJeremi Gosneyは640万のSHA-1パスワードハッシュデータベースの90%を1時間以内に解読しました。現代のハードウェアとHashcatのようなツールを使えば、良いGPUを持つ攻撃者はMD5ハッシュを毎秒100億回以上テストできます。SHA-256はそれほど良くありません — 毎秒約40億回です。
コスト10のbcryptはこれを毎秒約20,000回の試みに下げます。それが全てのポイントです。
正しいコストファクターの選択
コストファクターはbcryptで行う最も重要なチューニング決定です。
コスト10が広く推奨されるデフォルトです。現代のサーバーでは約100〜300msのハッシュを生成します。ログインするユーザーには感じられないほど遅いですが、攻撃者はコアあたり毎秒数千パスワードしかテストできません。
コスト12はコスト10と比べて計算時間を約4倍にします。セキュリティ要件が高いアプリケーション — 銀行、医療、機密の財務データを扱うもの — で400〜1000msのログイン時間が許容される場合に合理的な選択です。
コスト14以上は通常Webアプリケーションには過剰です。コスト14ではハッシングに数秒かかり、ユーザーエクスペリエンスに目に見えて影響します。
本番環境では絶対に8未満にしないでください。 一部の古いチュートリアルはコスト5や6を例として示しています。これらの値は速すぎて不十分な保護しか提供しません。
実用的なルール:実際のハードウェアでベンチマークし、期待される同時負荷でログイン時間を250ms以下に保つ最高のコストファクターを選んでください。その後、ハードウェアの向上に合わせて2〜3年ごとに見直してください。
Node.jsでbcryptを実装する
const bcrypt = require('bcrypt');
const COST_FACTOR = 10;
// パスワードのハッシュ化
async function hashPassword(plaintext) {
const hash = await bcrypt.hash(plaintext, COST_FACTOR);
return hash; // このストリングをデータベースに保存
}
// ログイン時の検証
async function verifyPassword(plaintext, storedHash) {
const match = await bcrypt.compare(plaintext, storedHash);
return match; // true または false
}
// 使用例
const hash = await hashPassword('hunter2');
console.log(hash); // $2b$10$...
const valid = await verifyPassword('hunter2', hash);
console.log(valid); // true
Pythonでbcryptを実装する
import bcrypt
COST_FACTOR = 10
def hash_password(plaintext: str) -> bytes:
password_bytes = plaintext.encode('utf-8')
hashed = bcrypt.hashpw(password_bytes, bcrypt.gensalt(rounds=COST_FACTOR))
return hashed
def verify_password(plaintext: str, stored_hash: bytes) -> bool:
password_bytes = plaintext.encode('utf-8')
return bcrypt.checkpw(password_bytes, stored_hash)
# 使用
hashed = hash_password("hunter2")
print(verify_password("hunter2", hashed)) # True
print(verify_password("wrong", hashed)) # False
PHPでbcryptを実装する
<?php
$options = ['cost' => 10];
// ハッシュ化
$hash = password_hash($plaintext, PASSWORD_BCRYPT, $options);
// 検証
$valid = password_verify($plaintext, $hash);
// ハッシュのアップグレードが必要か確認
if (password_needs_rehash($hash, PASSWORD_BCRYPT, $options)) {
$newHash = password_hash($plaintext, PASSWORD_BCRYPT, $options);
// $newHashをデータベースに保存
}
?>
Goでbcryptを実装する
import "golang.org/x/crypto/bcrypt"
func HashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 10)
return string(bytes), err
}
func CheckPasswordHash(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
bcryptを損なう一般的なミス
ログイン検証のたびにパスワードを再ハッシュすること。 これは重大なパフォーマンスミスです。ユーザーがログインするときは、bcrypt.compare()を使って保存されたハッシュに対してパスワードを比較してください。パスワードを再ハッシュして2つのハッシュを比較しないでください — 毎回新しいランダムソルトが生成されるため、絶対に一致しません。
平文パスワードの保存。 明白ですが、起こります。
パスワード保存にMD5またはSHAを使用すること。 手動でソルトを追加しても、高速ハッシュは攻撃者に過度のスループットを与えます。
72バイト制限の未処理。 bcryptは72バイトで入力を暗黙的に切り捨てます。
コストファクターが低すぎること。 コスト4を使っているなら、ポイントを外しています。遅さはセキュリティプロパティであり、バグではありません。
まとめ
- bcryptの強みは意図的な遅さからくるものであり、暗号の複雑さではない
- 検証には常に
bcrypt.compare()(または言語の同等物)を使用する — ハッシュして比較しない - コストファクター10がほとんどのアプリケーションの適切なデフォルト;数年ごとに見直す
- bcryptは自動的にソルティングを処理する — ソルトを管理する必要はない
- 異常に長いパスワードの72バイト切り捨て制限に注意する
- 新しいプロジェクトにはArgon2idを検討する価値がある;既存のbcryptシステムはそのまま維持する
パスワード保存は、安全なオプション — 合理的なコストファクターを持つbcrypt — が実装も簡単である数少ない分野の1つです。この空間の脆弱性のほとんどは、アルゴリズムの欠陥ではなく、アドバイスを無視することから生じます。