diff --git a/.env.development b/.env.development index f9872d701..5cd8b0a39 100644 --- a/.env.development +++ b/.env.development @@ -8,4 +8,6 @@ REDIS_URL=redis://solutions_redis:6379/12 APP_HOST=localhost RECAPTCHA_SITE_KEY=foocvi0PSRc_AyhjP7jcN RECAPTCHA_SECRET_KEY=bar8saabKym-6u5KGh -RACK_TIMEOUT_SERVICE_TIMEOUT=500 +VIOLET_SERVICE_TIMEOUT=500 +SECRET_KEY_BASE='aa4d8c6bf051258622bd2e923b926ab59b40f912b661216f764d993e8d6b8bbfbc33026e5c954b6c51f7k' +OLD_SECRET_KEY_BASE='76878aa4d8c6bf051258622bd2e923b926ab59b40f912b661216f764d993e8d6b8bbfbc33026e5c954b6c51f7k' diff --git a/app/models/concerns/encryptable.rb b/app/models/concerns/encryptable.rb index b6fb81333..3e0d7cd80 100644 --- a/app/models/concerns/encryptable.rb +++ b/app/models/concerns/encryptable.rb @@ -5,15 +5,22 @@ # also create salt if not exist module Encryptable extend ActiveSupport::Concern + included do + @encryptables = [] def build_salt self.salt = SecureRandom.random_bytes( ActiveSupport::MessageEncryptor.key_len ) end end - + class_methods do + + def encryptables + @encryptables + end + def attr_encrypted(*attributes) # rubocop:disable Metrics/AbcSize attributes.each do |attribute| define_method("#{attribute}=".to_sym) do |value| @@ -31,6 +38,8 @@ def attr_encrypted(*attributes) # rubocop:disable Metrics/AbcSize value = public_send("encrypted_#{attribute}".to_sym) EncryptionService.new(salt, value).decrypt if value.present? end + + @encryptables << attribute end end end diff --git a/app/models/subdomain.rb b/app/models/subdomain.rb index 79946a5b2..9bbe3e2f6 100755 --- a/app/models/subdomain.rb +++ b/app/models/subdomain.rb @@ -238,6 +238,8 @@ def create_tenant end def self.all_with_public_schema + return Subdomain.all if Subdomain.where(name: 'public').present? + subdomain = Subdomain.new(name: 'public') Subdomain.all.to_a.push(subdomain) end diff --git a/app/services/encryption_service.rb b/app/services/encryption_service.rb index f2ba79aa7..d0891f004 100644 --- a/app/services/encryption_service.rb +++ b/app/services/encryption_service.rb @@ -4,10 +4,10 @@ class EncryptionService delegate :encrypt_and_sign, :decrypt_and_verify, to: :encryptor - def initialize(salt, value) + def initialize(salt, value, secret = ENCRYPT_KEY_BASE) @value = value - key = ActiveSupport::KeyGenerator.new(ENCRYPT_KEY_BASE).generate_key(salt, KEY_LEN) + key = ActiveSupport::KeyGenerator.new(secret).generate_key(salt, KEY_LEN) @crypt = ActiveSupport::MessageEncryptor.new(key) end diff --git a/config/initializers/active_storge.rb b/config/initializers/active_storge.rb index fa5fe6f86..d1efb2c15 100644 --- a/config/initializers/active_storge.rb +++ b/config/initializers/active_storge.rb @@ -11,4 +11,13 @@ def ensure_storage_limit_not_exceeded end end end -end \ No newline at end of file +end + +if ENV['OLD_SECRET_KEY_BASE'].present? + Rails.application.config.after_initialize do |app| + key_generator = ActiveSupport::KeyGenerator.new(ENV['OLD_SECRET_KEY_BASE'], iterations: 1000, hash_digest_class: OpenSSL::Digest::SHA1) + secret = key_generator.generate_key("ActiveStorage") + app.message_verifier("ActiveStorage").rotate(secret) + end +end + diff --git a/config/initializers/cookie_rotator.rb b/config/initializers/cookie_rotator.rb new file mode 100644 index 000000000..f976a5b60 --- /dev/null +++ b/config/initializers/cookie_rotator.rb @@ -0,0 +1,21 @@ +if ENV['OLD_SECRET_KEY_BASE'].present? + Rails.application.config.after_initialize do |app| + 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 = ENV['OLD_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 + + 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 +end \ No newline at end of file diff --git a/config/secrets.yml b/config/secrets.yml index c4f059f70..9de2ed598 100644 --- a/config/secrets.yml +++ b/config/secrets.yml @@ -1,6 +1,9 @@ production: secret_key_base: <%= ENV.fetch('SECRET_KEY_BASE') { 'foobar' } %> +development: + secret_key_base: <%= ENV.fetch('SECRET_KEY_BASE') { 'foobar' } %> + staging: secret_key_base: <%= ENV.fetch('SECRET_KEY_BASE') { 'foobar' } %> diff --git a/lib/tasks/encryption.rake b/lib/tasks/encryption.rake new file mode 100644 index 000000000..1b9eb4cd2 --- /dev/null +++ b/lib/tasks/encryption.rake @@ -0,0 +1,22 @@ +namespace :encryption do + desc "tasks for decrypting secrets with old SECRET_KEY_BASE and reencrypting with new key" + + task :reencrypt => [:environment] do |t, args| + Rails.application.eager_load! + + Subdomain.all_with_public_schema.each do |subdomain| + Apartment::Tenant.switch subdomain.name do + ActiveRecord::Base.descendants.select { |klass| klass.respond_to? :encryptables }.each do |klass| + klass.encryptables&.each do |encrypted_attribute| + klass.where.not("encrypted_#{encrypted_attribute}".to_sym => nil).in_batches do |records| + records.each do |record| + value = EncryptionService.new(record.salt, record.public_send("encrypted_#{encrypted_attribute}".to_sym), ENV["OLD_SECRET_KEY_BASE"]).decrypt + record.update!("#{encrypted_attribute}".to_sym => value) + end + end + end + end + end + end + end +end \ No newline at end of file diff --git a/test/tasks/encryption_reencrypt_task_test.rb b/test/tasks/encryption_reencrypt_task_test.rb new file mode 100644 index 000000000..a2779a86f --- /dev/null +++ b/test/tasks/encryption_reencrypt_task_test.rb @@ -0,0 +1,37 @@ +require "test_helper" +require "rake" + +class EncryptionReencryptTaskTest < ActiveSupport::TestCase + setup do + @api_namespace = api_namespaces(:one) + Sidekiq::Testing.fake! + Rails.application.load_tasks if Rake::Task.tasks.empty? + ApiAction.destroy_all + end + + test 'should be able to access encrypted secret after changeing SECRET_KEY_BASE' do + Rails.application.secrets.secret_key_base = 'test_123' + + api_action = CreateApiAction.create(api_namespace_id: @api_namespace.id, action_type: 'send_web_request', bearer_token: 'my_bearer_token') + api_key = ApiKey.create(label: 'test', authentication_strategy: 'bearer_token') + + api_key_token = api_key.token + + assert_equal 'my_bearer_token', api_action.bearer_token + Rails.application.secrets.secret_key_base = 'new_test_123' + ENV['OLD_SECRET_KEY_BASE'] = 'test_123' + + Object.send(:remove_const, :EncryptionService) + load 'app/services/encryption_service.rb' + + assert_raises(ActiveSupport::MessageEncryptor::InvalidMessage) do + api_action.reload.bearer_token + api_key.reload.token + end + + Rake::Task["encryption:reencrypt"].invoke + + assert_equal 'my_bearer_token', api_action.reload.bearer_token + assert_equal api_key_token, api_key.reload.token + end +end \ No newline at end of file