cat posts/%e3%83%af%e3%82%a4%e3%81%ae%e5%ae%b6%e8%a8%88%e7%b0%bf%e3%82%a2%e3%83%97%e3%83%aa%e3%80%81%e3%83%91%e3%82%b9%e3%83%af%e3%83%bc%e3%83%89%e3%83%8f%e3%83%83%e3%82%b7%e3%83%a5%e5%bc%b7%e5%8c%96%e3%81%97.md

ワイの家計簿アプリ、パスワードハッシュ強化したら境界値でいきなり死んだ話

1 風吹けば名無し 2026/09/28(月) 21:04:11 ID:aB3xK9m0
ワイの家計簿アプリ、ログインのパスワードハッシュ強化したったで
scrypt使って前より頑丈にしたわ

2 風吹けば名無し 2026/09/28(月) 21:06:18 ID:qP7zR2n1
>>1
今まで何やっとったんや

3 風吹けば名無し 2026/09/28(月) 21:08:25 ID:aB3xK9m0
>>2
旧実装はscryptSyncでハッシュ化して salt:hash の形式で保存しとった
パラメータは固定で N=16384 r=8 p=1(Nodeのデフォルト値)やったんや

4 風吹けば名無し 2026/09/28(月) 21:10:32 ID:mK4tY8w3
>>3
scryptSyncって同期関数やろ
リクエストごとにそれ走らせて大丈夫なんか

5 風吹けば名無し 2026/09/28(月) 21:12:39 ID:aB3xK9m0
>>4
…気づいてなかったわ
同期実行やからハッシュ計算してる間イベントループ丸ごと止まっとった
せやから非同期化することにしたんや

6 風吹けば名無し 2026/09/28(月) 21:14:46 ID:aB3xK9m0
>>5 の続きやけど直す前のコードはこれや

// Before
import { scryptSync, randomBytes } from "node:crypto";

export function hashPassword(password: string): string {
  const salt = randomBytes(16).toString("hex");
  const derived = scryptSync(password, salt, 64).toString("hex");
  return `${salt}:${derived}`;
}

7 風吹けば名無し 2026/09/28(月) 21:16:53 ID:xL9cV0d5
>>6
普通にNode公式ドキュメント通りの実装やな
せやけど確かにSyncは同期処理やから怖いわ

8 風吹けば名無し 2026/09/28(月) 21:18:00 ID:aB3xK9m0
>>6
promisifyでasync化したで
こんな感じや

// After
import { scrypt, randomBytes } from "node:crypto";
import { promisify } from "node:util";
const scryptAsync = promisify(scrypt);

export async function hashPassword(password: string): Promise<string> {
  const salt = randomBytes(16).toString("hex");
  const derived = await scryptAsync(password, salt, 64, {
    N: 32768, r: 8, p: 1, maxmem: 64 * 1024 * 1024,
  });
  return `scrypt$32768$8$1$${salt}$${derived.toString("hex")}`;
}

9 風吹けば名無し 2026/09/28(月) 21:20:07 ID:qP7zR2n1
>>8
コストパラメータのNも上げとるやん
旧の16384から倍にしとるな

10 風吹けば名無し 2026/09/28(月) 21:22:14 ID:aB3xK9m0
>>9
せや、N=32768=2^15にした
せやのに上げた瞬間本番相当の環境で早速落ちたんやが

11 風吹けば名無し 2026/09/28(月) 21:24:21 ID:aB3xK9m0
>>10 の続き
出たエラーがこれや

Error: Invalid scrypt params
    at check (node:internal/crypto/scrypt:19:17)
    at Object.scrypt (node:internal/crypto/scrypt:66:3)
code: 'ERR_CRYPTO_INVALID_SCRYPT_PARAMS'

12 風吹けば名無し 2026/09/28(月) 21:26:28 ID:mK4tY8w3
>>11
ファッ!?
なんでや、Nodeの標準APIそのまま使ってるだけやろ

13 風吹けば名無し 2026/09/28(月) 21:28:35 ID:aB3xK9m0
>>12
scryptの必要メモリってざっくり128×N×rバイトなんやと
N=32768 r=8 やと計算上ちょうど32MiBになって、Nodeのデフォルトmaxmem上限(32MiB)にぴったり触れて弾かれとった
maxmemを64MiBに上げて解決したで

14 風吹けば名無し 2026/09/28(月) 21:30:42 ID:xL9cV0d5
>>13
境界値ぴったりで死ぬの草
せやけど旧形式のsalt:hashで保存されとる既存ユーザーはどうなるんや
強制ログアウトさせたんか

15 風吹けば名無し 2026/09/28(月) 21:32:49 ID:aB3xK9m0
>>14
いや、旧形式もverifyPasswordでそのまま検証できるように残しといた
ログイン成功したタイミングで旧形式やったら裏で新形式に書き換える透過的な移行方式にしたんや
新形式は scrypt$N$r$p$salt$hash でパラメータを埋め込んどるから、将来またコスト上げても古いハッシュを正しく検証できるで

16 風吹けば名無し 2026/09/28(月) 21:34:56 ID:qP7zR2n1
>>15
バージョン情報埋め込んどくの賢いな
テストはちゃんと通っとるんか

17 風吹けば名無し 2026/09/28(月) 21:36:03 ID:aB3xK9m0
>>16
単体テスト9件追加して既存179件と合わせて全188件、統合テスト24件も通した
実サーバー+DBでも旧ハッシュ持ちユーザーのログイン成功時にpasswordHashが新形式へ書き換わることをcurlとpsqlで確認済みや
一旦これで安心して寝れるわ