arkency/rails_event_store

View on GitHub
ruby_event_store/lib/ruby_event_store/mappers/transformation/encryption.rb

Summary

Maintainability
A
1 hr
Test Coverage
# frozen_string_literal: true

module RubyEventStore
  module Mappers
    module Transformation
      class Encryption
        class Leaf
          def self.===(hash)
            hash.keys.sort.eql? %i[cipher identifier iv]
          end
        end
        private_constant :Leaf

        class MissingEncryptionKey < StandardError
          def initialize(key_identifier)
            super "Could not find encryption key for '#{key_identifier}'"
          end
        end

        def initialize(key_repository, serializer: Serializers::YAML, forgotten_data: ForgottenData.new)
          @key_repository = key_repository
          @serializer = serializer
          @forgotten_data = forgotten_data
        end

        def dump(record)
          data = record.data
          metadata = record.metadata.dup
          event_class = Object.const_get(record.event_type)

          crypto_description = encryption_metadata(data, encryption_schema(event_class))
          metadata[:encryption] = crypto_description unless crypto_description.empty?

          Record.new(
            event_id: record.event_id,
            event_type: record.event_type,
            data: encrypt_data(deep_dup(data), crypto_description),
            metadata: metadata,
            timestamp: record.timestamp,
            valid_at: record.valid_at
          )
        end

        def load(record)
          metadata = record.metadata.dup
          crypto_description = Hash(metadata.delete(:encryption))

          Record.new(
            event_id: record.event_id,
            event_type: record.event_type,
            data: decrypt_data(record.data, crypto_description),
            metadata: metadata,
            timestamp: record.timestamp,
            valid_at: record.valid_at
          )
        end

        private

        attr_reader :key_repository, :serializer, :forgotten_data

        def encryption_schema(event_class)
          event_class.respond_to?(:encryption_schema) ? event_class.encryption_schema : {}
        end

        def deep_dup(hash)
          duplicate = hash.dup
          duplicate.each { |k, v| duplicate[k] = v.instance_of?(Hash) ? deep_dup(v) : v }
          duplicate
        end

        def encryption_metadata(data, schema)
          schema.inject({}) do |acc, (key, value)|
            case value
            when Hash
              acc[key] = encryption_metadata(data, value)
            when Proc
              key_identifier = value.call(data)
              encryption_key = key_repository.key_of(key_identifier)
              raise MissingEncryptionKey.new(key_identifier) unless encryption_key
              acc[key] = { cipher: encryption_key.cipher, iv: encryption_key.random_iv, identifier: key_identifier }
            end
            acc
          end
        end

        def encrypt_data(data, meta)
          meta.reduce(data) do |acc, (key, value)|
            acc[key] = encrypt_attribute(acc, key, value) if data.has_key?(key)
            acc
          end
        end

        def decrypt_data(data, meta)
          meta.reduce(data) do |acc, (key, value)|
            acc[key] = decrypt_attribute(data, key, value) if data.has_key?(key)
            acc
          end
        end

        def encrypt_attribute(data, attribute, meta)
          case meta
          when Leaf
            value = data.fetch(attribute)
            return if value.nil?

            encryption_key = key_repository.key_of(meta.fetch(:identifier))
            encryption_key.encrypt(serializer.dump(value), meta.fetch(:iv))
          when Hash
            encrypt_data(data.fetch(attribute), meta)
          end
        end

        def decrypt_attribute(data, attribute, meta)
          case meta
          when Leaf
            cryptogram = data.fetch(attribute)
            return unless cryptogram

            encryption_key = key_repository.key_of(meta.fetch(:identifier), cipher: meta.fetch(:cipher)) or
              return forgotten_data
            serializer.load(encryption_key.decrypt(cryptogram, meta.fetch(:iv)))
          when Hash
            decrypt_data(data.fetch(attribute), meta)
          end
        rescue OpenSSL::Cipher::CipherError
          forgotten_data
        end
      end
    end
  end
end