lib/chamber/filters/encryption_filter.rb
# frozen_string_literal: true
require 'openssl'
require 'yaml'
require 'chamber/encryption_methods/public_key'
require 'chamber/encryption_methods/ssl'
require 'chamber/encryption_methods/none'
require 'chamber/refinements/deep_dup'
module Chamber
module Filters
class EncryptionFilter
using ::Chamber::Refinements::DeepDup
BASE64_STRING_PATTERN = %r{\A[A-Za-z0-9+/]{342}==\z}.freeze
BASE64_SUBSTRING_PATTERN = %r{[A-Za-z0-9+/#]*={0,2}}.freeze
LARGE_DATA_STRING_PATTERN = /
\A
(#{BASE64_SUBSTRING_PATTERN})
\#
(#{BASE64_SUBSTRING_PATTERN})
\#
(#{BASE64_SUBSTRING_PATTERN})
\z
/x.freeze
attr_accessor :data,
:secure_key_token
attr_reader :encryption_keys
def self.execute(**args)
new(**args).__send__(:execute)
end
def initialize(data:, secure_key_prefix:, encryption_keys: {}, **_args)
self.encryption_keys = (encryption_keys || {}).transform_keys(&:to_s)
self.data = data.deep_dup
self.secure_key_token = /\A#{Regexp.escape(secure_key_prefix)}/
end
protected
def execute(raw_data = data, namespace = nil)
raw_data.each_with_object({}) do |(key, value), settings|
settings[key] = if value.respond_to? :each_pair
execute(value, namespace || key)
elsif key.match(secure_key_token)
encrypt(namespace, key, value)
else
value
end
end
end
def encryption_keys=(other)
@encryption_keys = other.each_with_object({}) do |(namespace, keyish), memo| # rubocop:disable Style/HashTransformValues
memo[namespace] = if keyish.is_a?(OpenSSL::PKey::RSA)
keyish
elsif ::File.readable?(::File.expand_path(keyish))
file_contents = ::File.read(::File.expand_path(keyish))
OpenSSL::PKey::RSA.new(file_contents)
else
OpenSSL::PKey::RSA.new(keyish)
end
end
end
private
def encrypt(namespace, key, value)
method = encryption_method(value)
encryption_key = encryption_keys[namespace] || encryption_keys['__default']
return value unless encryption_key
method.encrypt(key, value, encryption_key)
end
def encryption_method(value)
value_is_encrypted = value.is_a?(::String) &&
(value.match(BASE64_STRING_PATTERN) ||
value.match(LARGE_DATA_STRING_PATTERN))
if value_is_encrypted
EncryptionMethods::None
else
serialized_value = YAML.dump(value)
if serialized_value.length <= 128
EncryptionMethods::PublicKey
else
EncryptionMethods::Ssl
end
end
end
end
end
end