概要
- いわゆるパスワードを、非可逆化した状態でデータベース等に保存するのは現代のITの常識である。
- Webシステムにおいて、ユーザーがログインするときのパスワードと、そのシステム内でユーザーが重要な操作(例:金銭的な決済)をするときのパスワードとを、別々に設定させる手法が取られることがある。
- 以下、通常のログイン等で用いられるパスワードを 第一パスワード 、重要な手続きをする段階で入力を求められるパスワードを 第二パスワード と呼称する。
- 第一パスワードと第二パスワードを 同じにすることは許可しないように システムを組むべきだが、それを実現するには第一パスワードを平文かそれに近しい形で保存したり、あるいは非可逆化の際に使用するソルト値を第一と第二とで同じにしなければならない、といった明らかに間違いな言説 をツイッター上で見かけた。しかも、そこそこITリテラシー高めと思われる複数の人物の会話の中で。
- ツイッターに限らず、筆者の身近なエンジニアの会話のなかでも妙な誤解をしているケースが散見される。若手かベテランかは相関性がない。
- おそらく、妙な誤解をしている人の多くが「ハッシュ」「ソルト」といった用語をある程度正しく理解しているものの、 それらを使ってのパスワードという文字列の非可逆化とその照合 、という総合的な部分での理解不足と思われる。本記事ではそのあたりを中心に書く。
- もちろん、ユーザーが第一パスワードと第二パスワードを同じに設定するのを防ぐことは可能である。
パスワードの非可逆化はencodeとmatchesという2つの関数から成る
言語やフレームワークによって関数名は様々に違うが、とにかく 2種類の関数がある と思えばよい。ここではJVM系言語でよく使用されているSpringSecurityというフレームワークが提供するBCryptPasswordEncoderで説明する。
encodeとは
BCryptPasswordEncoderの encode メソッドは、hogehoge
というパスワードを下のような文字列に変換する。
String s = pwEncoder.encode("hogehoge");
System.put.println(s);
実行結果
$2a$10$RDhD6o6vq7.nu5Xtj19jku8/Q6nEv9kTiDSrVIIy0bPToOL6OCqem
ただし、まったく同じコードをもう一度実行するととまったく別な文字列になる。
$2a$10$yBamd8Kk5VG8DcsoM274fOUX2E1UtbvXPSaAqqwpXtLQePq9.feDy
hogehogeという同じ文字列がの非可逆化の結果がいちいち違う理由は、 encodeメソッドは実行のたびにソルト値をランダムに生成する から。上記の文字列を分解すると下のようになる。$記号は単なる区切り文字。ソルト値は22バイトで残りが非可逆化の結果。(それぞれの細かい説明はググってね)
種類 | 強度 | ソルト値 | 左の3つとhogehogeという文字列を使って非可逆化した結果 |
---|---|---|---|
2a | 10 | RDhD6o6vq7.nu5Xtj19jk | u8/Q6nEv9kTiDSrVIIy0bPToOL6OCqem |
2a | 10 | yBamd8Kk5VG8DcsoM274f | OUX2E1UtbvXPSaAqqwpXtLQePq9.feDy |
encode
メソッド は、ユーザーの新規登録段階あるいはパスワードの変更をするときに、ユーザーが入力した パスワードをデータベースに保存する文字列に変換する手段 として用いられるメソッドである。
matchとは
ユーザーはパスワード欄に hogehoge
と入力してsubmitボタンを押したが、はたしてそれはサーバ側のデータベースに保存されている $2a$10$RDhD6o6vq7.nu5Xtj19jku8/Q6nEv9kTiDSrVIIy0bPToOL6OCqem
という値に合致するか?という 検証の手段 が match
メソッドである。
boolean result = pwEncoder.matches("hogehoge", "$2a$10$RDhD6o6vq7.nu5Xtj19jku8/Q6nEv9kTiDSrVIIy0bPToOL6OCqem");
System.put.println(result);
実行結果
true (ユーザーが入力したパスワードは正しい!)
match関数は内部では次のことをやっている。
-
$2a$10$RDhD6o6vq7.nu5Xtj19jku8/Q6nEv9kTiDSrVIIy0bPToOL6OCqem
という文字列から、アルゴリズムの種類が2a, 強度が10, ソルトが先頭22バイトだからRDhD6o6vq7.nu5Xtj19jk
残りのu8/Q6nEv9kTiDSrVIIy0bPToOL6OCqem
がそれらを使って非可逆化したパスワードそのもの、という分解をする - 1で得られたアルゴリズムの種類、強度、ソルト値そして入力された
hogehoge
という値を使ってencodeと同じように非可逆化した結果がu8/Q6nEv9kTiDSrVIIy0bPToOL6OCqem
であれば、マッチしたとしてtrueを返す。さもなくばfalseを返す。
第一パスワードと第二パスワードを同じにすることを許可しないようにするにはどうするか
第一パスワード=最初のログイン処理等で使うと想定=がすでに設定済み、つまり非可逆化された状態でデータベースに保存済みな状態で、新たに第二パスワード=金銭のからむ処理で使うと想定=をユーザーに設定させるとき、第一と第二で同じパスワードを設定しようとしているか?を検知する方法は、次のような流れになる。
- ユーザーの第一パスワードは平文で表すと
hogehoge
であり、それをencode関数
を通すことで非可逆化された値$2a$10$RDhD6o6vq7.nu5Xtj19jku8/Q6nEv9kTiDSrVIIy0bPToOL6OCqem
がデータベースにすでに保存されているものとする。 - 第二パスワードとして使いたい文字列をユーザーに入力させる。ここでは
fugafuga
だとする - そのユーザーの第一パスワードの非可逆化された文字列、ここでは
$2a$10$RDhD6o6vq7.nu5Xtj19jku8/Q6nEv9kTiDSrVIIy0bPToOL6OCqem
をデータベースから取得する -
match関数
を実行してみるmatches("fugafuga", $2a$10$RDhD6o6vq7.nu5Xtj19jku8/Q6nEv9kTiDSrVIIy0bPToOL6OCqem)
- 結果がfalseなので第二パスワードとして設定しようとしている値は第一パスワードとは異なると判定できる。
- 第二パスワード
fugafuga
を encode()メソッドの引数に与え、得られた文字列をデータベースの DAI2_PASSWORD カラムに書き込む
誤解の原因(推定)
ここまで書いて&読んでしまうと、わざわざqiitaで力説するほどのことでもない。
しかし冒頭のとおり、「ユーザーが第一パスワードと第二パスワードを同じ値に設定してしまうことを防ぐことはソルトやハッシュという考え方からすると難しい」という勘違いを導き出してしまうエンジニアやライターがいる。筆者の身近なエンジニアとの会話のなかでも、妙な誤解の香りを感じることがある。なぜだろうか?
あくまで個人的な推測だが、以下のフシを感じる。
- パスワードの非可逆化保存という概念には
encode
とmatch
という2つの関数とその利用が必要になる、という理解が不足している。 おそらくencode関数だけがあると思っている。 - 非可逆化されたパスワードと、それに用いたソルト値は、 データベース上で別々のカラムに保存されていると思っているフシもある。 そういう実装は可能だし、実際そうしているシステムも存在するかもしれないが、上のサンプルの通り、 非可逆化されたパスワードというものは、それに用いたソルト値も含めた形で一つの情報として表現される ことが通常の実装方法である。
なお、今はなき7payに限らず、たとえば某白い犬系証券会社でも、第一パスワード(ログインパスワード)と、第二パスワード(株の売買注文時に入力させるパスワード)は同じ文字を設定できる。できてしまう。諸行無常の響あり。