[Rails] Active Record Encryptionを試してみる
Active Record と暗号化 - Railsガイド
Active Record Encryption — Ruby on Rails Guides
アプリケーションレイヤで暗号化する仕組みとしてActiveRecordEncryptionがRails7.0から導入されたようなので試してみようかと思います。
今まで自前実装でやっていたので、楽になるのかな?と
環境
- Docker: 20.10.22
- Ruby: 3.1.2
- Rails: 7.0.4
- postgres: 15.1
まずは普通にモデルを作ってみる
class CreateUsers < ActiveRecord::Migration[7.0]
def change
create_table :users do |t|
t.string :name
t.string :address
t.timestamps
end
end
end
こんな感じで name
address
を持った User
モデルを追加してみました。
irb(main):001:0> User.create(name: "test-1", address: "東京都xxx")
TRANSACTION (0.1ms) BEGIN
User Create (0.7ms) INSERT INTO "users" ("name", "address", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id" [["name", "test-1"], ["address", "東京都xxx"], ["created_at", "2022-12-25 13:12:36.001606"], ["updated_at", "2022-12-25 13:12:36.001606"]]
TRANSACTION (2.4ms) COMMIT
=> #<User:0x00007fc12250fce0 id: 1, name: "test-1", address: "東京都xxx", created_at: Sun, 25 Dec 2022 13:12:36.001606000 UTC +00:00, updated_at: Sun, 25 Dec 2022 13:12:36.001606000 UTC +00:00>
irb(main):002:0> User.create(name: "test-2", address: "神奈川県xxx")
TRANSACTION (0.3ms) BEGIN
User Create (0.4ms) INSERT INTO "users" ("name", "address", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id" [["name", "test-2"], ["address", "神奈川県xxx"], ["created_at", "2022-12-25 13:12:49.463373"], ["updated_at", "2022-12-25 13:12:49.463373"]]
TRANSACTION (2.5ms) COMMIT
=> #<User:0x00007fc124550400 id: 2, name: "test-2", address: "神奈川県xxx", created_at: Sun, 25 Dec 2022 13:12:49.463373000 UTC +00:00, updated_at: Sun, 25 Dec 2022 13:12:49.463373000 UTC +00:00>
ActiveRecord Encryptionを設定する
このあたりを見ながら設定してみます
Rails credentialsを初期化する
> bin/rails db:encryption:init
Add this entry to the credentials of the target environment:
active_record_encryption:
primary_key: Ot8iSvgBTpaNsAkLFhMQsrf1EWDtmWHK
deterministic_key: 0jefyhajq3hnr96sldPoqu8ISFpI9zRZ
key_derivation_salt: q6o3RhCV1hoiGqWnQEgVKGwZQieFrgcF
なるほど、ここで生成したキーを自分でcredentialsに追加しないとイケナイっぽいですね?
見てみましょう
> EDITOR=emacs bin/rails credentials:edit
# aws:
# access_key_id: 123
# secret_access_key: 345
# Used as the base secret for all MessageVerifiers in Rails, including the one protecting cookies.
secret_key_base: 0e61dd7f9eb28fa38b111013b5def8d164211ae01b107b258a44fb8e813a3aca2e40bd2803f19f8061d280bda58e39e21e1df74e4469e40758e8aca9b918bf4e
入ってないですね
developmentにのみ追加してみましょう
> EDITOR=emacs bin/rails credentials:edit --environment development
active_record_encryption:
primary_key: Ot8iSvgBTpaNsAkLFhMQsrf1EWDtmWHK
deterministic_key: 0jefyhajq3hnr96sldPoqu8ISFpI9zRZ
key_derivation_salt: q6o3RhCV1hoiGqWnQEgVKGwZQieFrgcF
> bin/rails c
irb(main):003:0> Rails.application.credentials[:active_record_encryption]
=> {:primary_key=>"Ot8iSvgBTpaNsAkLFhMQsrf1EWDtmWHK", :deterministic_key=>"0jefyhajq3hnr96sldPoqu8ISFpI9zRZ", :key_derivation_salt=>"q6o3RhCV1hoiGqWnQEgVKGwZQieFrgcF"}
設定できました。
Modelの設定
User#address
を暗号化するようにします。
class User < ApplicationRecord
encrypts :address
end
rails consoleから既存データを読み出してみます
irb(main):006:0> u = User.first
User Load (0.4ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT $1 [["LIMIT", 1]]
(Object doesn't support #inspect)
=>
irb(main):007:0> u.name
=> "test-1"
irb(main):008:0> u.address
/usr/local/bundle/gems/activerecord-7.0.4/lib/active_record/encryption/encryptor.rb:58:in `rescue in decrypt': ActiveRecord::Encryption::Errors::Decryption (ActiveRecord::Encryption::Errors::Decryption)
/usr/local/bundle/gems/activerecord-7.0.4/lib/active_record/encryption/message_serializer.rb:26:in `rescue in load': ActiveRecord::Encryption::Errors::Encoding (ActiveRecord::Encryption::Errors::Encoding)
/usr/local/lib/ruby/3.1.0/json/common.rb:216:in `parse': 859: unexpected token at '\xE6\x9D\xB1\xE4\xBA\xAC\xE9\x83\xBDxxx' (JSON::ParserError)
復号化エラーになりました。
暗号化されていないデータも扱えるようにする
config.active_record.encryption.support_unencrypted_data = true
にすれば良いようなのでやってみます
require_relative "boot"
require "rails/all"
Bundler.require(*Rails.groups)
module ArEncExample
class Application < Rails::Application
config.load_defaults 7.0
config.api_only = true
config.active_record.encryption.support_unencrypted_data = true
end
end
irb(main):003:0> u = User.first
User Load (0.4ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT $1 [["LIMIT", 1]]
=> #<User:0x00007fc71a9b1278 id: 1, name: "test-1", address: "東京都xxx", created_at: Wed, 28 Dec 2022 11:38:50.746083000 UTC +00:00, updated_at: Wed, 28 Dec 2022 11:38:50.746083000 UTC +00:00>
irb(main):004:0> u.name
=> "test-1"
irb(main):005:0> u.address
=> "東京都xxx"
読み出せるようになりました
このオプションは、非暗号化データと暗号化済みデータの共存が避けられないので、あくまで過渡期の利用が目的です。
デフォルトでは上記2つのオプションはどちらもfalseに設定されます。そしてこの設定は、あらゆるアプリケーションで推奨される目標でもあります。
と書かれている通り、暗号化への移行時にのみ使うべきオプションですね。
この状態で新しいデータを追加してみます
irb(main):006:0> u = User.create(name: "test-2", address: "神奈川県xxx")
TRANSACTION (0.3ms) BEGIN
User Create (0.7ms) INSERT INTO "users" ("name", "address", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id" [["name", "test-2"], ["address", "{\"p\":\"szVyhlY63H/AlQ2q223d\",\"h\":{\"iv\":\"gM1jzas7EJ+1WelR\",\"at\":\"7mR17c1ZT0yAyQS4VskWTQ==\"}}"], ["created_at", "2022-12-28 11:54:11.311483"], ["updated_at", "2022-12-28 11:54:11.311483"]]
TRANSACTION (2385.7ms) COMMIT
=> #<User:0x00007fc71a098038 id: 2, name: "test-2", address: "神奈川県xxx", created_at: Wed, 28 Dec 2022 11:54:11.311483000 UTC +00:00, updated_at: Wed, 28 Dec 2022 11:54:11.311483000 UTC +00:00>
irb(main):007:0> u.name
=> "test-2"
irb(main):008:0> u.address
=> "神奈川県xxx"
irb(main):010:0> User.all.map(&:attributes)
User Load (0.5ms) SELECT "users".* FROM "users"
=>
[{"id"=>1, "name"=>"test-1", "address"=>"東京都xxx", "created_at"=>Wed, 28 Dec 2022 11:38:50.746083000 UTC +00:00, "updated_at"=>Wed, 28 Dec 2022 11:38:50.746083000 UTC +00:00},
{"id"=>2, "name"=>"test-2", "address"=>"神奈川県xxx", "created_at"=>Wed, 28 Dec 2022 11:54:11.311483000 UTC +00:00, "updated_at"=>Wed, 28 Dec 2022 11:54:11.311483000 UTC +00:00}]
生データを見てみましょう
ar_enc_example_development> select * from users;
+----+--------+--------------------------------------------------------------------------------------------+----------------------------+----------------------------+
| id | name | address | created_at | updated_at |
|----+--------+--------------------------------------------------------------------------------------------+----------------------------+----------------------------|
| 1 | test-1 | 東京都xxx | 2022-12-28 11:38:50.746083 | 2022-12-28 11:38:50.746083 |
| 2 | test-2 | {"p":"szVyhlY63H/AlQ2q223d","h":{"iv":"gM1jzas7EJ+1WelR","at":"7mR17c1ZT0yAyQS4VskWTQ=="}} | 2022-12-28 11:54:11.311483 | 2022-12-28 11:54:11.311483 |
+----+--------+--------------------------------------------------------------------------------------------+----------------------------+----------------------------+
SELECT 2
こんな感じで暗号化データと非暗号化データが共存しています
既存データを暗号化する
irb(main):015:0> User.all.map(&:encrypt)
User Load (0.5ms) SELECT "users".* FROM "users"
TRANSACTION (0.2ms) BEGIN
User Update (0.5ms) UPDATE "users" SET "address" = $1 WHERE "users"."id" = $2 [["address", "{\"p\":\"icXAQWW0Cf/tgniI\",\"h\":{\"iv\":\"nGGWAr3jiKU0ZdKM\",\"at\":\"l4TZw3MUGMm8X7WBrVnYnw==\"}}"], ["id", 1]]
TRANSACTION (2385.4ms) COMMIT
TRANSACTION (0.2ms) BEGIN
User Update (0.3ms) UPDATE "users" SET "address" = $1 WHERE "users"."id" = $2 [["address", "{\"p\":\"VBAsDhBntKVjwKZLMTgt\",\"h\":{\"iv\":\"JLIG3OBmybJIxqcj\",\"at\":\"danhHvkqdkmGmH4trnu+Qg==\"}}"], ["id", 2]]
TRANSACTION (1.0ms) COMMIT
=> [nil, nil]
ar_enc_example_development> select * from users;
+----+--------+--------------------------------------------------------------------------------------------+----------------------------+----------------------------+
| id | name | address | created_at | updated_at |
|----+--------+--------------------------------------------------------------------------------------------+----------------------------+----------------------------|
| 1 | test-1 | {"p":"icXAQWW0Cf/tgniI","h":{"iv":"nGGWAr3jiKU0ZdKM","at":"l4TZw3MUGMm8X7WBrVnYnw=="}} | 2022-12-28 11:38:50.746083 | 2022-12-28 11:38:50.746083 |
| 2 | test-2 | {"p":"VBAsDhBntKVjwKZLMTgt","h":{"iv":"JLIG3OBmybJIxqcj","at":"danhHvkqdkmGmH4trnu+Qg=="}} | 2022-12-28 11:54:11.311483 | 2022-12-28 11:54:11.311483 |
+----+--------+--------------------------------------------------------------------------------------------+----------------------------+----------------------------+
SELECT 2
id:1
も暗号化されました。
irb(main):016:0> User.pluck(:address)
User Pluck (0.5ms) SELECT "users"."address" FROM "users"
=> ["東京都xxx", "神奈川県xxx"]
復号化もできてますね
暗号化したカラムをクエリしたい
決定論的暗号化と非決定論的暗号化
ActiveRecord暗号化では、デフォルトで非決定論的な(non-deterministic)暗号化を用います。
ここで言う非決定論的とは、同じコンテンツを同じパスワードで暗号化しても、暗号化のたびに異なる暗号文が生成されるという意味です。
非決定論的な暗号化手法によって、暗号解析の難易度を高めてデータベースへのクエリを不可能にすることで、セキュリティを向上させます。deterministic:オプションを指定することで、初期化ベクトルを決定論的な手法で生成できるようになり、暗号化データへのクエリを効率よく行えるようになります。
なるほど。
- 決定論的暗号化
- 毎回同じ値になる
- クエリ可能
- 非決定論的暗号化
- 毎回違う値になる
- クエリ不可
ということですかね。
デフォルトでは非決定論的暗号化になっている、と。
試してみます。
irb(main):021:0> ActiveRecord::Encryption.without_encryption { puts User.find(1).address }
User Load (0.4ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
{"p":"BvQLA2z+h1EXQ/qt","h":{"iv":"uKaRSBsOtbTtnyPH","at":"vOl94wHv1th3Qs8mAzUTsg=="}}
=> nil
irb(main):022:0> User.find(1).encrypt
User Load (0.5ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
TRANSACTION (0.2ms) BEGIN
User Update (0.4ms) UPDATE "users" SET "address" = $1 WHERE "users"."id" = $2 [["address", "{\"p\":\"CyRSVrp2PXlOkkJU\",\"h\":{\"iv\":\"v3CW3ypq6hhiuSaD\",\"at\":\"8HQi285sq9QCxnBQYF/k6g==\"}}"], ["id", 1]]
TRANSACTION (2.8ms) COMMIT
=> nil
irb(main):023:0> ActiveRecord::Encryption.without_encryption { puts User.find(1).address }
User Load (0.4ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
{"p":"CyRSVrp2PXlOkkJU","h":{"iv":"v3CW3ypq6hhiuSaD","at":"8HQi285sq9QCxnBQYF/k6g=="}}
=> nil
たしかに、User#encrypt
を呼ぶと違う結果になってますね。
決定論的暗号化に変更してみる
現状はデフォルト設定なので非決定論的暗号化になっています。
非決定論的暗号化で暗号化されたデータがある状態で決定論的暗号化に変更してみます。
class User < ApplicationRecord
encrypts :address, deterministic: true
end
※グローバルな設定で変更する方法は無いっぽい?(見つからなかった)
irb(main):004:0> User.find(1).address
User Load (0.5ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
=> "{\"p\":\"OsyJDjD/ul3qjzkT\",\"h\":{\"iv\":\"L5D8NnSvJk2Ehkyq\",\"at\":\"Lj2VJeOJJW9o8pxtAglSRA==\"}}"
復号化できないっぽいですね?
config.active_record.encryption.support_unencrypted_data = false
にすると
irb(main):001:0> User.find(1).address
User Load (0.4ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
/usr/local/bundle/gems/activerecord-7.0.4/lib/active_record/encryption/encryptor.rb:58:in `rescue in decrypt': ActiveRecord::Encryption::Errors::Decryption (ActiveRecord::Encryption::Errors::Decryption)
/usr/local/bundle/gems/activerecord-7.0.4/lib/active_record/encryption/cipher/aes256_gcm.rb:80:in `rescue in decrypt': ActiveRecord::Encryption::Errors::Decryption (ActiveRecord::Encryption::Errors::Decryption)
/usr/local/bundle/gems/activerecord-7.0.4/lib/active_record/encryption/cipher/aes256_gcm.rb:76:in `final': OpenSSL::Cipher::CipherError
エラーになりました。
これを読むと変更前の設定をpreviousとして設定できるっぽいですね。
やってみます。
class User < ApplicationRecord
encrypts :address, deterministic: true, previous: { deterministic: false }
end
irb(main):003:0> User.find(1).address
User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
=> "東京都xxx"
irb(main):004:0> ActiveRecord::Encryption.without_encryption { puts User.find(1).address }
User Load (0.4ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
{"p":"OsyJDjD/ul3qjzkT","h":{"iv":"L5D8NnSvJk2Ehkyq","at":"Lj2VJeOJJW9o8pxtAglSRA=="}}
=> nil
復号化できるようになりました。
もう一度暗号化してみます。
irb(main):012:0> User.find(1).encrypt; ActiveRecord::Encryption.without_encryption { puts User.find(1).address }
User Load (0.4ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
TRANSACTION (0.2ms) BEGIN
User Update (0.4ms) UPDATE "users" SET "address" = $1 WHERE "users"."id" = $2 [["address", "{\"p\":\"tzBwWlQfQh47o/ut\",\"h\":{\"iv\":\"4zExRkIqKxJ139uf\",\"at\":\"eTHXcQThFotUeIqA8aeTMQ==\"}}"], ["id", 1]]
TRANSACTION (2.5ms) COMMIT
User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
{"p":"tzBwWlQfQh47o/ut","h":{"iv":"4zExRkIqKxJ139uf","at":"eTHXcQThFotUeIqA8aeTMQ=="}}
=> nil
irb(main):013:0> User.find(1).encrypt; ActiveRecord::Encryption.without_encryption { puts User.find(1).address }
User Load (0.4ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
TRANSACTION (0.2ms) BEGIN
User Update (0.4ms) UPDATE "users" SET "address" = $1 WHERE "users"."id" = $2 [["address", "{\"p\":\"z4kSdPawN8SxiPRN\",\"h\":{\"iv\":\"MMyvg4HqUKc6N7nb\",\"at\":\"b1dY44p+HXsOdbZdX7iMVg==\"}}"], ["id", 1]]
TRANSACTION (2.5ms) COMMIT
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
{"p":"z4kSdPawN8SxiPRN","h":{"iv":"MMyvg4HqUKc6N7nb","at":"b1dY44p+HXsOdbZdX7iMVg=="}}
=> nil
毎回結果が変わってますね?
deterministic: true
にしているけど、非決定論的暗号化のままになっていそうですね。
以前の暗号化スキームを追加する場合、以下のように非決定論的/決定論的によって違いが生じます。
・非決定論的暗号化: 新しい情報は、常に最新の(すなわち現在の)暗号化スキームによって暗号化される
・決定論的暗号化: 新しい情報は、常にデフォルトで最も古い暗号化スキームによって暗号化される決定的暗号化をあえて利用する場合は、暗号文を変えたくないのが普通です。この振る舞いはdeterministic: { fixed: false }で変更可能です。これにより、新しいデータを暗号化するときに最新の暗号化スキームが用いられるようになります。
ふむ...
つまり
- current: 決定論的暗号化
- previous: 非決定論的暗号化
の場合は
決定論的暗号化: 新しい情報は、常にデフォルトで最も古い暗号化スキームによって暗号化される
に該当し、 最も古い暗号化スキーム
つまり previous: 非決定論的暗号化
で暗号化される
ということですかね?
この振る舞いはdeterministic: { fixed: false }で変更可能です。これにより、新しいデータを暗号化するときに最新の暗号化スキームが用いられるようになります。
と書いてあるので、やってみます。
class User < ApplicationRecord
encrypts :address, deterministic: true, previous: { deterministic: { fixed: false } }
end
irb(main):001:0> User.find(1).address
User Load (0.4ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
=> "{\"p\":\"z4kSdPawN8SxiPRN\",\"h\":{\"iv\":\"MMyvg4HqUKc6N7nb\",\"at\":\"b1dY44p+HXsOdbZdX7iMVg==\"}}"
復号化できませんでした。
previousじゃなくてcurrentの方を直さないといけない?
class User < ApplicationRecord
encrypts :address, deterministic: { fixed: false }, previous: { deterministic: false }
end
こうですかね?
irb(main):003:0> User.find(1).address
User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
=> "東京都xxx"
復号化できました。
irb(main):005:0> User.find(1).encrypt; ActiveRecord::Encryption.without_encryption { puts User.find(1).address }
User Load (0.5ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
TRANSACTION (0.2ms) BEGIN
User Update (0.4ms) UPDATE "users" SET "address" = $1 WHERE "users"."id" = $2 [["address", "{\"p\":\"4RKD+6fMtoGmrnbh\",\"h\":{\"iv\":\"r0wPnvWqbyV5pZ8q\",\"at\":\"Uwo/SseAguaw+adyeqcMZg==\"}}"], ["id", 1]]
TRANSACTION (2.5ms) COMMIT
User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
{"p":"4RKD+6fMtoGmrnbh","h":{"iv":"r0wPnvWqbyV5pZ8q","at":"Uwo/SseAguaw+adyeqcMZg=="}}
=> nil
irb(main):006:0> User.find(1).encrypt; ActiveRecord::Encryption.without_encryption { puts User.find(1).address }
User Load (0.5ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
TRANSACTION (0.2ms) BEGIN
User Update (0.4ms) UPDATE "users" SET "address" = $1 WHERE "users"."id" = $2 [["address", "{\"p\":\"4RKD+6fMtoGmrnbh\",\"h\":{\"iv\":\"r0wPnvWqbyV5pZ8q\",\"at\":\"Uwo/SseAguaw+adyeqcMZg==\"}}"], ["id", 1]]
TRANSACTION (2.5ms) COMMIT
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
{"p":"4RKD+6fMtoGmrnbh","h":{"iv":"r0wPnvWqbyV5pZ8q","at":"Uwo/SseAguaw+adyeqcMZg=="}}
=> nil
お、何回暗号化しても結果が変わらなくなりました。
irb(main):007:0> User.find(1).address
User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
=> "東京都xxx"
復号化もできました。
クエリしてみる
irb(main):030:0> User.where("address LIKE ?", "%bh%").count
User Count (0.7ms) SELECT COUNT(*) FROM "users" WHERE (address LIKE '%bh%')
=> 1
irb(main):031:0> User.where("address LIKE ?", "%東京都%").count
User Count (0.5ms) SELECT COUNT(*) FROM "users" WHERE (address LIKE '%東京都%')
=> 0
irb(main):032:0> User.where(address: "東京都xxx").count
User Count (0.5ms) SELECT COUNT(*) FROM "users" WHERE "users"."address" = $1 [["address", "{\"p\":\"4RKD+6fMtoGmrnbh\",\"h\":{\"iv\":\"r0wPnvWqbyV5pZ8q\",\"at\":\"Uwo/SseAguaw+adyeqcMZg==\"}}"]]
=> 1
なるほど
- Hashで条件を渡すと暗号化してくれる
-
?
を使う場合は暗号化してくれない - 暗号化済み文字列に対してLIKE検索はできてしまう(良いか悪いかは別)
joinした場合はどうなる?
class CreatePosts < ActiveRecord::Migration[7.0]
def change
create_table :posts do |t|
t.belongs_to :user
t.string :title
t.timestamps
end
end
end
class Post < ApplicationRecord
belongs_to :user
end
こんなテーブル/モデルを作ってみました
データを作って、クエリしてみます
irb(main):002:0> Post.create(title: "Title Test 1", user: User.find(1))
User Load (0.5ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
TRANSACTION (0.1ms) BEGIN
Post Create (0.8ms) INSERT INTO "posts" ("user_id", "title", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id" [["user_id", 1], ["title", "Title Test 1"], ["created_at", "2022-12-28 13:19:48.317981"], ["updated_at", "2022-12-28 13:19:48.317981"]]
TRANSACTION (2391.0ms) COMMIT
=> #<Post:0x00007f18ebae8a20 id: 1, user_id: 1, title: "Title Test 1", created_at: Wed, 28 Dec 2022 13:19:48.317981000 UTC +00:00, updated_at: Wed, 28 Dec 2022 13:19:48.317981000 UTC +00:00>
irb(main):005:0> Post.joins(:user).where("users.address" => "東京都xxx").count
Post Count (1.9ms) SELECT COUNT(*) FROM "posts" INNER JOIN "users" ON "users"."id" = "posts"."user_id" WHERE "users"."address" = $1 [["address", "{\"p\":\"4RKD+6fMtoGmrnbh\",\"h\":{\"iv\":\"r0wPnvWqbyV5pZ8q\",\"at\":\"Uwo/SseAguaw+adyeqcMZg==\"}}"]]
=> 1
irb(main):006:0> Post.joins(:user).where(user: { address: "東京都xxx" }).count
Post Count (0.7ms) SELECT COUNT(*) FROM "posts" INNER JOIN "users" "user" ON "user"."id" = "posts"."user_id" WHERE "user"."address" = $1 [["address", "{\"p\":\"4RKD+6fMtoGmrnbh\",\"h\":{\"iv\":\"r0wPnvWqbyV5pZ8q\",\"at\":\"Uwo/SseAguaw+adyeqcMZg==\"}}"]]
=> 1
おぉ。すごい。ちゃんと暗号化してくれてますね。
Ransackを使った検索はできる?
やってみます
gem "ransack"
irb(main):003:0> User.ransack(address_eq: "東京都xxx").result
User Load (1.2ms) SELECT "users".* FROM "users" WHERE "users"."address" = '{"p":"4RKD+6fMtoGmrnbh","h":{"iv":"r0wPnvWqbyV5pZ8q","at":"Uwo/SseAguaw+adyeqcMZg==","i":"YjQ0OQ=="}}'
=> [#<User:0x00007f5d761e1308 id: 1, name: "test-1", address: "東京都xxx", created_at: Wed, 28 Dec 2022 13:43:52.526214000 UTC +00:00, updated_at: Wed, 28 Dec 2022 14:02:51.261095000 UTC +00:00>]
irb(main):004:0> User.ransack(address_cont: "東京都xxx").result
User Load (0.5ms) SELECT "users".* FROM "users" WHERE "users"."address" ILIKE '{"p":"ZnroLeCWxv6+Pspap00=","h":{"iv":"0cktJDXBlk0/lFY2","at":"aBa0WOLOthh1JJL57gDz7g==","i":"YjQ0OQ=="}}'
=> []
irb(main):005:0> User.ransack(name_or_address_eq: "東京都xxx").result
User Load (0.5ms) SELECT "users".* FROM "users" WHERE ("users"."name" = '東京都xxx' OR "users"."address" = '{"p":"4RKD+6fMtoGmrnbh","h":{"iv":"r0wPnvWqbyV5pZ8q","at":"Uwo/SseAguaw+adyeqcMZg==","i":"YjQ0OQ=="}}')
=> [#<User:0x00007f5d74d587d8 id: 1, name: "test-1", address: "東京都xxx", created_at: Wed, 28 Dec 2022 13:43:52.526214000 UTC +00:00, updated_at: Wed, 28 Dec 2022 14:02:51.261095000 UTC +00:00>]
cont
はさすがにできませんが、 eq
の場合はちゃんと暗号化してくれました。
暗号化キーのローテーション
任意のタイミングでキーを変えたいときにローテーションできると便利ですね
その辺もサポートされてるみたいです。
ただし、制約もあるみたい
キーローテーションは、決定論的暗号化では現在サポートされていません。
Active Record暗号化は、キーローテーション処理の自動管理機能をまだ提供していません(実装は可能ですが未実装の状態です)。
なるほど。
一旦、非決定論的暗号化に戻しておきます。
class User < ApplicationRecord
encrypts :address
end
primary_keyを複数設定してみます。
まずは1個ランダムなキーを生成しておきます
irb(main):002:0> SecureRandom.hex
=> "e9afd0341ae5673010bda098db826ce3"
このキーを追加します
> EDITOR=emacs bin/rails credentials:edit --environment development
active_record_encryption:
primary_key:
- Ot8iSvgBTpaNsAkLFhMQsrf1EWDtmWHK
- e9afd0341ae5673010bda098db826ce3
deterministic_key: 0jefyhajq3hnr96sldPoqu8ISFpI9zRZ
key_derivation_salt: q6o3RhCV1hoiGqWnQEgVKGwZQieFrgcF
この状態で、復号化してみます
irb(main):001:0> User.find(1).address
User Load (0.4ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
=> "東京都xxx"
できました。
では、もう一度暗号化してみます。
irb(main):002:0> User.find(1).encrypt; ActiveRecord::Encryption.without_encryption { puts User.find(1).address }
User Load (0.5ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
TRANSACTION (0.2ms) BEGIN
User Update (0.5ms) UPDATE "users" SET "address" = $1 WHERE "users"."id" = $2 [["address", "{\"p\":\"2Dsm44vxRx7XW5xi\",\"h\":{\"iv\":\"0duQxq0uwnblGuRc\",\"at\":\"BJ3I/E2URhiDustzIv3vOg==\"}}"], ["id", 1]]
TRANSACTION (2383.0ms) COMMIT
User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
{"p":"2Dsm44vxRx7XW5xi","h":{"iv":"0duQxq0uwnblGuRc","at":"BJ3I/E2URhiDustzIv3vOg=="}}
=> nil
irb(main):003:0> User.find(1).address
User Load (0.4ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
=> "東京都xxx"
では新しいキーを一度消して、復号化してみます
active_record_encryption:
primary_key:
- Ot8iSvgBTpaNsAkLFhMQsrf1EWDtmWHK
deterministic_key: 0jefyhajq3hnr96sldPoqu8ISFpI9zRZ
key_derivation_salt: q6o3RhCV1hoiGqWnQEgVKGwZQieFrgcF
irb(main):001:0> User.find(1).address
User Load (0.5ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
=> "{\"p\":\"2Dsm44vxRx7XW5xi\",\"h\":{\"iv\":\"0duQxq0uwnblGuRc\",\"at\":\"BJ3I/E2URhiDustzIv3vOg==\"}}"
復号化できないですね。
キーの参照を保存する
この設定を有効にすると、システムがキーのリストを探索せずにキーを直接見つけられるようになり、復号のパフォーマンスが向上します。その代わり、暗号化データのサイズがやや肥大化します。
なるほど。
これがないとどのキーで暗号化されたデータなのかわからないので、全部試すけど参照が保存されていればどれを使ったのかわかるので速くなる、と。
以下の手順でやってみます。
- キー参照を保存する設定を追加
- primary_keyが1個の状態で暗号化
- primary_keyを2個に増やして復号化
- 再度暗号化(2個目のキーで暗号化)
- 1個目のキーを削除して復号化
1. キー参照を保存する設定を追加
config.active_record.encryption.store_key_references = true
2. primary_keyが1個の状態で暗号化
active_record_encryption:
primary_key:
- Ot8iSvgBTpaNsAkLFhMQsrf1EWDtmWHK
deterministic_key: 0jefyhajq3hnr96sldPoqu8ISFpI9zRZ
key_derivation_salt: q6o3RhCV1hoiGqWnQEgVKGwZQieFrgcF
irb(main):001:0> User.find(1).encrypt; ActiveRecord::Encryption.without_encryption { puts User.find(1).address }
User Load (0.4ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
TRANSACTION (0.1ms) BEGIN
User Update (0.3ms) UPDATE "users" SET "address" = $1 WHERE "users"."id" = $2 [["address", "{\"p\":\"z7JMNJCWItb8UBZx\",\"h\":{\"iv\":\"wG0bFED5BAjR2Eyw\",\"at\":\"mGXzZOnnd0Twl+hooPkurg==\",\"i\":\"ZTdhNA==\"}}"], ["id", 1]]
TRANSACTION (2.7ms) COMMIT
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
{"p":"z7JMNJCWItb8UBZx","h":{"iv":"wG0bFED5BAjR2Eyw","at":"mGXzZOnnd0Twl+hooPkurg==","i":"ZTdhNA=="}}
=> nil
i
が増えましたね。
3. primary_keyを2個に増やして復号化
active_record_encryption:
primary_key:
- Ot8iSvgBTpaNsAkLFhMQsrf1EWDtmWHK
- e9afd0341ae5673010bda098db826ce3
deterministic_key: 0jefyhajq3hnr96sldPoqu8ISFpI9zRZ
key_derivation_salt: q6o3RhCV1hoiGqWnQEgVKGwZQieFrgcF
irb(main):004:0> User.find(1).address
User Load (0.5ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
=> "東京都xxx"
irb(main):005:0> User.find(1).address_before_type_cast
User Load (0.5ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
=> "{\"p\":\"z7JMNJCWItb8UBZx\",\"h\":{\"iv\":\"wG0bFED5BAjR2Eyw\",\"at\":\"mGXzZOnnd0Twl+hooPkurg==\",\"i\":\"ZTdhNA==\"}}"
4. 再度暗号化(2個目のキーで暗号化)
irb(main):006:0> User.find(1).encrypt; User.find(1).address_before_type_cast
User Load (0.5ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
TRANSACTION (0.1ms) BEGIN
User Update (0.4ms) UPDATE "users" SET "address" = $1 WHERE "users"."id" = $2 [["address", "{\"p\":\"Uh0MVyUU2snhbF7A\",\"h\":{\"iv\":\"4qyRCEEQRWU61GzV\",\"at\":\"3khLmx4IgjloxzpN5+hEDg==\",\"i\":\"N2E4NQ==\"}}"], ["id", 1]]
TRANSACTION (2383.2ms) COMMIT
User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
=> "{\"p\":\"Uh0MVyUU2snhbF7A\",\"h\":{\"iv\":\"4qyRCEEQRWU61GzV\",\"at\":\"3khLmx4IgjloxzpN5+hEDg==\",\"i\":\"N2E4NQ==\"}}"
i
の値が変わりました
5. 1個目のキーを削除して復号化
active_record_encryption:
primary_key:
- e9afd0341ae5673010bda098db826ce3
deterministic_key: 0jefyhajq3hnr96sldPoqu8ISFpI9zRZ
key_derivation_salt: q6o3RhCV1hoiGqWnQEgVKGwZQieFrgcF
1個目のキーを削除しました
(2個目を残した)
irb(main):001:0> User.find(1).address
User Load (0.4ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
=> "東京都xxx"
復号化できました。
これでローテーション完了ですね。
i
はどういう値なのか?
キー参照の
DEFAULT_PROPERTIES = {
encrypted_data_key: "k",
encrypted_data_key_id: "i",
compressed: "c",
iv: "iv",
auth_tag: "at",
encoding: "e"
}
encrypted_data_key_id
というプロパティらしい
@encryption_key ||= @keys.last.tap do |key|
key.public_tags.encrypted_data_key_id = key.id if ActiveRecord::Encryption.config.store_key_references
end
key.id
が正体っぽいですね
なるほど、 sha1の先頭4文字 みたいですね。
ここで使われていました。
先程のデータの i
は N2E4NQ==
になっていたので
これをbase64 decodeすると 7a85
になるので、そういうことみたいですね。
暗号化のキーを環境変数から読み込みたい
Rails Credentialsを使いたくないケースもあると思うので
SecretManagerなどで管理しているキーを使う方法も考えてみます。
※個人的にはアプリケーションと設定値は切り離したいのでcredentialsはあまり積極的には使っていなかったりします。
ちゃんと用意されてますね。
KeyProviderを拡張する方法もあるみたいですが、設定でやるほうが楽なので今回はそっちでやってみます。
config.active_record.encryption.primary_key
config.active_record.encryption.deterministic_key
config.active_record.encryption.key_derivation_salt
この3つを設定すればrails credentialsは削除できそうです。
...
config.active_record.encryption.primary_key = ENV["AR_ENC_PRIMARY_KEY"]
config.active_record.encryption.deterministic_key = ENV["AR_ENC_DETERMINISTIC_KEY"]
config.active_record.encryption.key_derivation_salt = ENV["AR_ENC_KEY_DERIVATION_SALT"]
...
...
app:
container_name: app
build:
context: .
target: dev
args:
bundle_install_options: --jobs 10
cache_from:
- ar-enc-example-app
volumes:
- ./:/app:cached
environment:
RAILS_LOG_TO_STDOUT: 1
TZ: Asia/Tokyo
# 以下追加
AR_ENC_PRIMARY_KEY: e9afd0341ae5673010bda098db826ce3
AR_ENC_DETERMINISTIC_KEY: 0jefyhajq3hnr96sldPoqu8ISFpI9zRZ
AR_ENC_KEY_DERIVATION_SALT: q6o3RhCV1hoiGqWnQEgVKGwZQieFrgcF
ports:
- 3001:3000
tty: true
command: /bin/sh -l -c 'bundle && bin/rails s'
depends_on:
- postgres
...
credential系を削除します
❯ rm -rf config/credentials
❯ rm -rf config/credentials.yml.enc
再起動します
❯ docker compose up -d
[+] Running 2/2
⠿ Container postgres Running
⠿ Container app Started
rails consoleからデータを参照してみます
irb(main):001:0> User.find(1).address
User Load (0.4ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
=> "東京都xxx"
できましたね。
キーローテーションしたい場合は
config.active_record.encryption.primary_key = [ENV["AR_ENC_PRIMARY_KEY"], ENV["AR_ENC_PRIMARY_KEY_NEW"]]
config.active_record.encryption.deterministic_key = [ENV["AR_ENC_DETERMINISTIC_KEY"], ENV["AR_ENC_DETERMINISTIC_KEY_NEW"]]
配列で設定すれば大丈夫そうです。
まとめ
- ActiveRecord Encryption思ったより楽に使えそう
- 途中で暗号化スキームを変更する(非決定論的 -> 決定論的)をやると少し面倒
-
encrypts :address, deterministic: { fixed: false }, previous: { deterministic: false }
- これが直感的ではないので、コードから意味を理解しにくい気がする
- でもこれができるように設計されているのはすごい
-
- 決定論的暗号化を使う場面
- クエリしたい時
- 検索は全文検索エンジンにまかせてしまう、みたいな方針であれば困らないかも
- ユニーク制約をかけたい時
- 基本的にはデフォルトでもある非決定論的暗号化の方が安全で良い
- クエリしたい時
- キーローテーションができるの良い
- これ意外と面倒なので良い
- 決定論的暗号化でも出来ると嬉しい
Discussion