Current section
Files
Jump to
Current section
Files
lib/libp2p/secure_conn.ex
defmodule Libp2p.SecureConn do
@moduledoc """
Noise-secured connection wrapper.
- Performs Noise transport message framing (2-byte big-endian length prefix).
- Encrypts/decrypts payloads using `Libp2p.Noise.transport_encrypt/3` and
`Libp2p.Noise.transport_decrypt/3`.
This intentionally exposes a small interface; the Swarm/Connection process
owns the socket and calls these helpers.
"""
alias Libp2p.Noise
alias Libp2p.Transport.Tcp
@type cipher_state :: %{k: binary() | nil, n: non_neg_integer()}
@type t :: %__MODULE__{
socket: Tcp.socket(),
cs_in: cipher_state(),
cs_out: cipher_state(),
recv_buf: binary()
}
defstruct [:socket, :cs_in, :cs_out, recv_buf: <<>>]
@spec new(Tcp.socket(), cipher_state(), cipher_state()) :: t()
def new(socket, cs_in, cs_out) do
%__MODULE__{socket: socket, cs_in: cs_in, cs_out: cs_out, recv_buf: <<>>}
end
@spec send(t(), binary()) :: {:ok, t()} | {:error, term()}
def send(%__MODULE__{} = c, bytes) when is_binary(bytes) do
# Noise transport messages have a 2-byte BE length prefix and max length 65535.
do_send(c, bytes)
end
defp do_send(c, <<>>), do: {:ok, c}
defp do_send(%__MODULE__{} = c, bytes) do
# Leave room for tag; Noise module does not enforce size here but framing does.
chunk = binary_part(bytes, 0, min(byte_size(bytes), 65_535))
rest = binary_part(bytes, byte_size(chunk), byte_size(bytes) - byte_size(chunk))
{ct, cs_out2} = Noise.transport_encrypt(c.cs_out, chunk)
frame = Noise.frame(ct)
case Tcp.send(c.socket, frame) do
:ok -> do_send(%{c | cs_out: cs_out2}, rest)
{:error, reason} -> {:error, reason}
end
end
@doc """
Append raw encrypted bytes received from the socket.
"""
@spec ingest(t(), binary()) :: t()
def ingest(%__MODULE__{} = c, data) when is_binary(data) do
%{c | recv_buf: c.recv_buf <> data}
end
@doc """
Decrypt as many complete Noise transport messages as are currently buffered.
Returns `{plaintext_messages, conn2}` where each plaintext message corresponds
to one Noise transport message.
"""
@spec drain(t()) :: {[binary()], t()}
def drain(%__MODULE__{} = c) do
do_drain(c, [])
end
defp do_drain(%__MODULE__{} = c, acc) do
case Noise.deframe(c.recv_buf) do
{msg, rest} ->
{pt, cs_in2} = Noise.transport_decrypt(c.cs_in, msg)
do_drain(%{c | recv_buf: rest, cs_in: cs_in2}, [pt | acc])
:more ->
{Enum.reverse(acc), c}
end
end
@doc """
Receive *some* plaintext bytes (one Noise transport message).
Returns `{bytes, conn2}` where `bytes` may be empty only on protocol errors (raised).
"""
@spec recv(t(), timeout()) :: {:ok, binary(), t()} | {:error, term()}
def recv(%__MODULE__{} = c, timeout \\ 5_000) do
with {:ok, msg, c2} <- recv_frame(c, timeout) do
{pt, cs_in2} = Noise.transport_decrypt(c2.cs_in, msg)
{:ok, pt, %{c2 | cs_in: cs_in2}}
end
end
# --- framing ---
defp recv_frame(%__MODULE__{} = c, timeout) do
case Noise.deframe(c.recv_buf) do
{msg, rest} ->
{:ok, msg, %{c | recv_buf: rest}}
:more ->
case Tcp.recv(c.socket, 0, timeout) do
{:ok, data} -> recv_frame(%{c | recv_buf: c.recv_buf <> data}, timeout)
{:error, reason} -> {:error, reason}
end
end
end
end