ソース

# frozen_string_literal: true

# NOTE: 可逆式で暗号化して保存する
# 対象をcodeとしたい場合
# * テーブルに encrypted_code を定義する
# * テーブルに digested_code を定義する(indexに使用する)
# * モデルに attr_encrypted :code を定義する
# model_instance.code = xxx として使用する
module Encryptable
  extend ActiveSupport::Concern

  ENCRYPTE_PURPOSE = "XXXX"
  ENCRYPTION_SALT = if Rails.env.staging? || Rails.env.production?
                      ["SECRET_KEY_BASE"][1..32]
                    else
                      Rails.application.credentials.config[:secret_key_base][1..32]
                    end

  # NOTE: 下記が定義される
  # code, code=, digest_code
  included do
    def self.attr_encrypted(attribute) # rubocop:disable Metrics/AbcSize
      encrypt_column_name = "encrypted_#{attribute}"
      digest_column_name = "digested_#{attribute}"
      crypt = ActiveSupport::MessageEncryptor.new(ENCRYPTION_SALT)

      # NOTE: 非暗号の値のゲッター
      define_method(attribute.to_s) do
        encrypted_data = public_send(encrypt_column_name.to_s)
        return nil if encrypted_data.blank?

        decrypted_data = instance_variable_get("@#{attribute}")
        return decrypted_data if decrypted_data.present?

        decrypted_data = crypt.decrypt_and_verify(encrypted_data, purpose: ENCRYPTE_PURPOSE)
        instance_variable_set("@#{attribute}", decrypted_data)
      end

      # NOTE: 非暗号の値のセッター
      define_method("#{attribute}=") do |val|
        encrypted_data = public_send(encrypt_column_name.to_s)
        # NOTE: 値に差分がない場合なにもしない(同じ値でも暗号値が変わるため)
        return if encrypted_data.present? && val == crypt.decrypt_and_verify(encrypted_data, purpose: ENCRYPTE_PURPOSE)

        instance_variable_set("@#{attribute}", val)

        encrypted_data = crypt.encrypt_and_sign(val, purpose: ENCRYPTE_PURPOSE)
        public_send("#{encrypt_column_name}=", encrypted_data)
        public_send("#{digest_column_name}=", Encryptable.generate_digest(val || ""))
      end

      # NOTE: 生のデータから検索を行う条件を設定
      scope "digest_#{attribute}", ->(val) { where(sanitize_sql(digest_column_name) => Encryptable.generate_digest(val)) }
    end
  end

  def self.generate_digest(val)
    OpenSSL::Digest::SHA256.hexdigest(ENCRYPTION_SALT + val)
  end
end

参考

  • attr-encrypted/attr_encrypted: Generates attr_accessors that encrypt and decrypt attributes iv(initialize vector)の実装の必要性に気付き素直にgemを使用することにした。 (ちなみに、桁数が大きくなるほど、暗号化した後の桁数の幅は、自前で実装した方が少ない)

  • [可逆式、不可逆式で暗号化したい ベジタブルプログラム](https://www.blog.v41.me/posts/ab5008a3-27a0-4053-8632-8b6ff84fd0fe)