BackEnd

[Laravel]データベースの暗号化について考えてみる

投稿日:2019年6月27日 更新日:

はじめに

こんにちは。現在関わっているプロジェクトで、パスワードに限らずメールアドレスや電話番号・住所などの個人情報は暗号化してデータベースに格納してほしい、という要件がありました。
Laravelでは認証時どのようにパスワードを扱っているのか、暗号化と復号はどのように実装するか、パフォーマンスにどのくらい影響はあるのか…など、いざ考えてみると、きちんと把握できていない点が多かったことに気付かされます。
コードを追ったり実際に書いてみたりして、Laravelにおける自分なりの暗号化の実装パターンを考察してみました。

環境

  • PHP 7.3.6
  • Laravel 5.8.26
  • MariaDB 10.4.6

Laravelでの暗号化

チュートリアルでおなじみ(?)の make:auth で生成されるユーザ登録処理は以下のような実装になっています。

パスワード暗号化は Hash::make() ですね。LaravelのFacadeによる呼び出し方なので、Hashというクラスは存在するものの、makeというstaticメソッドはありません。
実際に呼び出されているのは Illuminate/Hashing/BcryptHashermake() メソッドで、こんな実装になっています。

PHP推奨のハッシュ関数である password_hash() が使われていて、アルゴリズムは CRYPT_BLOWFISH であることが分かります。
なお $optionscost というオプションが設定されており、configのデフォルト値「10」が渡されています。
password_hash() の挙動について詳しくは公式ドキュメントをご覧ください。
ハッシュ関数に渡す設定値はLaravel内の config/hashing.php に記述されていますので、気になる方はご覧になってみてください。

実際に登録されるレコードはこのようになります。

暗号化されたカラムは、そのままではLIKE検索ができない

問題点

今回あれこれと調べたり考えたりしたきっかけはこの点です。パスワードはデータの性格上、文字列の完全一致検索ができれば問題ありません。…が、メールアドレスや住所はそうもいかず、部分一致(LIKE)検索をする必要が出てきます。
上述した Hash::make() では常に60文字のハッシュを生成します。例えば「東京都中央区日本橋」でも「東京」でも異なる60文字の、しかも毎回異なるハッシュ値を返します。これではLIKE検索のしようがありません。

なので $users = User::where('email', 'LIKE', "%{Hash::make($email)}%")->get(); みたいな検索はできません。

解決案1:全件取得してPHP側でがんばって検索する

ボツ案です。正確には「DBからいったん全件取得してPHP側で復号・文字列の部分検索処理を実装する」という方法でした。
当然ながら、毎回全件取得してたらパフォーマンス劣化が激しいです。件数が増えるほど激しく劣化するでしょう。データベース-PHP間の通信コストも、PHPの処理コストも恐らく結構なことになると思います。

解決案2:暗号化も復号もMySQL側が担う

もっとスマートなやり方があるんじゃないかとは思うのですが、ひとまずこの方法に落ち着いています。
MySQLには暗号化用の AES_ENCRYPT() 、復号用の AES_DECRYPT がありますので、これらを使用して、実装例としては以下のようになります。

  • 登録時は email の値をSQL文として評価させるため \DB::raw() を使っています。
  • 検索時にもSQL文のまま評価させるために whereRaw() を使います。

名前が16進数の文字列で格納されています。

補足1:HEX/UNHEX

AES_ENCRYPT() は通常の文字列ではなくバイナリを返すため、そのままではvarchar型のカラムに格納できません。そのため何かしらの文字列に変換する必要があるので、ここでは HEX を使っています。カラムが VARBINARY 型になっていれば、文字列化は不要です。
検索時はまず UNHEX でバイナリに戻してから AES_DECRYPT で復号することでLIKE検索が可能になります。

補足2:CONVERT

検索は AES_DECRYPT(UNHEX()) で可能ですが、表示時はそのままだと16進数表記になってしまいます(マルチバイト文字限定)ので、 CONVERT を噛ませる必要があります。メールアドレスなどの半角英数であれば不要となります。

AES_DECRYPT の第2引数で渡しているハッシュ値は、Laravelの .env ファイルに保存されている APP_KEY です。

さいごに

フレームワークに任せず、アプリケーションに生SQLを書くことになってしまうので、あまりスマートな解決方法ではない気がしています。また、検索時は複合処理を毎回行うことになりますので、パフォーマンス面でも若干不安です。
大量のクエリをさばく必要がある場合はまた別の方法を模索することになると思いますが「性能より安全面」という要件であれば、今回紹介した方法も、実装例のひとつになるかと考えています。

おすすめ書籍

PHPフレームワーク Laravel Webアプリケーション開発 バージョン5.5 LTS対応 PHPフレームワーク Laravel入門

blog-page_footer_336




blog-page_footer_336




-BackEnd
-, ,

執筆者:

免責事項

このブログは、記事上部に記載のある投稿日時点の一般的な情報を提供するものであり、投資等の勧誘・法的・税務上の助言を提供するものではありません。仮想通貨の投資・損益計算は複雑であり、個々の取引状況や法律の変更によって異なる可能性があります。ブログに記載された情報は参考程度のものであり、特定の状況に基づいた行動の決定には専門家の助言を求めることをお勧めします。当ブログの情報に基づいた行動に関連して生じた損失やリスクについて、筆者は責任を負いかねます。最新の法律や税務情報を確認し、必要に応じて専門家に相談することをお勧めします。


comment

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA


関連記事

rails

【Puma】アプリサーバのチューニング

1 はじめに2 チューニングで注意する項目3 子プロセスの数3.1 最低3つの子プロセスを割り当てる3.2 最大子プロセス数3.3 CPUコア数と子プロセス数3.4 なぜPumaの子プロセス数を増やす ...

laravel logo

Laravelの便利なメールアドレスのバリデーション設定

1 はじめに2 不具合内容2.1 バリデーションルール設定2.2 不正なメールアドレス例3 対応3.1 email:rfc3.2 email:strict3.3 email:dns3.4 email: ...

Vue.js+TypeScriptな環境整備

1 はじめに2 vue-cliのインストール3 プロジェクトの作成3.1 機能の選択3.2 シンタックスの選択3.3 CSSプリプロセッサの設定3.4 Unit test3.5 E2E test3.6 ...

Stripe Connectを使って継続課金を実装

1 はじめに2 商品・価格の登録2.1 マイグレーション2.2 製品・価格登録処理の実装2.3 Stripe管理画面での確認3 サブスクリプション登録3.1 事前準備3.2 課金処理の実装3.3 St ...

php logo

PHPでGmail APIを利用してメールデータを取得してみる その2

1 はじめに2 メールの内容取得3 MessagePartオブジェクト3.1 件名3.2 本文4 multipartの場合4.1 本文の取得5 全文6 さいごに7 おすすめ書籍 はじめに 前回は、Gm ...

フォロー

blog-page_side_responsive

2019年6月
 1
2345678
9101112131415
16171819202122
23242526272829
30  

アプリ情報

私たちは無料アプリもリリースしています、ぜひご覧ください。 下記のアイコンから無料でダウンロードできます。