セキュリティは開発者の責任 – データベースに原文をそのまま保存してはいけません。

1. なぜ暗号化が必要なのか?



  • API Key / Secret / OAuthトークン のような値は、 一度漏れたら サービス全体が突破されたようなもの です。
  • settings.py にハードコーディングすると、 → Gitリポジトリ、デプロイアーティファクト、開発者のノートパソコンなどで そのまま露出 します。
  • 環境変数に入れたとしても、 → その値をそのまま DB に保存すると DB が漏れた瞬間に平文が露出 します。

もう一つ重要なポイント:

  • パスワード
  • サーバーは「原文」を知る必要がない → ハッシュ(BCrypt, Argon2) を使用
  • API Key / Secret / トークン
  • サーバーが原文を再利用する必要がある → 復号可能な暗号化 が必要

まとめ

  • 再利用が必要な機密データ暗号化して保存
  • 再利用が不要なデータ(パスワード等)ハッシュして保存

2. Fernet とは?

cryptography.fernet.Fernet は、以下を 一度に提供 する高レベル API です。

  • 内部で安全なアルゴリズム(AES + HMAC 等)を使用
  • ランダム IVタイムスタンプ整合性検証(HMAC) を自動で処理
  • 開発者は 「キーを一つしっかり保管」 し、
  • encrypt(平文) → トークン文字列
  • decrypt(トークン) → 平文 を呼び出すだけ

つまり、AES-CBC/IV/パディング/HMAC を直接扱う代わりに、

from cryptography.fernet import Fernet

key = Fernet.generate_key()      # キー生成
f = Fernet(key)

token = f.encrypt(b"hello")      # 暗号化
plain = f.decrypt(token)         # 復号化

この程度を理解していれば十分に使えます。

取り組むべきことは二つだけ

  1. キー管理(環境変数、Secret Manager 等)
  2. Django で 書き/読み時に自動で encrypt/decrypt する仕組みを作る

3. Fernet キー生成と Django 設定



3-1. Fernet キー生成

# Fernet キー 1 つ生成(Base64 文字列で出力)
python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"

出力例:

twwhe-3rGfW92V5rvvW3o4Jhr0rVg7x8lQM6zCj6lTY=

この文字列をサーバーの環境変数に入れます。

export DJANGO_FERNET_KEY="twwhe-3rGfW92V5rvvW3o4Jhr0rVg7x8lQM6zCj6lTY="

3-2. settings.py に Fernet インスタンスを準備

# settings.py
import os
from cryptography.fernet import Fernet

DJANGO_FERNET_KEY = os.environ["DJANGO_FERNET_KEY"]  # 文字列
FERNET = Fernet(DJANGO_FERNET_KEY.encode("utf-8"))

これで Django のどこからでも from django.conf import settings 後に settings.FERNET.encrypt(...), settings.FERNET.decrypt(...) を使えます。


4. Django モデルに Fernet 暗号化フィールドを追加

複雑なブロック暗号実装の代わりに、非常にシンプルなカスタムフィールド だけで十分です。

4-1. 設計ポイント

  • 実際の DB には 暗号化されたトークン文字列 だけを保存
  • モデル/ビュー/サービスコードでは常に 復号された平文 だけを扱う
  • _secret + secret パターンで DB カラムと実際の意味 を分離

4-2. EncryptedTextField 実装

# myapp/utils/encrypted_field.py
from django.db import models
from django.conf import settings


class EncryptedTextField(models.TextField):
    """
    Fernet ベースの暗号化 TextField
    - 保存時: 平文(str) → Fernet トークン(str) に暗号化
    - 取得時: Fernet トークン(str) → 平文(str) に復号化
    """

    description = "Fernet encrypted text field"

    def get_prep_value(self, value):
        """
        Python オブジェクト → DB に保存される値
        """
        if value is None:
            return None

        if isinstance(value, str):
            if value == "":
                return ""

            token = settings.FERNET.encrypt(value.encode("utf-8"))
            return token.decode("utf-8")

        return value

    def from_db_value(self, value, expression, connection):
        """
        DB 値 → Python オブジェクト
        """
        if value is None:
            return None

        try:
            token = value.encode("utf-8")
            decrypted = settings.FERNET.decrypt(token)
            return decrypted.decode("utf-8")
        except Exception:
            return value

    def to_python(self, value):
        """
        ORM が Python オブジェクトとして扱う値
        - from_db_value の後にもう一度呼ばれることがある
        - ここでは特別な処理はせずそのまま返す
        """
        return value

実務のヒント

  • 既に平文データが DB に入っている場合、 from_db_value で復号失敗時に平文として返すようにして、 読み取り → 再保存で暗号化された値に変換できるようにします。

5. モデルに適用する

