ソルト付きハッシュのソルトはどこに保存するのが一般的か

pictBLandとpictSQUAREに対する不正アクセスがあり、パスワードがソルトなしのMD5ハッシュで保存されていたことが話題になっています。

> 2023年8月16日に外部のフォーラムにpictSQUAREより窃取した情報と主張するデータ販売の取引を持ち掛ける投稿が行われた（中略）パスワードはMD5によるハッシュ化は行われているもののソルト付与は行われていなかったため、単純なパスワードが使用されていた29万4512件は元の文字列が判明していると投稿。（それ以外の26万8172件はまだMD5ハッシュ化されたままと説明。）
> [不正アクセスによるpictBLand、pictSQUAREの情報流出の可能性についてまとめてみた - piyolog](https://piyolog.hatenadiary.jp/entry/2023/08/17/030141)  より引用

これに関連してMD5ハッシュやソルトに関するツイート(post)を観察したところ、どうもソルトの理解が間違っている方が多いような気がしました。そこで、以下のような「試験問題」を出させていただきました。多くの方に回答いただきありがとうございます。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">ソルト化ハッシュが話題になっているので、ソルトに対する理解度測定試験問題を作りました。ふるってご参加ください。現在の知識で「これかな?」で答えていただけると幸いです。<br>【問題】パスワードをハッシュ値で保存する際のソルトはどこに保存するのが一般的な実装ですか?</p>&mdash; 徳丸 浩 (@ockeghem) <a href="https://twitter.com/ockeghem/status/1691966966117122462?ref_src=twsrc%5Etfw">August 17, 2023</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

想定正解は「ハッシュ値と共にデータベースに保存する」ですが、正答率は33.9%と大変低い結果となりました。私の「懸念」は現実のものとなったようです。

それでは、答え合わせをしていきましょう。

## パスワードのハッシュ値で保存する理由
そもそもパスワードをハッシュ値で保存する目的ですが、

- パスワード情報が漏洩してもパスワードの悪用をできるだけ遅らせたい
- 管理者といえどもユーザーのパスワードを知り得ると悪用の可能性が高まる
- サーバーに侵入される前提だと暗号などの鍵も窃取される可能性が高い

という理由で、復号可能で暗号鍵管理が必要な暗号化ではなく、ハッシュ値での保存が広く普及しています。Unix/Linux/Windowsなどもユーザーのパスワードはハッシュ値で保存されていますし、ウェブアプリケーションでも平文保存を避けてハッシュ値での保存が求められるようになりました。

## ソルトの目的
ソルト(salt)の目的は以下の通りです。

- 単純なハッシュ値だと、同じパスワードのユーザ間でハッシュ値も同一となり危険性が高まる
- [レインボーテーブル](https://ja.wikipedia.org/wiki/%E3%83%AC%E3%82%A4%E3%83%B3%E3%83%9C%E3%83%BC%E3%83%86%E3%83%BC%E3%83%96%E3%83%AB)という手法により8文字英数字程度のパスワードだと数秒～数分で平文パスワードを復元できるようになったので、「見かけのパスワード長」を長くする目的

下図は、パスワードをSHA-256で保存しているケースで、パスワードとして「password」を設定しているユーザのハッシュ値が一致している様子です。同一パスワードをつけていることがわかった時点で不正ログインの重大なヒントとなります。仮にハッシュの計算式が未知であっても、「同じパスワードをつけているということは安易なパスワードである可能性が高い」ことが分かり、リモートで辞書攻撃したり、攻撃者がわざと安易なパスワードを登録することでこれらのユーザのパスワードが一網打尽で判明します。

![password-hash.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/22718/6312110d-0e83-e252-bfc1-7335bb140754.png)

これを避けるために、ソルトという短い文字列を生成してパスワードの前後に付与してからハッシュ値を計算します。ソルトがユーザー毎に異なることで、同一パスワードでもハッシュ値は異なるという状況が作れます。

## ソルトはどこに保存するのが一般的か
前述のようにUnixでもパスワードの保存にハッシュ値を使用していましたが、システムやバージョン毎にハッシュ値の保存形式がまちまちであったため、相互運用上の問題がありました。これを解決するために、Modular Crypt Format(MCF)が考案され、現在のLinuxを含め広く利用されています（[参考](https://passlib.readthedocs.io/en/stable/modular_crypt_format.html)）。以下は、ソルト=SALT（実際はランダム文字列です）、パスワード=pokemonで、SHA-512形式のパスワードハッシュ値を計算した結果です。

```
$ openssl passwd -6 -salt=SALT pokemon
$6$SALT$ULXzhDaWogf6Q3KHTtpYdqKKEIaFPML8gl5wpHpvPJVkGgiKGubqkogwvqoVn3eDsrJuRB22w.RPWzAdEu1xD.
```

この形式は、Linuxの/etc/shadowにてパスワードのハッシュ値が保存される形式です。上記は以下のような意味です。

```
$6$  ハッシュ方式（このケースではSHA-512）
SALT ソルト（実際の運用ではランダム文字列を用いる）
ULX..以下　ハッシュ値
```

このように、ハッシュ値とソルト、ハッシュの方式はセットでファイル内の1行に「まとめて」保存されています。これらの情報はパスワードの照合に必要なので、セットで保存されているわけです。
MCFは元々はUnix/Linuxのパスワード保存ように考案されたものですが、現在ではウェブアプリケーションでパスワードを保存する場合でも標準的に用いられています。以下は、PHPのpassword_hash関数でパスワード=pokemonを変換した例です。

```
$ php -r "echo password_hash('pokemon', PASSWORD_DEFAULT), PHP_EOL;"
$2y$10$5MLO1PPVHzUiW/mggn8kAOkF/aoRk0RaLBSH3CwU47jmacYoGtrFa
$
```

このケースでは、下記の意味になります。

```
2y アルゴリズム=bcrypt
10 ストレッチング回数の指標（10回という意味ではない）
5MLO1PPVHzUiW/mggn8kAO ソルト
kF/aoRk0RaLBSH3CwU47jmacYoGtrFa ハッシュ値
```
PHPに限らず、主要なライブラリやフレームワークでMCF形式が使われており、アプリケーションはパスワード情報をMCF形式のままデータベースに保存する方式が一般的です。

したがって、先の問題の正解は（ソルトは）「ハッシュ値と共にデータベースに保存する」となります。MCFを用いることにより、例えばPHPで開発したウェブアプリケーションをRuby on Railsに移植しても、そのままパスワード情報を移行できます。

## ハッシュ値とソルトを一緒に保存したら流出後に悪用されるのでは?

ここで多くの方が疑問に持ったことが、ハッシュ値とソルトが一緒にしたら、流出後にパスワードが簡単に破られるのでは? ということだと思います。
確かに「時間をたっぷり掛ければ破られる」のですが、そう簡単に破られるわけではありません。ハッシュ関数は一方向なので、ハッシュ値とソルトから、数式などで平文パスワードが算出できるわけではなく、辞書攻撃や総当たり攻撃が必要となります。パスワードの保存には、MD5やSHA-2のような一般的で高速なハッシュ関数ではなく、bcrypt、scrypt、Argon2などパスワード保護用に開発された「低速の」アルゴリズムが望ましいとされます。これらは、アルゴリズム上の工夫に加えて、ストレッチングというハッシュ計算を繰り返し行うことも含んでいます（Linuxのパスワード保存に使われる\\$6$形式もストレッチはしています）。これらにより、パスワードのハッシュ値が漏洩しても、短時間では悪用されず、パスワード変更までの時間稼ぎができるという考え方です。
一方で、元のパスワード自体がpasswordや123456など安易なものですと、辞書攻撃で簡単に破られます。これはハッシュ値でパスワードを保存する場合の限界ですが、これに対抗するためにペッパー(pepper)というものを用いることも推奨され始めています。pepperは単一の秘密の文字列であり、これを混ぜることにより、パスワードの秘匿性を高めます。pepperの方は漏れると意味がないので、ハードウェアセキュリティモジュール(HSM)のような秘匿性の高いストレージへの保存が推奨されています。

## 誤答は何が問題か
以下、誤答の間違いのポイントについて解説します。

### 機密性の高いファイルに保存して環境変数で渡す
これは、ソルトの要件である「ユーザ毎に異なる」を満たせない実装です。ペッパーの方に使うなら有りえますが、環境変数での受け渡しは、侵入を前提とすると安全とは言えません。

### ハードウェアセキュリティモジュール(HSM)に保存
これも、ユーザ数が数十万以上になることを考慮すると、「ユーザごとに異なる」を満たすことが困難な実装であり、一方ペッパーの保存には適した方法です。ですが、せっかくHSMを用いるのであれば、暗号鍵はHSMの外に持ち出さず、パスワードハッシュ値をHSM内で暗号化する方がHSMの特性を活かした方法だと思います。

### ソルトは保存せず毎回ランダムに生成する
毎回ランダムに生成すること自体はよいのですが、ソルトを保存しないとパスワードの照合ができなくなります。

## まとめ
ソルトの保存方式についてクイズ形式で出題した後にその解題を行いました。ソルトの保存方法の主流はModular Crypt Format(MCF)であり、Linuxやウェブアプリケーションのパスワード保存方式として広く利用されています。ただ、そのことを知らなくても、安全性の高いパスワード保存のライブラリを使用していれば、見えないところでMCF形式が使われている場合が多いでしょう。
ソルトはハッシュと共に保存することが一般的であることは既に述べたとおりですが、それが心許ないというのであれば、ペッパー(pepper; シークレットソルトと呼ぶ場合もある)の利用も検討するとよいでしょう。その場合は、pepperを安全に管理することが課題になり、たとえばHSMやクラウドHSMの利用も選択肢としては考えられます。


## 宣伝
筆者のYouTubeチャンネルではパスワード保護について動画で解説しています。

<iframe width="560" height="315" src="https://www.youtube.com/embed/p_2tiP0qclQ" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

<iframe width="560" height="315" src="https://www.youtube.com/embed/D2ZK2z7hCj8" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

<iframe width="560" height="315" src="https://www.youtube.com/embed/5jHALFTQ0ow" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

EGセキュアソリューションズ株式会社では、セキュリティの初学者向けに半日程度で学べるeラーニング講座を開講しています。パスワードの安全な保管についても説明していますが、残念ながら今回のクイズの答えまでは解説していませんw ですが、パスワードの保護が必要な理由や具体的なパスワード保護の方法など基礎的な内容はカバーしています。

[エッセンシャルコース詳細 | Security Campus](https://www.security-campus.com/essential)