 require 'digest/sha2'

module Nova
  module Starbound
    module Encryptors

      # Handles encryption using the OpenSSL library.  Shares the
      # shared secret using RSA public key encryption, creates a
      # HMAC digest of the body using the shared secret as a key, and
      # encrypts the body using AES-256-CBC encryption.
      class OpenSSL < Encryptor

        # The RSA key size for the key exchange.
        RSA_KEY_SIZE = 4096

        # The shared secret size, in bytes.  If RSA_KEY_SIZE is 4096,
        # this is 256.
        SECRET_SIZE = RSA_KEY_SIZE / 16

        encryptor_name "openssl/rsa-#{RSA_KEY_SIZE}/aes-256-cbc"
        register! 1

        # see Encryptor.available?
        def self.available?
          @_available ||= begin
            require 'openssl'
            true
          rescue LoadError
            false
          end
        end

        # (see Encryptor#encrypt)
        def encrypt(packet)
          packet = packet.clone
          cipher = ::OpenSSL::Cipher::AES256.new(:CBC)
          cipher.encrypt
          cipher.key = options[:shared_secret]

          # we have to fit the packet's nonce size.
          packet[:nonce] = cipher.iv = ::OpenSSL::Random.random_bytes(24)

          encrypted = cipher.update(packet[:body]) + cipher.final

          packet.body = hmac_digest(encrypted) + encrypted
          packet
        end

        # (see Encryptor#decrypt)
        def decrypt(packet)
          packet = packet.clone
          decipher = ::OpenSSL::Cipher::AES256.new(:CBC)
          decipher.decrypt
          decipher.key = options[:shared_secret]
          decipher.iv  = packet[:nonce]

          digest = packet[:body][0..63]
          actual_body = packet[:body][64..-1]

          if hmac_digest(actual_body) != digest
            raise EncryptorError, "Digest doesn't match the body."
          end

          packet.body = decipher.update(actual_body) +
            decipher.final
          packet
        end

        # (see Encryptor#private_key!)
        def private_key!
          options[:private] = ::OpenSSL::PKey::RSA.new(RSA_KEY_SIZE)
        end

        # If we have already recieved the other public key, we'll
        # generate the secret here, and return the encrypted version
        # of that secret here.  Otherwise, we'll generate our private
        # key and return that in DER format.
        #
        # @return [String]
        def public_key
          if options[:other_public]
            options[:shared_secret] =
              ::OpenSSL::Random.random_bytes(SECRET_SIZE)
            options[:other_public].public_encrypt(
              options[:shared_secret])
          else
            options[:public] ||= options[:private].public_key.to_der
          end
        end

        # If we already have a public key, that means that the value
        # that's passed to this method is the shared secret.
        # Otherwise, it really is the public key of the other remote.
        # If the passed value is a shared secret, it's decrypted with
        # our private key and stored.  If it's the other public key,
        # it's instantized to a openssl RSA key, and stored.
        #
        # @return [void]
        def other_public_key=(public_key)
          if options[:public]
            options[:shared_secret] =
              options[:private].private_decrypt(public_key)
          else
            options[:other_public] =
              ::OpenSSL::PKey::RSA.new(public_key)
          end
        end

        private

        # Provides a digest of the data, using HMAC.
        #
        # @return [String]
        def hmac_digest(body)
          ::OpenSSL::HMAC.digest(::OpenSSL::Digest::SHA512.new,
            options[:shared_secret], body.to_s)
        end

      end
    end
  end
end
