Carpe Diem

備忘録

パスワードを保存するときに考慮すること

概要

パスワードを保存する際は平文で保存せずハッシュ化するのは当然です。しかし単にハッシュ化するだけでなく

  • 暗号学的に優れたハッシュ関数を使う
  • saltを付ける
  • 計算コストのかかるアルゴリズムを採用する
  • ストレッチングで計算コストを上げる

といった点を考慮することで、万が一ハッシュ化されたデータが漏洩してもそこから平文が算出されないようにすべきです。

説明

暗号学的に優れたハッシュ関数を使う

ハッシュ関数

などで使われていますが、用途によって異なった形で設計・最適化されています。

パスワードのような秘密情報をハッシュ化する場合は暗号学的ハッシュ関数を使用します。

またその中でもMD5SHA-1といった既に破られているアルゴリズムは採用すべきでないです。

saltを付ける

仮にハッシュ関数を適切に選んだとしても、あらかじめたくさんの文字列をそれらの関数で生成していた場合、照らし合わせて平文を見つけることができます。
これをレインボーテーブル攻撃といいます。

例えばfoobarを暗号強度的に十分と言われるsha256でハッシュ化した値は

c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2

ですが、検索すると以下のように既にハッシュ化された値がどこかしらに辞書として保持されています。

f:id:quoll00:20201004004248p:plain

ref: https://hashtoolkit.com/decrypt-sha256-hash/c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2

そこで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: On the Economics of Offline Password Cracking - IEEE Conference Publication

実際にどうすべきか

自前でこれらを考慮して実装するのは大変なので、先程の計算コストのかかるアルゴリズムで紹介したアルゴリズムを使用するのが良いです。

中でも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になることが確認できます。

ポイント

ポイントは以下です。

まとめ

パスワードをハッシュ化する上での考慮すべき点と、それらをライブラリで解決する方法を説明しました。

参考