1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00

Add support to declare previous encryption schemes globally

This commit is contained in:
Jorge Manrubia 2021-03-21 23:06:59 +01:00
parent d35905ccae
commit 7a1fb99302
8 changed files with 80 additions and 25 deletions

View file

@ -6,12 +6,21 @@ module ActiveRecord
class Config
attr_accessor :primary_key, :deterministic_key, :store_key_references, :key_derivation_salt,
:support_unencrypted_data, :encrypt_fixtures, :validate_column_size, :add_to_filter_parameters,
:excluded_from_filter_parameters, :extend_queries
:excluded_from_filter_parameters, :extend_queries, :previous_schemes
def initialize
set_defaults
end
# Configure previous encryption schemes.
#
# config.active_record.encryption.previous = [ { key_provider: MyOldKeyProvider.new } ]
def previous=(previous_schemes_properties)
previous_schemes_properties.each do |properties|
add_previous_scheme(**properties)
end
end
private
def set_defaults
self.store_key_references = false
@ -20,10 +29,15 @@ module ActiveRecord
self.validate_column_size = true
self.add_to_filter_parameters = true
self.excluded_from_filter_parameters = []
self.previous_schemes = []
# TODO: Setting to false for now as the implementation is a bit experimental
self.extend_queries = false
end
def add_previous_scheme(**properties)
previous_schemes << ActiveRecord::Encryption::Scheme.new(**properties)
end
end
end
end

View file

@ -45,7 +45,7 @@ module ActiveRecord
self.encrypted_attributes ||= Set.new # not using :default because the instance would be shared across classes
names.each do |name|
previous_schemes = Array.wrap(previous).collect { |scheme_config| ActiveRecord::Encryption::Scheme.new(**scheme_config) }
previous_schemes = ActiveRecord::Encryption.config.previous_schemes + Array.wrap(previous).collect { |scheme_config| ActiveRecord::Encryption::Scheme.new(**scheme_config) }
attribute_scheme = ActiveRecord::Encryption::Scheme.new \
key_provider: key_provider, key: key, deterministic: deterministic, downcase: downcase,
ignore_case: ignore_case, previous_schemes: previous_schemes, **context_properties

View file

@ -12,7 +12,7 @@ module ActiveRecord
attr_reader :scheme, :cast_type
delegate :key_provider, :previous_encrypted_types, :downcase?, :deterministic?, :with_context, to: :scheme
delegate :key_provider, :downcase?, :deterministic?, :with_context, to: :scheme
# === Options
#
@ -40,11 +40,10 @@ module ActiveRecord
old_value != new_value
end
def additional_encrypted_types # :nodoc:
if support_unencrypted_data?
@previous_encrypted_types_with_clean_text_type ||= previous_encrypted_types.including(clean_text_type)
else
previous_encrypted_types
def previous_encrypted_types # :nodoc:
@additional_encrypted_types ||= {} # Memoizing on support_unencrypted_data so that we can tweak it during tests
@additional_encrypted_types[support_unencrypted_data?] ||= previous_schemes.collect do |scheme|
EncryptedAttributeType.new(scheme: scheme)
end
end
@ -77,6 +76,10 @@ module ActiveRecord
end
end
def previous_schemes
scheme.previous_schemes.including((clean_text_scheme if support_unencrypted_data?)).compact
end
def support_unencrypted_data?
ActiveRecord::Encryption.config.support_unencrypted_data
end
@ -99,10 +102,9 @@ module ActiveRecord
@decryption_options ||= { key_provider: key_provider }.compact
end
def clean_text_type
@clean_text_type ||= begin
config = ActiveRecord::Encryption::Scheme.new(downcase: downcase?, encryptor: ActiveRecord::Encryption::NullEncryptor.new)
ActiveRecord::Encryption::EncryptedAttributeType.new(scheme: config)
def clean_text_scheme
@clean_text_scheme ||= begin
ActiveRecord::Encryption::Scheme.new(downcase: downcase?, encryptor: ActiveRecord::Encryption::NullEncryptor.new)
end
end
end

View file

@ -45,7 +45,7 @@ module ActiveRecord
if args.is_a?(Array) && (options = args.first).is_a?(Hash)
self.deterministic_encrypted_attributes&.each do |attribute_name|
type = type_for_attribute(attribute_name)
if !type.additional_encrypted_types.empty? && value = options[attribute_name]
if !type.previous_encrypted_types.empty? && value = options[attribute_name]
options[attribute_name] = process_encrypted_query_argument(value, check_for_additional_values, type)
end
end
@ -71,7 +71,7 @@ module ActiveRecord
end
def additional_values_for(value, type)
type.additional_encrypted_types.collect do |additional_type|
type.previous_encrypted_types.collect do |additional_type|
AdditionalValue.new(value, additional_type)
end
end

