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

[WIP] Change SECRET_KEY_BASE without breaking anything #1541

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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: 3 additions & 1 deletion .env.development
Original file line number Diff line number Diff line change
Expand Up @@ -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'
11 changes: 10 additions & 1 deletion app/models/concerns/encryptable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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|
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions app/models/subdomain.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions app/services/encryption_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
11 changes: 10 additions & 1 deletion config/initializers/active_storge.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,13 @@ def ensure_storage_limit_not_exceeded
end
end
end
end
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

21 changes: 21 additions & 0 deletions config/initializers/cookie_rotator.rb
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions config/secrets.yml
Original file line number Diff line number Diff line change
@@ -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' } %>

Expand Down
22 changes: 22 additions & 0 deletions lib/tasks/encryption.rake
Original file line number Diff line number Diff line change
@@ -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
37 changes: 37 additions & 0 deletions test/tasks/encryption_reencrypt_task_test.rb
Original file line number Diff line number Diff line change
@@ -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