activerecord 6.1.7.4 → 7.0.0
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 +1055 -1170
- 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 +18 -19
- 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 +186 -52
- 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 +90 -82
- 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/coders/yaml_column.rb +2 -14
- 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 +43 -82
- data/lib/active_record/connection_adapters/abstract/schema_creation.rb +4 -17
- data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +34 -13
- data/lib/active_record/connection_adapters/abstract/schema_statements.rb +69 -18
- 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 +35 -23
- data/lib/active_record/connection_adapters/mysql/quoting.rb +35 -21
- 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 +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 +50 -76
- 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 +27 -16
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +207 -107
- data/lib/active_record/connection_adapters/schema_cache.rb +39 -38
- data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +25 -19
- data/lib/active_record/connection_adapters/sqlite3/quoting.rb +15 -16
- 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 +47 -53
- data/lib/active_record/core.rb +121 -146
- data/lib/active_record/database_configurations/connection_url_resolver.rb +1 -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 +15 -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 +1 -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 +89 -10
- data/lib/active_record/migration/join_table.rb +1 -1
- data/lib/active_record/migration.rb +110 -80
- 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 +40 -36
- 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 +235 -63
- 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 +169 -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/store.rb +1 -6
- data/lib/active_record/suppressor.rb +11 -15
- data/lib/active_record/tasks/database_tasks.rb +116 -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 +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/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 +58 -14
@@ -0,0 +1,208 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Encryption
|
5
|
+
# This is the concern mixed in Active Record models to make them encryptable. It adds the +encrypts+
|
6
|
+
# attribute declaration, as well as the API to encrypt and decrypt records.
|
7
|
+
module EncryptableRecord
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
included do
|
11
|
+
class_attribute :encrypted_attributes
|
12
|
+
|
13
|
+
validate :cant_modify_encrypted_attributes_when_frozen, if: -> { has_encrypted_attributes? && ActiveRecord::Encryption.context.frozen_encryption? }
|
14
|
+
end
|
15
|
+
|
16
|
+
class_methods do
|
17
|
+
# Encrypts the +name+ attribute.
|
18
|
+
#
|
19
|
+
# === Options
|
20
|
+
#
|
21
|
+
# * <tt>:key_provider</tt> - Configure a +KeyProvider+ for serving the keys to encrypt and
|
22
|
+
# decrypt this attribute. If not provided, it will default to +ActiveRecord::Encryption.key_provider+.
|
23
|
+
# * <tt>:key</tt> - A password to derive the key from. It's a shorthand for a +:key_provider+ that
|
24
|
+
# serves derivated keys. Both options can't be used at the same time.
|
25
|
+
# * <tt>:key_provider</tt> - Set a +:key_provider+ to provide encryption and decryption keys. If not
|
26
|
+
# provided, it will default to the key provider set with `config.key_provider`.
|
27
|
+
# * <tt>:deterministic</tt> - By default, encryption is not deterministic. It will use a random
|
28
|
+
# initialization vector for each encryption operation. This means that encrypting the same content
|
29
|
+
# with the same key twice will generate different ciphertexts. When set to +true+, it will generate the
|
30
|
+
# initialization vector based on the encrypted content. This means that the same content will generate
|
31
|
+
# the same ciphertexts. This enables querying encrypted text with Active Record. Deterministic encryption
|
32
|
+
# will use the oldest encryption scheme to encrypt new data by default. You can change this by setting
|
33
|
+
# +deterministic: { fixed: false }+. That will make it use the newest encryption scheme for encrypting new
|
34
|
+
# data.
|
35
|
+
# * <tt>:downcase</tt> - When true, it converts the encrypted content to downcase automatically. This allows to
|
36
|
+
# effectively ignore case when querying data. Notice that the case is lost. Use +:ignore_case+ if you are interested
|
37
|
+
# in preserving it.
|
38
|
+
# * <tt>:ignore_case</tt> - When true, it behaves like +:downcase+ but, it also preserves the original case in a specially
|
39
|
+
# designated column +original_<name>+. When reading the encrypted content, the version with the original case is
|
40
|
+
# served. But you can still execute queries that will ignore the case. This option can only be used when +:deterministic+
|
41
|
+
# is true.
|
42
|
+
# * <tt>:context_properties</tt> - Additional properties that will override +Context+ settings when this attribute is
|
43
|
+
# encrypted and decrypted. E.g: +encryptor:+, +cipher:+, +message_serializer:+, etc.
|
44
|
+
# * <tt>:previous</tt> - List of previous encryption schemes. When provided, they will be used in order when trying to read
|
45
|
+
# the attribute. Each entry of the list can contain the properties supported by #encrypts. Also, when deterministic
|
46
|
+
# encryption is used, they will be used to generate additional ciphertexts to check in the queries.
|
47
|
+
def encrypts(*names, key_provider: nil, key: nil, deterministic: false, downcase: false, ignore_case: false, previous: [], **context_properties)
|
48
|
+
self.encrypted_attributes ||= Set.new # not using :default because the instance would be shared across classes
|
49
|
+
scheme = scheme_for key_provider: key_provider, key: key, deterministic: deterministic, downcase: downcase, \
|
50
|
+
ignore_case: ignore_case, previous: previous, **context_properties
|
51
|
+
|
52
|
+
names.each do |name|
|
53
|
+
encrypt_attribute name, scheme
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Returns the list of deterministic encryptable attributes in the model class.
|
58
|
+
def deterministic_encrypted_attributes
|
59
|
+
@deterministic_encrypted_attributes ||= encrypted_attributes&.find_all do |attribute_name|
|
60
|
+
type_for_attribute(attribute_name).deterministic?
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Given a attribute name, it returns the name of the source attribute when it's a preserved one.
|
65
|
+
def source_attribute_from_preserved_attribute(attribute_name)
|
66
|
+
attribute_name.to_s.sub(ORIGINAL_ATTRIBUTE_PREFIX, "") if /^#{ORIGINAL_ATTRIBUTE_PREFIX}/.match?(attribute_name)
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
def scheme_for(key_provider: nil, key: nil, deterministic: false, downcase: false, ignore_case: false, previous: [], **context_properties)
|
71
|
+
ActiveRecord::Encryption::Scheme.new(key_provider: key_provider, key: key, deterministic: deterministic,
|
72
|
+
downcase: downcase, ignore_case: ignore_case, **context_properties).tap do |scheme|
|
73
|
+
scheme.previous_schemes = global_previous_schemes_for(scheme) +
|
74
|
+
Array.wrap(previous).collect { |scheme_config| ActiveRecord::Encryption::Scheme.new(**scheme_config) }
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def global_previous_schemes_for(scheme)
|
79
|
+
ActiveRecord::Encryption.config.previous_schemes.collect do |previous_scheme|
|
80
|
+
scheme.merge(previous_scheme)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def encrypt_attribute(name, attribute_scheme)
|
85
|
+
encrypted_attributes << name.to_sym
|
86
|
+
|
87
|
+
attribute name do |cast_type|
|
88
|
+
ActiveRecord::Encryption::EncryptedAttributeType.new scheme: attribute_scheme, cast_type: cast_type
|
89
|
+
end
|
90
|
+
|
91
|
+
preserve_original_encrypted(name) if attribute_scheme.ignore_case?
|
92
|
+
ActiveRecord::Encryption.encrypted_attribute_was_declared(self, name)
|
93
|
+
end
|
94
|
+
|
95
|
+
def preserve_original_encrypted(name)
|
96
|
+
original_attribute_name = "#{ORIGINAL_ATTRIBUTE_PREFIX}#{name}".to_sym
|
97
|
+
|
98
|
+
if !ActiveRecord::Encryption.config.support_unencrypted_data && !column_names.include?(original_attribute_name.to_s)
|
99
|
+
raise Errors::Configuration, "To use :ignore_case for '#{name}' you must create an additional column named '#{original_attribute_name}'"
|
100
|
+
end
|
101
|
+
|
102
|
+
encrypts original_attribute_name
|
103
|
+
override_accessors_to_preserve_original name, original_attribute_name
|
104
|
+
end
|
105
|
+
|
106
|
+
def override_accessors_to_preserve_original(name, original_attribute_name)
|
107
|
+
include(Module.new do
|
108
|
+
define_method name do
|
109
|
+
if ((value = super()) && encrypted_attribute?(name)) || !ActiveRecord::Encryption.config.support_unencrypted_data
|
110
|
+
send(original_attribute_name)
|
111
|
+
else
|
112
|
+
value
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
define_method "#{name}=" do |value|
|
117
|
+
self.send "#{original_attribute_name}=", value
|
118
|
+
super(value)
|
119
|
+
end
|
120
|
+
end)
|
121
|
+
end
|
122
|
+
|
123
|
+
def load_schema!
|
124
|
+
super
|
125
|
+
|
126
|
+
add_length_validation_for_encrypted_columns if ActiveRecord::Encryption.config.validate_column_size
|
127
|
+
end
|
128
|
+
|
129
|
+
def add_length_validation_for_encrypted_columns
|
130
|
+
encrypted_attributes&.each do |attribute_name|
|
131
|
+
validate_column_size attribute_name
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def validate_column_size(attribute_name)
|
136
|
+
if limit = columns_hash[attribute_name.to_s]&.limit
|
137
|
+
validates_length_of attribute_name, maximum: limit
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# Returns whether a given attribute is encrypted or not.
|
143
|
+
def encrypted_attribute?(attribute_name)
|
144
|
+
ActiveRecord::Encryption.encryptor.encrypted? ciphertext_for(attribute_name)
|
145
|
+
end
|
146
|
+
|
147
|
+
# Returns the ciphertext for +attribute_name+.
|
148
|
+
def ciphertext_for(attribute_name)
|
149
|
+
read_attribute_before_type_cast(attribute_name)
|
150
|
+
end
|
151
|
+
|
152
|
+
# Encrypts all the encryptable attributes and saves the changes.
|
153
|
+
def encrypt
|
154
|
+
encrypt_attributes if has_encrypted_attributes?
|
155
|
+
end
|
156
|
+
|
157
|
+
# Decrypts all the encryptable attributes and saves the changes.
|
158
|
+
def decrypt
|
159
|
+
decrypt_attributes if has_encrypted_attributes?
|
160
|
+
end
|
161
|
+
|
162
|
+
private
|
163
|
+
ORIGINAL_ATTRIBUTE_PREFIX = "original_"
|
164
|
+
|
165
|
+
def encrypt_attributes
|
166
|
+
validate_encryption_allowed
|
167
|
+
|
168
|
+
update_columns build_encrypt_attribute_assignments
|
169
|
+
end
|
170
|
+
|
171
|
+
def decrypt_attributes
|
172
|
+
validate_encryption_allowed
|
173
|
+
|
174
|
+
decrypt_attribute_assignments = build_decrypt_attribute_assignments
|
175
|
+
ActiveRecord::Encryption.without_encryption { update_columns decrypt_attribute_assignments }
|
176
|
+
end
|
177
|
+
|
178
|
+
def validate_encryption_allowed
|
179
|
+
raise ActiveRecord::Encryption::Errors::Configuration, "can't be modified because it is encrypted" if ActiveRecord::Encryption.context.frozen_encryption?
|
180
|
+
end
|
181
|
+
|
182
|
+
def has_encrypted_attributes?
|
183
|
+
self.class.encrypted_attributes.present?
|
184
|
+
end
|
185
|
+
|
186
|
+
def build_encrypt_attribute_assignments
|
187
|
+
Array(self.class.encrypted_attributes).index_with do |attribute_name|
|
188
|
+
self[attribute_name]
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
def build_decrypt_attribute_assignments
|
193
|
+
Array(self.class.encrypted_attributes).collect do |attribute_name|
|
194
|
+
type = type_for_attribute(attribute_name)
|
195
|
+
encrypted_value = ciphertext_for(attribute_name)
|
196
|
+
new_value = type.deserialize(encrypted_value)
|
197
|
+
[attribute_name, new_value]
|
198
|
+
end.to_h
|
199
|
+
end
|
200
|
+
|
201
|
+
def cant_modify_encrypted_attributes_when_frozen
|
202
|
+
self.class&.encrypted_attributes.each do |attribute|
|
203
|
+
errors.add(attribute.to_sym, "can't be modified because it is encrypted") if changed_attributes.include?(attribute)
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
@@ -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> - A +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
|