module CryptKeeper
  module Helper
    module SQL
      private

      # Private: Sanitize an sql query and then execute it.
      #
      # query - the sql query
      # new_transaction - if the query should run inside a new transaction
      #
      # Returns the ActiveRecord response.
      def escape_and_execute_sql(query, new_transaction: false)
        query = ::ActiveRecord::Base.send :sanitize_sql_array, query

        # force binary encoding to avoid "invalid byte sequence in UTF-8" errors
        # when we send binary AES keys (f.ex) to the database
        query = query.b if query.respond_to?(:b)

        if CryptKeeper.silence_logs?
          ::ActiveRecord::Base.logger.silence do
            execute_sql(query, new_transaction: new_transaction)
          end
        else
          execute_sql(query, new_transaction: new_transaction)
        end
      end

      # Private: Executes the query.
      #
      # query - the sql query
      # new_transaction - if the query should run inside a new transaction
      #
      # Returns an Array.
      def execute_sql(query, new_transaction: false)
        if new_transaction
          ::ActiveRecord::Base.transaction(requires_new: true) do
            ::ActiveRecord::Base.connection.execute(query).first
          end
        else
          ::ActiveRecord::Base.connection.execute(query).first
        end
      end
    end

    module DigestPassphrase
      # Private: Hash algorithm to use when generating a PBKDF2 passphrase.
      #
      # Returns a String.
      HASH_ALGORITHM = "sha512".freeze

      # Private: Iterations to use when generating a PBKDF2 passphrase.
      #
      # Returns a String.
      ITERATIONS = 5000

      private_constant :HASH_ALGORITHM, :ITERATIONS

      # Public: Iterations to use to generate digest passphrase.
      #
      # Returns a String.
      mattr_accessor :iterations do
        if iter = ENV["ARMOR_ITER"]
          warn :ARMOR_ITER unless iter == ITERATIONS
          Integer(iter)
        else
          ITERATIONS
        end
      end

      # Public: Generates a hex passphrase using the given key and salt.
      #
      # key  - Encryption key
      # salt - Encryption salt
      #
      # Returns a String.
      def digest_passphrase(key, salt)
        raise ArgumentError.new("Missing :key")  if key.blank?
        raise ArgumentError.new("Missing :salt") if salt.blank?

        require "openssl"

        digest = OpenSSL::Digest.new(hash_algorithm)

        hmac = OpenSSL::PKCS5.pbkdf2_hmac(
          key,
          salt,
          iterations,
          digest.digest_length,
          digest
        )

        hmac.unpack("H*").first
      end

      private

      # Private: Hash algorithm to use for digest passphrase.
      #
      # Returns a String.
      def hash_algorithm
        if hash = ENV["ARMOR_HASH"]
          warn :ARMOR_HASH unless hash == HASH_ALGORITHM
          hash
        else
          HASH_ALGORITHM
        end
      end

      # Private: Warns about the deprecated ENV vars used with the Armor gem.
      #
      # key - The ENV variable name
      #
      # Returns a String.
      def warn(key)
        require "active_support/deprecation"

        ActiveSupport::Deprecation.warn <<-MSG.squish
          CryptKeeper no longer uses the Armor gem to generate passphrases for
          MySQL AES encryption. Your installation is using a non-standard
          value for `ENV["#{key}"]` which affects the way passphrases are
          generated. You will need to re-encrypt your data with this variable
          removed prior to CryptKeeper v3.0.0.
        MSG
      end
    end
  end
end
