Skip to content

Commit

Permalink
Add a new option to support previous data encrypted non-deterministic…
Browse files Browse the repository at this point in the history
…ally with a hash digest of SHA1

There is currently a problem with Active Record encryption for users updating from 7.0 to 7.1 Before
rails#44873, data encrypted with non-deterministic encryption was always using SHA-1. The reason is that
`ActiveSupport::KeyGenerator.hash_digest_class` is set in an after_initialize block in the railtie config,
but encryption config was running before that, so it was effectively using the previous default SHA1. That
means that existing users are using SHA256 for non deterministic encryption, and SHA1 for deterministic
encryption.

This adds a new option `use_sha1_digest_for_non_deterministic_data` that
users can enable to support for SHA1 and SHA256 when decrypting existing data.
  • Loading branch information
jorgemanrubia committed Jun 21, 2023
1 parent 18a0d08 commit bd07337
Show file tree
Hide file tree
Showing 9 changed files with 93 additions and 15 deletions.
16 changes: 16 additions & 0 deletions activerecord/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
* Support decrypting data encrypted non-deterministically with a SHA1 hash digest.

This adds a new Active Record encryption option to support decrypting data encrypted
non-deterministically with a SHA1 hash digest:

```
Rails.application.config.active_record.encryption.support_sha1_for_non_deterministic_encryption = true
```

The new option addresses a problem when upgrading from 7.0 to 7.1. Due to a bug in how Active Record
Encryption was getting initialized, the key provider used for non-deterministic encryption were using
SHA-1 as its digest class, instead of the one configured globally by Rails via
`Rails.application.config.active_support.key_generator_hash_digest_class`.

*Cadu Ribeiro and Jorge Manrubia*

* Apply scope to association subqueries. (belongs_to/has_one/has_many)

Given: `has_many :welcome_posts, -> { where(title: "welcome") }`
Expand Down
15 changes: 14 additions & 1 deletion activerecord/lib/active_record/encryption/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,22 @@ def previous=(previous_schemes_properties)
end
end

def support_sha1_for_non_deterministic_encryption=(value)
if value && has_primary_key?
sha1_key_generator = ActiveRecord::Encryption::KeyGenerator.new(hash_digest_class: OpenSSL::Digest::SHA1)
sha1_key_provider = ActiveRecord::Encryption::DerivedSecretKeyProvider.new(primary_key, key_generator: sha1_key_generator)
add_previous_scheme key_provider: sha1_key_provider
end
end

%w(key_derivation_salt primary_key deterministic_key).each do |key|
silence_redefinition_of_method key
define_method("has_#{key}?") do
instance_variable_get(:"@#{key}").presence
end

define_method(key) do
instance_variable_get(:"@#{key}").presence or
public_send("has_#{key}?") or
raise Errors::Configuration, "Missing Active Record encryption credential: active_record_encryption.#{key}"
end
end
Expand All @@ -42,6 +54,7 @@ def set_defaults
self.previous_schemes = []
self.forced_encoding_for_deterministic_encryption = Encoding::UTF_8
self.hash_digest_class = OpenSSL::Digest::SHA1
self.support_sha1_for_non_deterministic_encryption = false

# TODO: Setting to false for now as the implementation is a bit experimental
self.extend_queries = false
Expand Down
11 changes: 7 additions & 4 deletions activerecord/lib/active_record/encryption/configurable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,13 @@ def configure(primary_key: nil, deterministic_key: nil, key_derivation_salt: nil
config.key_derivation_salt = key_derivation_salt

properties.each do |name, value|
[:context, :config].each do |configurable_object_name|
configurable_object = ActiveRecord::Encryption.send(configurable_object_name)
configurable_object.send "#{name}=", value if configurable_object.respond_to?("#{name}=")
end
ActiveRecord::Encryption.config.send "#{name}=", value if ActiveRecord::Encryption.config.respond_to?("#{name}=")
end

ActiveRecord::Encryption.reset_default_context

properties.each do |name, value|
ActiveRecord::Encryption.context.send "#{name}=", value if ActiveRecord::Encryption.context.respond_to?("#{name}=")
end
end

Expand Down
6 changes: 5 additions & 1 deletion activerecord/lib/active_record/encryption/contexts.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ module Contexts
extend ActiveSupport::Concern

included do
mattr_reader :default_context, default: Context.new
mattr_accessor :default_context, default: Context.new
thread_mattr_accessor :custom_contexts
end

Expand Down Expand Up @@ -66,6 +66,10 @@ def context
def current_custom_context
self.custom_contexts&.last
end

def reset_default_context
self.default_context = Context.new
end
end
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,15 @@ module ActiveRecord
module Encryption
# A KeyProvider that derives keys from passwords.
class DerivedSecretKeyProvider < KeyProvider
def initialize(passwords)
super(Array(passwords).collect { |password| Key.derive_from(password) })
def initialize(passwords, key_generator: ActiveRecord::Encryption.key_generator)
super(Array(passwords).collect { |password| derive_key_from(password, using: key_generator) })
end

private
def derive_key_from(password, using: key_generator)
secret = using.derive_key_from(password)
ActiveRecord::Encryption::Key.new(secret)
end
end
end
end
8 changes: 7 additions & 1 deletion activerecord/lib/active_record/encryption/key_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ module ActiveRecord
module Encryption
# Utility for generating and deriving random keys.
class KeyGenerator
attr_reader :hash_digest_class

def initialize(hash_digest_class: ActiveRecord::Encryption.config.hash_digest_class)
@hash_digest_class = hash_digest_class
end

# Returns a random key. The key will have a size in bytes of +:length+ (configured +Cipher+'s length by default)
def generate_random_key(length: key_length)
SecureRandom.random_bytes(length)
Expand All @@ -30,7 +36,7 @@ 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, hash_digest_class: ActiveRecord::Encryption.config.hash_digest_class)
ActiveSupport::KeyGenerator.new(password, hash_digest_class: hash_digest_class)
.generate_key(key_derivation_salt, length)
end

