activerecord 6.1.4.1 → 7.0.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of activerecord might be problematic. Click here for more details.

Files changed (237) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1132 -936
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +1 -1
  5. data/lib/active_record/aggregations.rb +1 -1
  6. data/lib/active_record/association_relation.rb +0 -10
  7. data/lib/active_record/associations/association.rb +33 -17
  8. data/lib/active_record/associations/association_scope.rb +1 -3
  9. data/lib/active_record/associations/belongs_to_association.rb +15 -4
  10. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +10 -2
  11. data/lib/active_record/associations/builder/association.rb +8 -2
  12. data/lib/active_record/associations/builder/belongs_to.rb +19 -6
  13. data/lib/active_record/associations/builder/collection_association.rb +10 -3
  14. data/lib/active_record/associations/builder/has_many.rb +3 -2
  15. data/lib/active_record/associations/builder/has_one.rb +2 -1
  16. data/lib/active_record/associations/builder/singular_association.rb +2 -2
  17. data/lib/active_record/associations/collection_association.rb +34 -27
  18. data/lib/active_record/associations/collection_proxy.rb +8 -3
  19. data/lib/active_record/associations/disable_joins_association_scope.rb +59 -0
  20. data/lib/active_record/associations/has_many_association.rb +1 -1
  21. data/lib/active_record/associations/has_many_through_association.rb +2 -1
  22. data/lib/active_record/associations/has_one_association.rb +10 -7
  23. data/lib/active_record/associations/has_one_through_association.rb +1 -1
  24. data/lib/active_record/associations/join_dependency.rb +6 -2
  25. data/lib/active_record/associations/preloader/association.rb +187 -55
  26. data/lib/active_record/associations/preloader/batch.rb +48 -0
  27. data/lib/active_record/associations/preloader/branch.rb +147 -0
  28. data/lib/active_record/associations/preloader/through_association.rb +49 -13
  29. data/lib/active_record/associations/preloader.rb +39 -113
  30. data/lib/active_record/associations/singular_association.rb +8 -2
  31. data/lib/active_record/associations/through_association.rb +3 -3
  32. data/lib/active_record/associations.rb +118 -90
  33. data/lib/active_record/asynchronous_queries_tracker.rb +60 -0
  34. data/lib/active_record/attribute_assignment.rb +1 -1
  35. data/lib/active_record/attribute_methods/before_type_cast.rb +7 -2
  36. data/lib/active_record/attribute_methods/dirty.rb +49 -16
  37. data/lib/active_record/attribute_methods/primary_key.rb +2 -2
  38. data/lib/active_record/attribute_methods/query.rb +2 -2
  39. data/lib/active_record/attribute_methods/read.rb +7 -5
  40. data/lib/active_record/attribute_methods/serialization.rb +66 -12
  41. data/lib/active_record/attribute_methods/time_zone_conversion.rb +4 -3
  42. data/lib/active_record/attribute_methods/write.rb +7 -10
  43. data/lib/active_record/attribute_methods.rb +13 -14
  44. data/lib/active_record/attributes.rb +24 -35
  45. data/lib/active_record/autosave_association.rb +6 -21
  46. data/lib/active_record/base.rb +19 -1
  47. data/lib/active_record/callbacks.rb +2 -2
  48. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +292 -0
  49. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +209 -0
  50. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +76 -0
  51. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +47 -561
  52. data/lib/active_record/connection_adapters/abstract/database_limits.rb +0 -17
  53. data/lib/active_record/connection_adapters/abstract/database_statements.rb +46 -22
  54. data/lib/active_record/connection_adapters/abstract/query_cache.rb +24 -12
  55. data/lib/active_record/connection_adapters/abstract/quoting.rb +42 -72
  56. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +4 -17
  57. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +34 -9
  58. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +78 -22
  59. data/lib/active_record/connection_adapters/abstract/transaction.rb +15 -22
  60. data/lib/active_record/connection_adapters/abstract_adapter.rb +149 -74
  61. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +97 -81
  62. data/lib/active_record/connection_adapters/column.rb +4 -0
  63. data/lib/active_record/connection_adapters/mysql/database_statements.rb +37 -23
  64. data/lib/active_record/connection_adapters/mysql/quoting.rb +35 -21
  65. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +5 -1
  66. data/lib/active_record/connection_adapters/mysql2_adapter.rb +12 -6
  67. data/lib/active_record/connection_adapters/pool_config.rb +7 -7
  68. data/lib/active_record/connection_adapters/postgresql/column.rb +17 -1
  69. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +21 -12
  70. data/lib/active_record/connection_adapters/postgresql/oid/date.rb +8 -0
  71. data/lib/active_record/connection_adapters/postgresql/oid/date_time.rb +5 -0
  72. data/lib/active_record/connection_adapters/postgresql/oid/hstore.rb +53 -14
  73. data/lib/active_record/connection_adapters/postgresql/oid/range.rb +1 -1
  74. data/lib/active_record/connection_adapters/postgresql/oid/timestamp.rb +15 -0
  75. data/lib/active_record/connection_adapters/postgresql/oid/timestamp_with_time_zone.rb +30 -0
  76. data/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +18 -6
  77. data/lib/active_record/connection_adapters/postgresql/oid.rb +2 -0
  78. data/lib/active_record/connection_adapters/postgresql/quoting.rb +50 -50
  79. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +32 -0
  80. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +21 -1
  81. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +22 -1
  82. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +25 -0
  83. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +29 -18
  84. data/lib/active_record/connection_adapters/postgresql_adapter.rb +205 -105
  85. data/lib/active_record/connection_adapters/schema_cache.rb +29 -4
  86. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +27 -19
  87. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +28 -16
  88. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +16 -14
  89. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +89 -30
  90. data/lib/active_record/connection_adapters.rb +6 -5
  91. data/lib/active_record/connection_handling.rb +47 -53
  92. data/lib/active_record/core.rb +122 -132
  93. data/lib/active_record/database_configurations/connection_url_resolver.rb +2 -1
  94. data/lib/active_record/database_configurations/database_config.rb +12 -9
  95. data/lib/active_record/database_configurations/hash_config.rb +63 -5
  96. data/lib/active_record/database_configurations/url_config.rb +2 -2
  97. data/lib/active_record/database_configurations.rb +16 -32
  98. data/lib/active_record/delegated_type.rb +52 -11
  99. data/lib/active_record/destroy_association_async_job.rb +1 -1
  100. data/lib/active_record/disable_joins_association_relation.rb +39 -0
  101. data/lib/active_record/dynamic_matchers.rb +1 -1
  102. data/lib/active_record/encryption/cipher/aes256_gcm.rb +98 -0
  103. data/lib/active_record/encryption/cipher.rb +53 -0
  104. data/lib/active_record/encryption/config.rb +44 -0
  105. data/lib/active_record/encryption/configurable.rb +61 -0
  106. data/lib/active_record/encryption/context.rb +35 -0
  107. data/lib/active_record/encryption/contexts.rb +72 -0
  108. data/lib/active_record/encryption/derived_secret_key_provider.rb +12 -0
  109. data/lib/active_record/encryption/deterministic_key_provider.rb +14 -0
  110. data/lib/active_record/encryption/encryptable_record.rb +208 -0
  111. data/lib/active_record/encryption/encrypted_attribute_type.rb +140 -0
  112. data/lib/active_record/encryption/encrypted_fixtures.rb +38 -0
  113. data/lib/active_record/encryption/encrypting_only_encryptor.rb +12 -0
  114. data/lib/active_record/encryption/encryptor.rb +155 -0
  115. data/lib/active_record/encryption/envelope_encryption_key_provider.rb +55 -0
  116. data/lib/active_record/encryption/errors.rb +15 -0
  117. data/lib/active_record/encryption/extended_deterministic_queries.rb +160 -0
  118. data/lib/active_record/encryption/extended_deterministic_uniqueness_validator.rb +28 -0
  119. data/lib/active_record/encryption/key.rb +28 -0
  120. data/lib/active_record/encryption/key_generator.rb +42 -0
  121. data/lib/active_record/encryption/key_provider.rb +46 -0
  122. data/lib/active_record/encryption/message.rb +33 -0
  123. data/lib/active_record/encryption/message_serializer.rb +90 -0
  124. data/lib/active_record/encryption/null_encryptor.rb +21 -0
  125. data/lib/active_record/encryption/properties.rb +76 -0
  126. data/lib/active_record/encryption/read_only_null_encryptor.rb +24 -0
  127. data/lib/active_record/encryption/scheme.rb +99 -0
  128. data/lib/active_record/encryption.rb +55 -0
  129. data/lib/active_record/enum.rb +49 -42
  130. data/lib/active_record/errors.rb +67 -4
  131. data/lib/active_record/explain_registry.rb +11 -6
  132. data/lib/active_record/fixture_set/file.rb +15 -1
  133. data/lib/active_record/fixture_set/table_row.rb +41 -6
  134. data/lib/active_record/fixture_set/table_rows.rb +4 -4
  135. data/lib/active_record/fixtures.rb +17 -20
  136. data/lib/active_record/future_result.rb +139 -0
  137. data/lib/active_record/gem_version.rb +4 -4
  138. data/lib/active_record/inheritance.rb +55 -17
  139. data/lib/active_record/insert_all.rb +80 -14
  140. data/lib/active_record/integration.rb +4 -3
  141. data/lib/active_record/internal_metadata.rb +3 -5
  142. data/lib/active_record/legacy_yaml_adapter.rb +2 -39
  143. data/lib/active_record/locking/optimistic.rb +10 -9
  144. data/lib/active_record/locking/pessimistic.rb +9 -3
  145. data/lib/active_record/log_subscriber.rb +14 -3
  146. data/lib/active_record/middleware/database_selector/resolver.rb +6 -10
  147. data/lib/active_record/middleware/database_selector.rb +8 -3
  148. data/lib/active_record/middleware/shard_selector.rb +60 -0
  149. data/lib/active_record/migration/command_recorder.rb +4 -4
  150. data/lib/active_record/migration/compatibility.rb +107 -3
  151. data/lib/active_record/migration/join_table.rb +1 -1
  152. data/lib/active_record/migration.rb +109 -79
  153. data/lib/active_record/model_schema.rb +45 -58
  154. data/lib/active_record/nested_attributes.rb +13 -12
  155. data/lib/active_record/no_touching.rb +3 -3
  156. data/lib/active_record/null_relation.rb +2 -6
  157. data/lib/active_record/persistence.rb +219 -52
  158. data/lib/active_record/query_cache.rb +2 -2
  159. data/lib/active_record/query_logs.rb +138 -0
  160. data/lib/active_record/querying.rb +15 -5
  161. data/lib/active_record/railtie.rb +127 -17
  162. data/lib/active_record/railties/controller_runtime.rb +1 -1
  163. data/lib/active_record/railties/databases.rake +66 -129
  164. data/lib/active_record/readonly_attributes.rb +11 -0
  165. data/lib/active_record/reflection.rb +67 -50
  166. data/lib/active_record/relation/batches/batch_enumerator.rb +19 -5
  167. data/lib/active_record/relation/batches.rb +3 -3
  168. data/lib/active_record/relation/calculations.rb +43 -38
  169. data/lib/active_record/relation/delegation.rb +6 -6
  170. data/lib/active_record/relation/finder_methods.rb +31 -35
  171. data/lib/active_record/relation/merger.rb +20 -13
  172. data/lib/active_record/relation/predicate_builder.rb +1 -6
  173. data/lib/active_record/relation/query_attribute.rb +5 -11
  174. data/lib/active_record/relation/query_methods.rb +243 -61
  175. data/lib/active_record/relation/record_fetch_warning.rb +7 -9
  176. data/lib/active_record/relation/spawn_methods.rb +2 -2
  177. data/lib/active_record/relation/where_clause.rb +10 -19
  178. data/lib/active_record/relation.rb +184 -84
  179. data/lib/active_record/result.rb +17 -7
  180. data/lib/active_record/runtime_registry.rb +9 -13
  181. data/lib/active_record/sanitization.rb +11 -7
  182. data/lib/active_record/schema_dumper.rb +10 -3
  183. data/lib/active_record/schema_migration.rb +4 -4
  184. data/lib/active_record/scoping/default.rb +61 -12
  185. data/lib/active_record/scoping/named.rb +3 -11
  186. data/lib/active_record/scoping.rb +64 -34
  187. data/lib/active_record/serialization.rb +1 -1
  188. data/lib/active_record/signed_id.rb +1 -1
  189. data/lib/active_record/suppressor.rb +11 -15
  190. data/lib/active_record/tasks/database_tasks.rb +120 -58
  191. data/lib/active_record/tasks/mysql_database_tasks.rb +1 -1
  192. data/lib/active_record/tasks/postgresql_database_tasks.rb +19 -12
  193. data/lib/active_record/test_databases.rb +1 -1
  194. data/lib/active_record/test_fixtures.rb +4 -4
  195. data/lib/active_record/timestamp.rb +3 -4
  196. data/lib/active_record/transactions.rb +9 -14
  197. data/lib/active_record/translation.rb +2 -2
  198. data/lib/active_record/type/adapter_specific_registry.rb +32 -7
  199. data/lib/active_record/type/hash_lookup_type_map.rb +34 -1
  200. data/lib/active_record/type/internal/timezone.rb +2 -2
  201. data/lib/active_record/type/serialized.rb +1 -1
  202. data/lib/active_record/type/type_map.rb +17 -20
  203. data/lib/active_record/type.rb +1 -2
  204. data/lib/active_record/validations/associated.rb +1 -1
  205. data/lib/active_record/validations/uniqueness.rb +1 -1
  206. data/lib/active_record.rb +204 -28
  207. data/lib/arel/attributes/attribute.rb +0 -8
  208. data/lib/arel/crud.rb +28 -22
  209. data/lib/arel/delete_manager.rb +18 -4
  210. data/lib/arel/filter_predications.rb +9 -0
  211. data/lib/arel/insert_manager.rb +2 -3
  212. data/lib/arel/nodes/casted.rb +1 -1
  213. data/lib/arel/nodes/delete_statement.rb +12 -13
  214. data/lib/arel/nodes/filter.rb +10 -0
  215. data/lib/arel/nodes/function.rb +1 -0
  216. data/lib/arel/nodes/insert_statement.rb +2 -2
  217. data/lib/arel/nodes/select_core.rb +2 -2
  218. data/lib/arel/nodes/select_statement.rb +2 -2
  219. data/lib/arel/nodes/update_statement.rb +8 -3
  220. data/lib/arel/nodes.rb +1 -0
  221. data/lib/arel/predications.rb +11 -3
  222. data/lib/arel/select_manager.rb +10 -4
  223. data/lib/arel/table.rb +0 -1
  224. data/lib/arel/tree_manager.rb +0 -12
  225. data/lib/arel/update_manager.rb +18 -4
  226. data/lib/arel/visitors/dot.rb +80 -90
  227. data/lib/arel/visitors/mysql.rb +8 -2
  228. data/lib/arel/visitors/postgresql.rb +0 -10
  229. data/lib/arel/visitors/to_sql.rb +58 -2
  230. data/lib/arel.rb +2 -1
  231. data/lib/rails/generators/active_record/application_record/templates/application_record.rb.tt +1 -1
  232. data/lib/rails/generators/active_record/model/templates/abstract_base_class.rb.tt +1 -1
  233. data/lib/rails/generators/active_record/model/templates/model.rb.tt +1 -1
  234. data/lib/rails/generators/active_record/model/templates/module.rb.tt +2 -2
  235. data/lib/rails/generators/active_record/multi_db/multi_db_generator.rb +16 -0
  236. data/lib/rails/generators/active_record/multi_db/templates/multi_db.rb.tt +44 -0
  237. metadata +59 -14
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Encryption
5
+ # Automatically expand encrypted arguments to support querying both encrypted and unencrypted data
6
+ #
7
+ # Active Record Encryption supports querying the db using deterministic attributes. For example:
8
+ #
9
+ # Contact.find_by(email_address: "jorge@hey.com")
10
+ #
11
+ # The value "jorge@hey.com" will get encrypted automatically to perform the query. But there is
12
+ # a problem while the data is being encrypted. This won't work. During that time, you need these
13
+ # queries to be:
14
+ #
15
+ # Contact.find_by(email_address: [ "jorge@hey.com", "<encrypted jorge@hey.com>" ])
16
+ #
17
+ # This patches ActiveRecord to support this automatically. It addresses both:
18
+ #
19
+ # * ActiveRecord::Base: Used in +Contact.find_by_email_address(...)+
20
+ # * ActiveRecord::Relation: Used in +Contact.internal.find_by_email_address(...)+
21
+ #
22
+ # +ActiveRecord::Base+ relies on +ActiveRecord::Relation+ (+ActiveRecord::QueryMethods+) but it does
23
+ # some prepared statements caching. That's why we need to intercept +ActiveRecord::Base+ as soon
24
+ # as it's invoked (so that the proper prepared statement is cached).
25
+ #
26
+ # When modifying this file run performance tests in +test/performance/extended_deterministic_queries_performance_test.rb+ to
27
+ # make sure performance overhead is acceptable.
28
+ #
29
+ # We will extend this to support previous "encryption context" versions in future iterations
30
+ #
31
+ # @TODO Experimental. Support for every kind of query is pending
32
+ # @TODO It should not patch anything if not needed (no previous schemes or no support for previous encryption schemes)
33
+ module ExtendedDeterministicQueries
34
+ def self.install_support
35
+ ActiveRecord::Relation.prepend(RelationQueries)
36
+ ActiveRecord::Base.include(CoreQueries)
37
+ ActiveRecord::Encryption::EncryptedAttributeType.prepend(ExtendedEncryptableType)
38
+ Arel::Nodes::HomogeneousIn.prepend(InWithAdditionalValues)
39
+ end
40
+
41
+ module EncryptedQueryArgumentProcessor
42
+ extend ActiveSupport::Concern
43
+
44
+ private
45
+ def process_encrypted_query_arguments(args, check_for_additional_values)
46
+ if args.is_a?(Array) && (options = args.first).is_a?(Hash)
47
+ self.deterministic_encrypted_attributes&.each do |attribute_name|
48
+ type = type_for_attribute(attribute_name)
49
+ if !type.previous_types.empty? && value = options[attribute_name]
50
+ options[attribute_name] = process_encrypted_query_argument(value, check_for_additional_values, type)
51
+ end
52
+ end
53
+ end
54
+ end
55
+
56
+ def process_encrypted_query_argument(value, check_for_additional_values, type)
57
+ return value if check_for_additional_values && value.is_a?(Array) && value.last.is_a?(AdditionalValue)
58
+
59
+ case value
60
+ when String, Array
61
+ list = Array(value)
62
+ list + list.flat_map do |each_value|
63
+ if check_for_additional_values && each_value.is_a?(AdditionalValue)
64
+ each_value
65
+ else
66
+ additional_values_for(each_value, type)
67
+ end
68
+ end
69
+ else
70
+ value
71
+ end
72
+ end
73
+
74
+ def additional_values_for(value, type)
75
+ type.previous_types.collect do |additional_type|
76
+ AdditionalValue.new(value, additional_type)
77
+ end
78
+ end
79
+ end
80
+
81
+ module RelationQueries
82
+ include EncryptedQueryArgumentProcessor
83
+
84
+ def where(*args)
85
+ process_encrypted_query_arguments_if_needed(args)
86
+ super
87
+ end
88
+
89
+ def exists?(*args)
90
+ process_encrypted_query_arguments_if_needed(args)
91
+ super
92
+ end
93
+
94
+ def find_or_create_by(attributes, &block)
95
+ find_by(attributes.dup) || create(attributes, &block)
96
+ end
97
+
98
+ def find_or_create_by!(attributes, &block)
99
+ find_by(attributes.dup) || create!(attributes, &block)
100
+ end
101
+
102
+ private
103
+ def process_encrypted_query_arguments_if_needed(args)
104
+ process_encrypted_query_arguments(args, true) unless self.deterministic_encrypted_attributes&.empty?
105
+ end
106
+ end
107
+
108
+ module CoreQueries
109
+ extend ActiveSupport::Concern
110
+
111
+ class_methods do
112
+ include EncryptedQueryArgumentProcessor
113
+
114
+ def find_by(*args)
115
+ process_encrypted_query_arguments(args, false) unless self.deterministic_encrypted_attributes&.empty?
116
+ super
117
+ end
118
+ end
119
+ end
120
+
121
+ class AdditionalValue
122
+ attr_reader :value, :type
123
+
124
+ def initialize(value, type)
125
+ @type = type
126
+ @value = process(value)
127
+ end
128
+
129
+ private
130
+ def process(value)
131
+ type.serialize(value)
132
+ end
133
+ end
134
+
135
+ module ExtendedEncryptableType
136
+ def serialize(data)
137
+ if data.is_a?(AdditionalValue)
138
+ data.value
139
+ else
140
+ super
141
+ end
142
+ end
143
+ end
144
+
145
+ module InWithAdditionalValues
146
+ def proc_for_binds
147
+ -> value { ActiveModel::Attribute.with_cast_value(attribute.name, value, encryption_aware_type_caster) }
148
+ end
149
+
150
+ def encryption_aware_type_caster
151
+ if attribute.type_caster.is_a?(ActiveRecord::Encryption::EncryptedAttributeType)
152
+ attribute.type_caster.cast_type
153
+ else
154
+ attribute.type_caster
155
+ end
156
+ end
157
+ end
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Encryption
5
+ module ExtendedDeterministicUniquenessValidator
6
+ def self.install_support
7
+ ActiveRecord::Validations::UniquenessValidator.prepend(EncryptedUniquenessValidator)
8
+ end
9
+
10
+ module EncryptedUniquenessValidator
11
+ def validate_each(record, attribute, value)
12
+ super(record, attribute, value)
13
+
14
+ klass = record.class
15
+ klass.deterministic_encrypted_attributes&.each do |attribute_name|
16
+ encrypted_type = klass.type_for_attribute(attribute_name)
17
+ [ encrypted_type, *encrypted_type.previous_types ].each do |type|
18
+ encrypted_value = type.serialize(value)
19
+ ActiveRecord::Encryption.without_encryption do
20
+ super(record, attribute, encrypted_value)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Encryption
5
+ # A key is a container for a given +secret+
6
+ #
7
+ # Optionally, it can include +public_tags+. These tags are meant to be stored
8
+ # in clean (public) and can be used, for example, to include information that
9
+ # references the key for a future retrieval operation.
10
+ class Key
11
+ attr_reader :secret, :public_tags
12
+
13
+ def initialize(secret)
14
+ @secret = secret
15
+ @public_tags = Properties.new
16
+ end
17
+
18
+ def self.derive_from(password)
19
+ secret = ActiveRecord::Encryption.key_generator.derive_key_from(password)
20
+ ActiveRecord::Encryption::Key.new(secret)
21
+ end
22
+
23
+ def id
24
+ Digest::SHA1.hexdigest(secret).first(4)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ module ActiveRecord
6
+ module Encryption
7
+ # Utility for generating and deriving random keys.
8
+ class KeyGenerator
9
+ # Returns a random key. The key will have a size in bytes of +:length+ (configured +Cipher+'s length by default)
10
+ def generate_random_key(length: key_length)
11
+ SecureRandom.random_bytes(length)
12
+ end
13
+
14
+ # Returns a random key in hexadecimal format. The key will have a size in bytes of +:length+ (configured +Cipher+'s
15
+ # length by default)
16
+ #
17
+ # Hexadecimal format is handy for representing keys as printable text. To maximize the space of characters used, it is
18
+ # good practice including not printable characters. Hexadecimal format ensures that generated keys are representable with
19
+ # plain text
20
+ #
21
+ # To convert back to the original string with the desired length:
22
+ #
23
+ # [ value ].pack("H*")
24
+ def generate_random_hex_key(length: key_length)
25
+ generate_random_key(length: length).unpack("H*")[0]
26
+ end
27
+
28
+ # Derives a key from the given password. The key will have a size in bytes of +:length+ (configured +Cipher+'s length
29
+ # by default)
30
+ #
31
+ # The generated key will be salted with the value of +ActiveRecord::Encryption.key_derivation_salt+
32
+ def derive_key_from(password, length: key_length)
33
+ ActiveSupport::KeyGenerator.new(password).generate_key(ActiveRecord::Encryption.config.key_derivation_salt, length)
34
+ end
35
+
36
+ private
37
+ def key_length
38
+ @key_length ||= ActiveRecord::Encryption.cipher.key_length
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Encryption
5
+ # A +KeyProvider+ serves keys:
6
+ #
7
+ # * An encryption key
8
+ # * A list of potential decryption keys. Serving multiple decryption keys supports rotation-schemes
9
+ # where new keys are added but old keys need to continue working
10
+ class KeyProvider
11
+ def initialize(keys)
12
+ @keys = Array(keys)
13
+ end
14
+
15
+ # Returns the first key in the list as the active key to perform encryptions
16
+ #
17
+ # When +ActiveRecord::Encryption.config.store_key_references+ is true, the key will include
18
+ # a public tag referencing the key itself. That key will be stored in the public
19
+ # headers of the encrypted message
20
+ def encryption_key
21
+ @encryption_key ||= @keys.last.tap do |key|
22
+ key.public_tags.encrypted_data_key_id = key.id if ActiveRecord::Encryption.config.store_key_references
23
+ end
24
+
25
+ @encryption_key
26
+ end
27
+
28
+ # Returns the list of decryption keys
29
+ #
30
+ # When the message holds a reference to its encryption key, it will return an array
31
+ # with that key. If not, it will return the list of keys.
32
+ def decryption_keys(encrypted_message)
33
+ if encrypted_message.headers.encrypted_data_key_id
34
+ keys_grouped_by_id[encrypted_message.headers.encrypted_data_key_id]
35
+ else
36
+ @keys
37
+ end
38
+ end
39
+
40
+ private
41
+ def keys_grouped_by_id
42
+ @keys_grouped_by_id ||= @keys.group_by(&:id)
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Encryption
5
+ # A message defines the structure of the data we store in encrypted attributes. It contains:
6
+ #
7
+ # * An encrypted payload
8
+ # * A list of unencrypted headers
9
+ #
10
+ # See +Encryptor#encrypt+
11
+ class Message
12
+ attr_accessor :payload, :headers
13
+
14
+ def initialize(payload: nil, headers: {})
15
+ validate_payload_type(payload)
16
+
17
+ @payload = payload
18
+ @headers = Properties.new(headers)
19
+ end
20
+
21
+ def ==(other_message)
22
+ payload == other_message.payload && headers == other_message.headers
23
+ end
24
+
25
+ private
26
+ def validate_payload_type(payload)
27
+ unless payload.is_a?(String) || payload.nil?
28
+ raise ActiveRecord::Encryption::Errors::ForbiddenClass, "Only string payloads allowed"
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Encryption
5
+ # A message serializer that serializes +Messages+ with JSON.
6
+ #
7
+ # The generated structure is pretty simple:
8
+ #
9
+ # {
10
+ # p: <payload>,
11
+ # h: {
12
+ # header1: value1,
13
+ # header2: value2,
14
+ # ...
15
+ # }
16
+ # }
17
+ #
18
+ # Both the payload and the header values are encoded with Base64
19
+ # to prevent JSON parsing errors and encoding issues when
20
+ # storing the resulting serialized data.
21
+ class MessageSerializer
22
+ def load(serialized_content)
23
+ data = JSON.parse(serialized_content)
24
+ parse_message(data, 1)
25
+ rescue JSON::ParserError
26
+ raise ActiveRecord::Encryption::Errors::Encoding
27
+ end
28
+
29
+ def dump(message)
30
+ raise ActiveRecord::Encryption::Errors::ForbiddenClass unless message.is_a?(ActiveRecord::Encryption::Message)
31
+ JSON.dump message_to_json(message)
32
+ end
33
+
34
+ private
35
+ def parse_message(data, level)
36
+ validate_message_data_format(data, level)
37
+ ActiveRecord::Encryption::Message.new(payload: decode_if_needed(data["p"]), headers: parse_properties(data["h"], level))
38
+ end
39
+
40
+ def validate_message_data_format(data, level)
41
+ if level > 2
42
+ raise ActiveRecord::Encryption::Errors::Decryption, "More than one level of hash nesting in headers is not supported"
43
+ end
44
+
45
+ unless data.is_a?(Hash) && data.has_key?("p")
46
+ raise ActiveRecord::Encryption::Errors::Decryption, "Invalid data format: hash without payload"
47
+ end
48
+ end
49
+
50
+ def parse_properties(headers, level)
51
+ ActiveRecord::Encryption::Properties.new.tap do |properties|
52
+ headers&.each do |key, value|
53
+ properties[key] = value.is_a?(Hash) ? parse_message(value, level + 1) : decode_if_needed(value)
54
+ end
55
+ end
56
+ end
57
+
58
+ def message_to_json(message)
59
+ {
60
+ p: encode_if_needed(message.payload),
61
+ h: headers_to_json(message.headers)
62
+ }
63
+ end
64
+
65
+ def headers_to_json(headers)
66
+ headers.transform_values do |value|
67
+ value.is_a?(ActiveRecord::Encryption::Message) ? message_to_json(value) : encode_if_needed(value)
68
+ end
69
+ end
70
+
71
+ def encode_if_needed(value)
72
+ if value.is_a?(String)
73
+ ::Base64.strict_encode64 value
74
+ else
75
+ value
76
+ end
77
+ end
78
+
79
+ def decode_if_needed(value)
80
+ if value.is_a?(String)
81
+ ::Base64.strict_decode64(value)
82
+ else
83
+ value
84
+ end
85
+ rescue ArgumentError, TypeError
86
+ raise Errors::Encoding
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Encryption
5
+ # An encryptor that won't decrypt or encrypt. It will just return the passed
6
+ # values
7
+ class NullEncryptor
8
+ def encrypt(clean_text, key_provider: nil, cipher_options: {})
9
+ clean_text
10
+ end
11
+
12
+ def decrypt(encrypted_text, key_provider: nil, cipher_options: {})
13
+ encrypted_text
14
+ end
15
+
16
+ def encrypted?(text)
17
+ false
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Encryption
5
+ # This is a wrapper for a hash of encryption properties. It is used by
6
+ # +Key+ (public tags) and +Message+ (headers).
7
+ #
8
+ # Since properties are serialized in messages, it is important for storage
9
+ # efficiency to keep their keys as short as possible. It defines accessors
10
+ # for common properties that will keep these keys very short while exposing
11
+ # a readable name.
12
+ #
13
+ # message.headers.encrypted_data_key # instead of message.headers[:k]
14
+ #
15
+ # See +Properties#DEFAULT_PROPERTIES+, +Key+, +Message+
16
+ class Properties
17
+ ALLOWED_VALUE_CLASSES = [String, ActiveRecord::Encryption::Message, Numeric, TrueClass, FalseClass, Symbol, NilClass]
18
+
19
+ delegate_missing_to :data
20
+ delegate :==, to: :data
21
+
22
+ # For each entry it generates an accessor exposing the full name
23
+ DEFAULT_PROPERTIES = {
24
+ encrypted_data_key: "k",
25
+ encrypted_data_key_id: "i",
26
+ compressed: "c",
27
+ iv: "iv",
28
+ auth_tag: "at",
29
+ encoding: "e"
30
+ }
31
+
32
+ DEFAULT_PROPERTIES.each do |name, key|
33
+ define_method name do
34
+ self[key.to_sym]
35
+ end
36
+
37
+ define_method "#{name}=" do |value|
38
+ self[key.to_sym] = value
39
+ end
40
+ end
41
+
42
+ def initialize(initial_properties = {})
43
+ @data = {}
44
+ add(initial_properties)
45
+ end
46
+
47
+ # Set a value for a given key
48
+ #
49
+ # It will raise an +EncryptedContentIntegrity+ if the value exists
50
+ def []=(key, value)
51
+ raise Errors::EncryptedContentIntegrity, "Properties can't be overridden: #{key}" if key?(key)
52
+ validate_value_type(value)
53
+ data[key] = value
54
+ end
55
+
56
+ def validate_value_type(value)
57
+ unless ALLOWED_VALUE_CLASSES.find { |klass| value.is_a?(klass) }
58
+ raise ActiveRecord::Encryption::Errors::ForbiddenClass, "Can't store a #{value.class}, only properties of type #{ALLOWED_VALUE_CLASSES.inspect} are allowed"
59
+ end
60
+ end
61
+
62
+ def add(other_properties)
63
+ other_properties.each do |key, value|
64
+ self[key.to_sym] = value
65
+ end
66
+ end
67
+
68
+ def to_h
69
+ data
70
+ end
71
+
72
+ private
73
+ attr_reader :data
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Encryption
5
+ # A +NullEncryptor+ that will raise an error when trying to encrypt data
6
+ #
7
+ # This is useful when you want to reveal ciphertexts for debugging purposes
8
+ # and you want to make sure you won't overwrite any encryptable attribute with
9
+ # the wrong content.
10
+ class ReadOnlyNullEncryptor
11
+ def encrypt(clean_text, key_provider: nil, cipher_options: {})
12
+ raise Errors::Encryption, "This encryptor is read-only"
13
+ end
14
+
15
+ def decrypt(encrypted_text, key_provider: nil, cipher_options: {})
16
+ encrypted_text
17
+ end
18
+
19
+ def encrypted?(text)
20
+ false
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Encryption
5
+ # A container of attribute encryption options.
6
+ #
7
+ # It validates and serves attribute encryption options.
8
+ #
9
+ # See +EncryptedAttributeType+, +Context+
10
+ class Scheme
11
+ attr_accessor :previous_schemes
12
+
13
+ def initialize(key_provider: nil, key: nil, deterministic: nil, downcase: nil, ignore_case: nil,
14
+ previous_schemes: nil, **context_properties)
15
+ # Initializing all attributes to +nil+ as we want to allow a "not set" semantics so that we
16
+ # can merge schemes without overriding values with defaults. See +#merge+
17
+
18
+ @key_provider_param = key_provider
19
+ @key = key
20
+ @deterministic = deterministic
21
+ @downcase = downcase || ignore_case
22
+ @ignore_case = ignore_case
23
+ @previous_schemes_param = previous_schemes
24
+ @previous_schemes = Array.wrap(previous_schemes)
25
+ @context_properties = context_properties
26
+
27
+ validate_config!
28
+ end
29
+
30
+ def ignore_case?
31
+ @ignore_case
32
+ end
33
+
34
+ def downcase?
35
+ @downcase
36
+ end
37
+
38
+ def deterministic?
39
+ @deterministic
40
+ end
41
+
42
+ def fixed?
43
+ # by default deterministic encryption is fixed
44
+ @fixed ||= @deterministic && (!@deterministic.is_a?(Hash) || @deterministic[:fixed])
45
+ end
46
+
47
+ def key_provider
48
+ @key_provider ||= begin
49
+ validate_keys!
50
+ @key_provider_param || build_key_provider
51
+ end
52
+ end
53
+
54
+ def merge(other_scheme)
55
+ self.class.new(**to_h.merge(other_scheme.to_h))
56
+ end
57
+
58
+ def to_h
59
+ { key_provider: @key_provider_param, key: @key, deterministic: @deterministic, downcase: @downcase, ignore_case: @ignore_case,
60
+ previous_schemes: @previous_schemes_param, **@context_properties }.compact
61
+ end
62
+
63
+ def with_context(&block)
64
+ if @context_properties.present?
65
+ ActiveRecord::Encryption.with_encryption_context(**@context_properties, &block)
66
+ else
67
+ block.call
68
+ end
69
+ end
70
+
71
+ private
72
+ def validate_config!
73
+ raise Errors::Configuration, "ignore_case: can only be used with deterministic encryption" if @ignore_case && !@deterministic
74
+ raise Errors::Configuration, "key_provider: and key: can't be used simultaneously" if @key_provider_param && @key
75
+ end
76
+
77
+ def validate_keys!
78
+ validate_credential :key_derivation_salt
79
+ validate_credential :primary_key, "needs to be configured to use non-deterministic encryption" unless @deterministic
80
+ validate_credential :deterministic_key, "needs to be configured to use deterministic encryption" if @deterministic
81
+ end
82
+
83
+ def validate_credential(key, error_message = "is not configured")
84
+ unless ActiveRecord::Encryption.config.public_send(key).present?
85
+ raise Errors::Configuration, "#{key} #{error_message}. Please configure it via credential "\
86
+ "active_record_encryption.#{key} or by setting config.active_record.encryption.#{key}"
87
+ end
88
+ end
89
+
90
+ def build_key_provider
91
+ return DerivedSecretKeyProvider.new(@key) if @key.present?
92
+
93
+ if @deterministic && (deterministic_key = ActiveRecord::Encryption.config.deterministic_key)
94
+ DeterministicKeyProvider.new(deterministic_key)
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end