DBに保存する値を暗号化しつつ
検索(完全一致)もする実装をしたので、その時のメモです。
動作環境
rails 5.2.2.1
概要
Humanモデルのencrypted_nameカラムを暗号化します。
AttributeEncryptモジュール作成、Modelでincludeすることで
暗号化したいカラムを定義できます。
migrateion_file
class CreateHumen < ActiveRecord::Migration[5.2]
def change
create_table :humen do |t|
t.string :encrypted_name
end
end
end
app/model/human.rb
class Human < ApplicationRecord
include AttributeEncrypt
encrypt_attribute_accessor :encrypted_name
end
encrypt_attribute_accessorで指定したカラムのセッター、ゲッター
検索用のメソッドを定義してます。
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
使い方
# 保存
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)