# The key, iv and encrypted data are handled in their raw form, with no encoding.
module SymmetricEncryption
  class Key
    attr_reader :key, :iv, :cipher_name

    # Returns [Key] from cipher data usually extracted from the configuration file.
    #
    # Supports N level deep key encrypting keys.
    #
    # Configuration keys:
    # * key
    # * encrypted_key
    # * key_filename
    def self.from_config(key: nil, key_filename: nil, encrypted_key: nil, key_env_var: nil,
                         iv:, key_encrypting_key: nil, cipher_name: 'aes-256-cbc')

      if key_encrypting_key.is_a?(Hash)
        # Recurse up the chain returning the parent key_encrypting_key
        key_encrypting_key = from_config(cipher_name: cipher_name, **key_encrypting_key)
      end

      key ||=
        if encrypted_key
          raise(ArgumentError, 'Missing mandatory :key_encrypting_key when config includes :encrypted_key') unless key_encrypting_key
          Keystore::Memory.new(encrypted_key: encrypted_key, key_encrypting_key: key_encrypting_key).read
        elsif key_filename
          Keystore::File.new(file_name: key_filename, key_encrypting_key: key_encrypting_key).read
        elsif key_env_var
          raise(ArgumentError, 'Missing mandatory :key_encrypting_key when config includes :key_env_var') unless key_encrypting_key
          Keystore::Environment.new(key_env_var: key_env_var, key_encrypting_key: key_encrypting_key).read
        end

      new(key: key, iv: iv, cipher_name: cipher_name)
    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

    def initialize(key: :random, iv: :random, cipher_name: 'aes-256-cbc')
      @key         = key == :random ? ::OpenSSL::Cipher.new(cipher_name).random_key : key
      @iv          = iv == :random ? ::OpenSSL::Cipher.new(cipher_name).random_iv : iv
      @cipher_name = cipher_name
    end

    def encrypt(string)
      return if string.nil?
      string = string.to_s
      return string if string.empty?

      # Creates a new OpenSSL::Cipher with every call so that this key instance is thread-safe.
      openssl_cipher = ::OpenSSL::Cipher.new(cipher_name)
      openssl_cipher.encrypt
      openssl_cipher.key = key
      openssl_cipher.iv  = iv

      result = openssl_cipher.update(string)
      result << openssl_cipher.final
    end

    def decrypt(encrypted_string)
      return if encrypted_string.nil?
      encrypted_string = encrypted_string.to_s
      encrypted_string.force_encoding(SymmetricEncryption::BINARY_ENCODING)
      return encrypted_string if encrypted_string.empty?

      # Creates a new OpenSSL::Cipher with every call so that this key instance is thread-safe.
      openssl_cipher = ::OpenSSL::Cipher.new(cipher_name)
      openssl_cipher.decrypt
      openssl_cipher.key = key
      openssl_cipher.iv  = iv

      result = openssl_cipher.update(encrypted_string)
      result << openssl_cipher.final
    end
  end
end
