From ced61b46138e21c7e8ba8012953e85ce200b0e2b Mon Sep 17 00:00:00 2001 From: Colin Darie Date: Wed, 21 Aug 2024 12:48:26 +0200 Subject: [PATCH 1/5] chore: config cache format to rails 7 --- config/application.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/application.rb b/config/application.rb index 91b64e496a3..91b363d405e 100644 --- a/config/application.rb +++ b/config/application.rb @@ -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) From 98ff68f923157af652c6605287eef462ecad03cc Mon Sep 17 00:00:00 2001 From: Colin Darie Date: Wed, 21 Aug 2024 16:22:41 +0200 Subject: [PATCH 2/5] chore: rotate cookies with SHA256 hash --- config/initializers/cookie_rotator.rb | 36 ++++++++++--------- .../new_framework_defaults_7_0.rb | 2 +- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/config/initializers/cookie_rotator.rb b/config/initializers/cookie_rotator.rb index 53b40ec8f10..e307467638c 100644 --- a/config/initializers/cookie_rotator.rb +++ b/config/initializers/cookie_rotator.rb @@ -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 diff --git a/config/initializers/new_framework_defaults_7_0.rb b/config/initializers/new_framework_defaults_7_0.rb index 389456e6af8..ad3d94df822 100644 --- a/config/initializers/new_framework_defaults_7_0.rb +++ b/config/initializers/new_framework_defaults_7_0.rb @@ -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 From 3ac671576bf6fc73b134eb8cd730744d070f9ff0 Mon Sep 17 00:00:00 2001 From: Colin Darie Date: Thu, 22 Aug 2024 16:59:50 +0200 Subject: [PATCH 3/5] chore(encryption): rotate key with new default digest (SHA256) --- app/services/encryption_service.rb | 4 ++ spec/services/encryption_service_spec.rb | 85 ++++++++++++++++++++++++ 2 files changed, 89 insertions(+) diff --git a/app/services/encryption_service.rb b/app/services/encryption_service.rb index 8949d54a78a..3d0dbb92e21 100644 --- a/app/services/encryption_service.rb +++ b/app/services/encryption_service.rb @@ -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 end def encrypt(value) diff --git a/spec/services/encryption_service_spec.rb b/spec/services/encryption_service_spec.rb index 91540cc43bf..051c48cb131 100644 --- a/spec/services/encryption_service_spec.rb +++ b/spec/services/encryption_service_spec.rb @@ -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 From ca7100c7afefe28dd9835cb382e68a6e3baffcaa Mon Sep 17 00:00:00 2001 From: Colin Darie Date: Thu, 22 Aug 2024 17:33:45 +0200 Subject: [PATCH 4/5] chore(encryption): task rotating api particulier token encrypted attributes --- ...e_api_particulier_token_encryption_task.rb | 22 ++++++++ ..._particulier_token_encryption_task_spec.rb | 50 +++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 app/tasks/maintenance/rotate_api_particulier_token_encryption_task.rb create mode 100644 spec/tasks/maintenance/rotate_api_particulier_token_encryption_task_spec.rb diff --git a/app/tasks/maintenance/rotate_api_particulier_token_encryption_task.rb b/app/tasks/maintenance/rotate_api_particulier_token_encryption_task.rb new file mode 100644 index 00000000000..a47c6e13240 --- /dev/null +++ b/app/tasks/maintenance/rotate_api_particulier_token_encryption_task.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Maintenance + class RotateAPIParticulierTokenEncryptionTask < MaintenanceTasks::Task + def collection + # rubocop:disable DS/Unscoped + Procedure.unscoped.where.not(encrypted_api_particulier_token: nil) + # rubocop:enable DS/Unscoped + end + + def process(procedure) + decrypted_token = procedure.api_particulier_token + + procedure.api_particulier_token = decrypted_token + procedure.save!(validate: false) + end + + def count + collection.count + end + end +end diff --git a/spec/tasks/maintenance/rotate_api_particulier_token_encryption_task_spec.rb b/spec/tasks/maintenance/rotate_api_particulier_token_encryption_task_spec.rb new file mode 100644 index 00000000000..a1d08e4ad47 --- /dev/null +++ b/spec/tasks/maintenance/rotate_api_particulier_token_encryption_task_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require "rails_helper" + +module Maintenance + RSpec.describe RotateAPIParticulierTokenEncryptionTask do + describe "#process" do + subject { described_class.process(procedure) } + let(:token) { "secret-token-0123456789" } + let(:procedure) { create(:procedure) } + let(:legacy_encryption_service) do + EncryptionService.new.tap { |legacy_service| + legacy_key = ActiveSupport::KeyGenerator + .new(Rails.application.secrets.secret_key_base, hash_digest_class: OpenSSL::Digest::SHA1) + .generate_key(Rails.application.secrets.encryption_service_salt, ActiveSupport::MessageEncryptor.key_len) + legacy_encryptor = ActiveSupport::MessageEncryptor.new(legacy_key) + legacy_service.instance_variable_set(:@encryptor, legacy_encryptor) + } + end + + before do + # Encrypt the token using the legacy (SHA1) encryption service + legacy_encrypted_token = legacy_encryption_service.encrypt(token) + procedure.update_column(:encrypted_api_particulier_token, legacy_encrypted_token) + end + + it 're-encrypts the api_particulier_token' do + old_encrypted_value = procedure.encrypted_api_particulier_token + + expect { subject }.to change { procedure.reload.encrypted_api_particulier_token } + expect(procedure.api_particulier_token).to eq(token) + + encrypted_value = procedure.encrypted_api_particulier_token + + # Verify that the new encrypted value can't be decrypted with the legacy service + expect { legacy_encryption_service.decrypt(encrypted_value) } + .to raise_error(ActiveSupport::MessageEncryptor::InvalidMessage) + + # Verify that the new encrypted value can be decrypted with the current service + current_service = EncryptionService.new + expect(current_service.decrypt(encrypted_value)).to eq(token) + + # and with the services without rotations + current_service = EncryptionService.new + current_service.instance_variable_set(:@rotations, []) + expect(current_service.decrypt(encrypted_value)).to eq(token) + end + end + end +end From 5859ea4a23c58e18f5b03a509fe64ec7ede80284 Mon Sep 17 00:00:00 2001 From: Colin Darie Date: Thu, 22 Aug 2024 17:37:09 +0200 Subject: [PATCH 5/5] chore: load rails 7.0 defaults --- config/application.rb | 2 +- config/initializers/active_storage.rb | 1 + .../new_framework_defaults_7_0.rb | 135 ------------------ 3 files changed, 2 insertions(+), 136 deletions(-) delete mode 100644 config/initializers/new_framework_defaults_7_0.rb diff --git a/config/application.rb b/config/application.rb index 91b363d405e..b5798638850 100644 --- a/config/application.rb +++ b/config/application.rb @@ -13,7 +13,7 @@ module TPS class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. - config.load_defaults 6.1 + config.load_defaults 7.0 # Configuration for the application, engines, and railties goes here. # diff --git a/config/initializers/active_storage.rb b/config/initializers/active_storage.rb index f35c5e36b87..620e3f022d4 100644 --- a/config/initializers/active_storage.rb +++ b/config/initializers/active_storage.rb @@ -2,6 +2,7 @@ Rails.application.config.active_storage.service_urls_expire_in = 1.hour +Rails.application.config.active_storage.variant_processor = :mini_magick Rails.application.config.active_storage.analyzers.delete ActiveStorage::Analyzer::ImageAnalyzer Rails.application.config.active_storage.analyzers.delete ActiveStorage::Analyzer::VideoAnalyzer diff --git a/config/initializers/new_framework_defaults_7_0.rb b/config/initializers/new_framework_defaults_7_0.rb deleted file mode 100644 index ad3d94df822..00000000000 --- a/config/initializers/new_framework_defaults_7_0.rb +++ /dev/null @@ -1,135 +0,0 @@ -# frozen_string_literal: true - -# Be sure to restart your server when you modify this file. -# -# This file eases your Rails 7.0 framework defaults upgrade. -# -# Uncomment each configuration one by one to switch to the new default. -# Once your application is ready to run with all new defaults, you can remove -# this file and set the `config.load_defaults` to `7.0`. -# -# Read the Guide for Upgrading Ruby on Rails for more info on each option. -# https://guides.rubyonrails.org/upgrading_ruby_on_rails.html - -# `button_to` view helper will render `