あるサービスが「パスワードを平文で保存していた」というニュースに対してある有名な人が「平文で保存するなんてありえない!」と怒っていた。
「平文で保存していた」というニュースは何度か見たことがある。
では、平文で保存してはならないのならば、どうやって保存すればいいのか?
そう聞いたら、多くの人がこう答えるだろう。
「暗号化して保存する」
私もそうだと思っていた。
しかしそれは間違いで、正しくは
「パスワードのハッシュを保存する」
であった。
そういうとおそらく「暗号化もハッシュも同じようなものだ、そんな細かいことはどうでもいい」という人も多いだろう。
でも、そういう、「細かいことはどうでもいい」が積み重なって、「平文でも大丈夫」になってしまうのではないだろうか?
だから、ちょっと「細かいこと」にこだわってみよう。
ハッシュとか、暗号化とか、sha1とかmd5とかいうものの存在は知っていた。
ソフトウェアなどをダウンロードしたときにmd5チェックサムがついていて、
ファイルが壊れていないかを確認することができる、とか、
WEBサーバは、sha1は古いアルゴリズムだから、sha256を使わないといけないとか。
しかし、実際に自分がサーバ管理者やサービス提供者になったとして、
顧客のパスワードを安全に保存するにはどうすればいいかと聞かれたら答えられない。
最近、当たり前のことなのだが、知ってちょっと驚いたことがある。
それは、ハッシュというものは同じ値に対してはだれが計算しても同じ結果になる、
ということだ。
windowsのコマンドプロンプト(またはパワーシェル)で、
ハッシュを計算することができる。
別に何かのアプリなどをインストールする必要はない。
a.txt
というファイルがある。
中身は
あいうえお(改行)
で、文字コードをutf-8で保存した。
PS C:\users\taro\desktop> certutil -hashfile a.txt
SHA1 ハッシュ (対象 a.txt):
471399aeda50586bdc982b558bfde1da642c0fb3
CertUtil: -hashfile コマンドは正常に完了しました。
PS C:\users\taro\desktop> certutil -hashfile a.txt md5
MD5 ハッシュ (対象 a.txt):
0f2f3458c1553eb96d499508dc5184b4
CertUtil: -hashfile コマンドは正常に完了しました。
PS C:\users\taro\desktop> certutil -hashfile a.txt sha256
SHA256 ハッシュ (対象 a.txt):
0007a58ae5789f92155f87d6bc51edd4c9b036277d23154fe06a9daf33d0e514
CertUtil: -hashfile コマンドは正常に完了しました。
sha1(デフォルト)、md5、sha256で計算してみた。
それでは、このファイルを、私がさくらのVPSで使っているcentosのサーバにアップロードし、そこでハッシュを計算してみよう。
centosにもハッシュを計算するコマンドがある。
# sha1sum a.txt
471399aeda50586bdc982b558bfde1da642c0fb3 a.txt
# md5sum a.txt
0f2f3458c1553eb96d499508dc5184b4 a.txt
# sha256sum a.txt
0007a58ae5789f92155f87d6bc51edd4c9b036277d23154fe06a9daf33d0e514 a.txt
結果は全く同じである。
ハッシュというのは元の値に対して、人の目からは無意味である程度長く、元の値を推測も計算することもできないような値を生成する。
それはわかっていた。
しかしもうひとつ肝心なことは、同じアルゴリズム(sha1とかmd5とか)を使用すれば、
同じ値からは同じハッシュ値が生成されるということだ。
そうでなければ、たとえばユーザのパスワードのハッシュ値を保存しても、
ユーザのパスワードが正しいかを検証できない。
当たり前のことであるが。
私もパソコン、スマホ、さまざまなインターネット上のサービスなどを利用していて、
「パスワード」というものを何十個、何百個と設定して使用してきたが、
そのパスワードがテキストで読める状態で保存されているのは見たことがない。
パスワードをメールや郵便物に記載して送付されることはあったが。
あらためて、先ほどのcentosのサーバにパスワードがどのように保存されているかを見てみた。
/etc/shadow
というファイルである。
私は複数のアカウントを作成しているが、そのうちいくつかは異なるアカウントに同じパスワードを設定していた。
たとえば、taroというユーザのパスワードが password、
jiroというユーザのパスワードも password
という感じである。
「同じ値のhash値は誰が計算しても同じ」
ということは、taroとjiroのパスワードから計算したhash値は同じ文字列のはずだ......
しかし、実際には違う文字列であった。
あれ?
調べると、パスワードを保存する際にはhash値をもとの文字列からそのまま計算せず、
「ソルト(塩)」と呼ばれるランダムな値を付加して計算するのだそうだ。
その付加する方法は、文字列としてくっつけるのか、二進数でなんらかの演算をするのかわからないが、とにかくもうひと手間加えて、同じパスワードでも同じハッシュにならないようにする。
同じハッシュ値だからといっても、元の値を知ることは事実上不可能なことにはかわりないが、「複数のアカウントが同じパスワードである」という事実があれば、それらのアカウントに使用されているパスワードは誰もが思いつきそうな単純なパスワードであると推測できる。
また、よく使われそうなパスワード文字列を用意し、そのハッシュ値を計算しておいて、
ハッシュ値同士を比較すれば元の文字列がわかる。(これをレインボーテーブルという)
これらを防ぐために、ソルトというものが使われる。
しかし私はそこでひとつ疑問がわいた。
パスワードは正しい値であることを検証しなければならない。
再訪したユーザが入力したパスワード値にソルトを付加してハッシュ値を計算し、保存されたハッシュ値と同じであるかを比較しなければならない。
ではソルトをハッシュ値と一緒に保存するのか。
それは鍵をかけた金庫の横に鍵を置くようなものだ。
/etc/shadowを見てももちろんソルト値は書かれていない...
調べてみた。
ソルト値の隠し場所があった。
金庫のたとえで言うなら、鍵は金庫の横に置かれてこそいないが、
横にテープで貼り付けるくらいのことしかされていなかった。
ソルト値を使ってパスワードからhashを計算したら、
/etc/shadowに書かれているhash値と一致した。
# cat /etc/shadow | grep hiroshi
hiroshi:$6$9Qpwui9R$jrswaF/U2NRgq7Yhuk2YH7T2POuOYuDq/2eH71KMJkRDNaLxHEa25BLvmAnT3zz.ijxv/e6qUK.cEN24FN.GD0:18005:0:99999:7:::
# perl -e 'print crypt("hiroshidesu", "\$6\$9Qpwui9R");'
$6$9Qpwui9R$jrswaF/U2NRgq7Yhuk2YH7T2POuOYuDq/2eH71KMJkRDNaLxHEa25BLvmAnT3zz.ijxv/e6qUK.cEN24FN.GD0
pythonの例
# python -c 'import crypt; print(crypt.crypt("hiroshidesu", salt="$6$9Qpwui9R"))'
$6$9Qpwui9R$jrswaF/U2NRgq7Yhuk2YH7T2POuOYuDq/2eH71KMJkRDNaLxHEa25BLvmAnT3zz.ijxv/e6qUK.cEN24FN.GD0
そもそも、ハッシュ値で保存するのも、さらにその際にソルトを加えるのも、
サーバにアクセスされてかつサーバの管理アカウントでログインされてしまった場合を想定してのことである。
平文で保存していたのも、サーバにログインされないための対策を十分にしているから、
と考えてのことだろう。
sha512の計算方法とか、saltのかけ方とかはわからないが、
それを使ってますよ、というだけで、パスワードの保存方法というのはそんなに難しいことをしているわけではないというのが、分かった。
ただし、繰り返しになるが、それはあくまでも推測したパスワードからハッシュを計算して一致するかを調べる、ということであって、ハッシュ計算の脆弱性があるとか、ハッシュ値から元の値が算出できるとかいうことではない。
※関連エントリ
https://monqy.blogspot.com/2019/04/blog-post_27.html