Skip to content

Commit 5d7b6d8

Browse files
Add option to configure digest algorithm used by Active Record Encryption (rails#44873)
Before, it was using the configured by Rails. Having a mechanism to configure it for Active Record encryption makes sense to prevent problems with encrypted content when the default in Rails changes. Additionally, there was a bug making AR encryption use the older SHA1 before `ActiveSupport.hash_digest_class` got initialized to SHA256. This bug was exposed by rails#44540. We will now set SHA256 as the standard for 7.1+, and SHA1 for previous versions.
1 parent 0eaa58e commit 5d7b6d8

File tree

8 files changed

+48
-3
lines changed

8 files changed

+48
-3
lines changed

activerecord/lib/active_record/encryption/config.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ module ActiveRecord
44
module Encryption
55
# Container of configuration options
66
class Config
7-
attr_accessor :primary_key, :deterministic_key, :store_key_references, :key_derivation_salt,
7+
attr_accessor :primary_key, :deterministic_key, :store_key_references, :key_derivation_salt, :hash_digest_class,
88
:support_unencrypted_data, :encrypt_fixtures, :validate_column_size, :add_to_filter_parameters,
99
:excluded_from_filter_parameters, :extend_queries, :previous_schemes, :forced_encoding_for_deterministic_encryption
1010

@@ -39,6 +39,7 @@ def set_defaults
3939
self.excluded_from_filter_parameters = []
4040
self.previous_schemes = []
4141
self.forced_encoding_for_deterministic_encryption = Encoding::UTF_8
42+
self.hash_digest_class = OpenSSL::Digest::SHA1
4243

4344
# TODO: Setting to false for now as the implementation is a bit experimental
4445
self.extend_queries = false

activerecord/lib/active_record/encryption/key_generator.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ def generate_random_hex_key(length: key_length)
3030
#
3131
# The generated key will be salted with the value of +ActiveRecord::Encryption.key_derivation_salt+
3232
def derive_key_from(password, length: key_length)
33-
ActiveSupport::KeyGenerator.new(password).generate_key(key_derivation_salt, length)
33+
ActiveSupport::KeyGenerator.new(password, hash_digest_class: ActiveRecord::Encryption.config.hash_digest_class)
34+
.generate_key(key_derivation_salt, length)
3435
end
3536

3637
private

activerecord/test/cases/encryption/helper.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ class ActiveRecord::EncryptionTestCase < ActiveRecord::TestCase
156156
include ActiveRecord::Encryption::EncryptionHelpers, ActiveRecord::Encryption::PerformanceHelpers
157157

158158
ENCRYPTION_PROPERTIES_TO_RESET = {
159-
config: %i[ primary_key deterministic_key key_derivation_salt store_key_references
159+
config: %i[ primary_key deterministic_key key_derivation_salt store_key_references hash_digest_class
160160
key_derivation_salt support_unencrypted_data encrypt_fixtures
161161
forced_encoding_for_deterministic_encryption ],
162162
context: %i[ key_provider ]

activerecord/test/cases/encryption/key_generator_test.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ class ActiveRecord::Encryption::KeyGeneratorTest < ActiveRecord::EncryptionTestC
2727
assert_equal 10, [ @generator.generate_random_hex_key(length: 10) ].pack("H*").bytesize
2828
end
2929

30+
test "derive keys using the configured digest algorithm" do
31+
assert_derive_key "some secret", digest_class: OpenSSL::Digest::SHA1
32+
assert_derive_key "some secret", digest_class: OpenSSL::Digest::SHA256
33+
end
34+
3035
test "derive_key derives a key with from the provided password with the cipher key length by default" do
3136
assert_equal @generator.derive_key_from("some password"), @generator.derive_key_from("some password")
3237
assert_not_equal @generator.derive_key_from("some password"), @generator.derive_key_from("some other password")
@@ -38,4 +43,13 @@ class ActiveRecord::Encryption::KeyGeneratorTest < ActiveRecord::EncryptionTestC
3843
assert_not_equal @generator.derive_key_from("some password", length: 12), @generator.derive_key_from("some other password", length: 12)
3944
assert_equal 12, @generator.derive_key_from("some password", length: 12).length
4045
end
46+
47+
private
48+
def assert_derive_key(secret, digest_class: OpenSSL::Digest::SHA256, length: 20)
49+
expected_derived_key = ActiveSupport::KeyGenerator.new(secret, hash_digest_class: digest_class)
50+
.generate_key(ActiveRecord::Encryption.config.key_derivation_salt, length)
51+
assert_equal length, expected_derived_key.length
52+
ActiveRecord::Encryption.config.hash_digest_class = digest_class
53+
assert_equal expected_derived_key, @generator.derive_key_from(secret, length: length)
54+
end
4155
end

guides/source/active_record_encryption.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,10 @@ The salt used when deriving keys. It's preferred to configure it via the `active
475475
476476
The default encoding for attributes encrypted deterministically. You can disable forced encoding by setting this option to `nil`. It's `Encoding::UTF_8` by default.
477477

478+
#### `config.active_record.encryption.hash_digest_class`
479+
480+
The digest algorithm used to derive keys. `OpenSSL::Digest::SHA1` by default.
481+
478482
### Encryption Contexts
479483

480484
An encryption context defines the encryption components that are used in a given moment. There is a default encryption context based on your global configuration, but you can configure a custom context for a given attribute or when running a specific block of code.

guides/source/configuring.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ Below are the default values associated with each target version. In cases of co
6767
- [`config.active_record.before_committed_on_all_records`](#config-active-record-before-committed-on-all-records): `true`
6868
- [`config.active_record.belongs_to_required_validates_foreign_key`](#config-active-record-belongs-to-required-validates-foreign-key): `false`
6969
- [`config.active_record.default_column_serializer`](#config-active-record-default-column-serializer): `nil`
70+
- [`config.active_record.encryption.hash_digest_class`](#config-active-record-encryption-hash-digest-class): `OpenSSL::Digest::SHA256`
7071
- [`config.active_record.query_log_tags_format`](#config-active-record-query-log-tags-format): `:sqlcommenter`
7172
- [`config.active_record.raise_on_assign_to_attr_readonly`](#config-active-record-raise-on-assign-to-attr-readonly): `true`
7273
- [`config.active_record.run_commit_callbacks_on_first_saved_instances_in_transaction`](#config-active-record-run-commit-callbacks-on-first-saved-instances-in-transaction): `false`
@@ -1462,6 +1463,17 @@ whether a foreign key's name should be dumped to db/schema.rb or not. By
14621463
default, foreign key names starting with `fk_rails_` are not exported to the
14631464
database schema dump. Defaults to `/^fk_rails_[0-9a-f]{10}$/`.
14641465
1466+
#### `config.active_record.encryption.hash_digest_class`
1467+
1468+
Sets the digest algorithm used by Active Record Encryption.
1469+
1470+
The default value depends on the `config.load_defaults` target version:
1471+
1472+
| Starting with version | The default value is |
1473+
|-----------------------|---------------------------|
1474+
| (original) | `OpenSSL::Digest::SHA1` |
1475+
| 7.1 | `OpenSSL::Digest::SHA256` |
1476+
14651477
### Configuring Action Controller
14661478
14671479
`config.action_controller` includes a number of configuration settings:

railties/lib/rails/application/configuration.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,10 @@ def load_defaults(target_version)
314314
if respond_to?(:action_controller)
315315
action_controller.allow_deprecated_parameters_hash_equality = false
316316
end
317+
318+
if respond_to?(:active_record)
319+
active_record.encryption.hash_digest_class = OpenSSL::Digest::SHA256
320+
end
317321
else
318322
raise "Unknown version #{target_version.to_s.inspect}"
319323
end

railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_7_1.rb.tt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,15 @@
2929
# as equal to an equivalent `Hash` by default.
3030
# Rails.application.config.action_controller.allow_deprecated_parameters_hash_equality = false
3131

32+
# Active Record Encryption now uses SHA-256 as its hash digest algorithm. Important: If you have
33+
# data encrypted with previous versions, you should not set the new default or the existing data
34+
# will fail to decrypt. In this case, if you load the new 7.1 defaults, you need to configure the
35+
# previous algorithm SHA-1:
36+
# Rails.application.config.active_record.encryption.hash_digest_class = OpenSSL::Digest::SHA1
37+
# Alternatively, if you don't have data encrypted previously, you can configure the new digest for
38+
# Active Record Encryption with:
39+
# Rails.application.config.active_record.encryption.hash_digest_class = OpenSSL::Digest::256
40+
3241
# No longer run after_commit callbacks on the first of multiple Active Record
3342
# instances to save changes to the same database row within a transaction.
3443
# Instead, run these callbacks on the instance most likely to have internal

0 commit comments

Comments
 (0)