概要
パスワードを保存する際は平文で保存せずハッシュ化するのは当然です。しかし単にハッシュ化するだけでなく
- 暗号学的に優れたハッシュ関数を使う
- saltを付ける
- 計算コストのかかるアルゴリズムを採用する
- ストレッチングで計算コストを上げる
といった点を考慮することで、万が一ハッシュ化されたデータが漏洩してもそこから平文が算出されないようにすべきです。
説明
暗号学的に優れたハッシュ関数を使う
ハッシュ関数は
- チェックサム
- チェックディジット
- フィンガープリント
- 誤り訂正符号
- 暗号学的ハッシュ関数
などで使われていますが、用途によって異なった形で設計・最適化されています。
パスワードのような秘密情報をハッシュ化する場合は暗号学的ハッシュ関数を使用します。
またその中でもMD5
やSHA-1
といった既に破られているアルゴリズムは採用すべきでないです。
saltを付ける
仮にハッシュ関数を適切に選んだとしても、あらかじめたくさんの文字列をそれらの関数で生成していた場合、照らし合わせて平文を見つけることができます。
これをレインボーテーブル攻撃といいます。
例えばfoobar
を暗号強度的に十分と言われるsha256
でハッシュ化した値は
c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2
ですが、検索すると以下のように既にハッシュ化された値がどこかしらに辞書として保持されています。
ref: https://hashtoolkit.com/decrypt-sha256-hash/c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2
※上記サイトは閉鎖されていたので今使うならこちら
CrackStation - Online Password Hash Cracking - MD5, SHA1, Linux, Rainbow Tables, etc.
そこでsaltと呼ばれるランダムな値を平文に付けてハッシュ化することで、既存のレインボーテーブルと一致しないようにしておきます。
注意としてsalt自体もハッシュ化した値と一緒に保存しておく必要があります。そうでないと2回目以降そのハッシュ値が生成できないので。
例
項目 | 値 |
---|---|
平文 | foobar |
salt | qwertyuiopas |
ハッシュ化する値 | qwertyuiopasfoobar |
ハッシュ値 | ab541497f63a2b684dd4cd07e80c0bfc874eb7f5 |
DBに保存する値 (saltとハッシュ値のデリミタに : を使用) |
qwertyuiopas:ab541497f63a2b684dd4cd07e80c0bfc874eb7f5 |
またsaltは十分な長さ(推奨は32byte or 64byte)にすることで計算コストを上げる効果もあります。
計算コストのかかるアルゴリズムを採用する
近年ではグラフィックボードを使った高速な攻撃手法も存在するため、処理に一定の時間をかけられる変数(実行時間、使用できるメモリ量、鍵作成のラウンド数など)を持っているアルゴリズムを採用すると良いです。
具体的には
あたりがオススメです。
ストレッチングで計算コストを上げる
ストレッチングとは、ハッシュ値の計算を繰り返し行うことで計算コストを上げることです。
例えばパスワード管理ツールで有名なLastPassは100,000回以上もストレッチングを行うそうです。
In fact, LastPass' use of PBKDF2-SHA256 with 105 hash iterations exceeds
ref: https://ieeexplore.ieee.org/document/8418642
実際にどうすべきか
自前でこれらを考慮して実装するのは大変なので、先程の計算コストのかかるアルゴリズムで紹介したアルゴリズムを使用するのが良いです。
中でもbcryptを使うのがオススメです。
Argon2やscryptと違って使用できるメモリ量を制限できないため、高メモリなGPUを用いた攻撃に対してはそれらより強いと言えませんが、それでもPBKDF2よりも高いメモリ要件のため計算コストがかかります。
またArgon2やscryptよりも枯れている分、アルゴリズム自体も、各言語で実装されたライブラリ等も十分に検証されていると言えます。
bcryptで生成される文字列の構造
bcryptを使うと以下のような文字列が生成されます。
$2a$10$nQiTH2fD5IxrpZ3OetAFNuExYyAW34shDL4vmjS.9Yly9fAAZLdjC
これらは以下のように分解して説明できます。
値 | 説明 |
---|---|
$2a$ | アルゴリズム及びバージョン |
10 | コストパラメータ |
nQiTH2fD5IxrpZ3OetAFNu | salt |
ExYyAW34shDL4vmjS.9Yly9fAAZLdjC | ハッシュ値 |
Goでbcryptを使ってみる
では実際にどう使うかを説明します。
bcryptパッケージを使います。
package main import ( "fmt" "golang.org/x/crypto/bcrypt" ) func main() { password := "hogefugapiyo" hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { panic(err) } fmt.Printf("hashed: %s\n", hashed) // invalid pass err = bcrypt.CompareHashAndPassword(hashed, []byte("foobar")) if err != nil { fmt.Println(err) } // correct pass err = bcrypt.CompareHashAndPassword(hashed, []byte(password)) if err != nil { panic(err) } fmt.Println("correct") }
結果
hashed: $2a$10$nQiTH2fD5IxrpZ3OetAFNuExYyAW34shDL4vmjS.9Yly9fAAZLdjC crypto/bcrypt: hashedPassword is not the hash of the given password correct
異なるパスワードの場合エラーになり、正しいパスワードではnilになることが確認できます。
ポイント
ポイントは以下です。
- GenerateFromPasswordでハッシュ値生成
- 第2引数のcostにコストパラメータ(ラウンド数)を指定
- saltはランダムなので実行するたびにこの文字列は変わる
- CompareHashAndPasswordでパスワードとハッシュ値比較
まとめ
パスワードをハッシュ化する上での考慮すべき点と、それらをライブラリで解決する方法を説明しました。