ASP.NET でのパスワード格納
ASP.NET でのパスワード格納
昔はパスワード認証のためのパスワードをデータベースに生のまま書き込むの普通でしたけど、そういうのがだめになってからずいぶん経ちます。ASP.NET 系はパスワード認証のためのフレームワークを持っていますが、実際のところどうなんでしょう? また、ASP.NET から Rails に乗り換えるといったときにできるんでしょうか? とかいろいろありますので調べてみました。
パスワードの安全な保存
パスワードは極力システムが知らないほうが安全なわけです。ですので、パスワードを元に戻せない形に変換して保存します。
元に戻せない変換としては、MD5 や SHA-256 などの暗号学的ハッシュ関数が有名ですが、パスワード用の変換関数としてはこれは使えません。
暗号学的ハッシュ関数は同じソースからは同じ値になることで、ハッシュ値を比較することでソースが同じ (改ざんされていない) ことを証明します。この特性はパスワード用としてはあまりよくありません。
次がパスワードを MD5 で変換した例です。見ての通り、一行目と三行目は同じで同じパスワードだということがわかってしまいます。
899f80b9a6292c424ff54c9d6d071321
5177a58dc65c8a14dc90c69db3bf3dd2
899f80b9a6292c424ff54c9d6d071321
7b82aaedf8d94f7162e515f7bb09da7e
パスワード用の変換関数は同じパスワードでも違う値になる必要があります。
もう一つ。暗号学的ハッシュ関数の主な目的は改ざん検出ですので、速ければ速いほどいいのです。高速であればあるほど、ブルートフォースアタックの耐性が低くなります。パスワード用はそれなりに遅くなければなりません。さすがに利便性を損なうレベルで (ログインに 10 秒かかるとか) 遅いのはだめですけど...
ということで、パスワードの変換では暗号学的ハッシュ関数をそのまま使わず、専用のものを使うわけです。いろいろありますが、主流は次の二つだと思います。
- bcrypt
- PBKDF2
- scrypt
- Argon2
bcrypt
bcrypt は PHP の password_hash() のデフォルトですので、PHP ユーザーの方はおなじみなのではないでしょうか?
出力はこんな感じで、ストレッチング回数やサルトを含んだテキストになります。
$2y$10$.vGA1O9wmRjrwAVXD98HNOgsNpDczlqm3Jq7KnEd1rVAGv3Fykk1a
パラメーター | 意味 |
---|---|
2y |
bcrypt |
10 | ストレッチング回数を決める重み |
残り | サルト (固定長) とパスワードハッシュ (固定長) を結合したもの |
Linux の /etc/shadow
もフォーマットは同様で、$6$
から始まっています。$6$
は bcrypt ではなく、SHA512 だそうです。
PBKDF2
もう一つ有名なのは PBKDF2 (Password-Based Key Derivation Function 2) です。ASP.NET ではこちらを採用しています。
おそらく、NIST (米国標準技術研究所) の推奨になっているからではないかと思います。
NIST Special Publication 800-63B に、認証のこといろいろ書いてあるので参考になります。アプリケーションの要件に合わせてこうしたほうがいいみたいなのも書いてあるようです。
この PBKDF2 は鍵導出関数と呼ばれるもので、本来の使い方は暗号通信で共通鍵を自分と相手で生成するためのもののようです。そのせいか、bcrypt のようなサルト込みの文字列の形式というのがありません。
そのほか
ほかにも scrypt や Argon2 もあるようです。Argon2 は GPU や ASIC でのブルートフォースアタックへの耐性が高いモードがあるみたいなので、これがベストってことのようです。
ASP.NET と ASP.NET Core では?
ASP.NET MVC で使われている ASP.NET Identity と ASP.NET Core の ASP.NET Core Identity について調べました。
バージョン | フレームワーク | アルゴリズム | ストレッチング回数 | 備考 |
---|---|---|---|---|
Version 0 | ASP.NET | PBKDF2 (HMAC-SHA1) | 1,000 回 | |
Version 2 | ASP.NET Core | PBKDF2 (HMAC-SHA1) | 1,000 回 | Version 0 と同じ |
Version 3 | ASP.NET Core | PBKDF2 (HMAC-SHA256) | 10,000 回 | ASP.NET Core デフォルト |
ASP.NET ではストレッチング回数が 1,000 回です。NIST の推奨では 10,000 回となっていますので、今の時代には少なすぎです。ソースコードを見ると、ストレッチング回数は固定で変更できません。ですので、もっと強度の高い実装に入れ替えるか ASP.NET Core を使う方がよいでしょう。
ASP.NET Core は確認できた ASP.NET Core 2.1 ~ 6.0 までの間のものです。ソースコードには Version 2
と書かれていますが、ASP.NET の Version 0
と互換性はあるようです。(簡単な単体テストで確かめました) ASP.NET のパスワードテーブルは ASP.NET Core でもそのまま利用できます。
Version 2 / 3 ともにアルゴリズムは HMAC-SHA1 / HMAC-SHA256 がソースコードにハードコードされており変更できません。Version 3 のみストレッチング回数を変更できます。
なお、.NET 7 からアルゴリズムが HMAC-SHA512 に、ストレッチング回数のデフォルトが 100,000 回に変わります。ソースコードを見る限り、.NET 6 以前のパスワードテーブルはそのまま使えるようです。
ASP.NET Core によって生成されるパスワードハッシュは BASE64 エンコードされていて、元のバイナリは次のフォーマットです。
アドレス | 意味 |
---|---|
0 - 0 | 0x01 (0x0 の場合は Version 0 / 2) |
1 - 4 | アルゴリズム (0: HMAC-SHA1 / 1: HMAC-SHA256 / 2: HMAC-SHA512) |
5 - 8 | ストレッチング回数 |
9 - 12 | サルトの長さ () |
13 - | サルト |
残り | 導出された鍵 |
いろいろ
ほかの言語との互換性を調べてみる
PBKDF2 の仕様は PKCS #5 2.1 にあります。ですので、ほかの言語の PBKDF2 関数で作成した鍵を ASP.NET Core でも使えます。(Django は PBKDF2 ということで、Python の PBKDF2 の関数で作成した鍵を ASP.NET Core で使えるかテストしました)
とはいえ、bcrypt と違い、サルトやストレッチング回数込みの文字列の形式まで決まっているわけではないので、変換する必要があります。ちょっと面倒です。
ストレッチング回数を増やす
ストレッチングを行うのは、ブルートフォースアタックに対する耐性のためです。コンピューターの性能の向上に伴い、ブルートフォースアタックのコストも低下するため、ストレッチング回数を増やす必要があります。
ASP.NET Core は、強度の弱い形式でパスワードテーブルに保存している場合、ログインしたときにパスワードをより強度の高い形式でパスワードテーブルに上書きします。UserManager.cs のこのあたりを読めばわかると思います。
つまり、ASP.NET から ASP.NET Core に移行したり、ASP.NET Core 7 に移行したりしたあとにログインしたユーザーは新しい強度の高い形式に上書きされるわけです。もちろん、設定でストレッチング回数を増やした場合も同様です。
とはいえ、ログインしないユーザーはいつまでたっても強度が低いままです。あまりよくありません。
パスワードとサルトを結合して暗号的ハッシュ関数でハッシュ化して、そのハッシュにサルトを結合して再度ハッシュ化して... ってやってると思っていました。ですので、あとからストレッチング回数を増やせるだろうなと。
やってみたらだめでした。PBKFD2 はユーザーのパスワードは HMAC の鍵に使い、サルトの方のみをハッシュ化してストレッチングしているようです。
ユーザーのパスワードがわからない以上、どうあがいてもストレッチング回数を後から増やすことはできません。
強制的に強度をあげる
ログインしていないユーザーもそのままというわけにはいきません。何とかしたいです。
すぐ思いつくのはユーザーに「強度を上げるためにシステムにログインしてください」とメールを流す方法なんですが、どう見ても怪しいメールと同じですよね! それに、ユーザーがメールを読んでくれるとは限りません。むしろ、読んでくれる方が珍しいのです。
全然いい方法が思いつかない! どうすればいいんだろう? って思ってたら...
OWASP Cheet Sheet Series に Upgrading Legacy Hashes ってのがありました。
例えば、現行のシステムが MD5 を使っているとしたら、PBKDF2(MD5(password))
のようにしちゃうんだそうです。
ASP.NET の Version 0 を ASP.NET Core の Version 3 に変換することはできませんが、Version 0 -> Version 3 の順で変換してやればいいのですね。サルトの扱いとか面倒なんで結構大変ですけど、IPasswordHahser
の実装を書けば何とかなりそうです。
PBKDF2 以外を使う
ASP.NET Core 用の Argon2 / bcrypt / scrypt の実装はサードパーティですが Security.PasswordHasher にあります。
さらなる安全を
How Dropbox securely stores your passwords を読むと、Dropbox さんはもっといろいろとやってます。(過去にパスワード漏洩をやって痛い目を見たのでこの辺すごく頑張ってるようです)
bcrypt でハッシュ化したパスワードをさらにシステム共通鍵であるペッパーを使って AES256 暗号にかけているようです。
Going forward, we’re considering storing the global pepper in a hardware security module (HSM). At our scale, this is an undertaking with considerable complexity, but would significantly reduce the chances of a pepper compromise. We also plan to increase our bcrypt strength in our next update.
この共通鍵を HSM (Hardware Security Module) に突っ込むことも検討しているようです。このブログは 2016 年なのでさすがに対応済みでしょうか?
ユーザーとしてどうしたら?
結局のところパスワードが十分に長ければ、弱いといわれてるアルゴリズムでも十分安全です。
https://twitter.com/TerahashCorp/status/1155112559206383616
15 文字くらいの長さがあればやっちゃいけないといわれる MD5 でも十分安全だと思います。ユーザーとしてはパスワードをすごく長くするのが一番の対策でしょう。
生のまま保存しているサービスはどうしようもありませんが...
結論
パスワードを安全に扱うのってかなり難しいですよね。面倒なので、ASP.NET Core を信用してお任せしてます。とはいえ、本当のところはどうなんですか? ってのは疑問だったので調べてみました。思ったのとだいぶ違ったので、調べたかいがありました。
- ASP.NET 系はバージョンアップをしてれば、その時代時代にまあまあ十分な強度でパスワードを格納してくれる
- ただし、ログインしてくれないユーザーは古い強度のままになる
- 長期運用するシステムは、ログインしてくれないユーザーの対策のために、自力で
IPasswordHasher
インターフェイスを実装する必要がある
パスワードいらない世界に早くならないかな?
最後に
サービスがどのようにパスワードを扱っているかは利用者 (もしくは利用者の候補に) 公開すべきだと思っています。
Discussion