パスワードを安全に保管する方法

パスワードを保管する悪い方法は山ほどあります。 この記事で、悪い保管方法を紹介してから、良い保管方法を紹介します。

第1悪い保管方法 平文

パスワードを一般公開することを除いて、最も非セキュアな保管方法です。

データベースにて平文で保管すると、仮にそのデータベースに不正アクセスがあったら、全てのユーザのアカウントにアクセスすることができるようになります。

さらに、ユーザが別のサイトやウェブアプリケーションでパスワードを使い回していたら、それらのアカウントにもアクセスできるようになります。

さらにさらに、パスワードが平文のため、データベースにアクセス権があるアプリケーションと管理者も読めます。「絶対に見ない。約束」はセキュリティ対策ではありません。

ユーザが激おこします。

「そんなことする人いないでしょ!」と思うよね?まだけっこういます

第2悪い保管方法 sha1()

平文がだめなら、一方向ハッシュはどうでしょうか?

MD5 や SHA-1 のような一方向ハッシュを使って、パスワードが無意味な文字列に変換されますが、ハッシュからパスワードへ戻すことができません1

パスワードは平文ではなくなりましたが、アプリケーションはユーザ認証をすることができます。

Python で例を書きます。

def is_correct_password(user, password_attempt):
    return sha1(password_attempt) == user["sha1_password"]

sha1_password はデータベースに格納したパスワードのハッシュです。

平文よりはマシですが、いくつかの問題があります。

まず、例に挙げた MD5 と SHA-1 に弱点が発見され、一方向性がなくなってしまいました。

その影響、パスワード保管のため、あるいは一方向ハッシュを目的とした使用が非推奨になりました。

一方向ハッシュを使いたいなら、本記事投稿時にまだ脆弱性が発見されていない SHA-256 を使用すべきです。

しかし、もう一つの問題は、悪人はそもそもハッシュからパスワードへ変換し直そうとしません。よくあるパスワードのリストを使って総当りでパスワードとハッシュを紐づこうとします(総当り攻撃)。

「よくあるパスワード」というのは、実はそんなに多くなくて、ほとんどが一般公開されています

リストの数は多くに見えるかもしれませんが、現代パソコンは計算処理が早くて一秒に数百億回パスワード攻撃を試すことがでくるでしょう。一般公開ツールの John the Ripper または hashcat はまさにそういうことをしてくれます。

ハッシュ(特に古いハッシュ)のみでパスワードを保管するのはセキュリティ的に不十分というのが結論です。

第3悪い保管方法 ハッシュ + 固定のソルト

固定のソルトを「ペッパー」 (pepper) と呼ぶ人がいます。

セキュリティを向上させるため、パスワードをハッシュ化する前にソルト(salt)を使います。

ソルトは長くてランダムなバイツ文字列です。

悪意を持つ人がパスワードのデータベースを手に入れたら、ソルトされたパスワードを当ててみる難易度がだいぶ高くなります。

しかし、サーバ上にあるデータベースに不正アクセスできたら、ソースコードにも不正アクセスできたと推測できますね。固定ソルトがソースコードに書いてあったら、ソルトを使う意味が台無しになります。

第4悪い保管方法 ハッシュ + ユーザごとのソルト

では、固定ソルトがダメであれば、ユーザごとにソルトをランダムに生成することはどうでしょうか?

そのソルトは、ユーザが初めてアカウントを作る、あるいはパスワードを変更するときに生成されます。

ユーザ認証の難易度は大きく変わらないのに、悪人は全てのパスワードを同時に攻撃することができなくなる大きな利点があります

def is_correct_password(user, password_attempt):
    return sha1(user["salt"] + password_attempt) == user["password_hash"]

単純計算、ユーザ10万人がいれば、ユーザごとのソルトは全てのユーザのパスワードを当ててみることを10万回難しくします。

ただ、一つの問題が残っています。今まで MD5、 SHA-1 と SHA-256 で例えてきましたが、それらのハッシュは大きいファイルやバイナリに対しても、迅速に実行できることを考えて設計されています。

総当り攻撃を防ぐため、 CPU 時間が多ければ多いほど良いですね。

すなわち、以上のハッシュはパスワード保管に適していません。

第5悪い保管方法 独自暗号アルゴリズム

以上のハッシュ関数が速すぎるなら、数回ループして、わざと遅延させることで、問題が解決されるのではないでしょうか?

答えは「多分」です。すなわち「恐らくセキュアではない」

私は暗号研究者ではないので以下の解釈を鵜呑みせず自分で調べておいてください。どちらにせよ、独自暗号アルゴリズムを使わないのが結論です。

同じハッシュを繰り返して状況を悪化させることがあります。詳しくは Length extension attack (en) をご参照ください。

また、ハッシュ衝突の可能性が増えており、探索領域 (search space) が減ってしまうという副作用を及ぼすかもしれません。

鍵導出関数 (key derivation function, KDF) が丈夫2であれば、衝突の確率をパソコンで予測するのはなかなか難しいため、そのの脅威は無視して良いですが、独自アルゴリズムで使った関数の KDF が丈夫だと言い切れる自信がありますでしょうか?

