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

Rails 7.0 : finalise la migration des defaults #10712

Merged
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions app/services/encryption_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ def initialize
password = Rails.application.secrets.secret_key_base
key = ActiveSupport::KeyGenerator.new(password).generate_key(salt, len)
@encryptor = ActiveSupport::MessageEncryptor.new(key)

# Remove after all encrypted attributes have been rotated.
legacy_key = ActiveSupport::KeyGenerator.new(password, hash_digest_class: OpenSSL::Digest::SHA1).generate_key(salt, len)
@encryptor.rotate legacy_key
Copy link
Contributor

@mfo mfo Sep 3, 2024

Choose a reason for hiding this comment

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

til: https://api.rubyonrails.org/v7.1.0/classes/ActiveSupport/MessageEncryptor.html – vraiment cool j'avais vraiment pas creusé comme API :-)

end

def encrypt(value)
Expand Down
2 changes: 2 additions & 0 deletions config/application.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ class Application < Rails::Application
config.active_storage.queues.analysis = :active_storage_analysis
config.active_storage.queues.purge = :purge

config.active_support.cache_format_version = 7.0

config.to_prepare do
# Make main application helpers available in administrate
Administrate::ApplicationController.helper(TPS::Application.helpers)
Expand Down
36 changes: 20 additions & 16 deletions config/initializers/cookie_rotator.rb
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
# frozen_string_literal: true

# TODO: Enable cookies rotation when new SHA256 will be enforced
# See new_framework_defaults_7.0.rb
# key_generator_hash_digest_class = OpenSSL::Digest::SHA256 will be
#
# Rails.application.config.after_initialize do
# Rails.application.config.action_dispatch.cookies_rotations.tap do |cookies|
# salt = Rails.application.config.action_dispatch.authenticated_encrypted_cookie_salt
# secret_key_base = Rails.application.secret_key_base
# This cookie rotator converts cookies from the old SHA1 hash (Rails 6) to SHA256 hash (Rails 7 default).
# It should be kept enabled for approximately 1 month to ensure most users have their cookies rotated.
# After this period, it can be safely removed.
# Without this rotator, all users would have been signed out.
Rails.application.config.after_initialize do
Rails.application.config.action_dispatch.cookies_rotations.tap do |cookies|
authenticated_encrypted_cookie_salt = Rails.application.config.action_dispatch.authenticated_encrypted_cookie_salt
signed_cookie_salt = Rails.application.config.action_dispatch.signed_cookie_salt
secret_key_base = Rails.application.secret_key_base

# key_generator = ActiveSupport::KeyGenerator.new(
# secret_key_base, iterations: 1000, hash_digest_class: OpenSSL::Digest::SHA1
# )
# key_len = ActiveSupport::MessageEncryptor.key_len
# secret = key_generator.generate_key(salt, key_len)
key_generator = ActiveSupport::KeyGenerator.new(
secret_key_base, iterations: 1000, hash_digest_class: OpenSSL::Digest::SHA1 # Rails 6 hash
)
key_len = ActiveSupport::MessageEncryptor.key_len

# cookies.rotate :encrypted, secret
# end
# end
old_encrypted_secret = key_generator.generate_key(authenticated_encrypted_cookie_salt, key_len)
old_signed_secret = key_generator.generate_key(signed_cookie_salt)

cookies.rotate :encrypted, old_encrypted_secret
cookies.rotate :signed, old_signed_secret
end
end
2 changes: 1 addition & 1 deletion config/initializers/new_framework_defaults_7_0.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
#
# See upgrading guide for more information on how to build a rotator.
# https://guides.rubyonrails.org/v7.0/upgrading_ruby_on_rails.html
# Rails.application.config.active_support.key_generator_hash_digest_class = OpenSSL::Digest::SHA256
Rails.application.config.active_support.key_generator_hash_digest_class = OpenSSL::Digest::SHA256

# Change the digest class for ActiveSupport::Digest.
# Changing this default means that for example Etags change and
Expand Down
85 changes: 85 additions & 0 deletions spec/services/encryption_service_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,89 @@
it { expect { subject }.to raise_exception StandardError }
end
end

describe "key rotation" do
let(:password) { Rails.application.secrets.secret_key_base }
let(:salt) { Rails.application.secrets.encryption_service_salt }
let(:len) { ActiveSupport::MessageEncryptor.key_len }
let(:value) { "Sensitive information" }

let(:legacy_key) do
ActiveSupport::KeyGenerator.new(password, hash_digest_class: OpenSSL::Digest::SHA1)
.generate_key(salt, len)
end

let(:new_key) do
ActiveSupport::KeyGenerator.new(password)
.generate_key(salt, len)
end

let(:legacy_encryptor) { ActiveSupport::MessageEncryptor.new(legacy_key) }
let(:new_encryptor) { ActiveSupport::MessageEncryptor.new(new_key) }

describe "#decrypt" do
subject { EncryptionService.new.decrypt(encrypted_value) }

context "with a value encrypted using the legacy SHA1-based key" do
let(:encrypted_value) { legacy_encryptor.encrypt_and_sign(value) }

it "successfully decrypts the value" do
expect(subject).to eq(value)
end
end

context "with a value encrypted using the new SHA256-based key" do
let(:encrypted_value) { new_encryptor.encrypt_and_sign(value) }

it "successfully decrypts the value" do
expect(subject).to eq(value)
end
end
end

describe "transition from legacy to new encryption" do
let(:legacy_service) do
legacy_encryption_service = EncryptionService.new
legacy_encryption_service.instance_variable_set(:@encryptor, legacy_encryptor)
legacy_encryption_service
end

let(:new_service) { EncryptionService.new }

it "can decrypt values encrypted with the legacy key" do
legacy_encrypted = legacy_service.encrypt(value)
expect(new_service.decrypt(legacy_encrypted)).to eq(value)
end

it "uses the new key for new encryptions" do
new_encrypted = new_service.encrypt(value)
expect { legacy_encryptor.decrypt_and_verify(new_encrypted) }
.to raise_error(ActiveSupport::MessageEncryptor::InvalidMessage)
expect(new_encryptor.decrypt_and_verify(new_encrypted)).to eq(value)
end
end

describe "backwards compatibility" do
let(:value) { "Important data" }
let(:old_service) do # Test with a service encrypting data without rotation mechanism
Class.new do
def initialize(key)
@encryptor = ActiveSupport::MessageEncryptor.new(key)
end

def encrypt(value)
@encryptor.encrypt_and_sign(value)
end
end
end

it "can decrypt values from a hypothetical old version without rotation" do
old_key = ActiveSupport::KeyGenerator.new(password, hash_digest_class: OpenSSL::Digest::SHA1)
.generate_key(salt, len)
old_encrypted = old_service.new(old_key).encrypt(value)

expect(EncryptionService.new.decrypt(old_encrypted)).to eq(value)
end
end
end
end