# myapp/models.py
from django.db import models
from .utils.encrypted_field import EncryptedTextField


class MyModel(models.Model):
    name = models.CharField(max_length=100)
    _secret = EncryptedTextField()   # 実際の DB カラム(Fernet トークン)

    @property
    def secret(self):
        """
        外部で使用する際は常に平文でアクセス
        """
        return self._secret

    @secret.setter
    def secret(self, value: str):
        """
        平文を割り当てると保存時に自動で暗号化される
        """
        self._secret = value
  • _secret
  • 実際の DB カラム名
  • DB には Fernet トークン(暗号化文字列)が保存される
  • secret
  • コードで使用する「論理的」フィールド
  • 常に 平文 だけを扱う

このパターンにより、後でモデルを見る人も

obj.secret  # 平文
obj._secret # 暗号化された値が入っているカラム

関係を明確に理解できます。


6. 使用例

6-1. Django shell

>>> from myapp.models import MyModel

# 新しいオブジェクトを作成
>>> obj = MyModel(name="Test")
>>> obj.secret = "my-very-secret-key"   # 平文で割り当て
>>> obj.save()

# DB に保存された実際のカラムは暗号化されたトークン
>>> MyModel.objects.get(id=obj.id)._secret
'gAAAAABm...4xO8Uu1f0L3K0Ty6fX5y6Zt3_k6D7eE-'  # 例

# モデルで使用する際は平文
>>> obj.secret
'my-very-secret-key'

6-2. API で使用

実際には クライアントに secret をそのまま返すより、 バックエンド内部でのみ使用するケースが多いです。例としては以下のようにします。

# myapp/views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from .models import MyModel


class SecretView(APIView):
    def get(self, request, pk):
        obj = MyModel.objects.get(pk=pk)
        # obj.secret は平文(既に復号済み)
        return Response({"secret": obj.secret})

実務では通常、

  • obj.secret外部 API 呼び出しに使用 するだけで
  • クライアントには 絶対にそのまま露出しない パターンが多いです。

7. Fernet を使う際に知っておくべきこと

7-1. 検索/フィルタリングの制約

Fernet は毎回暗号化結果が 変わる 構造です。

obj1.secret = "abc"
obj2.secret = "abc"

両方とも "abc" ですが、DB に保存された _secret完全に別のトークン です。

そのため、次のようなクエリは動作しません。

MyModel.objects.filter(_secret="abc")       # 意味なし
MyModel.objects.filter(_secret=obj._secret) # トークンそのまま比較は可能だが、平文ベースの検索は不可

つまり、平文ベースの検索/ソート が必要な値なら:

  • 別の 検索用カラム(ハッシュ、プレフィックス等) を用意する
  • もしくは 暗号化対象の設計を再検討 する

7-2. キー変更(ローテーション)戦略

一つのキーをずっと使い続けるより、定期的に変更するのが理想です。

  • シンプルなスタート:
  • DJANGO_FERNET_KEY だけで始める
  • 後で高度化:
  • OLD_KEY, NEW_KEY の二つを持ち、
    • 暗号化は NEW_KEY でのみ
    • 復号は OLD/NEW 両方を試す
  • データが NEW_KEY で再暗号化されると、OLD_KEY を削除

この部分は記事の分量が足りないので、概念だけ触れました。🙂


8. セキュリティチェックリスト(Fernet バージョン)

項目 チェック
キー管理 DJANGO_FERNET_KEY は Git に載せず、AWS Secrets Manager / Parameter Store / Vault 等からロード
権限分離 本番サーバーでのみキーを注入し、開発/ローカル環境は別キーを使用
暗号化対象 本当に必要な値(API Key, Secret, トークン等)のみ暗号化し、残りは通常カラム
ログ 平文/トークン/キーをログに出力しない(特に DEBUG ログは注意)
バックアップ DB バックアップを取得しても、キーが無ければ意味のないデータ になるように
テスト 単体テストで encrypt -> decrypt が一致、マイグレーション(既存平文)処理まで検証

9. まとめ

  • 低レベルの AES 実装は 説明が難しく、ミスが多い
  • 代わりに Fernet を使えば:
  • 安全なアルゴリズムの組み合わせ
  • ランダム IV、タイムスタンプ、整合性検証
  • シンプルな API を一度に取得
  • Django では
  • EncryptedTextField などのカスタムフィールド +
  • _secret / secret プロパティパターンを使えば 開発者は 平文のみを扱うコード を書きつつ、 DB には 常に暗号化された値 を保存できる。

結論

  • 機密データは常に暗号化して保存
  • 複雑な暗号アルゴリズムはライブラリに任せる」 だけを守れば、Django サービスのセキュリティレベルは大幅に向上します。 🔐

image