diff --git a/implementations/elixir/ockam/ockam/lib/ockam/secure_channel/channel.ex b/implementations/elixir/ockam/ockam/lib/ockam/secure_channel/channel.ex index d7cf1ae3d5d..bd6d5472af8 100644 --- a/implementations/elixir/ockam/ockam/lib/ockam/secure_channel/channel.ex +++ b/implementations/elixir/ockam/ockam/lib/ockam/secure_channel/channel.ex @@ -520,7 +520,7 @@ defmodule Ockam.SecureChannel.Channel do defp continue_handshake({:continue, key_exchange_state}, state) do with {:ok, data, next} <- XX.out_payload(key_exchange_state) do msg = %{ - payload: :bare.encode(data, :data), + payload: data, onward_route: state.peer_route, return_route: [state.inner_address] } @@ -531,55 +531,54 @@ defmodule Ockam.SecureChannel.Channel do end defp handle_inner_message_impl(message, %Channel{channel_state: %Handshaking{xx: xx}} = state) do - with {:ok, data} <- bare_decode_strict(message.payload, :data), - {:ok, next} <- XX.in_payload(xx, data) do + with {:ok, next} <- XX.in_payload(xx, message.payload) do continue_handshake(next, %Channel{state | peer_route: message.return_route}) end end defp handle_inner_message_impl(message, %Channel{channel_state: channel_state} = state) do - with {:ok, ciphertext} <- bare_decode_strict(message.payload, :data), - {:ok, plaintext, decrypt_st} <- - Decryptor.decrypt("", ciphertext, channel_state.decrypt_st) do - case Messages.decode(plaintext) do - {:ok, %Messages.Payload{} = payload} -> - message = struct(Ockam.Message, Map.from_struct(payload)) - - handle_decrypted_message(message, %Channel{ - state - | channel_state: %{channel_state | decrypt_st: decrypt_st} - }) - - {:ok, :close} -> - Logger.debug("Peer closed secure channel, terminating #{inspect(state.address)}") - {:stop, :normal, channel_state} - - ## TODO: add tests - {:ok, %Messages.RefreshCredentials{contact: contact, credentials: credentials}} -> - with {:ok, peer_identity, peer_identity_id} <- Identity.validate_contact_data(contact), - true <- peer_identity_id == channel_state.peer_identity_id, - :ok <- process_credentials(credentials, peer_identity_id, state.authorities) do - {:ok, - %Channel{ - state - | channel_state: %{ - channel_state - | peer_identity: peer_identity, - decrypt_st: decrypt_st - } - }} - else - error -> - Logger.warning("Invalid credential refresh: #{inspect(error)}") - {:stop, {:error, :invalid_credential_refresh}, state} - end - - {:error, reason} -> - {:error, reason} - end - else - # The message couldn't be decrypted. State remains unchanged + case Decryptor.decrypt("", message.payload, channel_state.decrypt_st) do + {:ok, plaintext, decrypt_st} -> + case Messages.decode(plaintext) do + {:ok, %Messages.Payload{} = payload} -> + message = struct(Ockam.Message, Map.from_struct(payload)) + + handle_decrypted_message(message, %Channel{ + state + | channel_state: %{channel_state | decrypt_st: decrypt_st} + }) + + {:ok, :close} -> + Logger.debug("Peer closed secure channel, terminating #{inspect(state.address)}") + {:stop, :normal, channel_state} + + ## TODO: add tests + {:ok, %Messages.RefreshCredentials{contact: contact, credentials: credentials}} -> + with {:ok, peer_identity, peer_identity_id} <- + Identity.validate_contact_data(contact), + true <- peer_identity_id == channel_state.peer_identity_id, + :ok <- process_credentials(credentials, peer_identity_id, state.authorities) do + {:ok, + %Channel{ + state + | channel_state: %{ + channel_state + | peer_identity: peer_identity, + decrypt_st: decrypt_st + } + }} + else + error -> + Logger.warning("Invalid credential refresh: #{inspect(error)}") + {:stop, {:error, :invalid_credential_refresh}, state} + end + + {:error, reason} -> + {:error, reason} + end + error -> + # The message couldn't be decrypted. State remains unchanged Logger.warning("Failed to decrypt message, discarded: #{inspect(error)}") {:ok, state} end @@ -627,7 +626,6 @@ defmodule Ockam.SecureChannel.Channel do defp send_payload_over_encrypted_channel(payload, encrypt_st, peer_route) do with {:ok, encoded} <- Messages.encode(payload), {:ok, ciphertext, encrypt_st} <- Encryptor.encrypt("", encoded, encrypt_st) do - ciphertext = :bare.encode(ciphertext, :data) envelope = %{onward_route: peer_route, return_route: [], payload: ciphertext} Router.route(envelope) {:ok, encrypt_st} @@ -656,11 +654,4 @@ defmodule Ockam.SecureChannel.Channel do Keyword.merge(opts, worker_mod: __MODULE__, worker_options: worker_opts) end - - defp bare_decode_strict(data, type) do - case :bare.decode(data, type) do - {:ok, result, ""} -> {:ok, result} - error -> {:error, {:invalid_bare_data, type, error}} - end - end end diff --git a/implementations/elixir/ockam/ockam/lib/ockam/secure_channel/messages.ex b/implementations/elixir/ockam/ockam/lib/ockam/secure_channel/messages.ex index c3d9698a8a5..dbba0a87393 100644 --- a/implementations/elixir/ockam/ockam/lib/ockam/secure_channel/messages.ex +++ b/implementations/elixir/ockam/ockam/lib/ockam/secure_channel/messages.ex @@ -14,10 +14,10 @@ defmodule Ockam.SecureChannel.Messages do """ use TypedStruct - @address_schema {:struct, + @address_schema {:struct_values, %{ - type: %{key: 1, schema: :integer, required: true}, - value: %{key: 2, schema: :charlist, required: true} + type: %{key: 0, schema: :integer, required: true}, + value: %{key: 1, schema: :binary, required: true} }} def from_cbor_term(term) do addr = TypedCBOR.from_cbor_term(@address_schema, term) @@ -70,18 +70,36 @@ defmodule Ockam.SecureChannel.Messages do end end - @enum_schema {:variant_enum, - [ - {Ockam.SecureChannel.Messages.Payload, 0}, - {Ockam.SecureChannel.Messages.RefreshCredentials, 1}, - close: 2 - ]} + defmodule PaddedMessage do + @moduledoc """ + Top-level secure channel message, with padding support. + """ + use TypedStruct + + @enum_schema {:variant_enum, + [ + {Ockam.SecureChannel.Messages.Payload, 0}, + {Ockam.SecureChannel.Messages.RefreshCredentials, 1}, + close: 2 + ]} + typedstruct do + plugin(TypedCBOR.Plugin, encode_as: :list) + + field(:message, %Ockam.SecureChannel.Messages.Payload{} | %RefreshCredentials{} | :close, + minicbor: [key: 0, schema: @enum_schema] + ) + + field(:padding, binary(), minicbor: [key: 1, schema: :binary]) + end + end def decode(encoded) do - TypedCBOR.decode_strict(@enum_schema, encoded) + with {:ok, %PaddedMessage{message: message}} <- PaddedMessage.decode_strict(encoded) do + {:ok, message} + end end def encode(msg) do - TypedCBOR.encode(@enum_schema, msg) + PaddedMessage.encode(%PaddedMessage{message: msg, padding: <<>>}) end end diff --git a/implementations/elixir/ockam/ockam/lib/ockam/transport/tcp/client.ex b/implementations/elixir/ockam/ockam/lib/ockam/transport/tcp/client.ex index 5e7d532fe98..ec7f67c95c5 100644 --- a/implementations/elixir/ockam/ockam/lib/ockam/transport/tcp/client.ex +++ b/implementations/elixir/ockam/ockam/lib/ockam/transport/tcp/client.ex @@ -4,7 +4,7 @@ defmodule Ockam.Transport.TCP.Client do alias Ockam.Address alias Ockam.Message - alias Ockam.Transport.TCP + alias Ockam.Transport.TCP.TransportMessage alias Ockam.Transport.TCPAddress alias Ockam.Wire @@ -47,13 +47,17 @@ defmodule Ockam.Transport.TCP.Client do case :gen_tcp.connect(inet_address, port, [ :binary, protocol, - active: @active, send_timeout: @send_timeout, - packet: 2, - nodelay: true + nodelay: true, + active: false ]) do {:ok, socket} -> + # Connection Header, version "1" + :ok = :gen_tcp.send(socket, <<1>>) + {:ok, <<1>>} = :gen_tcp.recv(socket, 1, 5000) + :gen_tcp.controlling_process(socket, self()) + :ok = :inet.setopts(socket, active: @active, packet: 4) state = Map.merge(state, %{ @@ -95,7 +99,7 @@ defmodule Ockam.Transport.TCP.Client do @impl true def handle_info({:tcp, socket, data}, %{socket: socket} = state) do ## TODO: send/receive message in multiple TCP packets - case Wire.decode(data, :tcp) do + case TransportMessage.decode(data) do {:ok, message} -> forwarded_message = message @@ -170,16 +174,8 @@ defmodule Ockam.Transport.TCP.Client do defp encode_and_send_over_tcp(message, state) do forwarded_message = Message.forward(message) - with {:ok, encoded_message} <- Wire.encode(forwarded_message) do - ## TODO: send/receive message in multiple TCP packets - case byte_size(encoded_message) <= TCP.packed_size_limit() do - true -> - send_over_tcp(encoded_message, state) - - false -> - Logger.error("Message to big for TCP: #{inspect(message)}") - {:error, {:message_too_big, message}} - end + with {:ok, encoded_message} <- TransportMessage.encode(forwarded_message) do + send_over_tcp(encoded_message, state) end end diff --git a/implementations/elixir/ockam/ockam/lib/ockam/transport/tcp/handler.ex b/implementations/elixir/ockam/ockam/lib/ockam/transport/tcp/handler.ex index 86eaef506d5..fd44d60989e 100644 --- a/implementations/elixir/ockam/ockam/lib/ockam/transport/tcp/handler.ex +++ b/implementations/elixir/ockam/ockam/lib/ockam/transport/tcp/handler.ex @@ -5,7 +5,7 @@ defmodule Ockam.Transport.TCP.Handler do alias Ockam.Message alias Ockam.Telemetry - alias Ockam.Transport.TCP + alias Ockam.Transport.TCP.TransportMessage require Logger @@ -28,11 +28,16 @@ defmodule Ockam.Transport.TCP.Handler do {:ok, socket} = :ranch.handshake(ref, ranch_options) + # Header, protocol version "1" must be the first thing exchanged. + # It isn't send anymore after the initial exchange. + transport.send(socket, <<1>>) + {:ok, <<1>>} = transport.recv(socket, 1, 5000) + :ok = :inet.setopts(socket, [ {:active, @active}, {:send_timeout, @send_timeout}, - {:packet, 2}, + {:packet, 4}, {:nodelay, true} ]) @@ -91,7 +96,7 @@ defmodule Ockam.Transport.TCP.Handler do metadata: %{address: address} ) - case Ockam.Wire.decode(data, :tcp) do + case TransportMessage.decode(data) do {:ok, decoded} -> forwarded_message = decoded @@ -99,14 +104,13 @@ defmodule Ockam.Transport.TCP.Handler do send_to_router(forwarded_message) Telemetry.emit_event(function_name, metadata: %{name: "decoded_data"}) + {:noreply, state, idle_timeout} - {:error, %Ockam.Wire.DecodeError{} = e} -> + {:error, e} -> start_time = Telemetry.emit_start_event(function_name) Telemetry.emit_exception_event(function_name, start_time, e) - raise e + {:stop, {:error, e}, state} end - - {:noreply, state, idle_timeout} end def handle_info({:tcp_closed, socket}, %{socket: socket, transport: transport} = state) do @@ -165,23 +169,8 @@ defmodule Ockam.Transport.TCP.Handler do ) do forwarded_message = Message.forward(message) - case Ockam.Wire.encode(forwarded_message) do - {:ok, encoded} -> - ## TODO: send/receive message in multiple TCP packets - case byte_size(encoded) <= TCP.packed_size_limit() do - true -> - tcp_wrapper.wrap_tcp_call(transport, :send, [socket, encoded]) - - false -> - Logger.error("Message to big for TCP: #{inspect(message)}") - raise {:message_too_big, message} - end - - a -> - Logger.error("TCP transport send error #{inspect(a)}") - raise a - end - + {:ok, encoded} = TransportMessage.encode(forwarded_message) + :ok = tcp_wrapper.wrap_tcp_call(transport, :send, [socket, encoded]) {:ok, state} end diff --git a/implementations/elixir/ockam/ockam/lib/ockam/transport/tcp/listener.ex b/implementations/elixir/ockam/ockam/lib/ockam/transport/tcp/listener.ex index 698e28bffb3..9e6987732b9 100644 --- a/implementations/elixir/ockam/ockam/lib/ockam/transport/tcp/listener.ex +++ b/implementations/elixir/ockam/ockam/lib/ockam/transport/tcp/listener.ex @@ -26,7 +26,7 @@ if Code.ensure_loaded?(:ranch) do transport = :ranch_tcp transport_options = :ranch.normalize_opts(port: port, ip: ip) protocol = Ockam.Transport.TCP.Handler - protocol_options = [packet: 2, nodelay: true, handler_options: handler_options] + protocol_options = [nodelay: true, handler_options: handler_options] with {:ok, _apps} <- Application.ensure_all_started(:ranch) do {:ok, listener_address} = diff --git a/implementations/elixir/ockam/ockam/lib/ockam/transport/tcp/transport_message.ex b/implementations/elixir/ockam/ockam/lib/ockam/transport/tcp/transport_message.ex new file mode 100644 index 00000000000..206e1aaef74 --- /dev/null +++ b/implementations/elixir/ockam/ockam/lib/ockam/transport/tcp/transport_message.ex @@ -0,0 +1,77 @@ +defmodule Ockam.Transport.TCP.TransportMessage do + @moduledoc """ + Ockam messages encoding for TCP transmission + """ + alias Ockam.Address + alias Ockam.Message + alias Ockam.TypedCBOR + + require Logger + + defmodule AddressSchema do + @moduledoc """ + Ockam Address, cbor encoding + """ + use TypedStruct + + @address_schema {:struct_values, + %{ + type: %{key: 0, schema: :integer, required: true}, + value: %{key: 1, schema: :binary, required: true} + }} + def from_cbor_term(term) do + addr = TypedCBOR.from_cbor_term(@address_schema, term) + {:ok, Address.denormalize(addr)} + end + + def to_cbor_term(addr) do + {:ok, TypedCBOR.to_cbor_term(@address_schema, Address.normalize(addr))} + end + end + + defmodule TCPMessage do + @moduledoc """ + Secure channel message carrying user data + """ + use TypedStruct + + typedstruct do + plugin(TypedCBOR.Plugin, encode_as: :list) + field(:onward_route, list(Address.t()), minicbor: [key: 0, schema: {:list, AddressSchema}]) + field(:return_route, list(Address.t()), minicbor: [key: 1, schema: {:list, AddressSchema}]) + field(:payload, binary(), minicbor: [key: 2]) + field(:tracing_context, String.t() | nil, minicbor: [key: 3, required: false]) + end + end + + @spec decode(binary()) :: {:ok, Message.t()} | {:error, any()} + def decode(data) do + case TCPMessage.decode_strict(data) do + {:ok, msg} -> + {:ok, + %Message{ + onward_route: msg.onward_route, + return_route: msg.return_route, + payload: msg.payload, + local_metadata: %{ + source: :channel, + channel: :tcp, + tracing_context: msg.tracing_context + } + }} + + error -> + {:error, {:error_decoding_msg, error}} + end + end + + @spec encode(Message.t()) :: {:ok, binary()} + def encode(%Message{onward_route: o, return_route: r, payload: p, local_metadata: l}) do + TCPMessage.encode(%TCPMessage{ + onward_route: o, + return_route: r, + payload: p, + tracing_context: Map.get(l, :tracing_context, nil) + }) + end +end diff --git a/implementations/elixir/ockam/ockam/test/ockam/secure_channel/messages_test.exs b/implementations/elixir/ockam/ockam/test/ockam/secure_channel/messages_test.exs index d8db8b1d04a..705ad8d6a3e 100644 --- a/implementations/elixir/ockam/ockam/test/ockam/secure_channel/messages_test.exs +++ b/implementations/elixir/ockam/ockam/test/ockam/secure_channel/messages_test.exs @@ -7,7 +7,7 @@ defmodule Ockam.SecureChannel.Messages.Tests do test "refresh credential can be parsed" do # sample value encoded from rust hex_msg = - "8201818281825837830101583285f682008158208bd01513a019c95d96553015b1b0a3014e7bd67c7f3c8e6223e839111041f8d6f41a659c99fd1a78689cfd820081584067118992d037593809f8b217641aed54dc983f2847f95b2068207c3647beb73a60b0e14012c14bfee487c880f0f74d9f8a7d7abd3ef333f2f90f097ef366900c81828258e183010358dc855820904192e6e480915e0d1256d3abf9c3c9c67bc2b4c41ffca77550d8a3f12fbb1e5820904192e6e480915e0d1256d3abf9c3c9c67bc2b4c41ffca77550d8a3f12fbb1e8201a44b6f636b616d2d72656c6179412a4a6f636b616d2d726f6c6548656e726f6c6c65724a70726f6a6563745f6964582437363764343631632d393835312d346330622d626663362d3664333333346235626363325074727573745f636f6e746578745f6964582437363764343631632d393835312d346330622d626663362d3664333333346235626363321a659ca3cf1a659ca3ed8200815840cf27a0a3052c53b1f27bf7776ca95c7dcdd7a0f493cc5b1e30f9d35712bb8fde16f69065aa9f70b3689a8f1b7b05fc13e6e807efa719031d1a415d25b61c360b82587c8301025877855820b1fea1775d75079abdb1e78b96921fa9ec340bc2b5aa70f37e65342d859cf5505820b1fea1775d75079abdb1e78b96921fa9ec340bc2b5aa70f37e65342d859cf5508201818200815820389852b0f4fee7b6b962442a924d1672c56b8813fb846a50da80db7e1bbe41591a659c9a2f1a6f029baf8200815840adf4a66e097de839d3539694a6c6a82a978ff009205df8bf7eb03b9990958b7c4e7037fd9d365cf220a2757ed60f0542f47965d8b9f354fd950841ce66304606" + "828201818281825837830101583285f682008158208bd01513a019c95d96553015b1b0a3014e7bd67c7f3c8e6223e839111041f8d6f41a659c99fd1a78689cfd820081584067118992d037593809f8b217641aed54dc983f2847f95b2068207c3647beb73a60b0e14012c14bfee487c880f0f74d9f8a7d7abd3ef333f2f90f097ef366900c81828258e183010358dc855820904192e6e480915e0d1256d3abf9c3c9c67bc2b4c41ffca77550d8a3f12fbb1e5820904192e6e480915e0d1256d3abf9c3c9c67bc2b4c41ffca77550d8a3f12fbb1e8201a44b6f636b616d2d72656c6179412a4a6f636b616d2d726f6c6548656e726f6c6c65724a70726f6a6563745f6964582437363764343631632d393835312d346330622d626663362d3664333333346235626363325074727573745f636f6e746578745f6964582437363764343631632d393835312d346330622d626663362d3664333333346235626363321a659ca3cf1a659ca3ed8200815840cf27a0a3052c53b1f27bf7776ca95c7dcdd7a0f493cc5b1e30f9d35712bb8fde16f69065aa9f70b3689a8f1b7b05fc13e6e807efa719031d1a415d25b61c360b82587c8301025877855820b1fea1775d75079abdb1e78b96921fa9ec340bc2b5aa70f37e65342d859cf5505820b1fea1775d75079abdb1e78b96921fa9ec340bc2b5aa70f37e65342d859cf5508201818200815820389852b0f4fee7b6b962442a924d1672c56b8813fb846a50da80db7e1bbe41591a659c9a2f1a6f029baf8200815840adf4a66e097de839d3539694a6c6a82a978ff009205df8bf7eb03b9990958b7c4e7037fd9d365cf220a2757ed60f0542f47965d8b9f354fd950841ce6630460640" {:ok, b} = Base.decode16(hex_msg, case: :lower) {:ok, %Messages.RefreshCredentials{}} = Messages.decode(b) @@ -16,8 +16,8 @@ defmodule Ockam.SecureChannel.Messages.Tests do describe "Ockam.SecureChannel.Messages.Close" do test ":close can be parsed" do - # sample value encoded from rust - hex_msg = "820280" + # sample value + hex_msg = "8282028040" {:ok, b} = Base.decode16(hex_msg, case: :lower) {:ok, :close} = Messages.decode(b) diff --git a/implementations/elixir/ockam/ockam/test/ockam/secure_channel_test.exs b/implementations/elixir/ockam/ockam/test/ockam/secure_channel_test.exs index ac836a4e3cc..0f3c4268b2c 100644 --- a/implementations/elixir/ockam/ockam/test/ockam/secure_channel_test.exs +++ b/implementations/elixir/ockam/ockam/test/ockam/secure_channel_test.exs @@ -139,17 +139,8 @@ defmodule Ockam.SecureChannel.Tests do test "secure channel trash packets" do replay = fn %Message{payload: payload} = message, n -> if rem(n, 2) == 0 do - # Payload is actually _not_ the raw encrypted bytes.. it's the encrypted bytes encoded with bare. - # That means that we can have two different kind of "bad" packets: things that can't - # be decoded from bare, and things that can be decoded from bare, but then can't be decrypted. - # We put both here. trash1 = %Message{message | payload: payload <> "s"} |> Message.forward_trace() - {:ok, raw, ""} = :bare.decode(payload, :data) - - trash2 = - %Message{message | payload: :bare.encode(raw <> "s", :data)} |> Message.forward_trace() - - [trash1, trash2] + [trash1] else [Message.forward_trace(message)] end diff --git a/implementations/elixir/ockam/ockam_typed_cbor/lib/typed_cbor.ex b/implementations/elixir/ockam/ockam_typed_cbor/lib/typed_cbor.ex index e5d21ec0056..f6684864989 100644 --- a/implementations/elixir/ockam/ockam_typed_cbor/lib/typed_cbor.ex +++ b/implementations/elixir/ockam/ockam_typed_cbor/lib/typed_cbor.ex @@ -231,6 +231,8 @@ defmodule Ockam.TypedCBOR do struct = values |> Enum.with_index(&{&2, &1}) + # field not present + |> Enum.filter(fn {_, v} -> v != nil end) |> Enum.into(%{}) from_cbor_term({:struct, fields}, struct) diff --git a/implementations/elixir/ockam/ockam_typed_cbor/lib/typed_cbor/plugin.ex b/implementations/elixir/ockam/ockam_typed_cbor/lib/typed_cbor/plugin.ex index 6f59cf25388..dd264d93aad 100644 --- a/implementations/elixir/ockam/ockam_typed_cbor/lib/typed_cbor/plugin.ex +++ b/implementations/elixir/ockam/ockam_typed_cbor/lib/typed_cbor/plugin.ex @@ -45,6 +45,10 @@ defmodule Ockam.TypedCBOR.Plugin do %{schema: {:enum, mappings}, required: true} end + defp field_schema({:variant_enum, mappings}, t) when is_list(t) do + %{schema: {:variant_enum, mappings}, required: true} + end + defp field_schema(schema, t) when is_list(t), do: raise("provider schema #{inspect(schema)} must match enum type #{inspect(t)}")