Skip to content

Commit

Permalink
Add option to configure digest algorithm used by Active Record Encryp…
Browse files Browse the repository at this point in the history
…tion (#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 #44540. We will now set SHA256 as the standard
for 7.1+, and SHA1 for previous versions.
  • Loading branch information
jorgemanrubia committed Feb 27, 2023
1 parent 0eaa58e commit 5d7b6d8
Show file tree
Hide file tree
Showing 8 changed files with 48 additions and 3 deletions.
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

0 comments on commit 5d7b6d8

Please sign in to comment.