Closed10

パスワード保存時のPepperについて考える

noonworksnoonworks

前提

  • Pepperとは別に、当然ハッシュ化(ソルト付加/ストレッチング付き)も行う
  • 2021年12月現在の選択肢として、Argon2を使用する
  • ハッシュ化アルゴリズムが「バイナリセーフである」ことを前提とする

そのうえで、Global Pepperを用いた暗号化を「いつ」行うのか考える

noonworksnoonworks

Dropboxの例

参考: How Dropbox securely stores your passwords - Dropbox

2016年付けなのでやや古い記事だが、これを見るとDropboxでは以下の流れで処理している。

生パスワード → SHA-512 → bcrypt → AES256

SHA-512してるのはなぜ?

生パスワードをSHA-512しているのは、bcryptの72文字切り詰め問題を回避するためっぽい?
参考: bcryptの72文字制限をSHA-512ハッシュで回避する方式の注意点 | 徳丸浩の日記

上記参考ブログにもあるように、この方法は「SHA-512のバイナリをbcryptに渡す」ときに有効であることに注意。(一般的なhex文字列やBase64で文字列化すると72バイトを超えてしまい、結局切り詰めが発生する。)

SHA-512はメッセージダイジェスト関数であるから、生パスワードの長さを無制限とした場合、理論上は生パスワードそのままよりも情報量が少なくなる。
ただしSHA-512の結果は512ビット(64バイト)=1.340781e+154通りなので、現実的に人間が運用する範囲のパスワード長から考えれば十分な情報量なのだろう。
(※暗号学は素人なのでエントロピーのことはわかりません……)

ただ、今回の前提(Argon2)ではそもそも切り詰めがないため、わざわざSHA-512をはさむ必要はないと考える。

noonworksnoonworks

Global Pepperのタイミング

本題。

上記Dropboxの例では、SHA-512 → bcrypt の後に、Global Pepperを用いてAES256をしている。
これを「bcryptの前にしちゃダメなのかな?」と思ったのが今回の発端。

noonworksnoonworks

おさらい:ハッシュ化(+ソルト+ストレッチング)とペッパーの役割の違い

  • 通常、ハッシュ化されたパスワードはDBに保存され、ソルトも一緒に保存される
  • ペッパーはDBとは異なる場所に保存される

ペッパーの役割は、「攻撃者にハッシュ化(+ソルト+ストレッチング)されたパスワードが知られても、ペッパーを知らなければ解読できない」という状態にすること。
典型的には、SQLインジェクション等でDBのテーブルがそのまま攻撃者の手に渡った場合。その場合でも、その内容がペッパーで暗号化されていた場合、攻撃者はペッパーを手に入れなければ解析を始められない。

そのため、ペッパーはDBとは異なる場所に保存する必要がある。専用のセキュリティハードウェアや、各種クラウドサービスのキー保管ソリューションなんかを使うようだ。

noonworksnoonworks

ハッシュ化H(x)とペッパー使用暗号化Pe(x)の順について、順番を以下の2通り考える。

  1. パスワード → ハッシュ化 → ペッパー暗号化 Pe( H( pass ) )
  2. パスワード → ペッパー暗号化 → ハッシュ化 H( Pe( pass ) )

1の場合、攻撃者はまずペッパーを入手しなければ、ハッシュの解読に取り掛かれない。
2の場合、攻撃者はハッシュの解読を始めることができるが、仮にハッシュを解読できたとしても手に入るのはペッパー暗号化済みパスワードであるため、ペッパーを入手しなければパスワードは得られない。

どちらでも結局、ペッパーを入手しない限りは攻撃者はパスワードを得られない。

noonworksnoonworks

おさらい:攻撃手法と防御方法

ハッシュ化されたパスワードの解析方法は、以下のようなものがある。

  • オフライン攻撃の手法
    • 総当たり攻撃(パスワードとしてあり得る文字列を全部試す)
    • レインボーテーブル攻撃(事前にハッシュを計算して保存しておいたデータから逆引きを試みる)
  • 加えて、オンライン攻撃で使われる攻撃手法も可能
    • アカウントリスト攻撃(すでに他所から流出しているID/パスワードの組み合わせを試す)
    • 辞書攻撃(よくあるパスワードや辞書にある単語を試す)

オンライン攻撃(実際にサーバーにアクセスする攻撃)は、ネットワーク越しで低速であること、サーバーが大量アクセスを拒否する/処理できずダウンする可能性があることから、総当たりのような大量試行が必要な攻撃は行えない。そのため、他所で流出済みのパスワードやよくあるパスワードなど、「弱いパスワード」を狙った攻撃しか行えない。

一方でオフライン攻撃は、攻撃者がパスワードの保存されたDB等を入手してから、手元で解析する手法である。DBを入手するというハードルさえクリアできれば、高性能な機器を使って高速/大量に解析を試行できる。

