Ruby+ActiveRecordでDBの暗号化を行うGemとしてattr_encryptedがありますが、暗号化する関係でFinderがちょっとめんどくさくなったりしたので適当にラッパを書きました、という話です。
( :per_attribute_iv_and_salt
モードでは使えません)
現状
uuid
, email
というフィールドを暗号化しておきたいとすると、次のようなメソッドがGemにより自動的に使えるようになります。
user = User.find_by_uuid(plain_uuid)
しかし残念ながらwhere(uuid: plain_uuid)
のような呼び方ができないのはちょっと不便です。
group.users.where(uuid: array_of_plain_uuids) # => マッチしない
また、メソッドチェーンの最後にfind_by_uuid()
のような呼び出しを置くこともできません。find_by_xxx()
はattr_encryptedされたクラスに実装されているためです。
group.users.find_by_email(plain_email) # => NoMethodError
対策
というわけで、次のようなモンキーパッチを当てると(自分の環境では)気持ちよくコードがかけるようになります。
module ActiveRecord
module QueryMethods
def where_encrypted(opts = :chain, *rest)
return where(opts, *rest) unless opts.is_a?(Hash)
klass = self.klass
encrypted_attrs = klass.try(:encrypted_attributes)
return where(opts, *rest) unless encrypted_attrs
# TODO: error handling?
encrypted_arg =
opts.map { |k, v|
if (enc_info = encrypted_attrs[k])
# key: wrap with prefix / suffix
enc_k = "#{enc_info[:prefix]}#{k}#{enc_info[:suffix]}"
# value:
# Array: each of its content must be encrypted
# Other: encrypted its value
enc_v =
if v.is_a? Array
v.map { |each_v| klass.encrypt(k, each_v) }
else
klass.encrypt(k, v)
end
[enc_k, enc_v]
else
[k, v]
end
}.to_h
where(encrypted_arg)
end
def find_encrypted_by(attrs)
where_encrypted(attrs).take
end
def find_encrypted_by!(attrs)
where_encrypted(attrs).take!
end
end
end
結果
これによって、次のような呼び出し方ができるようになります。
users = group.users.where_encrypted(uuid: array_of_plain_uuids)
user = group.users.find_encrypted_by!(email: plain_email)
ただし、attr_encryptedで暗号化されているフィールドとされていないフィールドを一つのwhere_encrypted
に含めることはできません。その場合は
hoge.where(not_enctypted_field: value).where_encrypted(enctypted_field: val)
のようにチェーンしてください。ORMがよしなにしてくれるので効率も問題ないはずです。
注意
…で、opts = :chain
って何ですかね…?こういう変更かけても大丈夫なの?と不安ですが、とりあえず手元ではちゃんと動いてくれているようです。このコードだとこういう場合にうまく動かないよ、というようなご指摘があれば是非よろしくお願いします。