LaravelでDBの個人情報を暗号化する方法|検索と鍵管理の注意点

LaravelでDBの個人情報を暗号化する方法|検索と鍵管理の注意点

2026/10/09

あるプロジェクトで、データベースに保存した個人情報にあたるデータは暗号化しようという要件がありました。
アプリケーションはLaravelで実装しており、DBはpostgresqlです。
暗号化をするにあたっては特に以下がポイントとなります。それぞれ解説していきます

①スキーマの変更
②検索への対応
③鍵の管理
④実装方法

①スキーマの変更

暗号文は平文より長くなるため、カラムの型をTEXT型にするか、長さを拡張する必要があります。
基本的にTEXT型であればほぼ問題ないと言えるでしょう。
実際に暗号化して保存すると以下のような状態になります。

id(ユーザーID)name(名前)
1eyJpdiI6IkxUYXlq…di9jZ3JwclRDOWI0LzQ1Y3JZVTVvT…ZDMwODE2IiwidGFnIjoiIn0=

また、暗号化対象カラムにかかっていたユニーク制約やインデックスは外し、後述②のブラインドインデックスのカラムなどに付け直す必要があります。

②検索への対応

平文で保存されたカラムでは以下のようなSQLでそのまま完全一致・部分一致検索が可能です。

SELECT * FROM users WHERE name = 'sunnygem';
SELECT * FROM users WHERE email LIKE '%sunnygem%';

Laravelでは暗号化されたカラムを取得する際、複合化まで実行された後フロントに表示しています。
ところが検索においてはDBのカラムをそのままの値で参照することになるため、平文と同じようクエリでは検索ができません。
つまり①のデータで例えると以下のようにしないと作動しないわけです。これは実質不可能です。

SELECT * from users WHERE name = 'eyJpdiI6IkxUYXlq...di9jZ3JwclRDOWI0LzQ1Y3JZVTVvT...ZDMwODE2IiwidGFnIjoiIn0=';

A:完全一致

完全一致で検索したいカラムは、ブラインドインデックスを別カラムに持たせます。これは平文を正規化してからHMAC(鍵付きハッシュ)をとった値で、平文そのものを保存せずに一致判定ができます。

idname(名前)name_blind_index(hash化した名前を保存するカラム)
1eyJpdiI6IkxU…widGFnIjoiIn0=$2y$12$XR3OyyjTAHPp86GR…cdOnzEyvRO1RdmBNBDu1Gu37lq

これで例えばログイン時に以下のような照会を行うことができるようになりました。

// 氏名で検索(完全一致)
public function getDataByName(string $name)
{
    // hash値の計算は保存時と同じ計算である必要があります
    $hashValue = hash_hmac('sha256', $name, config('app.bidx_key'));
    return User::where('name_blind_index', $hashValue)->get(['id', 'name']);
}

ただしこちらはLaravelの機能ではなく、自前で保存時にハッシュ化してから保存する処理を入れる必要があります。
以下のコードはModel内でbooted()を使用し、名前の保存時に連動してブラインドインデックスカラムの登録を行っています。

protected static function booted()
{
    // savingイベント: データを保存する前に実行される
    static::saving(function (CustomerAdminUser $user) {
        $name = $user->name;
        $user->name_blind_index = ($name === null || $name === '') ? null : hash_hmac('sha256', $name, config('app.bidx_key'));
    });
}

B:部分一致

部分一致はブラインドインデックスをもってしても不可能です。
データの取得後であれば複合化されているので、全件取得→絞り込みというフローであれば可能です。
ただしこれは件数にもよります。今回の実装要件では多くてもデータ数が1000個ほどであったため、この方法でOKでした。
数千件数万件のデータを扱うプロジェクトになるとこの方法では難しいと言えるでしょう。
そのような要件において部分一致検索を求められる際には、暗号化を行うべきかどうかを見直す必要があります。

③鍵の管理

Laravelでは暗号化および複合化の際、鍵(デフォルトはAPP_KEY)を使用して実行しています。つまり鍵が漏れてしまうと攻撃者でもDBの値の複合化が可能です。そのため以下のような管理が望ましいと言えます。

・鍵はDBと別の場所に置く(環境変数、AWS KMS、GCP KMSなど)
・鍵を定期的に入れ替えたり、漏洩時に交換したりする手順(ローテーション)を最初に決めておく
・可能であれば、暗号化用の鍵をマスター鍵で更に暗号化する方式(エンベロープ暗号化)にする

