pgcrypto for Django
Django, postgresで運用しているサービスで特定のカラムについて暗号化する必要が出てきたので調べてやってみました。
postgresの拡張機能であるpgcrypto
で暗号化します。
https://www.postgresql.jp/document/10/html/pgcrypto.html
pgcryptoの有効化
後述の通りdjango-pgcrypto-fields
を使う場合はmigrationファイルがよしなにやってくれるので必要ないけど
CREATE EXTENSION pgcrypto;
/* 確認 */
SELECT * FROM pg_extentions;
もしlib/pgcrypto.so
がない場合は
yum install postgresql-contrib
とかで用意しておく。
django-pgcrypto-fieldsのインストール
Djangoとのやりとりはdjango-pgcrypto-fieldsを介して行うので インストールする。
pip install django-pgcrypto-fields
鍵の作成
pgcryptoには共通鍵暗号方式による暗号化と公開鍵暗号方式による暗号化があって
今回は公開鍵暗号方式を採用したの処理を怠るとので鍵を用意する必要がある。
鍵の作成方法はdocumentにも書いてある通りGnuPGを使用する。
以下のコマンドでインタラクティブに鍵が作成できる。
gpg --gen-key
推奨するキー種類は「DSAとElgamal」です。
― https://www.postgresql.jp/document/10/html/pgcrypto.html#id-1.11.7.35.7.19
らしい。
* ssh経由で鍵を作成する際、エントロピー不足で鍵の作成に時間がかかる(もしくは終わらない)場合があるけどその場合はこれで解決した
作成できたら確認しよう。
gpg --list-secret-keys
確認時に表示される鍵IDで指定して公開鍵、秘密鍵をエクスポートする。
gpg -a --export KEYID > public.key
gpg -a --export-secret-keys KEYID > private.key
settingsに記述
ここまでで下準備が完了!
次はsettigns.py
に必要な記述をしていく。
PUBLIC_PGP_KEY_PATH
, PRIVATE_PGP_KEY_PATH
はさっき作成した鍵を指定する。
django-pgcrypto-fields
の説明のまんまだけど
PUBLIC_PGP_KEY_PATH = os.path.abspath(os.path.join(BASEDIR, 'public.key'))
PRIVATE_PGP_KEY_PATH = os.path.abspath(os.path.join(BASEDIR, 'private.key'))
PUBLIC_PGP_KEY = open(PUBLIC_PGP_KEY_PATH).read()
PRIVATE_PGP_KEY = open(PRIVATE_PGP_KEY_PATH).read()
INSTALLED_APPS += ["pgcrypto"]
実際にコードに組み込む
こんな感じのコードがあったとして
class User(models.Model):
username = models.CharField(max_length=32)
first_name = models.CharField(max_length=32)
last_name = models.CharField(max_length=32)
email = models.EmailField()
以下のように書き換える。
class User(models.Model):
username = models.CharField(max_length=32)
first_name = CharPGPPublicKeyField(max_length=32)
last_name = CharPGPPublicKeyField(max_length=32)
email = EmailPGPPublicKeyField()
それぞれのfieldに対応するfieldはこちら的なチャートがあるのでそこを参考にする。
https://github.com/incuna/django-pgcrypto-fields#django-model-field-equivalents
であとはmigrationファイルを作ってmigrateするだけなんだけど
特にデータが入ってないなら問題ない!
が!すでにデータが入っているとmigrationの過程で
カラムの型がCharater
からbytea
に変換されてしまい、取り返しのつかないことになってしまう。
ので作成されたmigrationファイルを編集していく。
migrationの編集
migration.RunPython
で変換していくと思うけど、(上の例みたいに)まとめて3つのカラムの型変換と値のUPDATEとかやってるとOperationalError: cannot ALTER TABLE "mytable" because it has pending trigger events.
で怒られるのでそれぞれ別のmigrationファイルに分けるとか、atomic=False
とかする必要がある。(結構ハマった)
トランザクション中の DDL 使用をサポートしているデータベース(SQLite や PostgreSQL)においては、RunPython は各マイグレーションに対して作成されたトランザクション以外に自動的にトランザクションを保持しません。そのため、例えば PostgreSQL では、スキーマ変更と RunPython を同一のマイグレーション内で結合させて用いるのは避けるべきであり、そうしなければ OperationalError: cannot ALTER TABLE “mytable” because it has pending trigger events のようなエラーに遭遇する可能性が有ります。
― https://docs.djangoproject.com/ja/3.0/ref/migration-operations/#django.db.migrations.operations.RunPython
あとスキーマ変更で気をつける点としては対象カラム内のデータに\
が含まれてるとError: invalid input syntax for type bytea
がでるので
エスケープ処理s/\/\\/
しておく。
流れ的には
- エスケープ
- 変換対象のキャッシュ
- スキーマ変更
- 2のキャッシュから更新
っていう感じで結構な力技になってしまった。
以上です。
あとはあんまり暗号化とか意識しなくて普通に使用するだけでよいので楽ちんでした。
ありがたい。