lib/symmetric_encryption/keystore.rb in symmetric-encryption-4.0.1 vs lib/symmetric_encryption/keystore.rb in symmetric-encryption-4.1.0.beta1

- old
+ new

@@ -1,14 +1,36 @@ module SymmetricEncryption # Encryption keys are secured in Keystores module Keystore # @formatter:off + autoload :Aws, 'symmetric_encryption/keystore/aws' autoload :Environment, 'symmetric_encryption/keystore/environment' autoload :File, 'symmetric_encryption/keystore/file' + autoload :Heroku, 'symmetric_encryption/keystore/heroku' autoload :Memory, 'symmetric_encryption/keystore/memory' # @formatter:on + # Returns [Hash] a new keystore configuration after generating data keys for each environment. + def self.generate_data_keys(keystore:, environments: %i[development test release production], **args) + keystore_class = keystore.is_a?(Symbol) || keystore.is_a?(String) ? constantize_symbol(keystore) : keystore + + configs = {} + environments.each do |environment| + environment = environment.to_sym + configs[environment] = + if %i[development test].include?(environment) + dev_config + else + cfg = keystore_class.generate_data_key(environment: environment, **args) + { + ciphers: [cfg] + } + end + end + configs + end + # Returns [Hash] a new configuration file after performing key rotation. # # Perform key rotation for each of the environments in the configuration file, by # * generating a new key, and iv with an incremented version number. # @@ -25,14 +47,17 @@ # Then in a subsequent deploy the key can be moved into the first position to activate it. # In this way during a rolling deploy encrypted values written by updated servers will be readable # by the servers that have not been updated yet. # Default: false # + # keystore: [Symbol] + # If supplied, changes the keystore during key rotation. + # # Notes: # * iv_filename is no longer supported and is removed when creating a new random cipher. # * `iv` does not need to be encrypted and is included in the clear. - def self.rotate_keys!(full_config, environments: [], app_name:, rolling_deploy: false) + def self.rotate_keys!(full_config, environments: [], app_name:, rolling_deploy: false, keystore: nil) full_config.each_pair do |environment, cfg| # Only rotate keys for specified environments. Default, all next if !environments.empty? && !environments.include?(environment.to_sym) # Find the highest version number @@ -41,26 +66,28 @@ config = cfg[:ciphers].first # Only generate new keys for keystore's that have a key encrypting key next unless config[:key_encrypting_key] || config[:private_rsa_key] - cipher_name = config[:cipher_name] || 'aes-256-cbc' - new_key_config = - if config.key?(:key_filename) - key_path = ::File.dirname(config[:key_filename]) - Keystore::File.new_key_config(key_path: key_path, cipher_name: cipher_name, app_name: app_name, version: version, environment: environment) - elsif config.key?(:key_env_var) - Keystore::Environment.new_key_config(cipher_name: cipher_name, app_name: app_name, version: version, environment: environment) - elsif config.key?(:encrypted_key) - Keystore::Memory.new_key_config(cipher_name: cipher_name, app_name: app_name, version: version, environment: environment) - end + cipher_name = config[:cipher_name] || 'aes-256-cbc' + keystore_class = keystore ? constantize_symbol(keystore) : keystore_for(config) + + args = { + cipher_name: cipher_name, + app_name: app_name, + version: version, + environment: environment + } + args[:key_path] = ::File.dirname(config[:key_filename]) if config.key?(:key_filename) + new_data_key = keystore_class.generate_data_key(args) + # Add as second key so that key can be published now and only used in a later deploy. if rolling_deploy - cfg[:ciphers].insert(1, new_key_config) + cfg[:ciphers].insert(1, new_data_key) else - cfg[:ciphers].unshift(new_key_config) + cfg[:ciphers].unshift(new_data_key) end end full_config end @@ -81,31 +108,33 @@ version -= 1 always_add_header = config.delete(:always_add_header) encoding = config.delete(:encoding) - Key.migrate_config!(config) + migrate_config!(config) # The current data encrypting key without any of the key encrypting keys. - key = Key.from_config(config) + key = Keystore.read_key(config) cipher_name = key.cipher_name - new_key_config = - if config.key?(:key_filename) - key_path = ::File.dirname(config[:key_filename]) - Keystore::File.new_key_config(key_path: key_path, cipher_name: cipher_name, app_name: app_name, version: version, environment: environment, dek: key) - elsif config.key?(:key_env_var) - Keystore::Environment.new_key_config(cipher_name: cipher_name, app_name: app_name, version: version, environment: environment, dek: key) - elsif config.key?(:encrypted_key) - Keystore::Memory.new_key_config(cipher_name: cipher_name, app_name: app_name, version: version, environment: environment, dek: key) - end + keystore_class = keystore_for(config) - new_key_config[:always_add_header] = always_add_header - new_key_config[:encoding] = encoding + args = { + cipher_name: cipher_name, + app_name: app_name, + version: version, + environment: environment, + dek: key + } + args[:key_path] = ::File.dirname(config[:key_filename]) if config.key?(:key_filename) + new_config = keystore_class.generate_data_key(args) + new_config[:always_add_header] = always_add_header + new_config[:encoding] = encoding + # Replace existing config entry cfg[:ciphers].shift - cfg[:ciphers].unshift(new_key_config) + cfg[:ciphers].unshift(new_config) end full_config end # The default development config. @@ -120,7 +149,94 @@ version: 1 } ] } end + + # Returns [Key] by recursively navigating the config tree. + # + # Supports N level deep key encrypting keys. + def self.read_key(key: nil, iv:, key_encrypting_key: nil, cipher_name: 'aes-256-cbc', keystore: nil, version: 0, **args) + if key_encrypting_key.is_a?(Hash) + # Recurse up the chain returning the parent key_encrypting_key + key_encrypting_key = read_key(cipher_name: cipher_name, **key_encrypting_key) + end + + unless key + keystore_class = keystore ? constantize_symbol(keystore) : keystore_for(args) + store = keystore_class.new(key_encrypting_key: key_encrypting_key, **args) + key = store.read + end + + Key.new(key: key, iv: iv, cipher_name: cipher_name) + end + + # + # Internal use only methods + # + + def self.keystore_for(config) + if config[:keystore] + constantize_symbol(config[:keystore]) + elsif config[:encrypted_key] + Keystore::Memory + elsif config[:key_filename] + Keystore::File + elsif config[:key_env_var] + Keystore::Environment + else + raise(ArgumentError, 'Unknown keystore supplied in config') + end + end + + def self.constantize_symbol(symbol, namespace = 'SymmetricEncryption::Keystore') + klass = "#{namespace}::#{camelize(symbol.to_s)}" + begin + Object.const_get(klass) + rescue NameError + raise(ArgumentError, "Keystore: #{symbol.inspect} not found. Looking for: #{klass}") + end + end + + # Borrow from Rails, when not running Rails + def self.camelize(term) + string = term.to_s + string = string.sub(/^[a-z\d]*/, &:capitalize) + string.gsub!(/(?:_|(\/))([a-z\d]*)/i) { "#{Regexp.last_match(1)}#{Regexp.last_match(2).capitalize}" } + string.gsub!('/'.freeze, '::'.freeze) + string + end + + # Migrate a prior config. + # + # Note: + # * The config cannot be saved back to the config file once + # migrated, without generating new Key Encrypting Keys. + # * Only run this migration in the target environment so that the + # current key encrypting files are present. + def self.migrate_config!(config) + # Backward compatibility - Deprecated + private_rsa_key = config.delete(:private_rsa_key) + + # Migrate old encrypted_iv + if (encrypted_iv = config.delete(:encrypted_iv)) && private_rsa_key + encrypted_iv = RSAKey.new(private_rsa_key).decrypt(encrypted_iv) + config[:iv] = ::Base64.decode64(encrypted_iv) + end + + # Migrate old iv_filename + if (file_name = config.delete(:iv_filename)) && private_rsa_key + encrypted_iv = ::File.read(file_name) + config[:iv] = RSAKey.new(private_rsa_key).decrypt(encrypted_iv) + end + + # Backward compatibility - Deprecated + config[:key_encrypting_key] = RSAKey.new(private_rsa_key) if private_rsa_key + + # Migrate old encrypted_key to new binary format + if (encrypted_key = config[:encrypted_key]) && private_rsa_key + config[:encrypted_key] = ::Base64.decode64(encrypted_key) + end + end + end end