activerecord 6.1.4.1 → 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 +729 -1161
- 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 +24 -25
- 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 -49
- 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/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 +14 -7
- data/lib/active_record/connection_adapters/abstract/schema_creation.rb +4 -17
- data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +30 -9
- 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 -21
- data/lib/active_record/connection_adapters/mysql/quoting.rb +16 -1
- data/lib/active_record/connection_adapters/mysql/schema_statements.rb +3 -0
- 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 -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 +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 -6
- 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 +157 -100
- data/lib/active_record/connection_adapters/schema_cache.rb +26 -3
- data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +23 -17
- 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 +113 -112
- 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 +18 -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 +3 -5
- data/lib/active_record/legacy_yaml_adapter.rb +1 -1
- 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 +83 -1
- 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 +46 -32
- 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 +80 -56
- 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 +41 -28
- data/lib/active_record/relation/delegation.rb +6 -6
- data/lib/active_record/relation/finder_methods.rb +32 -23
- 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 +232 -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 +10 -6
- 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/tasks/database_tasks.rb +107 -23
- 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 +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.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 +55 -16
@@ -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
|