diff --git a/apps/amethyst/lib/apps/connection_handler.ex b/apps/amethyst/lib/apps/connection_handler.ex index b9d2267..d8232a2 100644 --- a/apps/amethyst/lib/apps/connection_handler.ex +++ b/apps/amethyst/lib/apps/connection_handler.ex @@ -48,11 +48,8 @@ defmodule Amethyst.ConnectionHandler do @spec loop(:gen_tcp.socket(), atom(), integer(), map()) :: no_return() defp loop(socket, connstate, version, state) do receive do - :closed -> - Logger.info("Connection #{inspect(socket)} closed.") - Process.exit(self(), :normal) {:disconnect, reason} -> - disconnect(socket, reason, connstate, version) + disconnect(socket, reason, connstate, version, state) Process.exit(self(), :normal) {:set_state, newstate} -> Logger.debug("Switching to state #{newstate} from #{connstate}") @@ -60,6 +57,12 @@ defmodule Amethyst.ConnectionHandler do {:set_version, newversion} -> Logger.debug("Switching to version #{newversion} from #{version}") loop(socket, connstate, newversion, state) + {:set_encryption, secret} -> + Logger.debug("Enabling encryption with shared secret #{inspect(secret)}") + encryption_state = :crypto.crypto_init(:aes_128_cfb8, secret, secret, true) + decryption_state = :crypto.crypto_init(:aes_128_cfb8, secret, secret, false) + state = state |> Map.put(:encryption_state, encryption_state) |> Map.put(:decryption_state, decryption_state) + loop(socket, connstate, version, state) {:set_position, position} -> prev_position = Map.get(state, :position) state = Map.put(state, :position, position) @@ -97,11 +100,18 @@ defmodule Amethyst.ConnectionHandler do end loop(socket, connstate, version, state) {:send_packet, packet} -> - # Logger.debug("Sending packet #{inspect(packet)}") - send_packet(socket, connstate, packet, version) + Logger.debug("Sending packet #{inspect(packet)}") + send_packet(socket, connstate, packet, version, state) loop(socket, connstate, version, state) after 0 -> + # Received stuff from the connection receiver is lower priority receive do + :closed -> + Logger.info("Connection #{inspect(socket)} closed.") + Process.exit(self(), :normal) + {:get_encryption, from} -> + send(from, Map.get(state, :decryption_state)) + loop(socket, connstate, version, state) {:packet, id, data} -> state = handle_packet(id, data, connstate, version, state) loop(socket, connstate, version, state) @@ -237,11 +247,17 @@ defmodule Amethyst.ConnectionHandler do end end - defp send_packet(socket, connstate, packet, version) do + defp send_packet(socket, connstate, packet, version, state) do try do data = connstate.serialize(packet, version) length = byte_size(data) |> Amethyst.Minecraft.Write.varint() - :gen_tcp.send(socket, length <> data) + case Map.get(state, :encryption_state) do + nil -> + :gen_tcp.send(socket, length <> data) + estate -> + encrypted = :crypto.crypto_update(estate, length <> data) + :gen_tcp.send(socket, encrypted) + end rescue e -> Logger.error("Error sending packet #{inspect(packet)} in state #{connstate}: #{Exception.format(:error, e, __STACKTRACE__)}") @@ -249,12 +265,12 @@ defmodule Amethyst.ConnectionHandler do end end - defp disconnect(socket, reason, connstate, version) do + defp disconnect(socket, reason, connstate, version, state) do Logger.info("Disconnecting connection #{inspect(socket)}") Logger.debug("Disconnecting connection #{inspect(socket)}: #{reason}") case connstate.disconnect(reason) do nil -> nil - packet -> send_packet(socket, connstate, packet, version) + packet -> send_packet(socket, connstate, packet, version, state) end :gen_tcp.close(socket) end diff --git a/apps/amethyst/lib/apps/connection_receiver.ex b/apps/amethyst/lib/apps/connection_receiver.ex index ea11a77..fd34386 100644 --- a/apps/amethyst/lib/apps/connection_receiver.ex +++ b/apps/amethyst/lib/apps/connection_receiver.ex @@ -35,7 +35,7 @@ defmodule Amethyst.ConnectionReceiver do {:ok, spawn(fn -> Process.set_label("ConnectionReceiver for #{inspect(socket)}") {:ok, pid} = Amethyst.ConnectionHandler.start_link(socket, Amethyst.ConnectionState.Handshake, 0) - receive(socket, pid) + receive(socket, pid, nil) end)} end @@ -44,42 +44,64 @@ defmodule Amethyst.ConnectionReceiver do {:ok, spawn_link(fn -> Process.set_label("ConnectionReceiver for #{inspect(socket)}") {:ok, pid} = Amethyst.ConnectionHandler.start_link(socket, Amethyst.ConnectionState.Handshake, 0) - receive(socket, pid) + receive(socket, pid, nil) end)} end - @spec receive(:gen_tcp.socket(), pid()) :: no_return() - def receive(socket, sender) do - case get_packet(socket) do + @spec receive(:gen_tcp.socket(), pid(), nil | :crypto.crypto_state()) :: no_return() + def receive(socket, sender, cstate) do + case get_packet(socket, cstate) do :closed -> send(sender, :closed) Process.exit(self(), :normal) {:error, error} -> Logger.error("Error reading packet: #{error}") {id, data} -> send(sender, {:packet, id, data}) end - receive(socket, sender) + if cstate == nil do + # Ask the handler if the encryption state has changed + send(sender, {:get_encryption, self()}) + Logger.debug("Asking for news on encryption...") + receive do + nil -> receive(socket, sender, cstate) + some -> + Logger.debug("Enabling decryption!") + receive(socket, sender, some) + end + else + receive(socket, sender, cstate) + end end - def get_packet(client) do - case get_varint(client, "") do + def get_packet(client, cstate) do + case get_varint(client, "", cstate) do :closed -> :closed {:error, error} -> {:error, error} {[length], ""} -> recv = :gen_tcp.recv(client, length) case recv do - {:ok, full_packet} -> ({[id], data} = Read.start(full_packet) |> Read.varint() |> Read.stop() - {id, data}) + {:ok, full_packet} -> + full_packet = case cstate do + nil -> full_packet + ds -> :crypto.crypto_update(ds, full_packet) + end + ({[id], data} = Read.start(full_packet) |> Read.varint() |> Read.stop() + {id, data}) {:error, :closed} -> :closed {:error, error} -> {:error, error} end end end - defp get_varint(client, acc) do + defp get_varint(client, acc, cstate) do case :gen_tcp.recv(client, 1) do - {:ok, byte} -> case byte do - <<0::1, _::7>> -> Read.start(acc <> byte) |> Read.varint() |> Read.stop() - <<1::1, _::7>> -> get_varint(client, acc <> byte) - end + {:ok, byte} -> + byte = case cstate do + nil -> byte + ds -> :crypto.crypto_update(ds, byte) + end + case byte do + <<0::1, _::7>> -> Read.start(acc <> byte) |> Read.varint() |> Read.stop() + <<1::1, _::7>> -> get_varint(client, acc <> byte, cstate) + end {:error, :closed} -> :closed {:error, error} -> {:error, error} end diff --git a/apps/amethyst/lib/data.ex b/apps/amethyst/lib/data.ex index db3c94c..12fd126 100644 --- a/apps/amethyst/lib/data.ex +++ b/apps/amethyst/lib/data.ex @@ -216,6 +216,11 @@ defmodule Amethyst.Minecraft.Read do {[data | acc], rest, :reversed} end + def byte_array({acc, data, :reversed}) do + {[length], rest} = start(data) |> varint |> stop + raw({acc, rest, :reversed}, length) + end + @doc """ Reads a varint. `read` tracks the number of bytes read and `nacc` tracks the number being read. """ diff --git a/apps/amethyst/lib/encryption.ex b/apps/amethyst/lib/encryption.ex index fb72d80..2ab344c 100644 --- a/apps/amethyst/lib/encryption.ex +++ b/apps/amethyst/lib/encryption.ex @@ -30,10 +30,6 @@ defmodule Amethyst.Keys do GenServer.start_link(__MODULE__, bits, name: __MODULE__) end - def get_priv do - GenServer.call(__MODULE__, :get_priv) - end - def get_pub do GenServer.call(__MODULE__, :get_pub) end @@ -52,22 +48,19 @@ defmodule Amethyst.Keys do rsa_public_key = {:RSAPublicKey, modulus, public_exponent} Logger.info("Generated RSA keys") - {:ok, {rsa_public_key, rsa_private_key}} + {:ok, {rsa_public_key, rsa_private_key, bits}} end @impl true - def handle_call(:get_priv, _from, {pubkey, privkey}) do - {:reply, :public_key.der_encode(:RSAPrivateKey, privkey), {pubkey, privkey}} + def handle_call(:get_pub, _from, {pubkey, privkey, bits}) do + {:SubjectPublicKeyInfo, pk, :not_encrypted} = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, pubkey) + # Logger.debug("#{inspect(pem_encoded, limit: :infinity)}") + {:reply, pk, {pubkey, privkey, bits}} end @impl true - def handle_call(:get_pub, _from, {pubkey, privkey}) do - {:reply, :public_key.der_encode(:RSAPublicKey, pubkey), {pubkey, privkey}} - end - - @impl true - def handle_call({:decrypt, encrypted}, _from, {pubkey, privkey}) do + def handle_call({:decrypt, encrypted}, _from, {pubkey, privkey, bits}) do plaintext = :public_key.decrypt_private(encrypted, privkey) - {:reply, plaintext, {pubkey, privkey}} + {:reply, plaintext, {pubkey, privkey, bits}} end end diff --git a/apps/amethyst/lib/states/login.ex b/apps/amethyst/lib/states/login.ex index e50030e..a6f22fe 100644 --- a/apps/amethyst/lib/states/login.ex +++ b/apps/amethyst/lib/states/login.ex @@ -60,10 +60,20 @@ defmodule Amethyst.ConnectionState.Login do Macros.defpacket_serverbound :login_acknowledged, 0x03, 767, [] Macros.defpacket_serverbound :cookie_response, 0x04, 767, [identifier: :string, payload: {:optional, :byte_array}] - def handle(%{packet_type: :login_start, name: name, player_uuid: player_uuid}, 767, _state) do + def handle(%{packet_type: :login_start, name: name, player_uuid: player_uuid}, 767, state) do Logger.debug("Received login start for #{name} with UUID #{player_uuid}") - if Application.fetch_env!(:amethyst, :encryption) do - raise RuntimeError, "Encryption is not currently supported" + if Application.get_env(:amethyst, :encryption, true) do + verify_token = :crypto.strong_rand_bytes(4) + public_key = Amethyst.Keys.get_pub() + Logger.debug("Public key: #{inspect(public_key, limit: :infinity)}") + send(self(), {:send_packet, %{ + packet_type: :encryption_request, + server_id: "", + public_key: public_key, + verify_token: verify_token, + should_authenticate: Application.get_env(:amethyst, :auth, false) + }}) + state |> Map.put(:verify_token, verify_token) |> Map.put(:name, name) |> Map.put(:uuid, player_uuid) else send(self(), {:send_packet, %{ packet_type: :login_success, @@ -75,7 +85,23 @@ defmodule Amethyst.ConnectionState.Login do :ok end end - + def handle(%{packet_type: :encryption_response, shared_secret: secret, verify_token: verify_token}, 767, state) do + secret = Amethyst.Keys.decrypt(secret) + verify_token = Amethyst.Keys.decrypt(verify_token) + if verify_token == Map.get(state, :verify_token, :never) do + send(self(), {:set_encryption, secret}) + send(self(), {:send_packet, %{ + packet_type: :login_success, + uuid: Map.get(state, :uuid), + username: Map.get(state, :name), + properties: [], + strict_error_handling: true + }}) + :ok + else + raise RuntimeError, "Invalid verify token. Broken encryption?" + end + end def handle(%{packet_type: :login_acknowledged}, 767, _state) do Logger.debug("Received login acknowledged") send(self(), {:set_state, Amethyst.ConnectionState.Configuration}) diff --git a/config/runtime.exs b/config/runtime.exs index f36f7b8..c879757 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -2,6 +2,6 @@ import Config config :amethyst, port: 25599, # Bogus port for testing, avoids unexpected conflicts - encryption: false, # Whether or not to request encryption from clients. + encryption: true, # Whether or not to request encryption from clients. auth: false, # Whether or not users should be authenticated with Mojang. default_game: Example.Game # Which game new players should be sent to