Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to configure digest algorithm in Active Record Encryption #44873

Merged
merged 1 commit into from Feb 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion activerecord/lib/active_record/encryption/config.rb
Expand Up @@ -4,7 +4,7 @@ module ActiveRecord
module Encryption
# Container of configuration options
class Config
attr_accessor :primary_key, :deterministic_key, :store_key_references, :key_derivation_salt,
attr_accessor :primary_key, :deterministic_key, :store_key_references, :key_derivation_salt, :hash_digest_class,
:support_unencrypted_data, :encrypt_fixtures, :validate_column_size, :add_to_filter_parameters,
:excluded_from_filter_parameters, :extend_queries, :previous_schemes, :forced_encoding_for_deterministic_encryption

Expand Down Expand Up @@ -39,6 +39,7 @@ def set_defaults
self.excluded_from_filter_parameters = []
self.previous_schemes = []
self.forced_encoding_for_deterministic_encryption = Encoding::UTF_8
self.hash_digest_class = OpenSSL::Digest::SHA1

# TODO: Setting to false for now as the implementation is a bit experimental
self.extend_queries = false
Expand Down
3 changes: 2 additions & 1 deletion activerecord/lib/active_record/encryption/key_generator.rb
Expand Up @@ -30,7 +30,8 @@ def generate_random_hex_key(length: key_length)
#
# The generated key will be salted with the value of +ActiveRecord::Encryption.key_derivation_salt+
def derive_key_from(password, length: key_length)
ActiveSupport::KeyGenerator.new(password).generate_key(key_derivation_salt, length)
ActiveSupport::KeyGenerator.new(password, hash_digest_class: ActiveRecord::Encryption.config.hash_digest_class)
.generate_key(key_derivation_salt, length)
end

private
Expand Down
2 changes: 1 addition & 1 deletion activerecord/test/cases/encryption/helper.rb
Expand Up @@ -156,7 +156,7 @@ class ActiveRecord::EncryptionTestCase < ActiveRecord::TestCase
include ActiveRecord::Encryption::EncryptionHelpers, ActiveRecord::Encryption::PerformanceHelpers

ENCRYPTION_PROPERTIES_TO_RESET = {
config: %i[ primary_key deterministic_key key_derivation_salt store_key_references
config: %i[ primary_key deterministic_key key_derivation_salt store_key_references hash_digest_class
key_derivation_salt support_unencrypted_data encrypt_fixtures
forced_encoding_for_deterministic_encryption ],
context: %i[ key_provider ]
Expand Down
14 changes: 14 additions & 0 deletions activerecord/test/cases/encryption/key_generator_test.rb
Expand Up @@ -27,6 +27,11 @@ class ActiveRecord::Encryption::KeyGeneratorTest < ActiveRecord::EncryptionTestC
assert_equal 10, [ @generator.generate_random_hex_key(length: 10) ].pack("H*").bytesize
end

test "derive keys using the configured digest algorithm" do
assert_derive_key "some secret", digest_class: OpenSSL::Digest::SHA1
assert_derive_key "some secret", digest_class: OpenSSL::Digest::SHA256
end

test "derive_key derives a key with from the provided password with the cipher key length by default" do
assert_equal @generator.derive_key_from("some password"), @generator.derive_key_from("some password")
assert_not_equal @generator.derive_key_from("some password"), @generator.derive_key_from("some other password")
Expand All @@ -38,4 +43,13 @@ class ActiveRecord::Encryption::KeyGeneratorTest < ActiveRecord::EncryptionTestC
assert_not_equal @generator.derive_key_from("some password", length: 12), @generator.derive_key_from("some other password", length: 12)
assert_equal 12, @generator.derive_key_from("some password", length: 12).length
end

private
def assert_derive_key(secret, digest_class: OpenSSL::Digest::SHA256, length: 20)
expected_derived_key = ActiveSupport::KeyGenerator.new(secret, hash_digest_class: digest_class)
.generate_key(ActiveRecord::Encryption.config.key_derivation_salt, length)
assert_equal length, expected_derived_key.length
ActiveRecord::Encryption.config.hash_digest_class = digest_class
assert_equal expected_derived_key, @generator.derive_key_from(secret, length: length)
end
end
4 changes: 4 additions & 0 deletions guides/source/active_record_encryption.md
Expand Up @@ -475,6 +475,10 @@ The salt used when deriving keys. It's preferred to configure it via the `active

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.

#### `config.active_record.encryption.hash_digest_class`

The digest algorithm used to derive keys. `OpenSSL::Digest::SHA1` by default.

### Encryption Contexts

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.
Expand Down
12 changes: 12 additions & 0 deletions guides/source/configuring.md
Expand Up @@ -67,6 +67,7 @@ Below are the default values associated with each target version. In cases of co
- [`config.active_record.before_committed_on_all_records`](#config-active-record-before-committed-on-all-records): `true`
- [`config.active_record.belongs_to_required_validates_foreign_key`](#config-active-record-belongs-to-required-validates-foreign-key): `false`
- [`config.active_record.default_column_serializer`](#config-active-record-default-column-serializer): `nil`
- [`config.active_record.encryption.hash_digest_class`](#config-active-record-encryption-hash-digest-class): `OpenSSL::Digest::SHA256`
- [`config.active_record.query_log_tags_format`](#config-active-record-query-log-tags-format): `:sqlcommenter`
- [`config.active_record.raise_on_assign_to_attr_readonly`](#config-active-record-raise-on-assign-to-attr-readonly): `true`
- [`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`
Expand Down Expand Up @@ -1462,6 +1463,17 @@ whether a foreign key's name should be dumped to db/schema.rb or not. By
default, foreign key names starting with `fk_rails_` are not exported to the
database schema dump. Defaults to `/^fk_rails_[0-9a-f]{10}$/`.

#### `config.active_record.encryption.hash_digest_class`

Sets the digest algorithm used by Active Record Encryption.

The default value depends on the `config.load_defaults` target version:

| Starting with version | The default value is |
|-----------------------|---------------------------|
| (original) | `OpenSSL::Digest::SHA1` |
| 7.1 | `OpenSSL::Digest::SHA256` |

### Configuring Action Controller

`config.action_controller` includes a number of configuration settings:
Expand Down
4 changes: 4 additions & 0 deletions railties/lib/rails/application/configuration.rb
Expand Up @@ -314,6 +314,10 @@ def load_defaults(target_version)
if respond_to?(:action_controller)
action_controller.allow_deprecated_parameters_hash_equality = false
end

if respond_to?(:active_record)
active_record.encryption.hash_digest_class = OpenSSL::Digest::SHA256
end
else
raise "Unknown version #{target_version.to_s.inspect}"
end
Expand Down
Expand Up @@ -29,6 +29,15 @@
# as equal to an equivalent `Hash` by default.
# Rails.application.config.action_controller.allow_deprecated_parameters_hash_equality = false

# Active Record Encryption now uses SHA-256 as its hash digest algorithm. Important: If you have
# data encrypted with previous versions, you should not set the new default or the existing data
# will fail to decrypt. In this case, if you load the new 7.1 defaults, you need to configure the
# previous algorithm SHA-1:
# Rails.application.config.active_record.encryption.hash_digest_class = OpenSSL::Digest::SHA1
# Alternatively, if you don't have data encrypted previously, you can configure the new digest for
# Active Record Encryption with:
# Rails.application.config.active_record.encryption.hash_digest_class = OpenSSL::Digest::256

# No longer run after_commit callbacks on the first of multiple Active Record
# instances to save changes to the same database row within a transaction.
# Instead, run these callbacks on the instance most likely to have internal
Expand Down