Rails7のActive Recordを利用したデータの暗号化について

# 基本的な導入例
給与などを決定する評価テーブル(MySQL)を暗号化することを仮定します。

## テーブル設計
一旦暗号化のことを考えず以下のように作成したいテーブルの定義を考えます。
```sql
CREATE TABLE `staff_evaluations`
(
    `id`                        int(11) NOT NULL AUTO_INCREMENT COMMENT '評価ID',
    `evaluation_rank`           char(1) COMMENT '評価ランク(A,B,C,D)',
    `evaluation_reason`         varchar(5000) COMMENT '評価理由',
    `monthly_income_up_amount`  int(11) COMMENT '月収アップ額',
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
```
上記において暗号化したいカラムは`評価ランク`、`評価理由`、`月収アップ額`です。
暗号化には文字列型しか利用できないため全てvarcharにし、暗号化に際して付随されるデータ分のメモリを合わせて指定します。暗号化に際して付随されるデータは255バイト確保していればいいらしいので255文字、余分に指定します。
text型を指定しても問題ないです。
```sql
CREATE TABLE `staff_evaluations`
(
    `id`                        int(11) NOT NULL AUTO_INCREMENT COMMENT '評価ID',
    `evaluation_rank`           varchar(256) COMMENT '評価ランク(A,B,C,D) 本来は1文字で事足りるが暗号化しているため1+255',
    `evaluation_reason`         varchar(5255) COMMENT '評価理由 本来は5000文字だが暗号化しているため5000+255',
    `monthly_income_up_amount`  varchar(262) COMMENT '月収アップ額 本来は数字型だが、月収100万アップもすれば十分と考え7+255',
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
```

## credential設定
以下のコマンドを実行し、結果をコピーして、credential.yml.encに反映させます。
```bash
$ bin/rails db:encryption:init
Add this entry to the credentials of the target environment:

active_record_encryption:
  primary_key: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  deterministic_key: yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
  key_derivation_salt: zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz
```

## Modelを作成
```ruby
class StaffEvaluation < ApplicationRecord
  self.table_name = 'staff_evaluations'
  self.primary_key = 'id'

  encrypts :evaluation_rank
  encrypts :evaluation_reason
  encrypts :monthly_income_up_amount
end
```
これで`StaffEvaluation`モデルを利用した保存処理は暗号化、抽出処理は複合化されます。
# 暗号化する際の注意点
## 暗号化するカラムは文字列型でなければならない
暗号化すると以下のような文字列データを保存しようとします。
```
{\"p\":\"lw==\",\"h\":{\"iv\":\"UTTluJISotgrWzzl\",\"at\":\"bLfxNV/EVTsMlPzCX7TUTQ==\"}}
```
よって暗号化するカラムは文字列型にしなければいけません。

## 暗号化したカラムは保存したい文字数よりも255バイト多く確保する
> **公式から引用**
> 値のほかにBase64エンコーディングとメタデータも保存されるので、暗号化を利用する場合はカラムの容量を余分に必要とします。組み込みのエンベロープ暗号化キープロバイダが使われる場合、最悪で250バイトほど余分に必要になると見積もれます。これは中大規模のtextカラムでは無視できる量ですが、255バイトのstringカラムではこれに応じて上限を増やしておく必要があります（推奨は510バイト）。

上記より255バイトのStringカラムに対しては510バイトを設定することが推奨されています。
**255バイト**(510-255)多く確保すれば問題なさそうです。
ギリギリを攻めるのであれば250バイトでもいいかもしれませんが、`最悪で250バイトほど余分に必要`という記述の`ほど`という言葉が曖昧なので推奨値に倣って5バイト(255-250)多く取っておきます。

## 決定論的な暗号化と非決定論的な暗号化
Rails7のActive Recordではデフォルトで`非決定論的な暗号化`を行います。
これは同じ値でもその時々で毎回違う値に暗号化して保存を行うのでセキュリティが向上します。
`決定論的な暗号化`を行いたい場合は以下のように`deterministic: true`を設定します。
```ruby
class StaffEvaluation < ApplicationRecord
  self.table_name = 'staff_evaluations'
  self.primary_key = 'id'

  encrypts :evaluation_rank, deterministic: true # 決定論的な暗号化
  encrypts :evaluation_reason
  encrypts :monthly_income_up_amount
end
```
`決定論的な暗号化`を行うことで一意なデータを設定することができるようになります。
ちなみに非決定論的な暗号化から決定論的な暗号化に単純に変更するとデータを正常に読み込むことができなくなります。
逆も同様です。
詳しくは[公式ドキュメント](https://railsguides.jp/active_record_encryption.html)をご参照ください。
### 決定論的な暗号化と非決定論的な暗号化の比較
||決定論的な暗号化|非決定論的な暗号化
|:-----------|:------------:|:------------:|
|primary_keyの変更|複雑|容易|
|抽出|◯|×|
|インデックスの作成|◯|×|
|一意なデータ作成|◯|×|
|セキュリティ|△|◯|


### 非決定論的な暗号化の場合は暗号化したカラムに対して抽出が使えない理由
まずは以下をご覧ください。
```ruby
StaffEvaluation.where(evaluation_rank: 'B')
# SELECT `staff_evaluations`.* FROM `staff_evaluations`
#  WHERE `staff_evaluations`.`evaluation_rank` = '{\"p\":\"Ew==\",\"h\":{\"iv\":\"Wmp74MrF6nJQ5W1O\",\"at\":\"6Sy9rgokhy9TYkyx5yrKHQ==\"}}'
StaffEvaluation.where(evaluation_rank: 'B')
# SELECT `staff_evaluations`.* FROM `staff_evaluations`
#  WHERE `staff_evaluations`.`evaluation_rank` = '{\"p\":\"wQ==\",\"h\":{\"iv\":\"BOpADUPosSCG1Xxn\",\"at\":\"jtYF0P8eG4t3v8sXdrBbMw==\"}}'
```
2回`StaffEvaluation.where(evaluation_rank: 'B')`を実行した結果なのですが、1回目と2回目でwhere文が変更されます。
この結果から都度`'B'`が暗号化されてその値を利用して抽出を行ってると考えることができます。
よってwhere文を利用することはできません。

## nilはNULLで保存される
アクティブレコード上でnilが設定されているカラムはDB上で暗号化されずにNULLで保存されます。

# おまけ
## 暗号化のprimary_keyを変更(非決定論的な暗号化の場合のみ)
定期的に暗号化のキーを変更することでキー流出などのセキュリティ対策を行うことができます。
Rails7では非決定論的な暗号化を行なっている場合にキーローテンションの仕組みを利用することで簡単に実現できます。
### キーローテーションの設定方法
以下のようにcredentialにprimary_keyを足すだけで新しいキーを追加することができます。
```
active_record_encryption:
  primary_key:
    - xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # 以前のキーは引き続き既存コンテンツを復号する
    - xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx2 # 新しいコンテンツを暗号化するアクティブなキー
  daterministic_key: yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
  key_derivation_salt: zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz
```
このように足されたキーは次回から暗号化時に一番下の行に設定したプライマリーキーが利用されます。
複合時には設定されているキーを下から順に利用し、複合が成功するまで繰り返されます。
完全にプライマリーキーを以降したい場合は一度データを取り出して再度、新しいキーを利用して暗号化する必要があります。


# 参考
https://railsguides.jp/active_record_encryption.html