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