④実装方法

以下の2つの方法があげられます。実際のプロジェクトではBを採用しました。
内部的には双方で「シリアライズ無しのencrypt()」を使用しており、暗号化の強度としては同等と言えます。

A:Cryptファサードを使用する

https://readouble.com/laravel/11.x/ja/encryption.html

上記Readoubleにも記載されている方法です。ただしこちらは明示的にファサードを呼び出して暗号化・複合化を行う方法となります。

use Illuminate\Support\Facades\Crypt;

// 保存時に暗号化
public function store(Request $request): RedirectResponse
{
    $request->user()->fill([
        'token' => Crypt::encryptString($request->token),
    ])->save();

    return redirect('/secrets');
}

// 取得時に複合化
$decrypted = Crypt::decryptString($encryptedValue);

B:Modelのcast機能を使用する

Modelの$castsは暗号化・複合化をLaravel側で解決してくれる仕組みです。
暗号化させるには指定のカラムの$castsに「encrypted」を指定します。

protected $casts = [
    'name' => 'encrypted', // encryptedを指定して自動的に暗号化で保存
    'password' => 'hashed',
];

public function saveById(int $userId, string $name): int
{
    $user = User::where('id', $userId)->first();
    if ($user === null)
    {
        return 0;
    }

    $user->name = $name;
    $saved = $user->save();

    return ($saved === true) ? 1 : 0;
}

ただし$castsはモデルのインスタンスを経由して属性をセット/取得した場合のみ作動します。

・save()
・Model::create()
・$model->update([…]) (一度$modelインスタンスを生成している)

よって以下のようなケースでは作動しません。

・DB::table()
・Model::where(…)->update([…])(クエリビルダ直行)

保存方法がcastに対応する書き方になっているかは確認してみてください。

各処理の機能早見表

処理方法Crypt::encrypt()Crypt::encryptString()encrypted cast
内部処理encrypt($v, true)encrypt($v, false)encrypt($v, false)
シリアライズありなしなし
暗号方式・形式AES-256-CBC、iv/value/mac の JSON を base64 化(3者共通)同左同左
暗号強度同じ同じ同じ
復号時の処理復号後に unserialize()復号のみ復号のみ(encrypted:array 等は JSON デコードも)
扱える値任意の型(配列・オブジェクト含む)文字列のみ文字列(:array / :json / :collection / :object で配列等も可)
APP_KEY 漏洩時のリスクunserialize() 経由で RCE の可能性あり任意文字列を復号させられる程度同左
cast との相互互換なし(読むとシリアライズ文字列が返る)あり―
実行タイミング明示的に呼んだとき明示的に呼んだとき代入時に暗号化、アクセス時に復号(自動)
where()->update()使える使える効かない
Encrypter の差し替え不可(Facade 固定)不可(Facade 固定)Model::encryptUsing() で可(9.x 以降)
推奨度低(使う理由がほぼない)高高

まとめ

①スキーマの変更
カラムを暗号化の文字数に耐えうる型にする。
ユニーク制約やインデックスはブラインドインデックス用のカラムに設定する。

②検索への対応
完全一致検索はブラインドインデックス用のカラムを作成して照合する。
部分一致検索を行うには一度全件取得する必要があるので、プロジェクトの性質によって可不可がある。

③鍵の管理
アプリケーションとは分離して管理することが適切。
鍵のローテーションも検討する。

④実装方法
Cryptファサード、またはModelのcast機能を使用して実装する。


おわりに

ここまで読んでいただき、ありがとうございました。
私たちは、できることを誠実にやる会社です。

Web・システム・AIのことで気になることがあれば、
「何から相談すればいいか分からない」段階でも大丈夫です。
お気軽にご相談ください。

▶ お問い合わせ

本記事は、Claudeでのやり取りから本記事を作成し、加筆・修正しております。

  • #セキュリティ
  • #Laravel
  • #PHP
  • #Eloquent
  • #暗号化

サニージェム公式note

アーカイブ記事

すべて

contact us お問い合わせ

Contact お問い合わせ・ご相談

「何から相談すればいいかわからない」
そんな段階でも、お気軽にご連絡ください。

Recruit 求人へのご応募

サニージェムの考え方に共感してくれる方と、
ぜひ一緒に働きたいと思っています。