activerecord 6.1.7.10 → 7.0.0.alpha1
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of activerecord might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +726 -1404
- data/MIT-LICENSE +1 -1
- data/README.rdoc +1 -1
- data/lib/active_record/aggregations.rb +1 -1
- data/lib/active_record/association_relation.rb +0 -10
- data/lib/active_record/associations/association.rb +31 -9
- data/lib/active_record/associations/association_scope.rb +1 -3
- data/lib/active_record/associations/belongs_to_association.rb +15 -4
- data/lib/active_record/associations/belongs_to_polymorphic_association.rb +10 -2
- data/lib/active_record/associations/builder/association.rb +8 -2
- data/lib/active_record/associations/builder/belongs_to.rb +19 -6
- data/lib/active_record/associations/builder/collection_association.rb +1 -1
- data/lib/active_record/associations/builder/has_many.rb +3 -2
- data/lib/active_record/associations/builder/has_one.rb +2 -1
- data/lib/active_record/associations/builder/singular_association.rb +2 -2
- data/lib/active_record/associations/collection_association.rb +14 -23
- data/lib/active_record/associations/collection_proxy.rb +8 -3
- data/lib/active_record/associations/disable_joins_association_scope.rb +59 -0
- data/lib/active_record/associations/has_many_association.rb +1 -1
- data/lib/active_record/associations/has_many_through_association.rb +2 -1
- data/lib/active_record/associations/has_one_association.rb +10 -7
- data/lib/active_record/associations/has_one_through_association.rb +1 -1
- data/lib/active_record/associations/preloader/association.rb +161 -47
- data/lib/active_record/associations/preloader/batch.rb +51 -0
- data/lib/active_record/associations/preloader/branch.rb +147 -0
- data/lib/active_record/associations/preloader/through_association.rb +37 -11
- data/lib/active_record/associations/preloader.rb +46 -110
- data/lib/active_record/associations/singular_association.rb +8 -2
- data/lib/active_record/associations/through_association.rb +1 -1
- data/lib/active_record/associations.rb +76 -81
- data/lib/active_record/asynchronous_queries_tracker.rb +57 -0
- data/lib/active_record/attribute_assignment.rb +1 -1
- data/lib/active_record/attribute_methods/before_type_cast.rb +7 -2
- data/lib/active_record/attribute_methods/dirty.rb +41 -16
- data/lib/active_record/attribute_methods/primary_key.rb +2 -2
- data/lib/active_record/attribute_methods/query.rb +2 -2
- data/lib/active_record/attribute_methods/read.rb +7 -5
- data/lib/active_record/attribute_methods/serialization.rb +66 -12
- data/lib/active_record/attribute_methods/time_zone_conversion.rb +4 -3
- data/lib/active_record/attribute_methods/write.rb +7 -10
- data/lib/active_record/attribute_methods.rb +6 -9
- data/lib/active_record/attributes.rb +24 -35
- data/lib/active_record/autosave_association.rb +3 -18
- data/lib/active_record/base.rb +19 -1
- data/lib/active_record/callbacks.rb +2 -2
- data/lib/active_record/coders/yaml_column.rb +2 -14
- data/lib/active_record/connection_adapters/abstract/connection_handler.rb +312 -0
- data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +209 -0
- data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +76 -0
- data/lib/active_record/connection_adapters/abstract/connection_pool.rb +31 -558
- data/lib/active_record/connection_adapters/abstract/database_statements.rb +45 -21
- data/lib/active_record/connection_adapters/abstract/query_cache.rb +24 -12
- data/lib/active_record/connection_adapters/abstract/quoting.rb +12 -14
- data/lib/active_record/connection_adapters/abstract/schema_creation.rb +4 -17
- data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +30 -13
- data/lib/active_record/connection_adapters/abstract/schema_statements.rb +60 -16
- data/lib/active_record/connection_adapters/abstract/transaction.rb +3 -3
- data/lib/active_record/connection_adapters/abstract_adapter.rb +112 -66
- data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +96 -81
- data/lib/active_record/connection_adapters/mysql/database_statements.rb +33 -23
- data/lib/active_record/connection_adapters/mysql/quoting.rb +16 -1
- data/lib/active_record/connection_adapters/mysql/schema_statements.rb +1 -1
- data/lib/active_record/connection_adapters/mysql2_adapter.rb +12 -6
- data/lib/active_record/connection_adapters/pool_config.rb +1 -3
- data/lib/active_record/connection_adapters/postgresql/database_statements.rb +19 -14
- data/lib/active_record/connection_adapters/postgresql/oid/date.rb +8 -0
- data/lib/active_record/connection_adapters/postgresql/oid/date_time.rb +5 -0
- data/lib/active_record/connection_adapters/postgresql/oid/hstore.rb +53 -14
- data/lib/active_record/connection_adapters/postgresql/oid/range.rb +1 -1
- data/lib/active_record/connection_adapters/postgresql/oid/timestamp.rb +15 -0
- data/lib/active_record/connection_adapters/postgresql/oid/timestamp_with_time_zone.rb +28 -0
- data/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +18 -6
- data/lib/active_record/connection_adapters/postgresql/oid.rb +2 -0
- data/lib/active_record/connection_adapters/postgresql/quoting.rb +6 -32
- data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +32 -0
- data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +5 -1
- data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +12 -12
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +159 -102
- data/lib/active_record/connection_adapters/schema_cache.rb +36 -37
- data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +23 -19
- data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +4 -2
- data/lib/active_record/connection_adapters/sqlite3_adapter.rb +61 -30
- data/lib/active_record/connection_adapters.rb +6 -5
- data/lib/active_record/connection_handling.rb +20 -38
- data/lib/active_record/core.rb +111 -125
- data/lib/active_record/database_configurations/connection_url_resolver.rb +0 -1
- data/lib/active_record/database_configurations/database_config.rb +12 -0
- data/lib/active_record/database_configurations/hash_config.rb +27 -1
- data/lib/active_record/database_configurations/url_config.rb +2 -2
- data/lib/active_record/database_configurations.rb +17 -9
- data/lib/active_record/delegated_type.rb +33 -11
- data/lib/active_record/destroy_association_async_job.rb +1 -1
- data/lib/active_record/disable_joins_association_relation.rb +39 -0
- data/lib/active_record/dynamic_matchers.rb +1 -1
- data/lib/active_record/encryption/cipher/aes256_gcm.rb +98 -0
- data/lib/active_record/encryption/cipher.rb +53 -0
- data/lib/active_record/encryption/config.rb +44 -0
- data/lib/active_record/encryption/configurable.rb +61 -0
- data/lib/active_record/encryption/context.rb +35 -0
- data/lib/active_record/encryption/contexts.rb +72 -0
- data/lib/active_record/encryption/derived_secret_key_provider.rb +12 -0
- data/lib/active_record/encryption/deterministic_key_provider.rb +14 -0
- data/lib/active_record/encryption/encryptable_record.rb +208 -0
- data/lib/active_record/encryption/encrypted_attribute_type.rb +140 -0
- data/lib/active_record/encryption/encrypted_fixtures.rb +38 -0
- data/lib/active_record/encryption/encrypting_only_encryptor.rb +12 -0
- data/lib/active_record/encryption/encryptor.rb +155 -0
- data/lib/active_record/encryption/envelope_encryption_key_provider.rb +55 -0
- data/lib/active_record/encryption/errors.rb +15 -0
- data/lib/active_record/encryption/extended_deterministic_queries.rb +160 -0
- data/lib/active_record/encryption/extended_deterministic_uniqueness_validator.rb +29 -0
- data/lib/active_record/encryption/key.rb +28 -0
- data/lib/active_record/encryption/key_generator.rb +42 -0
- data/lib/active_record/encryption/key_provider.rb +46 -0
- data/lib/active_record/encryption/message.rb +33 -0
- data/lib/active_record/encryption/message_serializer.rb +80 -0
- data/lib/active_record/encryption/null_encryptor.rb +21 -0
- data/lib/active_record/encryption/properties.rb +76 -0
- data/lib/active_record/encryption/read_only_null_encryptor.rb +24 -0
- data/lib/active_record/encryption/scheme.rb +99 -0
- data/lib/active_record/encryption.rb +55 -0
- data/lib/active_record/enum.rb +41 -41
- data/lib/active_record/errors.rb +66 -3
- data/lib/active_record/fixture_set/file.rb +15 -1
- data/lib/active_record/fixture_set/table_row.rb +40 -5
- data/lib/active_record/fixture_set/table_rows.rb +4 -4
- data/lib/active_record/fixtures.rb +16 -11
- data/lib/active_record/future_result.rb +139 -0
- data/lib/active_record/gem_version.rb +4 -4
- data/lib/active_record/inheritance.rb +55 -17
- data/lib/active_record/insert_all.rb +34 -5
- data/lib/active_record/integration.rb +1 -1
- data/lib/active_record/internal_metadata.rb +1 -5
- data/lib/active_record/locking/optimistic.rb +10 -9
- data/lib/active_record/log_subscriber.rb +6 -2
- data/lib/active_record/middleware/database_selector/resolver.rb +6 -10
- data/lib/active_record/middleware/database_selector.rb +8 -3
- data/lib/active_record/migration/command_recorder.rb +4 -4
- data/lib/active_record/migration/compatibility.rb +89 -10
- data/lib/active_record/migration/join_table.rb +1 -1
- data/lib/active_record/migration.rb +109 -79
- data/lib/active_record/model_schema.rb +45 -31
- data/lib/active_record/nested_attributes.rb +3 -3
- data/lib/active_record/no_touching.rb +2 -2
- data/lib/active_record/null_relation.rb +2 -6
- data/lib/active_record/persistence.rb +134 -45
- data/lib/active_record/query_cache.rb +2 -2
- data/lib/active_record/query_logs.rb +203 -0
- data/lib/active_record/querying.rb +15 -5
- data/lib/active_record/railtie.rb +117 -17
- data/lib/active_record/railties/controller_runtime.rb +1 -1
- data/lib/active_record/railties/databases.rake +72 -48
- data/lib/active_record/readonly_attributes.rb +11 -0
- data/lib/active_record/reflection.rb +45 -44
- data/lib/active_record/relation/batches/batch_enumerator.rb +19 -5
- data/lib/active_record/relation/batches.rb +3 -3
- data/lib/active_record/relation/calculations.rb +39 -26
- data/lib/active_record/relation/delegation.rb +6 -6
- data/lib/active_record/relation/finder_methods.rb +31 -22
- data/lib/active_record/relation/merger.rb +20 -13
- data/lib/active_record/relation/predicate_builder.rb +1 -6
- data/lib/active_record/relation/query_attribute.rb +5 -11
- data/lib/active_record/relation/query_methods.rb +230 -49
- data/lib/active_record/relation/record_fetch_warning.rb +2 -2
- data/lib/active_record/relation/spawn_methods.rb +2 -2
- data/lib/active_record/relation/where_clause.rb +8 -4
- data/lib/active_record/relation.rb +166 -77
- data/lib/active_record/result.rb +17 -2
- data/lib/active_record/runtime_registry.rb +2 -4
- data/lib/active_record/sanitization.rb +11 -7
- data/lib/active_record/schema_dumper.rb +3 -3
- data/lib/active_record/schema_migration.rb +0 -4
- data/lib/active_record/scoping/default.rb +61 -12
- data/lib/active_record/scoping/named.rb +3 -11
- data/lib/active_record/scoping.rb +40 -22
- data/lib/active_record/serialization.rb +1 -1
- data/lib/active_record/signed_id.rb +1 -1
- data/lib/active_record/store.rb +1 -6
- data/lib/active_record/tasks/database_tasks.rb +106 -22
- data/lib/active_record/tasks/mysql_database_tasks.rb +1 -1
- data/lib/active_record/tasks/postgresql_database_tasks.rb +14 -11
- data/lib/active_record/test_databases.rb +1 -1
- data/lib/active_record/test_fixtures.rb +9 -13
- data/lib/active_record/timestamp.rb +3 -4
- data/lib/active_record/transactions.rb +9 -14
- data/lib/active_record/translation.rb +2 -2
- data/lib/active_record/type/adapter_specific_registry.rb +32 -7
- data/lib/active_record/type/hash_lookup_type_map.rb +34 -1
- data/lib/active_record/type/internal/timezone.rb +2 -2
- data/lib/active_record/type/serialized.rb +1 -1
- data/lib/active_record/type/type_map.rb +17 -20
- data/lib/active_record/type.rb +1 -2
- data/lib/active_record/validations/associated.rb +1 -1
- data/lib/active_record.rb +170 -2
- data/lib/arel/attributes/attribute.rb +0 -8
- data/lib/arel/crud.rb +18 -22
- data/lib/arel/delete_manager.rb +2 -4
- data/lib/arel/insert_manager.rb +2 -3
- data/lib/arel/nodes/casted.rb +1 -1
- data/lib/arel/nodes/delete_statement.rb +8 -13
- data/lib/arel/nodes/insert_statement.rb +2 -2
- data/lib/arel/nodes/select_core.rb +2 -2
- data/lib/arel/nodes/select_statement.rb +2 -2
- data/lib/arel/nodes/update_statement.rb +3 -2
- data/lib/arel/predications.rb +1 -1
- data/lib/arel/select_manager.rb +10 -4
- data/lib/arel/table.rb +0 -1
- data/lib/arel/tree_manager.rb +0 -12
- data/lib/arel/update_manager.rb +2 -4
- data/lib/arel/visitors/dot.rb +80 -90
- data/lib/arel/visitors/mysql.rb +6 -1
- data/lib/arel/visitors/postgresql.rb +0 -10
- data/lib/arel/visitors/to_sql.rb +43 -2
- data/lib/arel.rb +1 -1
- data/lib/rails/generators/active_record/application_record/templates/application_record.rb.tt +1 -1
- data/lib/rails/generators/active_record/model/templates/abstract_base_class.rb.tt +1 -1
- data/lib/rails/generators/active_record/model/templates/model.rb.tt +1 -1
- data/lib/rails/generators/active_record/model/templates/module.rb.tt +2 -2
- metadata +52 -14
@@ -0,0 +1,140 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Encryption
|
5
|
+
# An +ActiveModel::Type+ that encrypts/decrypts strings of text.
|
6
|
+
#
|
7
|
+
# This is the central piece that connects the encryption system with +encrypts+ declarations in the
|
8
|
+
# model classes. Whenever you declare an attribute as encrypted, it configures an +EncryptedAttributeType+
|
9
|
+
# for that attribute.
|
10
|
+
class EncryptedAttributeType < ::ActiveRecord::Type::Text
|
11
|
+
include ActiveModel::Type::Helpers::Mutable
|
12
|
+
|
13
|
+
attr_reader :scheme, :cast_type
|
14
|
+
|
15
|
+
delegate :key_provider, :downcase?, :deterministic?, :previous_schemes, :with_context, :fixed?, to: :scheme
|
16
|
+
delegate :accessor, to: :cast_type
|
17
|
+
|
18
|
+
# === Options
|
19
|
+
#
|
20
|
+
# * <tt>:scheme</tt> - An +Scheme+ with the encryption properties for this attribute.
|
21
|
+
# * <tt>:cast_type</tt> - A type that will be used to serialize (before encrypting) and deserialize
|
22
|
+
# (after decrypting). +ActiveModel::Type::String+ by default.
|
23
|
+
def initialize(scheme:, cast_type: ActiveModel::Type::String.new, previous_type: false)
|
24
|
+
super()
|
25
|
+
@scheme = scheme
|
26
|
+
@cast_type = cast_type
|
27
|
+
@previous_type = previous_type
|
28
|
+
end
|
29
|
+
|
30
|
+
def deserialize(value)
|
31
|
+
cast_type.deserialize decrypt(value)
|
32
|
+
end
|
33
|
+
|
34
|
+
def serialize(value)
|
35
|
+
if serialize_with_oldest?
|
36
|
+
serialize_with_oldest(value)
|
37
|
+
else
|
38
|
+
serialize_with_current(value)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def changed_in_place?(raw_old_value, new_value)
|
43
|
+
old_value = raw_old_value.nil? ? nil : deserialize(raw_old_value)
|
44
|
+
old_value != new_value
|
45
|
+
end
|
46
|
+
|
47
|
+
def previous_types # :nodoc:
|
48
|
+
@previous_types ||= {} # Memoizing on support_unencrypted_data so that we can tweak it during tests
|
49
|
+
@previous_types[support_unencrypted_data?] ||= build_previous_types_for(previous_schemes_including_clean_text)
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
def previous_schemes_including_clean_text
|
54
|
+
previous_schemes.including((clean_text_scheme if support_unencrypted_data?)).compact
|
55
|
+
end
|
56
|
+
|
57
|
+
def previous_types_without_clean_text
|
58
|
+
@previous_types_without_clean_text ||= build_previous_types_for(previous_schemes)
|
59
|
+
end
|
60
|
+
|
61
|
+
def build_previous_types_for(schemes)
|
62
|
+
schemes.collect do |scheme|
|
63
|
+
EncryptedAttributeType.new(scheme: scheme, previous_type: true)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def previous_type?
|
68
|
+
@previous_type
|
69
|
+
end
|
70
|
+
|
71
|
+
def decrypt(value)
|
72
|
+
with_context do
|
73
|
+
encryptor.decrypt(value, **decryption_options) unless value.nil?
|
74
|
+
end
|
75
|
+
rescue ActiveRecord::Encryption::Errors::Base => error
|
76
|
+
if previous_types_without_clean_text.blank?
|
77
|
+
handle_deserialize_error(error, value)
|
78
|
+
else
|
79
|
+
try_to_deserialize_with_previous_encrypted_types(value)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def try_to_deserialize_with_previous_encrypted_types(value)
|
84
|
+
previous_types.each.with_index do |type, index|
|
85
|
+
break type.deserialize(value)
|
86
|
+
rescue ActiveRecord::Encryption::Errors::Base => error
|
87
|
+
handle_deserialize_error(error, value) if index == previous_types.length - 1
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def handle_deserialize_error(error, value)
|
92
|
+
if error.is_a?(Errors::Decryption) && support_unencrypted_data?
|
93
|
+
value
|
94
|
+
else
|
95
|
+
raise error
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def serialize_with_oldest?
|
100
|
+
@serialize_with_oldest ||= fixed? && previous_types_without_clean_text.present?
|
101
|
+
end
|
102
|
+
|
103
|
+
def serialize_with_oldest(value)
|
104
|
+
previous_types.first.serialize(value)
|
105
|
+
end
|
106
|
+
|
107
|
+
def serialize_with_current(value)
|
108
|
+
casted_value = cast_type.serialize(value)
|
109
|
+
casted_value = casted_value&.downcase if downcase?
|
110
|
+
encrypt(casted_value.to_s) unless casted_value.nil?
|
111
|
+
end
|
112
|
+
|
113
|
+
def encrypt(value)
|
114
|
+
with_context do
|
115
|
+
encryptor.encrypt(value, **encryption_options)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def encryptor
|
120
|
+
ActiveRecord::Encryption.encryptor
|
121
|
+
end
|
122
|
+
|
123
|
+
def support_unencrypted_data?
|
124
|
+
ActiveRecord::Encryption.config.support_unencrypted_data && !previous_type?
|
125
|
+
end
|
126
|
+
|
127
|
+
def encryption_options
|
128
|
+
@encryption_options ||= { key_provider: key_provider, cipher_options: { deterministic: deterministic? } }.compact
|
129
|
+
end
|
130
|
+
|
131
|
+
def decryption_options
|
132
|
+
@decryption_options ||= { key_provider: key_provider }.compact
|
133
|
+
end
|
134
|
+
|
135
|
+
def clean_text_scheme
|
136
|
+
@clean_text_scheme ||= ActiveRecord::Encryption::Scheme.new(downcase: downcase?, encryptor: ActiveRecord::Encryption::NullEncryptor.new)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Encryption
|
5
|
+
module EncryptedFixtures
|
6
|
+
def initialize(fixture, model_class)
|
7
|
+
@clean_values = {}
|
8
|
+
encrypt_fixture_data(fixture, model_class)
|
9
|
+
process_preserved_original_columns(fixture, model_class)
|
10
|
+
super
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
def encrypt_fixture_data(fixture, model_class)
|
15
|
+
model_class&.encrypted_attributes&.each do |attribute_name|
|
16
|
+
if clean_value = fixture[attribute_name.to_s]
|
17
|
+
@clean_values[attribute_name.to_s] = clean_value
|
18
|
+
|
19
|
+
type = model_class.type_for_attribute(attribute_name)
|
20
|
+
encrypted_value = type.serialize(clean_value)
|
21
|
+
fixture[attribute_name.to_s] = encrypted_value
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def process_preserved_original_columns(fixture, model_class)
|
27
|
+
model_class&.encrypted_attributes&.each do |attribute_name|
|
28
|
+
if source_attribute_name = model_class.source_attribute_from_preserved_attribute(attribute_name)
|
29
|
+
clean_value = @clean_values[source_attribute_name.to_s]
|
30
|
+
type = model_class.type_for_attribute(attribute_name)
|
31
|
+
encrypted_value = type.serialize(clean_value)
|
32
|
+
fixture[attribute_name.to_s] = encrypted_value
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Encryption
|
5
|
+
# An encryptor that can encrypt data but can't decrypt it.
|
6
|
+
class EncryptingOnlyEncryptor < Encryptor
|
7
|
+
def decrypt(encrypted_text, key_provider: nil, cipher_options: {})
|
8
|
+
encrypted_text
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,155 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "openssl"
|
4
|
+
require "zlib"
|
5
|
+
require "active_support/core_ext/numeric"
|
6
|
+
|
7
|
+
module ActiveRecord
|
8
|
+
module Encryption
|
9
|
+
# An encryptor exposes the encryption API that +ActiveRecord::Encryption::EncryptedAttributeType+
|
10
|
+
# uses for encrypting and decrypting attribute values.
|
11
|
+
#
|
12
|
+
# It interacts with a +KeyProvider+ for getting the keys, and delegate to
|
13
|
+
# +ActiveRecord::Encryption::Cipher+ the actual encryption algorithm.
|
14
|
+
class Encryptor
|
15
|
+
# Encrypts +clean_text+ and returns the encrypted result
|
16
|
+
#
|
17
|
+
# Internally, it will:
|
18
|
+
#
|
19
|
+
# 1. Create a new +ActiveRecord::Encryption::Message+
|
20
|
+
# 2. Compress and encrypt +clean_text+ as the message payload
|
21
|
+
# 3. Serialize it with +ActiveRecord::Encryption.message_serializer+ (+ActiveRecord::Encryption::SafeMarshal+
|
22
|
+
# by default)
|
23
|
+
# 4. Encode the result with Base 64
|
24
|
+
#
|
25
|
+
# === Options
|
26
|
+
#
|
27
|
+
# [:key_provider]
|
28
|
+
# Key provider to use for the encryption operation. It will default to
|
29
|
+
# +ActiveRecord::Encryption.key_provider+ when not provided
|
30
|
+
#
|
31
|
+
# [:cipher_options]
|
32
|
+
# +Cipher+-specific options that will be passed to the Cipher configured in
|
33
|
+
# +ActiveRecord::Encryption.cipher+
|
34
|
+
def encrypt(clear_text, key_provider: default_key_provider, cipher_options: {})
|
35
|
+
clear_text = force_encoding_if_needed(clear_text) if cipher_options[:deterministic]
|
36
|
+
|
37
|
+
validate_payload_type(clear_text)
|
38
|
+
serialize_message build_encrypted_message(clear_text, key_provider: key_provider, cipher_options: cipher_options)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Decrypts a +clean_text+ and returns the result as clean text
|
42
|
+
#
|
43
|
+
# === Options
|
44
|
+
#
|
45
|
+
# [:key_provider]
|
46
|
+
# Key provider to use for the encryption operation. It will default to
|
47
|
+
# +ActiveRecord::Encryption.key_provider+ when not provided
|
48
|
+
#
|
49
|
+
# [:cipher_options]
|
50
|
+
# +Cipher+-specific options that will be passed to the Cipher configured in
|
51
|
+
# +ActiveRecord::Encryption.cipher+
|
52
|
+
def decrypt(encrypted_text, key_provider: default_key_provider, cipher_options: {})
|
53
|
+
message = deserialize_message(encrypted_text)
|
54
|
+
keys = key_provider.decryption_keys(message)
|
55
|
+
raise Errors::Decryption unless keys.present?
|
56
|
+
uncompress_if_needed(cipher.decrypt(message, key: keys.collect(&:secret), **cipher_options), message.headers.compressed)
|
57
|
+
rescue *(ENCODING_ERRORS + DECRYPT_ERRORS)
|
58
|
+
raise Errors::Decryption
|
59
|
+
end
|
60
|
+
|
61
|
+
# Returns whether the text is encrypted or not
|
62
|
+
def encrypted?(text)
|
63
|
+
deserialize_message(text)
|
64
|
+
true
|
65
|
+
rescue Errors::Encoding, *DECRYPT_ERRORS
|
66
|
+
false
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
DECRYPT_ERRORS = [OpenSSL::Cipher::CipherError, Errors::EncryptedContentIntegrity, Errors::Decryption]
|
71
|
+
ENCODING_ERRORS = [EncodingError, Errors::Encoding]
|
72
|
+
THRESHOLD_TO_JUSTIFY_COMPRESSION = 140.bytes
|
73
|
+
|
74
|
+
def default_key_provider
|
75
|
+
ActiveRecord::Encryption.key_provider
|
76
|
+
end
|
77
|
+
|
78
|
+
def validate_payload_type(clear_text)
|
79
|
+
unless clear_text.is_a?(String)
|
80
|
+
raise ActiveRecord::Encryption::Errors::ForbiddenClass, "The encryptor can only encrypt string values (#{clear_text.class})"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def cipher
|
85
|
+
ActiveRecord::Encryption.cipher
|
86
|
+
end
|
87
|
+
|
88
|
+
def build_encrypted_message(clear_text, key_provider:, cipher_options:)
|
89
|
+
key = key_provider.encryption_key
|
90
|
+
|
91
|
+
clear_text, was_compressed = compress_if_worth_it(clear_text)
|
92
|
+
cipher.encrypt(clear_text, key: key.secret, **cipher_options).tap do |message|
|
93
|
+
message.headers.add(key.public_tags)
|
94
|
+
message.headers.compressed = true if was_compressed
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def serialize_message(message)
|
99
|
+
serializer.dump(message)
|
100
|
+
end
|
101
|
+
|
102
|
+
def deserialize_message(message)
|
103
|
+
raise Errors::Encoding unless message.is_a?(String)
|
104
|
+
serializer.load message
|
105
|
+
rescue ArgumentError, TypeError, Errors::ForbiddenClass
|
106
|
+
raise Errors::Encoding
|
107
|
+
end
|
108
|
+
|
109
|
+
def serializer
|
110
|
+
ActiveRecord::Encryption.message_serializer
|
111
|
+
end
|
112
|
+
|
113
|
+
# Under certain threshold, ZIP compression is actually worse that not compressing
|
114
|
+
def compress_if_worth_it(string)
|
115
|
+
if string.bytesize > THRESHOLD_TO_JUSTIFY_COMPRESSION
|
116
|
+
[compress(string), true]
|
117
|
+
else
|
118
|
+
[string, false]
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def compress(data)
|
123
|
+
Zlib::Deflate.deflate(data).tap do |compressed_data|
|
124
|
+
compressed_data.force_encoding(data.encoding)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def uncompress_if_needed(data, compressed)
|
129
|
+
if compressed
|
130
|
+
uncompress(data)
|
131
|
+
else
|
132
|
+
data
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def uncompress(data)
|
137
|
+
Zlib::Inflate.inflate(data).tap do |uncompressed_data|
|
138
|
+
uncompressed_data.force_encoding(data.encoding)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def force_encoding_if_needed(value)
|
143
|
+
if forced_encoding_for_deterministic_encryption && value && value.encoding != forced_encoding_for_deterministic_encryption
|
144
|
+
value.encode(forced_encoding_for_deterministic_encryption, invalid: :replace, undef: :replace)
|
145
|
+
else
|
146
|
+
value
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def forced_encoding_for_deterministic_encryption
|
151
|
+
ActiveRecord::Encryption.config.forced_encoding_for_deterministic_encryption
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Encryption
|
5
|
+
# Implements a simple envelope encryption approach where:
|
6
|
+
#
|
7
|
+
# * It generates a random data-encryption key for each encryption operation
|
8
|
+
# * It stores the generated key along with the encrypted payload. It encrypts this key
|
9
|
+
# with the master key provided in the credential +active_record.encryption.master key+
|
10
|
+
#
|
11
|
+
# This provider can work with multiple master keys. It will use the last one for encrypting.
|
12
|
+
#
|
13
|
+
# When `config.store_key_references` is true, it will also store a reference to
|
14
|
+
# the specific master key that was used to encrypt the data-encryption key. When not set,
|
15
|
+
# it will try all the configured master keys looking for the right one, in order to
|
16
|
+
# return the right decryption key.
|
17
|
+
class EnvelopeEncryptionKeyProvider
|
18
|
+
def encryption_key
|
19
|
+
random_secret = generate_random_secret
|
20
|
+
ActiveRecord::Encryption::Key.new(random_secret).tap do |key|
|
21
|
+
key.public_tags.encrypted_data_key = encrypt_data_key(random_secret)
|
22
|
+
key.public_tags.encrypted_data_key_id = active_primary_key.id if ActiveRecord::Encryption.config.store_key_references
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def decryption_keys(encrypted_message)
|
27
|
+
secret = decrypt_data_key(encrypted_message)
|
28
|
+
secret ? [ActiveRecord::Encryption::Key.new(secret)] : []
|
29
|
+
end
|
30
|
+
|
31
|
+
def active_primary_key
|
32
|
+
@active_primary_key ||= primary_key_provider.encryption_key
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
def encrypt_data_key(random_secret)
|
37
|
+
ActiveRecord::Encryption.cipher.encrypt(random_secret, key: active_primary_key.secret)
|
38
|
+
end
|
39
|
+
|
40
|
+
def decrypt_data_key(encrypted_message)
|
41
|
+
encrypted_data_key = encrypted_message.headers.encrypted_data_key
|
42
|
+
key = primary_key_provider.decryption_keys(encrypted_message)&.collect(&:secret)
|
43
|
+
ActiveRecord::Encryption.cipher.decrypt encrypted_data_key, key: key if key
|
44
|
+
end
|
45
|
+
|
46
|
+
def primary_key_provider
|
47
|
+
@primary_key_provider ||= DerivedSecretKeyProvider.new(ActiveRecord::Encryption.config.primary_key)
|
48
|
+
end
|
49
|
+
|
50
|
+
def generate_random_secret
|
51
|
+
ActiveRecord::Encryption.key_generator.generate_random_key
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Encryption
|
5
|
+
module Errors
|
6
|
+
class Base < StandardError; end
|
7
|
+
class Encoding < Base; end
|
8
|
+
class Decryption < Base; end
|
9
|
+
class Encryption < Base; end
|
10
|
+
class Configuration < Base; end
|
11
|
+
class ForbiddenClass < Base; end
|
12
|
+
class EncryptedContentIntegrity < Base; end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Automatically expand encrypted arguments to support querying both encrypted and unencrypted data
|
4
|
+
#
|
5
|
+
# Active Record Encryption supports querying the db using deterministic attributes. For example:
|
6
|
+
#
|
7
|
+
# Contact.find_by(email_address: "jorge@hey.com")
|
8
|
+
#
|
9
|
+
# The value "jorge@hey.com" will get encrypted automatically to perform the query. But there is
|
10
|
+
# a problem while the data is being encrypted. This won't work. During that time, you need these
|
11
|
+
# queries to be:
|
12
|
+
#
|
13
|
+
# Contact.find_by(email_address: [ "jorge@hey.com", "<encrypted jorge@hey.com>" ])
|
14
|
+
#
|
15
|
+
# This patches ActiveRecord to support this automatically. It addresses both:
|
16
|
+
#
|
17
|
+
# * ActiveRecord::Base: Used in +Contact.find_by_email_address(...)+
|
18
|
+
# * ActiveRecord::Relation: Used in +Contact.internal.find_by_email_address(...)+
|
19
|
+
#
|
20
|
+
# +ActiveRecord::Base+ relies on +ActiveRecord::Relation+ (+ActiveRecord::QueryMethods+) but it does
|
21
|
+
# some prepared statements caching. That's why we need to intercept +ActiveRecord::Base+ as soon
|
22
|
+
# as it's invoked (so that the proper prepared statement is cached).
|
23
|
+
#
|
24
|
+
# When modifying this file run performance tests in +test/performance/extended_deterministic_queries_performance_test.rb+ to
|
25
|
+
# make sure performance overhead is acceptable.
|
26
|
+
#
|
27
|
+
# We will extend this to support previous "encryption context" versions in future iterations
|
28
|
+
#
|
29
|
+
# @TODO Experimental. Support for every kind of query is pending
|
30
|
+
# @TODO It should not patch anything if not needed (no previous schemes or no support for previous encryption schemes)
|
31
|
+
module ActiveRecord
|
32
|
+
module Encryption
|
33
|
+
module ExtendedDeterministicQueries
|
34
|
+
def self.install_support
|
35
|
+
ActiveRecord::Relation.prepend(RelationQueries)
|
36
|
+
ActiveRecord::Base.include(CoreQueries)
|
37
|
+
ActiveRecord::Encryption::EncryptedAttributeType.prepend(ExtendedEncryptableType)
|
38
|
+
Arel::Nodes::HomogeneousIn.prepend(InWithAdditionalValues)
|
39
|
+
end
|
40
|
+
|
41
|
+
module EncryptedQueryArgumentProcessor
|
42
|
+
extend ActiveSupport::Concern
|
43
|
+
|
44
|
+
private
|
45
|
+
def process_encrypted_query_arguments(args, check_for_additional_values)
|
46
|
+
if args.is_a?(Array) && (options = args.first).is_a?(Hash)
|
47
|
+
self.deterministic_encrypted_attributes&.each do |attribute_name|
|
48
|
+
type = type_for_attribute(attribute_name)
|
49
|
+
if !type.previous_types.empty? && value = options[attribute_name]
|
50
|
+
options[attribute_name] = process_encrypted_query_argument(value, check_for_additional_values, type)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def process_encrypted_query_argument(value, check_for_additional_values, type)
|
57
|
+
return value if check_for_additional_values && value.is_a?(Array) && value.last.is_a?(AdditionalValue)
|
58
|
+
|
59
|
+
case value
|
60
|
+
when String, Array
|
61
|
+
list = Array(value)
|
62
|
+
list + list.flat_map do |each_value|
|
63
|
+
if check_for_additional_values && each_value.is_a?(AdditionalValue)
|
64
|
+
each_value
|
65
|
+
else
|
66
|
+
additional_values_for(each_value, type)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
else
|
70
|
+
value
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def additional_values_for(value, type)
|
75
|
+
type.previous_types.collect do |additional_type|
|
76
|
+
AdditionalValue.new(value, additional_type)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
module RelationQueries
|
82
|
+
include EncryptedQueryArgumentProcessor
|
83
|
+
|
84
|
+
def where(*args)
|
85
|
+
process_encrypted_query_arguments_if_needed(args)
|
86
|
+
super
|
87
|
+
end
|
88
|
+
|
89
|
+
def exists?(*args)
|
90
|
+
process_encrypted_query_arguments_if_needed(args)
|
91
|
+
super
|
92
|
+
end
|
93
|
+
|
94
|
+
def find_or_create_by(attributes, &block)
|
95
|
+
find_by(attributes.dup) || create(attributes, &block)
|
96
|
+
end
|
97
|
+
|
98
|
+
def find_or_create_by!(attributes, &block)
|
99
|
+
find_by(attributes.dup) || create!(attributes, &block)
|
100
|
+
end
|
101
|
+
|
102
|
+
private
|
103
|
+
def process_encrypted_query_arguments_if_needed(args)
|
104
|
+
process_encrypted_query_arguments(args, true) unless self.deterministic_encrypted_attributes&.empty?
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
module CoreQueries
|
109
|
+
extend ActiveSupport::Concern
|
110
|
+
|
111
|
+
class_methods do
|
112
|
+
include EncryptedQueryArgumentProcessor
|
113
|
+
|
114
|
+
def find_by(*args)
|
115
|
+
process_encrypted_query_arguments(args, false) unless self.deterministic_encrypted_attributes&.empty?
|
116
|
+
super
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
class AdditionalValue
|
122
|
+
attr_reader :value, :type
|
123
|
+
|
124
|
+
def initialize(value, type)
|
125
|
+
@type = type
|
126
|
+
@value = process(value)
|
127
|
+
end
|
128
|
+
|
129
|
+
private
|
130
|
+
def process(value)
|
131
|
+
type.serialize(value)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
module ExtendedEncryptableType
|
136
|
+
def serialize(data)
|
137
|
+
if data.is_a?(AdditionalValue)
|
138
|
+
data.value
|
139
|
+
else
|
140
|
+
super
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
module InWithAdditionalValues
|
146
|
+
def proc_for_binds
|
147
|
+
-> value { ActiveModel::Attribute.with_cast_value(attribute.name, value, encryption_aware_type_caster) }
|
148
|
+
end
|
149
|
+
|
150
|
+
def encryption_aware_type_caster
|
151
|
+
if attribute.type_caster.is_a?(ActiveRecord::Encryption::EncryptedAttributeType)
|
152
|
+
attribute.type_caster.cast_type
|
153
|
+
else
|
154
|
+
attribute.type_caster
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Encryption
|
5
|
+
module ExtendedDeterministicUniquenessValidator
|
6
|
+
def self.install_support
|
7
|
+
ActiveRecord::Validations::UniquenessValidator.prepend(EncryptedUniquenessValidator)
|
8
|
+
end
|
9
|
+
|
10
|
+
module EncryptedUniquenessValidator
|
11
|
+
def validate_each(record, attribute, value)
|
12
|
+
super(record, attribute, value)
|
13
|
+
|
14
|
+
klass = record.class
|
15
|
+
if klass.deterministic_encrypted_attributes&.each do |attribute_name|
|
16
|
+
encrypted_type = klass.type_for_attribute(attribute_name)
|
17
|
+
[ encrypted_type, *encrypted_type.previous_types ].each do |type|
|
18
|
+
encrypted_value = type.serialize(value)
|
19
|
+
ActiveRecord::Encryption.without_encryption do
|
20
|
+
super(record, attribute, encrypted_value)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Encryption
|
5
|
+
# A key is a container for a given +secret+
|
6
|
+
#
|
7
|
+
# Optionally, it can include +public_tags+. These tags are meant to be stored
|
8
|
+
# in clean (public) and can be used, for example, to include information that
|
9
|
+
# references the key for a future retrieval operation.
|
10
|
+
class Key
|
11
|
+
attr_reader :secret, :public_tags
|
12
|
+
|
13
|
+
def initialize(secret)
|
14
|
+
@secret = secret
|
15
|
+
@public_tags = Properties.new
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.derive_from(password)
|
19
|
+
secret = ActiveRecord::Encryption.key_generator.derive_key_from(password)
|
20
|
+
ActiveRecord::Encryption::Key.new(secret)
|
21
|
+
end
|
22
|
+
|
23
|
+
def id
|
24
|
+
Digest::SHA1.hexdigest(secret).first(4)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "securerandom"
|
4
|
+
|
5
|
+
module ActiveRecord
|
6
|
+
module Encryption
|
7
|
+
# Utility for generating and deriving random keys.
|
8
|
+
class KeyGenerator
|
9
|
+
# Returns a random key. The key will have a size in bytes of +:length+ (configured +Cipher+'s length by default)
|
10
|
+
def generate_random_key(length: key_length)
|
11
|
+
SecureRandom.random_bytes(length)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Returns a random key in hexadecimal format. The key will have a size in bytes of +:length+ (configured +Cipher+'s
|
15
|
+
# length by default)
|
16
|
+
#
|
17
|
+
# Hexadecimal format is handy for representing keys as printable text. To maximize the space of characters used, it is
|
18
|
+
# good practice including not printable characters. Hexadecimal format ensures that generated keys are representable with
|
19
|
+
# plain text
|
20
|
+
#
|
21
|
+
# To convert back to the original string with the desired length:
|
22
|
+
#
|
23
|
+
# [ value ].pack("H*")
|
24
|
+
def generate_random_hex_key(length: key_length)
|
25
|
+
generate_random_key(length: length).unpack("H*")[0]
|
26
|
+
end
|
27
|
+
|
28
|
+
# Derives a key from the given password. The key will have a size in bytes of +:length+ (configured +Cipher+'s length
|
29
|
+
# by default)
|
30
|
+
#
|
31
|
+
# The generated key will be salted with the value of +ActiveRecord::Encryption.key_derivation_salt+
|
32
|
+
def derive_key_from(password, length: key_length)
|
33
|
+
ActiveSupport::KeyGenerator.new(password).generate_key(ActiveRecord::Encryption.config.key_derivation_salt, length)
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
def key_length
|
38
|
+
@key_length ||= ActiveRecord::Encryption.cipher.key_length
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|