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:
parent
d35905ccae
commit
7a1fb99302
8 changed files with 80 additions and 25 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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|
|
||||
|
|
|
@ -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`:
|
||||
|
|
Loading…
Reference in a new issue