activerecord 6.1.4.1 → 7.0.1
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 +1132 -936
- 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 +33 -17
- 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 +10 -3
- 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 +34 -27
- 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/join_dependency.rb +6 -2
- data/lib/active_record/associations/preloader/association.rb +187 -55
- data/lib/active_record/associations/preloader/batch.rb +48 -0
- data/lib/active_record/associations/preloader/branch.rb +147 -0
- data/lib/active_record/associations/preloader/through_association.rb +49 -13
- data/lib/active_record/associations/preloader.rb +39 -113
- data/lib/active_record/associations/singular_association.rb +8 -2
- data/lib/active_record/associations/through_association.rb +3 -3
- data/lib/active_record/associations.rb +118 -90
- data/lib/active_record/asynchronous_queries_tracker.rb +60 -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 +49 -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 +13 -14
- data/lib/active_record/attributes.rb +24 -35
- data/lib/active_record/autosave_association.rb +6 -21
- data/lib/active_record/base.rb +19 -1
- data/lib/active_record/callbacks.rb +2 -2
- data/lib/active_record/connection_adapters/abstract/connection_handler.rb +292 -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 +47 -561
- data/lib/active_record/connection_adapters/abstract/database_limits.rb +0 -17
- data/lib/active_record/connection_adapters/abstract/database_statements.rb +46 -22
- data/lib/active_record/connection_adapters/abstract/query_cache.rb +24 -12
- data/lib/active_record/connection_adapters/abstract/quoting.rb +42 -72
- data/lib/active_record/connection_adapters/abstract/schema_creation.rb +4 -17
- data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +34 -9
- data/lib/active_record/connection_adapters/abstract/schema_statements.rb +78 -22
- data/lib/active_record/connection_adapters/abstract/transaction.rb +15 -22
- data/lib/active_record/connection_adapters/abstract_adapter.rb +149 -74
- data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +97 -81
- data/lib/active_record/connection_adapters/column.rb +4 -0
- data/lib/active_record/connection_adapters/mysql/database_statements.rb +37 -23
- data/lib/active_record/connection_adapters/mysql/quoting.rb +35 -21
- data/lib/active_record/connection_adapters/mysql/schema_statements.rb +5 -1
- data/lib/active_record/connection_adapters/mysql2_adapter.rb +12 -6
- data/lib/active_record/connection_adapters/pool_config.rb +7 -7
- data/lib/active_record/connection_adapters/postgresql/column.rb +17 -1
- data/lib/active_record/connection_adapters/postgresql/database_statements.rb +21 -12
- 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 +30 -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 +50 -50
- data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +32 -0
- data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +21 -1
- data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +22 -1
- data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +25 -0
- data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +29 -18
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +205 -105
- data/lib/active_record/connection_adapters/schema_cache.rb +29 -4
- data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +27 -19
- data/lib/active_record/connection_adapters/sqlite3/quoting.rb +28 -16
- data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +16 -14
- data/lib/active_record/connection_adapters/sqlite3_adapter.rb +89 -30
- data/lib/active_record/connection_adapters.rb +6 -5
- data/lib/active_record/connection_handling.rb +47 -53
- data/lib/active_record/core.rb +122 -132
- data/lib/active_record/database_configurations/connection_url_resolver.rb +2 -1
- data/lib/active_record/database_configurations/database_config.rb +12 -9
- data/lib/active_record/database_configurations/hash_config.rb +63 -5
- data/lib/active_record/database_configurations/url_config.rb +2 -2
- data/lib/active_record/database_configurations.rb +16 -32
- data/lib/active_record/delegated_type.rb +52 -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 +28 -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 +90 -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 +49 -42
- data/lib/active_record/errors.rb +67 -4
- data/lib/active_record/explain_registry.rb +11 -6
- data/lib/active_record/fixture_set/file.rb +15 -1
- data/lib/active_record/fixture_set/table_row.rb +41 -6
- data/lib/active_record/fixture_set/table_rows.rb +4 -4
- data/lib/active_record/fixtures.rb +17 -20
- 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 +80 -14
- data/lib/active_record/integration.rb +4 -3
- data/lib/active_record/internal_metadata.rb +3 -5
- data/lib/active_record/legacy_yaml_adapter.rb +2 -39
- data/lib/active_record/locking/optimistic.rb +10 -9
- data/lib/active_record/locking/pessimistic.rb +9 -3
- data/lib/active_record/log_subscriber.rb +14 -3
- 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/middleware/shard_selector.rb +60 -0
- data/lib/active_record/migration/command_recorder.rb +4 -4
- data/lib/active_record/migration/compatibility.rb +107 -3
- 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 -58
- data/lib/active_record/nested_attributes.rb +13 -12
- data/lib/active_record/no_touching.rb +3 -3
- data/lib/active_record/null_relation.rb +2 -6
- data/lib/active_record/persistence.rb +219 -52
- data/lib/active_record/query_cache.rb +2 -2
- data/lib/active_record/query_logs.rb +138 -0
- data/lib/active_record/querying.rb +15 -5
- data/lib/active_record/railtie.rb +127 -17
- data/lib/active_record/railties/controller_runtime.rb +1 -1
- data/lib/active_record/railties/databases.rake +66 -129
- data/lib/active_record/readonly_attributes.rb +11 -0
- data/lib/active_record/reflection.rb +67 -50
- 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 +43 -38
- data/lib/active_record/relation/delegation.rb +6 -6
- data/lib/active_record/relation/finder_methods.rb +31 -35
- 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 +243 -61
- data/lib/active_record/relation/record_fetch_warning.rb +7 -9
- data/lib/active_record/relation/spawn_methods.rb +2 -2
- data/lib/active_record/relation/where_clause.rb +10 -19
- data/lib/active_record/relation.rb +184 -84
- data/lib/active_record/result.rb +17 -7
- data/lib/active_record/runtime_registry.rb +9 -13
- data/lib/active_record/sanitization.rb +11 -7
- data/lib/active_record/schema_dumper.rb +10 -3
- data/lib/active_record/schema_migration.rb +4 -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 +64 -34
- data/lib/active_record/serialization.rb +1 -1
- data/lib/active_record/signed_id.rb +1 -1
- data/lib/active_record/suppressor.rb +11 -15
- data/lib/active_record/tasks/database_tasks.rb +120 -58
- data/lib/active_record/tasks/mysql_database_tasks.rb +1 -1
- data/lib/active_record/tasks/postgresql_database_tasks.rb +19 -12
- data/lib/active_record/test_databases.rb +1 -1
- data/lib/active_record/test_fixtures.rb +4 -4
- 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/validations/uniqueness.rb +1 -1
- data/lib/active_record.rb +204 -28
- data/lib/arel/attributes/attribute.rb +0 -8
- data/lib/arel/crud.rb +28 -22
- data/lib/arel/delete_manager.rb +18 -4
- data/lib/arel/filter_predications.rb +9 -0
- data/lib/arel/insert_manager.rb +2 -3
- data/lib/arel/nodes/casted.rb +1 -1
- data/lib/arel/nodes/delete_statement.rb +12 -13
- data/lib/arel/nodes/filter.rb +10 -0
- data/lib/arel/nodes/function.rb +1 -0
- 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 +8 -3
- data/lib/arel/nodes.rb +1 -0
- data/lib/arel/predications.rb +11 -3
- 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 +18 -4
- data/lib/arel/visitors/dot.rb +80 -90
- data/lib/arel/visitors/mysql.rb +8 -2
- data/lib/arel/visitors/postgresql.rb +0 -10
- data/lib/arel/visitors/to_sql.rb +58 -2
- data/lib/arel.rb +2 -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
- data/lib/rails/generators/active_record/multi_db/multi_db_generator.rb +16 -0
- data/lib/rails/generators/active_record/multi_db/templates/multi_db.rb.tt +44 -0
- metadata +59 -14
@@ -0,0 +1,160 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Encryption
|
5
|
+
# Automatically expand encrypted arguments to support querying both encrypted and unencrypted data
|
6
|
+
#
|
7
|
+
# Active Record Encryption supports querying the db using deterministic attributes. For example:
|
8
|
+
#
|
9
|
+
# Contact.find_by(email_address: "jorge@hey.com")
|
10
|
+
#
|
11
|
+
# The value "jorge@hey.com" will get encrypted automatically to perform the query. But there is
|
12
|
+
# a problem while the data is being encrypted. This won't work. During that time, you need these
|
13
|
+
# queries to be:
|
14
|
+
#
|
15
|
+
# Contact.find_by(email_address: [ "jorge@hey.com", "<encrypted jorge@hey.com>" ])
|
16
|
+
#
|
17
|
+
# This patches ActiveRecord to support this automatically. It addresses both:
|
18
|
+
#
|
19
|
+
# * ActiveRecord::Base: Used in +Contact.find_by_email_address(...)+
|
20
|
+
# * ActiveRecord::Relation: Used in +Contact.internal.find_by_email_address(...)+
|
21
|
+
#
|
22
|
+
# +ActiveRecord::Base+ relies on +ActiveRecord::Relation+ (+ActiveRecord::QueryMethods+) but it does
|
23
|
+
# some prepared statements caching. That's why we need to intercept +ActiveRecord::Base+ as soon
|
24
|
+
# as it's invoked (so that the proper prepared statement is cached).
|
25
|
+
#
|
26
|
+
# When modifying this file run performance tests in +test/performance/extended_deterministic_queries_performance_test.rb+ to
|
27
|
+
# make sure performance overhead is acceptable.
|
28
|
+
#
|
29
|
+
# We will extend this to support previous "encryption context" versions in future iterations
|
30
|
+
#
|
31
|
+
# @TODO Experimental. Support for every kind of query is pending
|
32
|
+
# @TODO It should not patch anything if not needed (no previous schemes or no support for previous encryption schemes)
|
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,28 @@
|
|
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
|
+
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
|
@@ -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
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Encryption
|
5
|
+
# A +KeyProvider+ serves keys:
|
6
|
+
#
|
7
|
+
# * An encryption key
|
8
|
+
# * A list of potential decryption keys. Serving multiple decryption keys supports rotation-schemes
|
9
|
+
# where new keys are added but old keys need to continue working
|
10
|
+
class KeyProvider
|
11
|
+
def initialize(keys)
|
12
|
+
@keys = Array(keys)
|
13
|
+
end
|
14
|
+
|
15
|
+
# Returns the first key in the list as the active key to perform encryptions
|
16
|
+
#
|
17
|
+
# When +ActiveRecord::Encryption.config.store_key_references+ is true, the key will include
|
18
|
+
# a public tag referencing the key itself. That key will be stored in the public
|
19
|
+
# headers of the encrypted message
|
20
|
+
def encryption_key
|
21
|
+
@encryption_key ||= @keys.last.tap do |key|
|
22
|
+
key.public_tags.encrypted_data_key_id = key.id if ActiveRecord::Encryption.config.store_key_references
|
23
|
+
end
|
24
|
+
|
25
|
+
@encryption_key
|
26
|
+
end
|
27
|
+
|
28
|
+
# Returns the list of decryption keys
|
29
|
+
#
|
30
|
+
# When the message holds a reference to its encryption key, it will return an array
|
31
|
+
# with that key. If not, it will return the list of keys.
|
32
|
+
def decryption_keys(encrypted_message)
|
33
|
+
if encrypted_message.headers.encrypted_data_key_id
|
34
|
+
keys_grouped_by_id[encrypted_message.headers.encrypted_data_key_id]
|
35
|
+
else
|
36
|
+
@keys
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
def keys_grouped_by_id
|
42
|
+
@keys_grouped_by_id ||= @keys.group_by(&:id)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Encryption
|
5
|
+
# A message defines the structure of the data we store in encrypted attributes. It contains:
|
6
|
+
#
|
7
|
+
# * An encrypted payload
|
8
|
+
# * A list of unencrypted headers
|
9
|
+
#
|
10
|
+
# See +Encryptor#encrypt+
|
11
|
+
class Message
|
12
|
+
attr_accessor :payload, :headers
|
13
|
+
|
14
|
+
def initialize(payload: nil, headers: {})
|
15
|
+
validate_payload_type(payload)
|
16
|
+
|
17
|
+
@payload = payload
|
18
|
+
@headers = Properties.new(headers)
|
19
|
+
end
|
20
|
+
|
21
|
+
def ==(other_message)
|
22
|
+
payload == other_message.payload && headers == other_message.headers
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
def validate_payload_type(payload)
|
27
|
+
unless payload.is_a?(String) || payload.nil?
|
28
|
+
raise ActiveRecord::Encryption::Errors::ForbiddenClass, "Only string payloads allowed"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Encryption
|
5
|
+
# A message serializer that serializes +Messages+ with JSON.
|
6
|
+
#
|
7
|
+
# The generated structure is pretty simple:
|
8
|
+
#
|
9
|
+
# {
|
10
|
+
# p: <payload>,
|
11
|
+
# h: {
|
12
|
+
# header1: value1,
|
13
|
+
# header2: value2,
|
14
|
+
# ...
|
15
|
+
# }
|
16
|
+
# }
|
17
|
+
#
|
18
|
+
# Both the payload and the header values are encoded with Base64
|
19
|
+
# to prevent JSON parsing errors and encoding issues when
|
20
|
+
# storing the resulting serialized data.
|
21
|
+
class MessageSerializer
|
22
|
+
def load(serialized_content)
|
23
|
+
data = JSON.parse(serialized_content)
|
24
|
+
parse_message(data, 1)
|
25
|
+
rescue JSON::ParserError
|
26
|
+
raise ActiveRecord::Encryption::Errors::Encoding
|
27
|
+
end
|
28
|
+
|
29
|
+
def dump(message)
|
30
|
+
raise ActiveRecord::Encryption::Errors::ForbiddenClass unless message.is_a?(ActiveRecord::Encryption::Message)
|
31
|
+
JSON.dump message_to_json(message)
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
def parse_message(data, level)
|
36
|
+
validate_message_data_format(data, level)
|
37
|
+
ActiveRecord::Encryption::Message.new(payload: decode_if_needed(data["p"]), headers: parse_properties(data["h"], level))
|
38
|
+
end
|
39
|
+
|
40
|
+
def validate_message_data_format(data, level)
|
41
|
+
if level > 2
|
42
|
+
raise ActiveRecord::Encryption::Errors::Decryption, "More than one level of hash nesting in headers is not supported"
|
43
|
+
end
|
44
|
+
|
45
|
+
unless data.is_a?(Hash) && data.has_key?("p")
|
46
|
+
raise ActiveRecord::Encryption::Errors::Decryption, "Invalid data format: hash without payload"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def parse_properties(headers, level)
|
51
|
+
ActiveRecord::Encryption::Properties.new.tap do |properties|
|
52
|
+
headers&.each do |key, value|
|
53
|
+
properties[key] = value.is_a?(Hash) ? parse_message(value, level + 1) : decode_if_needed(value)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def message_to_json(message)
|
59
|
+
{
|
60
|
+
p: encode_if_needed(message.payload),
|
61
|
+
h: headers_to_json(message.headers)
|
62
|
+
}
|
63
|
+
end
|
64
|
+
|
65
|
+
def headers_to_json(headers)
|
66
|
+
headers.transform_values do |value|
|
67
|
+
value.is_a?(ActiveRecord::Encryption::Message) ? message_to_json(value) : encode_if_needed(value)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def encode_if_needed(value)
|
72
|
+
if value.is_a?(String)
|
73
|
+
::Base64.strict_encode64 value
|
74
|
+
else
|
75
|
+
value
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def decode_if_needed(value)
|
80
|
+
if value.is_a?(String)
|
81
|
+
::Base64.strict_decode64(value)
|
82
|
+
else
|
83
|
+
value
|
84
|
+
end
|
85
|
+
rescue ArgumentError, TypeError
|
86
|
+
raise Errors::Encoding
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Encryption
|
5
|
+
# An encryptor that won't decrypt or encrypt. It will just return the passed
|
6
|
+
# values
|
7
|
+
class NullEncryptor
|
8
|
+
def encrypt(clean_text, key_provider: nil, cipher_options: {})
|
9
|
+
clean_text
|
10
|
+
end
|
11
|
+
|
12
|
+
def decrypt(encrypted_text, key_provider: nil, cipher_options: {})
|
13
|
+
encrypted_text
|
14
|
+
end
|
15
|
+
|
16
|
+
def encrypted?(text)
|
17
|
+
false
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Encryption
|
5
|
+
# This is a wrapper for a hash of encryption properties. It is used by
|
6
|
+
# +Key+ (public tags) and +Message+ (headers).
|
7
|
+
#
|
8
|
+
# Since properties are serialized in messages, it is important for storage
|
9
|
+
# efficiency to keep their keys as short as possible. It defines accessors
|
10
|
+
# for common properties that will keep these keys very short while exposing
|
11
|
+
# a readable name.
|
12
|
+
#
|
13
|
+
# message.headers.encrypted_data_key # instead of message.headers[:k]
|
14
|
+
#
|
15
|
+
# See +Properties#DEFAULT_PROPERTIES+, +Key+, +Message+
|
16
|
+
class Properties
|
17
|
+
ALLOWED_VALUE_CLASSES = [String, ActiveRecord::Encryption::Message, Numeric, TrueClass, FalseClass, Symbol, NilClass]
|
18
|
+
|
19
|
+
delegate_missing_to :data
|
20
|
+
delegate :==, to: :data
|
21
|
+
|
22
|
+
# For each entry it generates an accessor exposing the full name
|
23
|
+
DEFAULT_PROPERTIES = {
|
24
|
+
encrypted_data_key: "k",
|
25
|
+
encrypted_data_key_id: "i",
|
26
|
+
compressed: "c",
|
27
|
+
iv: "iv",
|
28
|
+
auth_tag: "at",
|
29
|
+
encoding: "e"
|
30
|
+
}
|
31
|
+
|
32
|
+
DEFAULT_PROPERTIES.each do |name, key|
|
33
|
+
define_method name do
|
34
|
+
self[key.to_sym]
|
35
|
+
end
|
36
|
+
|
37
|
+
define_method "#{name}=" do |value|
|
38
|
+
self[key.to_sym] = value
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def initialize(initial_properties = {})
|
43
|
+
@data = {}
|
44
|
+
add(initial_properties)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Set a value for a given key
|
48
|
+
#
|
49
|
+
# It will raise an +EncryptedContentIntegrity+ if the value exists
|
50
|
+
def []=(key, value)
|
51
|
+
raise Errors::EncryptedContentIntegrity, "Properties can't be overridden: #{key}" if key?(key)
|
52
|
+
validate_value_type(value)
|
53
|
+
data[key] = value
|
54
|
+
end
|
55
|
+
|
56
|
+
def validate_value_type(value)
|
57
|
+
unless ALLOWED_VALUE_CLASSES.find { |klass| value.is_a?(klass) }
|
58
|
+
raise ActiveRecord::Encryption::Errors::ForbiddenClass, "Can't store a #{value.class}, only properties of type #{ALLOWED_VALUE_CLASSES.inspect} are allowed"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def add(other_properties)
|
63
|
+
other_properties.each do |key, value|
|
64
|
+
self[key.to_sym] = value
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def to_h
|
69
|
+
data
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
attr_reader :data
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Encryption
|
5
|
+
# A +NullEncryptor+ that will raise an error when trying to encrypt data
|
6
|
+
#
|
7
|
+
# This is useful when you want to reveal ciphertexts for debugging purposes
|
8
|
+
# and you want to make sure you won't overwrite any encryptable attribute with
|
9
|
+
# the wrong content.
|
10
|
+
class ReadOnlyNullEncryptor
|
11
|
+
def encrypt(clean_text, key_provider: nil, cipher_options: {})
|
12
|
+
raise Errors::Encryption, "This encryptor is read-only"
|
13
|
+
end
|
14
|
+
|
15
|
+
def decrypt(encrypted_text, key_provider: nil, cipher_options: {})
|
16
|
+
encrypted_text
|
17
|
+
end
|
18
|
+
|
19
|
+
def encrypted?(text)
|
20
|
+
false
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Encryption
|
5
|
+
# A container of attribute encryption options.
|
6
|
+
#
|
7
|
+
# It validates and serves attribute encryption options.
|
8
|
+
#
|
9
|
+
# See +EncryptedAttributeType+, +Context+
|
10
|
+
class Scheme
|
11
|
+
attr_accessor :previous_schemes
|
12
|
+
|
13
|
+
def initialize(key_provider: nil, key: nil, deterministic: nil, downcase: nil, ignore_case: nil,
|
14
|
+
previous_schemes: nil, **context_properties)
|
15
|
+
# Initializing all attributes to +nil+ as we want to allow a "not set" semantics so that we
|
16
|
+
# can merge schemes without overriding values with defaults. See +#merge+
|
17
|
+
|
18
|
+
@key_provider_param = key_provider
|
19
|
+
@key = key
|
20
|
+
@deterministic = deterministic
|
21
|
+
@downcase = downcase || ignore_case
|
22
|
+
@ignore_case = ignore_case
|
23
|
+
@previous_schemes_param = previous_schemes
|
24
|
+
@previous_schemes = Array.wrap(previous_schemes)
|
25
|
+
@context_properties = context_properties
|
26
|
+
|
27
|
+
validate_config!
|
28
|
+
end
|
29
|
+
|
30
|
+
def ignore_case?
|
31
|
+
@ignore_case
|
32
|
+
end
|
33
|
+
|
34
|
+
def downcase?
|
35
|
+
@downcase
|
36
|
+
end
|
37
|
+
|
38
|
+
def deterministic?
|
39
|
+
@deterministic
|
40
|
+
end
|
41
|
+
|
42
|
+
def fixed?
|
43
|
+
# by default deterministic encryption is fixed
|
44
|
+
@fixed ||= @deterministic && (!@deterministic.is_a?(Hash) || @deterministic[:fixed])
|
45
|
+
end
|
46
|
+
|
47
|
+
def key_provider
|
48
|
+
@key_provider ||= begin
|
49
|
+
validate_keys!
|
50
|
+
@key_provider_param || build_key_provider
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def merge(other_scheme)
|
55
|
+
self.class.new(**to_h.merge(other_scheme.to_h))
|
56
|
+
end
|
57
|
+
|
58
|
+
def to_h
|
59
|
+
{ key_provider: @key_provider_param, key: @key, deterministic: @deterministic, downcase: @downcase, ignore_case: @ignore_case,
|
60
|
+
previous_schemes: @previous_schemes_param, **@context_properties }.compact
|
61
|
+
end
|
62
|
+
|
63
|
+
def with_context(&block)
|
64
|
+
if @context_properties.present?
|
65
|
+
ActiveRecord::Encryption.with_encryption_context(**@context_properties, &block)
|
66
|
+
else
|
67
|
+
block.call
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
def validate_config!
|
73
|
+
raise Errors::Configuration, "ignore_case: can only be used with deterministic encryption" if @ignore_case && !@deterministic
|
74
|
+
raise Errors::Configuration, "key_provider: and key: can't be used simultaneously" if @key_provider_param && @key
|
75
|
+
end
|
76
|
+
|
77
|
+
def validate_keys!
|
78
|
+
validate_credential :key_derivation_salt
|
79
|
+
validate_credential :primary_key, "needs to be configured to use non-deterministic encryption" unless @deterministic
|
80
|
+
validate_credential :deterministic_key, "needs to be configured to use deterministic encryption" if @deterministic
|
81
|
+
end
|
82
|
+
|
83
|
+
def validate_credential(key, error_message = "is not configured")
|
84
|
+
unless ActiveRecord::Encryption.config.public_send(key).present?
|
85
|
+
raise Errors::Configuration, "#{key} #{error_message}. Please configure it via credential "\
|
86
|
+
"active_record_encryption.#{key} or by setting config.active_record.encryption.#{key}"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def build_key_provider
|
91
|
+
return DerivedSecretKeyProvider.new(@key) if @key.present?
|
92
|
+
|
93
|
+
if @deterministic && (deterministic_key = ActiveRecord::Encryption.config.deterministic_key)
|
94
|
+
DeterministicKeyProvider.new(deterministic_key)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|