View file

@ -8,6 +8,8 @@ module ActiveRecord
#
# See +EncryptedAttributeType+, +Context+
class Scheme
attr_reader :previous_schemes
def initialize(key_provider: nil, key: nil, deterministic: false, downcase: false, ignore_case: false,
previous_schemes: [], **context_properties)
@key_provider_param = key_provider
@ -37,10 +39,6 @@ module ActiveRecord
@key_provider ||= @key_provider_param || build_key_provider
end
def previous_encrypted_types
@previous_encrypted_types ||= @previous_schemes.collect { |previous_config| ActiveRecord::Encryption::EncryptedAttributeType.new(scheme: previous_config) }
end
def with_context(&block)
if @context_properties.present?
ActiveRecord::Encryption.with_encryption_context(**@context_properties, &block)

View file

@ -2,6 +2,7 @@
require "cases/encryption/helper"
require "models/author_encrypted"
require "models/book"
class ActiveRecord::Encryption::EncryptionSchemesTest < ActiveRecord::TestCase
test "can decrypt encrypted_value encrypted with a different encryption scheme" do
@ -38,6 +39,31 @@ class ActiveRecord::Encryption::EncryptionSchemesTest < ActiveRecord::TestCase
assert_equal author, EncryptedAuthor2.find_by_name("1")
end
test "use global previous schemes to decrypt data encrypted with previous schemes" do
ActiveRecord::Encryption.config.support_unencrypted_data = false
ActiveRecord::Encryption.config.previous = [ { encryptor: TestEncryptor.new("0" => "1") }, { encryptor: TestEncryptor.new("1" => "2") } ]
# We want to evaluate .encrypts *after* tweaking the config property
encrypted_author_class = Class.new(Author) do
self.table_name = "authors"
encrypts :name
end
assert_equal 2, encrypted_author_class.type_for_attribute(:name).previous_encrypted_types.count
previoys_type_1, previoys_type_2 = encrypted_author_class.type_for_attribute(:name).previous_encrypted_types
author = ActiveRecord::Encryption.without_encryption do
encrypted_author_class.create name: previoys_type_1.serialize("1")
end
assert_equal "0", author.reload.name
author = ActiveRecord::Encryption.without_encryption do
encrypted_author_class.create name: previoys_type_2.serialize("2")
end
assert_equal "1", author.reload.name
end
private
class TestEncryptor
def initialize(ciphertexts_by_clear_value)

View file

@ -142,7 +142,7 @@ class ActiveRecord::TestCase
# , PerformanceHelpers
ENCRYPTION_ERROR_FLAGS = %i[ primary_key store_key_references key_derivation_salt support_unencrypted_data
encrypt_fixtures ]
encrypt_fixtures previous_schemes ]
setup do
ENCRYPTION_ERROR_FLAGS.each do |property|

View file

@ -157,18 +157,33 @@ To ease migrations of unencrypted data, the library includes the option `config.
Changing encryption properties of attributes can break existing data. For example, imagine you wan to make a "deterministic" attribute "not deterministic". If you just change the declaration in the model, reading existing ciphertexts will fail because they are different now.
To support these situations, you can use `:previous` to declare previous encryption schemes:
To support these situations, you can declare previous encryption schemes that will be used in two scenarios:
* When reading encrypted data, Active Record Encryption will try previous encryption schemes if the current scheme doesn't work.
* When querying deterministic data, it will add ciphertexts using previous schemes to the queries so that queries work seamlessly with data encrypted with different scheme. You need to set `config.active_record.encryption.extend_queries = true` to enable this.
You can configure previous encryption schemes:
* Gloabally
* On a per-attribute basis
#### Global previous encryption schemes
You can add previous encryption schemes by adding them as list of properties using the `previous` config property in your `application.rb`:
```ruby
config.active_record.encryption.previous = [ { key_provider: MyOldKeyProvider.new } ]
```
#### Per-attribute encryption schemes
Use `:previous` when declaring the attribute:
```ruby
class Article
encrypts :title, deterministic: true, previous: { deterministic: false }
end
```
This declaration has 2 effects:
* When reading encrypted data, Active Record Encryption will try previous encryption schemes if the current scheme doesn't work.
* When querying deterministic data, it will add ciphertexts using previous schemes to the queries so that queries work seamlessly with data encrypted with different scheme. You need to set `config.active_record.encryption.extend_queries = true` to enable this.
### Filtering params named as encrypted columns
By default, encrypted columns are configured to be [automatically filtered in Rails logs](https://guides.rubyonrails.org/action_controller_overview.html#parameters-filtering). You can disable this behavior by adding this to your `application.rb`: