Rails
ActiveRecord

Railsで透過的にカラム暗号化

個人情報を扱うサービスを考えると、どうしても特定のカラムを暗号化する必要が出てくることがある。
いくつかのgemを試してみて、どうもしっくりこなかったので、gemを作ることにした。

既存gemの紹介と、新しい暗号化gemの比較をしてみよう 👀

attr_encrypted

ActiveRecord + 暗号化 で調べると真っ先に出てくるgemのひとつ
定義をする側はシンプルなのだが、使う時になかなかクセがある。

暗号化したいカラムにつき、2つのカラムが必要になるようだ。

class Model < ActiveRecord::Base
  attr_encrypted :field_name, key: ENV["KEY"]
end

# migrationは下記のような定義になる
create_table(:models) do |t|
  t.string(:encrypted_field_name)
  t.string(:encrypted_field_name_iv)
end

暗号化自体は、とてもシンプルに扱うことができる。
しかし、 #changes の値が分かりづらかったり、 field_was で過去の値を取得できないなどの問題がある。

特定のケースでおかしな挙動をするので、正直言うと実用に耐えない。

instance = Model.create(field: 'hello')
=> <Model id: 1, encrypted_field: "d9thb/Pvj6gMm2TPLHoola3aHsWy\n", encrypted_field_iv: "UfosEdeGJJmIBUHs\n">

instance.field   
=> "hello"

# changes や _was など
instance.field = "hello world"

instance.changes_to_save
=> {"encrypted_field"=>["d9thb/Pvj6gMm2TPLHoola3aHsWy\n", "RxJTGKPG3PMi1NA0LY/tSu9r16sMWfalAFRc\n"],
 "encrypted_field_iv"=>["UfosEdeGJJmIBUHs\n", "1lMAgBaLBrqQUPUU\n"],
 "field"=>[nil, nil]}

instance.field_changed? #=> true
instance.field_was      #=> nil

crypt_keeper

透過的暗号化を謳う暗号化gemだ。
serializeの機能を活用して、内部実装はかなりシンプルにできている。

class MyModel < ApplicationRecord
  crypt_keeper :field, encryptor: :active_support, key: ENV['KEY'], salt: ENV['SALT']
end

create_table(:models) do |t|
  t.binary(:field)
end

そして、透過的暗号化を謳っているだけあって挙動もシンプル!!
#changes などの挙動も正しく、問題なさそうである。

暗号形式の拡張も柔軟にできるため、通常使う分には十分実用に耐えうると感じた。

instance = Model.new(field: 'hello')
instance.save
instance         #=> <Model id: 1, field: "hello">

instance.field   #=> "hello"

instance.field = 'hello world'

instance.changes
# => {"field"=>["hello world", "hello new world"]}

active_record_encryption

今回作成した新しい暗号化gemである。
このgemの特徴は、Attribute APIを使っており、好きな型を扱えるようになった点である。
他のgemは、string型しか扱えない。

下記の通り、第二引数にて、好きな型を指定することができる。
また、 default:引数でデフォルト値を設定することも用意である!!

class Model < ApplicationRecord
  encrypted_attribute :field, :date 
  encrypted_attribute :field_2, Money.new
  encrypted_attribute :field_3, :string, default: 'default value'
end

実際に使ってみると分かるのだが、だいたい他の挙動については、crypt_keeperと同等に透過的である。
serialize なども使える分、 crypt_keeper よりも透過的かもしれない。

instance = Model.new(field: '2000/01/01', field_2: 100)
instance.field_1 #=> Sat, 01 Jan 2000
instance.field_2 #=> <Money value: 100>
instance.field_3 #=> 'default value'

まとめ

attr_encrypted はあまりにも使いにくすぎるので、いずれのケースからも選択肢からは外れそうだ。
さようなら :pray:

String型のみ暗号化する場合は crypt_keeperを使うだろう。
そして、様々な型のデータを暗号化したいケースの場合は、active_record_encryptionを使うといいだろう :smile: :thumbsup: