Fessではユーザーパスワードの保存形式を、これまでのソルトなしSHA-256(1ラウンド)から、Spring Security v5.8互換のBCryptベースの形式へと刷新しました。既存の環境を止めることなく、ログインをきっかけに新形式へ段階的に移行できるようになっています。
変更の背景
これまでのFessはパスワードを sha256(plain) の16進文字列としてそのまま保存していました。ソルトがなくラウンド数も1回のため、万が一パスワードハッシュが漏洩した場合にレインボーテーブルやGPUによる総当たりで平文を復元されるリスクがありました。今回の対応では、保存形式を {bcrypt}$2a$10$... に切り替え、ログインごとに古い形式を新形式へ置き換える仕組みを導入しています。
パスワードハッシュ化の刷新
BCryptへの移行
新たに PasswordHashHelper (org.codelibs.fess.helper.PasswordHashHelper)を追加しました。ComponentUtil.getPasswordHashHelper() 経由で取得し、encode / matches / upgradeEncoding という公開APIを通して利用します。
保存形式にはSpring Securityの DelegatingPasswordEncoder と互換性のある {bcrypt} プレフィクス付きの文字列を採用しました。将来的にアルゴリズムを切り替える場合も、プレフィクスを見ることで旧方式と新方式を混在させたまま検証できます。
jBCryptの同梱
新規のMaven依存は追加せず、jBCrypt v0.4(ISCライセンス)を org.codelibs.fess.crypto.bcrypt.BCrypt としてソースに同梱しました。NOTICEファイルおよびlicense-plugin・formatter-pluginの除外リストもあわせて更新しています。jBCrypt 0.4は $2a$ のみをサポートし、他システムで使われる $2b$ / $2y$ は受け付けません。
既存ハッシュの互換性
プレフィクスのないレガシーな値は、app.digest.algorithm の設定(sha256 / sha512 / md5)に従って小文字16進で比較します。比較は MessageDigest.isEqual による定数時間比較で行い、未知の {id} プレフィクスや不正な値は常に false を返します。
認証フローの変更
FessLoginAssist.doAuthenticateLocal では、これまで「ユーザー名+ハッシュ済みパスワード」でDBを引いていた処理を、「ユーザー名のみで引いて PasswordHashHelper.matches で照合する」形に変更しました。BCryptはレコードごとにソルトを持つため、事前にハッシュを計算してWHERE句に入れるやり方は使えないためです。
ユーザー不在・null・レガシーhex・未知プレフィクスなどの失敗経路はすべて、applyTimingPadding でダミーのBCrypt検証を1回走らせ、成功時と同じ処理コストがかかるようにしています。これによりユーザーの存在有無が応答時間の差から推測されるタイミング攻撃を防いでいます。
ログイン時の遅延リハッシュ
既存ユーザーの旧形式パスワードは、ログイン成功のタイミングで自動的にBCryptへ置き換わります。この処理は app.password.upgrade.enabled=true(デフォルト有効)かつ upgradeEncoding が true を返した場合のみ走ります。
書き込みには UserService.updateStoredPasswordHash(username, expectedCurrentHash, newEncodedPassword) を新設し、AuthenticationManager を経由しない専用のリハッシュパスとしました。LDAPやSSOなど外部で管理されているユーザーには影響を与えないためです。
更新はOpenSearchの楽観的並行制御で原子性を担保しており、読み出し時の _seq_no / _primary_term を IndexRequestBuilder.setIfSeqNo / setIfPrimaryTerm に伝搬させています。バージョン競合はDEBUGレベルでログ出力するにとどめ、ログインそのものは成功扱いのままとします。
書き込みパスの整理
UserService.changePassword、AdminUserAction.getUser、初期管理ユーザーをブートストラップする SearchEngineClient では、ComponentUtil.getPasswordHashHelper().encode(plain) を直接呼ぶように変更しました。これにより UserService から FessLoginAssist への不自然な依存を取り除いています。
新たなコードからパスワードを書き込む場合も、FessLoginAssist.encryptPassword は使わず PasswordHashHelper.encode を呼ぶようにしてください。
新しい設定項目
fess_config.properties に以下の設定を追加しました。
app.password.algorithm=bcrypt: 新規書き込み時のアルゴリズムapp.password.bcrypt.cost=10: BCryptのコストパラメータapp.password.upgrade.enabled=true: 旧形式からの遅延リハッシュの有効化
app.digest.algorithm はレガシーハッシュの検証用として残していますが、今後は新規書き込みには使われません。
ダウングレード時の注意
本変更以降、保存されるパスワードは {bcrypt}$2a$10$... の形式になります。BCrypt対応前のFessへロールバックするとこれらの値は検証できなくなるため、管理者パスワードのリセットが必要になります。運用でダウングレードを想定する場合はリリースノートでの周知と、手順の準備をおすすめします。
スキーマやAPI、i18nリソースへの変更はありません。
詳細
詳細はPR #3116を参照してください。