関連話、反復回数をどう決めますか?

直感的に、1,000回で充分?10,000回?100,000回?

よく見かける勘違いは、中間結果の複雑さが総当たり攻撃に対して防御をしてくれると考えることです。

勘違いの原因は攻撃者が最後のハッシュからパスワードを数回逆算するだろうと考えるのだと思います。

先程よくあるパスワードのリストで触れましたが、そのリストを使えば、逆算をする必要がそもそもありません。

もう一つの勘違いは「悪人が知らない反復回を使えば安全」です。しかし、強い暗号方式は、ケルクホフスの原理によれば「秘密であることを必要とせず、敵の手に落ちても不都合がないこと」。

最後に、技術の発展と共にパソコンの計算処理が早くなった場合、その独自アルゴリズムが耐えられるかどうかと、簡単に改善できるかという問題が残ります。

関数の反復回数が多ければ、総当たり攻撃が、しにくくなり、ソルトを使えば辞書攻撃に強くなるのは確かです。

が、その条件を正しく揃えることは非常に難しいです。念の為、そしてセキュリティのため、良い保管法を使いましょう。

第1良い保管方法 公開暗号アルゴリズム

世の中には、以上の問題を数年間考えて、ソリューションを一般公開してくださった賢い方たちがいらっしゃいます。

せっかくなので使いましょう!

記事投稿時、以下のどれかをこの順番で使えばセキュアでしょう。

  1. Argon2
  2. scrypt
  3. bcrypt
  4. PBKDF2
それぞれに違いがありますので、実装で実現したいことにやって十分にそれらの特徴を確認しましょう。また、非推奨にはなっていませんが、 PBKDF2 と bcrypt は ASICGPU 攻撃により弱いため、その脅威があれば別のアルゴリズムを使いましょう。

Bcrypt を例にします。 Bcrypt はパスワード保管のため設計されたハッシュ関数です。一方向ハッシュの上に、意図的に遅いです。

Bcrypt は SHA-1 より1万倍遅いです。人間にとっては誤差(100ミリ秒)ですが、パソコンにとっては大変な差です。

CPU 時間があがると、一時間にパスワード攻撃を試せる回数が減ります。したがって、一つのパスワードを見つけるのに時間とお金がかかりますので、全員ではありませんが、ほとんどの悪人は諦めるでしょう。

Bcrypt の設計の数学は僕のレベルを超えますが、興味があればペーパーはこちら(PDF)です。

大雑把に、内部暗号化・ハッシュ関数をループで数回回しています。ちなみに、回数は設定できます。設定は対数ですので桁に間違わないようにね。

from bcrypt import hashpw, gensalt

hashed = hashpw("hunter2".encode("utf-8"), gensalt(13))
print(hashed)

# 出力例:
# $2b$13$yxyBS33ultFrKvTQ.KEcmO6H6fpQNrR32m55tQF8OiJrRGBWfplWm
# |  |  |                      |
# $bcrypt_id                   |
#    |  |                      |
#    $log_rounds               |
#       |                      |
#       $128-bit-salt          |
#                              $184-bit-hash

回数の設定は $log_rounds です。既存値は 12 で、上記は 13 に設定しました3

なお、 Bcrypt は遅いだけではなく、ユーザごとのソルトを自動で生成してくれます(上記の $128-bit-salt)!

そのため、ソルト($128-bit-salt)も、ハッシュ($184-bit-hash)も毎回違うものになります。

Python の bcrypt で例えると、ユーザ認証の際、パスワード試しとソルトを hashpw() メソッドに引数として渡して、データベースに格納したハッシュ化パスワードと比較します。

実際に、ソルトではなくてハッシュ化パスワードをそのまま渡して、 hashpw() メソッドが自動でハッシュからソルトを抽出してくれます。

def is_correct_password(user, password_attempt):
    return hashpw(password_attempt, hashed) == hashed

ボーナス:第2良い保管方法 パスワードを保管しない

パスワードをセキュアに保管するのは大変な責任です。認証はあなたのアプリケーションの一部だけなのに、時間とリソースを使ってしまう機能ですね。

パスワードを保管しないことを検討してみてはいかがでしょうか?

パースワードレスなアプリケーションは認証機能を「アイデンティティ・プロバイダ」に任せることで、アプリケーションの本質に集中することができます。

激しい議論を招きますが、一部のセキュリティ専門家はパスワードレスがメール・パスワードより安全と主張しています。

Tailscale とかは認証を ID プロバイダに任せることで、自動で鍵のローテーションを簡易に行うことができるそうです。

僕はまだその議論に参加するほど、 SSO/IDP/IAM アイデンティティ・プロバイダについて詳しくありませんが、パスワードとは別の認証方法の発展に興味深いです。


  1. そのハッシュが脆弱でない前提 ↩︎

  2. 丈夫とは「計算上、ランダムと区別が付かない」 ↩︎

  3. 出力に設定値が書いてあるのに驚いているなら、ケルクホフスの原理を思い出してください。 ↩︎