[Rails5]カラムを暗号化しつつ、検索したい

DBに保存する値を暗号化しつつ
検索(完全一致)もする実装をしたので、その時のメモです。

### 動作環境
rails 5.2.2.1

### 概要
Humanモデルのencrypted_nameカラムを暗号化します。
AttributeEncryptモジュール作成、Modelでincludeすることで
暗号化したいカラムを定義できます。

```ruby:migrateion_file
class CreateHumen < ActiveRecord::Migration[5.2]
  def change
    create_table :humen do |t|
      t.string :encrypted_name
    end
  end
end

```

```ruby:app/model/human.rb
class Human < ApplicationRecord
  include AttributeEncrypt
  encrypt_attribute_accessor :encrypted_name
end
```

encrypt_attribute_accessorで指定したカラムのセッター、ゲッター
検索用のメソッドを定義してます。

```ruby:app/model/concerns/attribute_encrypt.rb
module AttributeEncrypt
  extend ActiveSupport::Concern
  class_methods do
    IV = ENV['ENCRYPTION_IV']
    SECURE = ENV['ENCRYPTION_KEY']
    def encrypt_attribute_accessor(*attr_names)
      attr_names.each do |attr_name|
        define_method(attr_name.to_s) do
          value = self[attr_name]
          value.present? ? self.class.decrypt(value) : value
        end

        define_method("#{attr_name}=") do |val|
          self[attr_name] = self.class.encrypt(val) if val.present?
        end

        singleton_class.send(:define_method, "encrypt_find_by_#{attr_name.to_s}",lambda { |val|
          val.present? ? where("#{attr_name} = '#{self.encrypt(val)}'") : self.all
        })
      end
    end

    # 暗号化
    def encrypt(password)
      crypt = StaticIVEncryptor.new(IV, SECURE, cipher: 'aes-256-cbc')
      crypt.encrypt_and_sign(password)
    end

    # 復号化
    def decrypt(password)
      crypt = StaticIVEncryptor.new(IV, SECURE, cipher: 'aes-256-cbc')
      crypt.decrypt_and_verify(password)
    end
  end

  class StaticIVEncryptor < ::ActiveSupport::MessageEncryptor
    def initialize(iv, secret, *signature_key_or_options)
      @iv = iv
      super(secret, *signature_key_or_options)
    end

    private
      # override
      # https://github.com/rails/rails/blob/32cfb82922bfc905b14d7d025287dac0b85b1668/activesupport/lib/active_support/message_encryptor.rb#L166
      def _encrypt(value, **metadata_options)
        cipher = new_cipher
        cipher.encrypt
        cipher.key = @secret

        # Rely on OpenSSL for the initialization vector
        cipher.iv = @iv
        iv = @iv
        cipher.auth_data = "" if aead_mode?

        encrypted_data = cipher.update(::ActiveSupport::Messages::Metadata.wrap(@serializer.dump(value), **metadata_options))
        encrypted_data << cipher.final

        blob = "#{::Base64.strict_encode64 encrypted_data}--#{::Base64.strict_encode64 iv}"
        blob = "#{blob}--#{::Base64.strict_encode64 cipher.auth_tag}" if aead_mode?
        blob
      end
  end
end

```

### 使い方

```ruby
# 保存
h = Human.find(1)
h.encrypted_name = 'なまえ' # 暗号化しつつ、保存されます
h.save
h[:encrypted_name] # dbに保存されている値
# => "U3VEY0ZTajRYeXRVNVljQU5lWklCa2J3cGRnc3NwVlFrUlpROU5SUTBndz0tLVFuUXlNbTk0YkdKc1JuUnFVM2RIYlE9PQ==--93f45e44659d00d09e9596f1a9c9dd962126644c"

# 表示
h.encrypted_name # 値は復号して取り出せます
# => "なまえ"

# 検索
# encrypt_find_by_カラム名 のメソッドが追加されるのでそれを使う
Human.encrypt_find_by_encrypted_name('なまえ')
# Human Load (0.5ms)  SELECT `humen`.* FROM `humen` WHERE (encrypted_name = 'U3VEY0ZTajRYeXRVNVljQU5lWklCa2J3cGRnc3NwVlFrUlpROU5SUTBndz0tLVFuUXlNbTk0YkdKc1JuUnFVM2RIYlE9PQ==--93f45e44659d00d09e9596f1a9c9dd962126644c')

# 注意
# whereとか使っても自動で反映されない
Human.where(encrypted_name: 'なまえ')
#  Human Load (1.1ms)  SELECT `humen`.* FROM `humen` WHERE `humen`.`encrypted_name` = 'なまえ'
# => []

# whereで使うのであれば下記のように
encrypted_val = Human.encrypt('なまえ')
Human.where(encrypted_name: encrypted_val)
```

参考
https://qiita.com/shgkt/items/1b5a05b0a8b1960889d1