9.5 パスワードの保存
ほんの少し前から、多くのページにおいてユーザのパスワードが漏洩する事件が発生しています。これにはトップレベルのインターネット企業が含まれます - Linkedin, 国内ではCSDNの事件が国内のインターネット上を駆け巡りました。また多玩遊戯の800万のユーザデータが漏洩しました。また人人網、開心網、天涯社区、世紀佳縁、百合網といったコミュニティもハッカーの次の目標になったと噂されています。尽きることのない似たような事件がユーザのネット生活に巨大な影響を与えています。人々は自衛し、人々は往々にして異なるウェブサイトで同じパスワードを使用することに慣れてしまっていますから、一箇所でデータベースが暴かれてしまうとすべてに影響がでます。
Webアプリケーションの開発者として、パスワードの保存方法を選択する際はどのような罠に陥りがちになるのでしょうか。またどのようにしてこれらの罠を回避すべきでしょうか?
普通の方法
現在最も多く使用されているパスワードの保存方法は平文のパスワードを単方向ハッシュにかけて保存するものです。単方向ハッシュアルゴリズムの特徴は:ハッシュされたあとのダイジェスト(digest)からはオリジナルのデータを復元することができないということです。これが"単方向"であるということの所以です。よく使われる単方向ハッシュアルゴリズムにはSHA-256、SHA-1、MD5等があります。
Go言語のこの三種類の暗号化アルゴリズムの実装は以下の通り:
//import "crypto/sha256"
h := sha256.New()
io.WriteString(h, "His money is twice tainted: 'taint yours and 'taint mine.")
fmt.Printf("% x", h.Sum(nil))
//import "crypto/sha1"
h := sha1.New()
io.WriteString(h, "His money is twice tainted: 'taint yours and 'taint mine.")
fmt.Printf("% x", h.Sum(nil))
//import "crypto/md5"
h := md5.New()
io.WriteString(h, "暗号化する必要のあるパスワード")
fmt.Printf("%x", h.Sum(nil))
単方向ハッシュには2つの特徴があります:
- 1)同じパスワードを単方向ハッシュにかけると、いつでもユニークな確定したダイジェストを得ます。
- 2)速い計算速度。技術の進歩により、現在は一秒間に数十億回の単方向ハッシュ計算が可能です。
上の2つの特徴を総合して、多数の人間が使用するパスワードをよくあるセットとして考えると、攻撃者はすべてのパスワードのよくあるセットに対して単方向ハッシュをかけることで、ダイジェストのセット得ることができます。その後データベースの中のダイジェストと比較することで対応するパスワードを取得することができます。このダイジェストのセットをrainbow table
と呼びます。
そのため、単方向暗号化を行ったあとに保存されたデータは平文で保存されるのとあまり違いはありません。ですので一旦ウェブサイトのデータベースが漏洩すると、すべてのユーザのパスワード自身が白日のもとに晒されることになります。
よりよい方法
上ではハッカーがrainbow table
を使用することでハッシュされたパスワードをクラックできるとご紹介しました。大抵の場合は暗号化時に使用されたハッシュアルゴリズムが公開されているものであることが原因です。もしハッカーが暗号化のハッシュアルゴリズムが何かを知らなければ、どこから手をつけてよいかわかりません。
直接的な解決方法の一つは、自分でハッシュアルゴリズムをデザインすることです。しかしながら、優良なハッシュアルゴリズムはとてもデザインが難しいのです- - 衝突を避けなければなりませんし、分かりやすいルールであってもいけません。この2つを満たすのは想像よりもずっと困難です。そのため、実際のアプリケーションでは既存のハッシュアルゴリズムを利用して複数回ハッシュすることが行われます。
しかし単純な複数回ハッシュでは、ハッカーを止めることはできません。二回のMD5や三回のMD5といった我々でも思いつく方法は、ハッカーも当然思いつきます。特にいくつかのオープンソースに対しては、このようなハッシュは直接アルゴリズムをハッカーに告げているのと同じことです。
破られない盾はありません。しかし折れない矛もまたありません。現在セキュリティが比較的優秀なウェブサイトはいずれも"ソルト"とよばれる方法によってパスワードを保存しています。よく言われる"salt"のことです。彼らの通常の方法はまずユーザが入力したパスワードに対してMD5(または他のハッシュアルゴリズム)で一度暗号化します。得られたMD5の値の前後に管理者自身だけが知っているランダムな文字列を追加して、再度MD5で暗号化します。このランダムな文字列にはなんらかの一定の文字列が含まれていてもかまいません。ユーザ名が含まれていてもかまいません。(各ユーザの暗号化に使用された秘密鍵が一致しないことを保証するために使用します)。
//import "crypto/md5"
//ユーザ名をabc、パスワードを123456とします
h := md5.New()
io.WriteString(h, "暗号化が必要なパスワード")
//pwmd5はe10adc3949ba59abbe56e057f20f883eです。
pwmd5 :=fmt.Sprintf("%x", h.Sum(nil))
//saltを2つ指定します: salt1 = @#$% salt2 = ^&*()
salt1 := "@#$%"
salt2 := "^&*()"
//salt1+ユーザ名+salt2+MD5を連結します。
io.WriteString(h, salt1)
io.WriteString(h, "abc")
io.WriteString(h, salt2)
io.WriteString(h, pwmd5)
last :=fmt.Sprintf("%x", h.Sum(nil))
2つのsaltが漏洩していなければ、ハッカーはもし最後のこの暗号化された文字列を手に入れてもオリジナルのパスワードが何だったのか推測するのはほとんど不可能です。
専門的な方法
上の"よりよい方法"は数年前には十分安全な方法であったかもしれません。攻撃者はこれほど多くのrainbow table
を作成するだけの十分なリソースが無かったためです。しかし、今日に至っては並列計算能力の向上によりこのような攻撃はすでにまったくもって可能です。
どうやってこの問題解決するのでしょうか?時間とリソースが許せば、クラックできないパスワードはありません。ですので方法は:故意にパスワードの計算に必要となるリソースと時間を増加させることによって、誰にもrainbow table
を作成するのに必要となるリソースを与えないようにするのです。
この方法にはひとつの特徴があります。アルゴリズムにはどれも因子があります。パスワードのダイジェストを計算するのに必要となるリソースと時間を説明するのに使われ、計算強度でもあります。計算強度が大きければ大きいほど、攻撃者がrainbow table
を作成するのが困難になり、ついには継続できなくなります。
ここではscrypt
の方法をおすすめしましょう。scryptは有名なFreeBSDハッカーであるColin Percivalが彼の予備のサービスとしてTarsnapで開発しました。
現在Go言語でサポートされているライブラリhttp://code.google.com/p/go/source/browse?repo=crypto#hg%2Fscrypt
dk := scrypt.Key([]byte("some password"), []byte(salt), 16384, 8, 1, 32)
上の方法によってユニークな対応するパスワードの値を取得することができます。これは現在までもっともクラックが難しいものです。
まとめ
ここまででもしあなたに危機感が芽生えたのだとすれば、行動すべきです:
- 1)もし普通のユーザであれば、LastPassによってパスワードを保存/生成するのをおすすめします。異なるサイトで異なるパスワードを使用します。
- 2)もしデベロッパであれば、専門的な方法でパスワードを保存するよう強くおすすめします。
links
- 目次
- 前へ: SQLインジェクションの回避
- 次へ: データを暗号化/復号する