4
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

attr_encryptedでもwhereとかfind_byしたい

Posted at

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

対策

というわけで、次のようなモンキーパッチを当てると(自分の環境では)気持ちよくコードがかけるようになります。

attr_encrypted_patch.rb
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って何ですかね…?こういう変更かけても大丈夫なの?と不安ですが、とりあえず手元ではちゃんと動いてくれているようです。このコードだとこういう場合にうまく動かないよ、というようなご指摘があれば是非よろしくお願いします。

4
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?