オフラインでの総当たり攻撃は、攻撃にかかる時間を無視すれば、どんなに強いパスワードでも必ず解析可能な手法である。根本的には防ぐ方法がない。
ただし現実には時間は有限であるため、「攻撃者の解析が完了する前に攻撃に気付いてパスワードを変更してしまえば不正ログイン被害は受けない」し、「攻撃者が諦めるくらい長い時間がかかるならば攻撃されない」。よって、パスワード解析にかかる時間が稼げれば、現実的には被害を防ぐことができる。

ストレッチングは総当たりにかかる時間を延ばすための防御策で、単純に、同じハッシュ化計算を何度も繰り返して時間をかける。
正当なユーザーのパスワードを確認する際にも同様に計算負荷がかかるようになるため、あまりに多いストレッチングはできない。しかし、正当なユーザーはパスワード確認に0.1秒ほどかかってもまず気にしないのに対して、何千万通りも試行する攻撃者にとっては1回0.1秒の増加は大きな負担となる。計算機の性能にもよるが、正当なユーザーにとっては気にならず、攻撃者には大きな負担となるような回数のストレッチングを行う。

レインボーテーブル攻撃は総当たり攻撃にかかる時間を減らすための工夫である。レインボーテーブルとはざっくり言うと「事前に大量のパスワード候補のハッシュ値を計算して保存しておけば、それと照らし合わせるだけでいいから解析が早く終わるが、総当たりの結果を全部保存しておくのはデータサイズ的に不可能なので、上手いこと圧縮して保存したテーブル」である。
とはいえ、いくらレインボーテーブルでも「すべての」総当たり結果を保存することはできない。攻撃者はパスワードとして使われそうな文字列(文字数や英数記号の種類)やよく使われるハッシュ関数に絞って、ある程度の範囲のレインボーテーブルを作成しておく。

ソルトを使うとレインボーテーブルへの耐性が向上する。ソルトはハッシュ化の前にパスワードに付加される文字列で、文字列の長さと複雑さを高めることで「レインボーテーブルに保存された範囲外」になることを目的とする。
極端な例を出せば、パスワードの範囲が「数字4桁のみ」の1万通りなら、全部をレインボーテーブルとして保存しておくことは容易だ(というかこの量なら、レインボーテーブルじゃなくてもいいんだけど、そこはまぁおいておく)。しかしそこに、ユーザーごとに異なるランダム生成の英数記号30文字を付加すると、組み合わせ数が増加し、レインボーテーブルを用意するコストが格段に上昇する。
また、ソルトの肝は「レインボーテーブルを事前に用意しておくのが難しくなること」なので、ハッシュ化済みパスワードと同時になら、攻撃者に知られてしまっても性能に問題はない。

注意すべき点は、いくらストレッチングとソルト付加を行っていても、あまりに弱いパスワードは結局すぐに解析されてしまうということ。攻撃者は総当たりするにしても、よくありそうなパスワードから順に試すだろうから……。

noonworksnoonworks

ここまでのまとめ

  • ハッシュ化済みパスワードが攻撃者に知られても、ソルト付加とストレッチングを適切に行っていれば解析には時間がかかり、その間に対策をとれる
  • ペッパーは、ハッシュ化済みパスワードとは異なる場所に保存されることで、もう一段階の防御を担う
  • 弱いパスワードや流出済みのパスワードでは、オフライン攻撃だけでなくオンライン攻撃でも不正ログインされてしまうため、パスワード保存方法を工夫しても無力
    • こういった場合に備えてサービス側ができるのは多要素認証や不正ログイン検知などだろう
noonworksnoonworks

では、「ハッシュ化済みパスワードとペッパーが両方漏れた前提」では、前述の①Pe(H(pass))と②H(Pe(pass))のどっちの方がいいのだろうか?

Pe(H(pass))の場合、ペッパーを用いて復号したあとは、通常のハッシュ化済みパスワードと同じ方法で解析を行うことができる。
ただし前述の通り、ハッシュ化済みパスワードの時点で解析にかかる時間を十分に稼げている(はず)なので、安全性に問題ない。

H(Pe(pass))の場合は少し事情が異なる。
パスワードpassは通常英数記号の文字列であるが、Pe(pass)はその範囲とは限らない(バイナリデータである場合が多い)。そのため、攻撃用のレインボーテーブルを用意するのがさらに困難になるのではないだろうか?
もっとも、文字列のソルトだけでも十分にレインボーテーブル用意のコストは高いはずなので、あまり意味はないかもしれない……。

その他の観点として、①の方法はDB側の機能で全体を暗号化することで実現可能であるというメリットがある。クラウドサービスのフルマネージドDBを使っている場合には安全で簡単な管理が可能そう。
ただし、ストレージ層での暗号化機能を用いる場合、アプリケーション層でのSQLインジェクション等では復号された後のデータが漏れるため、意味がない。(防げるのはサーバーへの直接侵入等の経路。クラウドサービスのマネージドDBのデータセンターが攻撃されてそこから漏れるって、あるのかな……?)

noonworksnoonworks

結論

どっちでもよさそうだが、①の方が実装が楽そう。
もしかしたら、Pe(H(Pe(pass)))でもいいかもしれない。

このスクラップは2021/12/10にクローズされました