Expand Down
8 changes: 4 additions & 4 deletions activerecord/lib/active_record/railtie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -357,15 +357,15 @@ class Railtie < Rails::Railtie # :nodoc:
end

initializer "active_record_encryption.configuration" do |app|
active_record_encryption_config = config.active_record.encryption

ActiveSupport.on_load(:active_record) do
config.after_initialize do |app|
ActiveRecord::Encryption.configure \
primary_key: app.credentials.dig(:active_record_encryption, :primary_key),
deterministic_key: app.credentials.dig(:active_record_encryption, :deterministic_key),
key_derivation_salt: app.credentials.dig(:active_record_encryption, :key_derivation_salt),
**active_record_encryption_config
**config.active_record.encryption
end

ActiveSupport.on_load(:active_record) do
# Support extended queries for deterministic attributes and validations
if ActiveRecord::Encryption.config.extend_queries
ActiveRecord::Encryption::ExtendedDeterministicQueries.install_support
Expand Down
32 changes: 31 additions & 1 deletion activerecord/test/cases/encryption/encryptable_record_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ class ActiveRecord::Encryption::EncryptableRecordTest < ActiveRecord::Encryption
end

test "encrypts multiple attributes with different options at the same time" do
post = EncryptedPost.create!\
post = EncryptedPost.create! \
title: title = "The Starfleet is here!",
body: body = "<p>the Starfleet is here, we are safe now!</p>"

Expand Down Expand Up @@ -308,7 +308,37 @@ def name
assert_equal book, Marshal.load(Marshal.dump(book))
end

test "supports decrypting data encrypted non deterministically with SHA1 when digest class is SHA256" do
ActiveRecord::Encryption.configure \
primary_key: "the primary key",
deterministic_key: "the deterministic key",
key_derivation_salt: "the salt",
support_sha1_for_non_deterministic_encryption: true

key_provider_sha1 = build_derived_key_provider_with OpenSSL::Digest::SHA1
key_provider_sha256 = build_derived_key_provider_with OpenSSL::Digest::SHA256

encrypted_post_class_sha_1 = Class.new(Post) do
self.table_name = "posts"
encrypts :title, key_provider: key_provider_sha1
end
encrypted_post_class_sha_1.create! title: "Post 1", body: "The post body", type: nil

encrypted_post_class_sha_256 = Class.new(Post) do
self.table_name = "posts"
encrypts :title, key_provider: key_provider_sha256
end

assert_equal "Post 1", encrypted_post_class_sha_256.last.title
end

private
def build_derived_key_provider_with(hash_digest_class)
ActiveRecord::Encryption.with_encryption_context(key_generator: ActiveRecord::Encryption::KeyGenerator.new(hash_digest_class: hash_digest_class)) do
ActiveRecord::Encryption::DerivedSecretKeyProvider.new(ActiveRecord::Encryption.config.primary_key)
end
end

class FailingKeyProvider
def decryption_key(message) end

Expand Down
2 changes: 1 addition & 1 deletion activerecord/test/cases/encryption/key_generator_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,6 @@ def assert_derive_key(secret, digest_class: OpenSSL::Digest::SHA256, length: 20)
.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)
assert_equal expected_derived_key, ActiveRecord::Encryption::KeyGenerator.new(hash_digest_class: digest_class).derive_key_from(secret, length: length)
end
end

0 comments on commit bd07337

Please sign in to comment.