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

Conversation

jorgemanrubia
Copy link
Contributor

@jorgemanrubia jorgemanrubia commented Apr 11, 2022

This adds a new option to configure the digest algorithm in Active Record Encryption. It sets SHA-256 as the new default starting in 7.1 and SHA-1 for previous versions.

This is a second take on this problem (see #42929). I forgot about the first PR and it went unmerged 🙄.

Rails 7 uses SHA256 as the new default digest. However, due to this bug , Active Record Encryption kept using SHA-1 until it was unintentionally fixed by this change. Because that fix is recent, I think setting SHA-1 as the pre-7.1 default makes sense.

This shows why a mechanism to fix the digest algorithm in AR encryption is positive. Changing the digest prevents existing encrypted contents from decrypting, so we don't want the digest to change inadvertently when picking a new one in Rails.

To configure the new option:

config.active_record.encryption.hash_digest_class = OpenSSL::Digest::SHA256

Hat tip to @boomer196 for the original report, to @georgeclaghorn for the great troubleshooting here, and to @matthewd for the change that exposed this bug.

cc @matthewd

@jorgemanrubia jorgemanrubia force-pushed the configure-digest-class-in-are branch 3 times, most recently from 5622b0c to cc5b2b8 Compare April 13, 2022 16:34
@matthewd
Copy link
Member

Thanks!

How do we feel about leaving apps that started in an older version on SHA1 for the long term? Is our recommendation that for this use case it's really fine either way, despite their differing security profiles... or is this a situation where using SHA1 really is less-advisable, and we're just warning that it's hard to change once you have data stored?

Regardless of how strongly we encourage it, if people do want to switch, is there a story for how they can go about it? How much of A Thing would it be for us to provide read-old-write-new compatibility, as [I believe?] we do for key transitions?

@jorgemanrubia
Copy link
Contributor Author

How do we feel about leaving apps that started in an older version on SHA1 for the long term? Is our recommendation that for this use case it's really fine either way, despite their differing security profiles... or is this a situation where using SHA1 really is less-advisable, and we're just warning that it's hard to change once you have data stored?

I don't think using SHA1 represents any security flaw here. Active Record encryption uses the digest algorithm to derive keys with the size expected by the cipher (AES256-GCM) from the configured secrets. The generated hashes are calculated on the fly and aren't exposed anywhere, so an attacker can't really exploit collision-related flaws in the algorithm to guess keys. Of course, as long as the original secrets remain private.

Regardless of how strongly we encourage it, if people do want to switch, is there a story for how they can go about it? How much of A Thing would it be for us to provide read-old-write-new compatibility, as [I believe?] we do for key transitions?

There is a path, but right now it's not directly supported. The system is prepared to work with multiple decryption keys, so it would be a matter of injecting keys with multiple hash digest algorithms (in ActiveRecord::Encryption::DerivedSecretKeyProvider). Possibly as an option, as we don't want to add more keys than necessary in general. I can explore this in a separated PR, but I don't think it's a pressing issue right now.

Comment on lines 32 to 34
# 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.
# Rails.application.config.active_record.encryption.hash_digest_class = OpenSSL::Digest::SHA256
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is sufficient. If you have data encrypted with previous versions, then you should set the old version as a config. That way if you later do load_defaults 7.1 it won't change this under the hood.

Suggested change
# 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.
# Rails.application.config.active_record.encryption.hash_digest_class = OpenSSL::Digest::SHA256
# Active Record Encryption hash digest algorithm: 2 options
#
# The new default hash digest algorithm is SHA-256. If you haven't used Active Record Encryption
# yet, it is safe to upgrade.
#
# **Important**: If you have data encrypted already, it is likely encrypted using SHA-1. You should keep the old default or the existing data will fail to decrypt.
#
# New default:
# Rails.application.config.active_record.encryption.hash_digest_class = OpenSSL::Digest::SHA256
#
# Old default:
# Rails.application.config.active_record.encryption.hash_digest_class = OpenSSL::Digest::SHA1

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point @ghiculescu. I'll review this and rebase the branch shortly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ghiculescu I have reviewed the template for the new 7.1 defaults explaining things better. If someone decides to skip the "upgrade" path and just loads the new 7.1 defaults, decryption will fail, but that's what happens with other similar changes (e.g: with cookie serialization back in the day), so I think that's OK. Do you have a different suggestion here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think your changes look good 👍

@ghiculescu
Copy link
Member

ghiculescu commented Dec 5, 2022

@matthewd @jorgemanrubia can we add this to the 7.1 milestone? Unless I'm misreading, #44540 introduced a fairly bad bug into Rails main which this fixes, but we shouldn't launch 7.1 with it un-fixed.

activerecord/test/cases/encryption/key_generator_test.rb Outdated Show resolved Hide resolved
Comment on lines 32 to 34
# 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.
# Rails.application.config.active_record.encryption.hash_digest_class = OpenSSL::Digest::SHA256
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point @ghiculescu. I'll review this and rebase the branch shortly.

@jorgemanrubia jorgemanrubia force-pushed the configure-digest-class-in-are branch 4 times, most recently from c453211 to 1034c6c Compare December 5, 2022 21:53
ActiveRecord::Encryption.config.hash_digest_class = other_digest_class
derived_with_other_digest_class = @generator.derive_key_from("some password", length: 12)

assert_not_equal derived_with_digest_class, derived_with_other_digest_class
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(I'm not an encryption expert at all...) beyond asserting a difference, is there a way of asserting that a specific string was encrypted with a specific digest?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 I changed the test to actually check that it's deriving the expected key.

@jorgemanrubia jorgemanrubia force-pushed the configure-digest-class-in-are branch 5 times, most recently from fa385e1 to df4f651 Compare December 5, 2022 22:16
@dhh dhh merged commit 5d7b6d8 into rails:main Feb 27, 2023
@dhh dhh deleted the configure-digest-class-in-are branch February 27, 2023 09:16
duduribeiro added a commit to duduribeiro/rails that referenced this pull request Jun 19, 2023
This commit changes the active_record_encryption.configuration
to be set on an after initialize block. After PR rails#44873, we can
configure now hash_digest_class that will be used on
ActiveRecord::Encryption. But the docs said that we could change
it via an initializer by setting
Rails.application.config.active_record.encryption.hash_digest_class.
This wasn't working because the railtie
"active_record_encryption.configuration" was being set before the
initializer was loaded. This changes this behave to configure
the active_record encryption after the initialize was loaded.
jorgemanrubia added a commit to basecamp/rails that referenced this pull request Jun 21, 2023
…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.
dhh pushed a commit that referenced this pull request Jun 25, 2023
…inistically with a SHA1 hash digest (#48530)

* Make sure active record encryption configuration happens after initializers have run

Co-authored-by: Cadu Ribeiro <mail@cadu.dev>

* Add a new option to support previous data encrypted non-deterministically 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
#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.

* Set a default value of true for `support_sha1_for_non_deterministic_encryption` and proper initializer values.

We want to enable the flag existing versions (< 7.1), and we want it to be false moving by
default moving forward.

* Make sure the system to auto-filter params supports different initialization orders

This reworks the system to auto-filter params so that it works when encrypted
attributes are declared before the encryption configuration logic runs.

Co-authored-by: Cadu Ribeiro <mail@cadu.dev>

---------

Co-authored-by: Cadu Ribeiro <mail@cadu.dev>
@ClaytonPassmore
Copy link

ClaytonPassmore commented Dec 18, 2023

We introduced some deterministic encrypted columns while using Rails 7.0. These were encrypted with SHA256. It must have been the default.

When we upgraded to Rails 7.1 while still using the 7.0 framework defaults, the encryption algorithm changed to SHA1. This resulted in decryption errors and could have resulted in mixed values had new records been inserted. Fortunately we realized what was going on before that could happen.

It's possible that I missed something during the upgrade but it seems like upgrading changed the default algorithm even though we were still using the 7.0 defaults. Apologies if this isn't the right spot for this, just thought I would post it here for visibility.

Edit: Looks like this might be tracked in #48204

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants