サービスを建築する時、ユーザーパスワードを安全に保存する必要があります。2020年になって、人々がまだ同じパスワードを複数のサイドやサービスで使っているです。(僕もその一人ですw)万が一データベースなどからデータが流出された場合だとしてもパスワードがそのまま晒されないのは重要です。この度、bcryptを使ってみたので、それについてちょっと語りたいと思います。
bcryptは何?
bcryptは1999年に公開された、Blowfish暗号を基礎にしたパスワードハッシュ化関数です。「ソルト」を組み込んでレインボーテーブル攻撃防止し、キー拡張を反復執行することによってブルートフォース攻撃にも対抗できる仕様になっています。(後続に説明します。)
オリジナルのbcrypt仕様には、接頭辞が $2$
として定義しています。後続に「ASCII以外の文字を扱うようになった」($2a$
)、「PHPで8ビット文字列の処理バグを修復した」($2x$
, $2y$
)と「OpenBSDライブラリによって255文字数制を越えるバグを修復した」($2b$
)バージョンが発表され、更新された。今回Javascript(Nodejs)で導入したのは $2b$
から始まるです。
興味がある方は「パスワードハッシュ化関数」に関して調べたらbcrypt以外のハッシュ化方法が見つかると思います。
仕組み
簡単に言いますと、生のパスワードにソルトを加えて、指定の回数でハッシュ化するです。
例としてこのハッシュを見ていきましょう。
$2b$10$cBSbxLZTaiayiFlN3FkmZezgCGjvv26v4cc7tqUynmrPhlqKiPQau
最初の四文字は上に説明したハッシュアルゴリズムのバージョンです。その後の”10”はストレッチングの回数です(後続で説明します)。七文字目の”$”以降の22文字はソルトです。この場合は”cBSbxLZTaiayiFlN3FkmZe”の部分です。残り31桁はハッシュ化されたパスワード本体です。
「ソルト(Salt)」とは?
従来の「パスワードを直接にハッシュ化」という方法は一定な安全問題があります。ハッシュのアルゴリズムを把握し、事前によくあるパスワードをハッシュしてテーブルを作ったら、ハッシュされたパスワードが流出時点でテーブルを照会できて、パスワードが容易く手に入ります。つまりハッシュ化という暗号化過程はなしと同然です。このテーブル作ってパスワードを推測するのはレインボーテーブル攻撃と言います。
このレインボーテーブル攻撃を防止するため、生のパスワードをハッシュ化するの代わりにソルトをパスワードに加えてハッシュ化します。その結果、事前に計算したテーブルへの照会は難しくなります。(なお、不可能ではありません。)
ちなみにソルトを導入してレインボーテーブル攻撃を防ぐのはbcrypt特有ではなく、結構一般的なやる方です。
「ストレッチング」とは?
万が一ソルトがバレたらハッシュの逆算ができるようになります。逆算、及び事前計算のパスワードテーブルを作らせないようにストレッチングを行います。
ストレッチングとは、「生成したハッシュを再度ハッシュ化することです」。これを何度も繰り返すことによって、ハッシュを逆算するに必要な計算時間が増えていきます。
なお、ストレッチングの回数を増えることによって、正規でハッシュ化計算を行うサーバーへの負荷も増えます。パフォーマンスや環境を気にかけて、適切なストレッチング回数を設定することが大事です。
bcryptの使用上に注意する2点
一つ目はユーザーインプットの長さです。OpenBSDへの導入に合わせて、主なbcrypt導入は最大72桁までのユーザーインプット(つまり生パスワード)しか処理できません。安全性の考えを加えて、72文字以上のパスワードを設定させないようにする必要があります。
二つ目は使うbcryptのバージョンです。PHPやOpenBSDに関する修正があるため、できれば最新の2bを使うことが推奨されております。けれど一部のライブラリーにはバージョン2bではない可能性もありますので、実装する前には確認した方がいいと思います。
実際に使ってみよう!
今度の導入例はJavascript(Nodejs)なものです。私が最初にbcryptを使ってみたのはExpressを使ってログインAPIを作る練習でした、そのプロジェクトレポはこちらです。
ライブラリーのインストールと導入
NPMから”bcrypt”をインストールします。
> npm install bcrypt
* node-pre-gypの実行がインストールの一部になっています。もしnode-pre-gypのエラーが出た場合、開発環境の設定をチェックしてください。(macOSの場合はXCode CLIのインストールや設定が正しく完了していないかのせいが多いです。)
使うスクリプトにbcryptを導入します。
const bcrypt = require("bcrypt");
パスワードをハッシュ化
公式からの説明ではasyncでbcryptを実行するのを推奨しています。なおハッシュ化のやり方は二つあります。(これらのコードサンプルは公式説明からです。)
その1、ソルトの生成とハッシュ化を別々に実行する
bcrypt.genSalt(saltRounds, function(err, salt) {
bcrypt.hash(myPlaintextPassword, salt, function(err, hash) {
// ハッシュ(hash)の処理(データベースへの登録など)
});
});
最初は”bcrypt.genSalt”関数からソルトを生成します。その中に、”saltRounds”はストレッチング回数です。その後、コールバック関数からsaltを”bcrypt.hash”関数へ引き継げます。その中に、”myPlaintextPassword”は生パスワードです。最後に再びコールバック関数内でデーターベースへの登録などのハッシュの処理を実行します。
その2、ソルトの生成とハッシュ化を一緒に実行する
bcrypt.hash(myPlaintextPassword, saltRounds, function(err, hash) {
// ハッシュ(hash)の処理(データベースへの登録など)
});
この例の中では”bcrypt.genSalt”関数はないですか、ソルトはちゃんと生成されます。どちらの方法にしても結果は同じです。
パスワードを認証する
bcrypt.compare(authPassword, hash, function(err, result) {
// result == true
});
”bcrypt.compare”関数から”authPassword”を認証します。この場合、”hash”がハッシュされたパスワード(データベースなどから引いたもの)です。その後、コール爆関数の中にある”result”が返されます、パスワードがあってる場合は”true”で、合わなかった場合は”false”になります。
PromiseやAsyncの応用もできます
コールバック関数の代わりに”.then”を使ってPromiseとして使うこともできます。また、async関数の中に、直接定数をawaitの形でbcrypt関数に指定することもできます。
Sync関数もあります(非推奨)
"bcrypt.genSaltSync", "bcrypt.hashSync"及び"bcrypt.compareSaltSync"もあります。なお、このSync関数の使用は非推奨です。原因として、Sync関数はイベントをブロックします。サーバー環境の場合、Sync関数が実行している間、他のインバウンドイベントは実行されません。また、bcryptは説明した通り、ハッシュ計算からCPUに与える負担が重いです。イベントへのブロックはアプリケーションの実行に大きな影響が出ると思います。その代わりに、Async関数は別のイベントプールでbcryptを実行し、メインイベントループをブロックすることはありません。
これについて詳しく知りたい方は「Async関数とイベントループ」で検索すれば関連説明が出ると思います。
bcryptは完璧ではありまぜん
説明通りbcryptは多く使われている暗号化関数です。なお、タイミング攻撃(あるリクエストへ応答するにはどれくらい時間がかかるのを測定することにより、システムから情報が漏れる可能性があるということです。)には防止できません。パスワードを認証する”bcrypt.compare”にはタイミング攻撃を受けられる可能性があります。けれど、攻撃者はこの場合(bcrypt、及び上の実装例の場合)ではデータや暗号化過程に関する情報をてにすることができません。一般的な応用は心配要りませんか、他の場合で使うことは気をつけるべきです。
最後に
ここまで読んでくれてありがとございます。この度Expressを使ってログインAPIを作ってみて初めてbcryptを使ってみた感想とそれに関する説明を書いてみったです。この変異関する日本語の文章が少ないと思いますので、役に立ったら嬉しいです。僕は日本語が第三言語なので多少言葉のズレや伝えにくいところもあると思いますので、改善や間違いを指摘して貰えるとありがたいです。これの続きにJsonwebtokenを使って、認証セッションの記録と認証に関する記事も書きますので、興味がある方はその時是非読んでください。
(この記事は私のブログにも載っています。まだ立ち上がったばかりなのでコンテンツがほぼないですが、そちらもよろしくお願いします。*広告、収益化なしです。)