パスワードハッシュ
そのまま保存してはいけない
会員登録を作るとき、いちばんやってはいけないのがこれです。
JavaScript
db.prepare("INSERT INTO users (name, password) VALUES (?, ?)").run(name, password);プレースホルダは正しく使っています。ですがパスワードがそのまま保存されています。
データベースが漏れたとき、全員のパスワードがそのまま読まれます。しかも多くの人は同じパスワードを他のサービスでも使っているので、被害は fleama の中では終わりません。
元に戻せない形にする
ハッシュ化は、値を元に戻せない形に変える処理です。
JavaScript
const crypto = require("crypto");
const hashed = crypto.scryptSync(password, salt, 32).toString("hex");同じ入力からは必ず同じ結果が出ます。逆に、結果から元のパスワードを求めることはできません。
保存するのはこの結果だけです。ログインのときは、送られてきたパスワードを同じ手順でハッシュ化して、保存してあるものと一致するかを見ます。元に戻す必要はありません。
salt を混ぜる
salt は、ハッシュ化のときに一緒に混ぜる文字列です。
これが無いと、同じパスワードを使っている人は同じ結果になります。漏れたときに「この 2 人は同じパスワードだ」と分かってしまいますし、よくあるパスワードの結果を並べた表と照合されると元が割れます。
利用者ごとに違う salt を混ぜれば、同じパスワードでも結果が変わります。salt 自体は秘密ではないので、ハッシュと一緒に保存して構いません。
比べ方にも作法がある
JavaScript
if (hashed === stored) { ... }動きますが、より安全な比べ方があります。
JavaScript
crypto.timingSafeEqual(Buffer.from(hashed), Buffer.from(stored));=== は違いが見つかった時点で比較をやめるので、一致した文字数によって処理時間がわずかに変わります。その差を測って 1 文字ずつ当てていく攻撃があります。timingSafeEqual は最後まで比べるので、時間から情報が漏れません。
返すときも気をつける
登録の応答に、ハッシュを含めてはいけません。
JavaScript
res.status(201).json({ id: user.id, name: user.name });第3章から繰り返してきた「返すものは選ぶ」が、いちばん効く場面です。
手を動かす
演習では、会員登録の処理を書きます。保存する形と、返す形の両方に気を配ってください。
要件
- パスワードをハッシュ化して
password_hash列に保存する(平文は保存しない) storedは保存された{id, name, password_hash}を返すresponseは{id, name}だけにして、ハッシュも平文も含めない
入出力例
register("田中", "himitsu", "salt-a") → {"response":{"id":1,"name":"田中"},"stored":{"id":1,"name":"田中","password_hash":"704b7637ecc20719df70b70e9fae8b384de6f9c0281ec1823b72c090c46ce731"}}
register("鈴木", "himitsu", "salt-b") → {"response":{"id":1,"name":"鈴木"},"stored":{"id":1,"name":"鈴木","password_hash":"1ea22361123475abc9f1c31a29a8bebe87b3d69c7752eda0152985e7c3d05bff"}}