activerecord 6.1.3.2 → 7.0.0.alpha2
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 +734 -1058
- 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 +35 -7
- data/lib/active_record/associations/association_scope.rb +1 -3
- data/lib/active_record/associations/belongs_to_association.rb +16 -6
- 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/coders/yaml_column.rb +11 -1
- 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 +5 -18
- 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 +17 -6
- data/lib/active_record/connection_adapters/abstract_adapter.rb +115 -69
- data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +96 -81
- data/lib/active_record/connection_adapters/legacy_pool_manager.rb +6 -2
- 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/pool_manager.rb +5 -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 +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 +35 -4
- data/lib/active_record/connection_adapters/sql_type_metadata.rb +0 -2
- 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 +8 -5
- data/lib/active_record/connection_handling.rb +20 -38
- data/lib/active_record/core.rb +129 -117
- 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 +44 -46
- 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 +39 -6
- 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 +83 -58
- 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 +42 -25
- 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 +233 -50
- 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 +22 -15
- data/lib/active_record/relation.rb +170 -87
- 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 +62 -15
- 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/statement_cache.rb +2 -2
- 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 +45 -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/numericality.rb +1 -1
- data/lib/active_record.rb +170 -2
- data/lib/arel/attributes/attribute.rb +0 -8
- data/lib/arel/collectors/bind.rb +2 -2
- data/lib/arel/collectors/composite.rb +3 -3
- data/lib/arel/collectors/sql_string.rb +1 -1
- data/lib/arel/collectors/substitute_binds.rb +1 -1
- 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/homogeneous_in.rb +4 -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 +3 -2
- data/lib/arel/predications.rb +3 -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 +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 +44 -3
- 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,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Encryption
|
5
|
+
module ExtendedDeterministicUniquenessValidator
|
6
|
+
def self.install_support
|
7
|
+
ActiveRecord::Validations::UniquenessValidator.prepend(EncryptedUniquenessValidator)
|
8
|
+
end
|
9
|
+
|
10
|
+
module EncryptedUniquenessValidator
|
11
|
+
def validate_each(record, attribute, value)
|
12
|
+
super(record, attribute, value)
|
13
|
+
|
14
|
+
klass = record.class
|
15
|
+
if klass.deterministic_encrypted_attributes&.each do |attribute_name|
|
16
|
+
encrypted_type = klass.type_for_attribute(attribute_name)
|
17
|
+
[ encrypted_type, *encrypted_type.previous_types ].each do |type|
|
18
|
+
encrypted_value = type.serialize(value)
|
19
|
+
ActiveRecord::Encryption.without_encryption do
|
20
|
+
super(record, attribute, encrypted_value)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Encryption
|
5
|
+
# A key is a container for a given +secret+
|
6
|
+
#
|
7
|
+
# Optionally, it can include +public_tags+. These tags are meant to be stored
|
8
|
+
# in clean (public) and can be used, for example, to include information that
|
9
|
+
# references the key for a future retrieval operation.
|
10
|
+
class Key
|
11
|
+
attr_reader :secret, :public_tags
|
12
|
+
|
13
|
+
def initialize(secret)
|
14
|
+
@secret = secret
|
15
|
+
@public_tags = Properties.new
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.derive_from(password)
|
19
|
+
secret = ActiveRecord::Encryption.key_generator.derive_key_from(password)
|
20
|
+
ActiveRecord::Encryption::Key.new(secret)
|
21
|
+
end
|
22
|
+
|
23
|
+
def id
|
24
|
+
Digest::SHA1.hexdigest(secret).first(4)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "securerandom"
|
4
|
+
|
5
|
+
module ActiveRecord
|
6
|
+
module Encryption
|
7
|
+
# Utility for generating and deriving random keys.
|
8
|
+
class KeyGenerator
|
9
|
+
# Returns a random key. The key will have a size in bytes of +:length+ (configured +Cipher+'s length by default)
|
10
|
+
def generate_random_key(length: key_length)
|
11
|
+
SecureRandom.random_bytes(length)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Returns a random key in hexadecimal format. The key will have a size in bytes of +:length+ (configured +Cipher+'s
|
15
|
+
# length by default)
|
16
|
+
#
|
17
|
+
# Hexadecimal format is handy for representing keys as printable text. To maximize the space of characters used, it is
|
18
|
+
# good practice including not printable characters. Hexadecimal format ensures that generated keys are representable with
|
19
|
+
# plain text
|
20
|
+
#
|
21
|
+
# To convert back to the original string with the desired length:
|
22
|
+
#
|
23
|
+
# [ value ].pack("H*")
|
24
|
+
def generate_random_hex_key(length: key_length)
|
25
|
+
generate_random_key(length: length).unpack("H*")[0]
|
26
|
+
end
|
27
|
+
|
28
|
+
# Derives a key from the given password. The key will have a size in bytes of +:length+ (configured +Cipher+'s length
|
29
|
+
# by default)
|
30
|
+
#
|
31
|
+
# The generated key will be salted with the value of +ActiveRecord::Encryption.key_derivation_salt+
|
32
|
+
def derive_key_from(password, length: key_length)
|
33
|
+
ActiveSupport::KeyGenerator.new(password).generate_key(ActiveRecord::Encryption.config.key_derivation_salt, length)
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
def key_length
|
38
|
+
@key_length ||= ActiveRecord::Encryption.cipher.key_length
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -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,80 @@
|
|
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
|
+
raise ActiveRecord::Encryption::Errors::Decryption, "More than one level of hash nesting in headers is not supported" if level > 2
|
37
|
+
ActiveRecord::Encryption::Message.new(payload: decode_if_needed(data["p"]), headers: parse_properties(data["h"], level))
|
38
|
+
end
|
39
|
+
|
40
|
+
def parse_properties(headers, level)
|
41
|
+
ActiveRecord::Encryption::Properties.new.tap do |properties|
|
42
|
+
headers&.each do |key, value|
|
43
|
+
properties[key] = value.is_a?(Hash) ? parse_message(value, level + 1) : decode_if_needed(value)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def message_to_json(message)
|
49
|
+
{
|
50
|
+
p: encode_if_needed(message.payload),
|
51
|
+
h: headers_to_json(message.headers)
|
52
|
+
}
|
53
|
+
end
|
54
|
+
|
55
|
+
def headers_to_json(headers)
|
56
|
+
headers.transform_values do |value|
|
57
|
+
value.is_a?(ActiveRecord::Encryption::Message) ? message_to_json(value) : encode_if_needed(value)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def encode_if_needed(value)
|
62
|
+
if value.is_a?(String)
|
63
|
+
::Base64.strict_encode64 value
|
64
|
+
else
|
65
|
+
value
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def decode_if_needed(value)
|
70
|
+
if value.is_a?(String)
|
71
|
+
::Base64.strict_decode64(value)
|
72
|
+
else
|
73
|
+
value
|
74
|
+
end
|
75
|
+
rescue ArgumentError, TypeError
|
76
|
+
raise Errors::Encoding
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
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
|