Home

Awesome

ECNet2

ECNet2 is an encrypted networking library for CC:Tweaked. You can find usage examples in the examples directory.

Dependencies

Goals

Non-goals

API Reference

ecnet2.open(modem: string)

Opens a modem for communications.

ecnet2.close([modem: string])

Closes a modem for communications.

ecnet2.isOpen([modem: string])

Returns whether a modem is open for communications.

ecnet2.Identity(path: string)

Creates or loads in an identity from a given identity directory path.

ecnet2.address(): string

DEPRECATED. Use ecnet2.Identity("/.ecnet2").address instead.

ecnet2.Protocol(interface: IProtocol): Protocol

DEPRECATED. Use ecnet2.Identity("/.ecnet2"):Protocol(...) instead.

ecnet2.daemon

Function used for managing listener and connection events. Intended to be put in parallel with users code.

Type Identity

Contains secret data needed to communicate under an address.

Identity.address: string

The address for connecting to this device through this identity.

Identity:Protocol(interface: IProtocol): Protocol

Creates a protocol from a given interface under this identity.

Type IProtocol

A table containing a description for a protocol.

IProtocol.name: string

The protocol's name.

Iprotocol.serialize(object: any): string

A serializer for protocol objects.

IProtocol.deserialize(str: string): any

A deserializer for protocol objects.

Type Protocol

A namespace for interpreting messages received over connections.

Procotol:connect(address: string, modem: string): Connection

Creates a new connection using this protocol and a modem.

Protocol:listen(): Listener

Creates a listener for this protocol on all open modems.

Type Listener

A listener for incoming connection requests.

Listener.id: string

The listener's ID, used in resolving ecnet2_request events.

Listener:accept(reply: any[, request: Request]): Connection

Accepts a request and builds a connection. Waits for the next request if none are provided.

Throws "invalid listener for this request" if the supplied request isn't meant for this listener.

Returns a dummy connection if the request is malformed, or if the request has already been accepted.

Type Connection

An encrypted tunnel operating over a network.

Connection.id: string

The connection's ID, used in ecnet2_message events.

Connection:send(message: any)

Sends a message.

Throws "can't send on an incomplete connection" until at least one message has been received.

Connection:receive([timeout: number]): string, any

Yields until a message is received. Returns the sender and contents, or nil on timeout.

Events

"ecnet2_request", listenerId: string, request: Request, side: string

A connection request.

"ecnet2_message", connectionId: string, sender: string, message: any

A message in a connection.

Technical Details

Descriptors

Every ECNet2 packet has a 32 byte prefix known as the descriptor. Descriptors allow the receiver to know whether it is supposed to process a packet. Furthermore, secret descriptors allow for some resistance against decryption failure denial-of-service attacks on networks with no wormholes.

The listener descriptor is defined as BLAKE3(BLAKE3(pk .. BLAKE3(protocol))), where pk is the listener's public key and protocol is the protocol name.

The connection descriptors are derived from the current decryption key, which is ratcheted every time a new message is received.

Handshake

We use the noise XK handshake, its pattern is:

XK:
  <- s
  ...
  -> e, es
  <- e, ee
  -> s, se

The contents of each handshake payload are:

-> e, es

This payload currently contains only padding.

<- e, ee

This payload contains a user-defined reply and padding. Neither the user nor ECNet know who the initiator is. As a result, naive assumptions match exactly what the payload security properties (2, 1) are.

-> s, se

This payload contains a user-defined message and padding. The naive assumptions match exactly what the payload security properties (2, 5) are.

Why _K?

We need the initiator to have a secret descriptor at the first response, otherwise an attacker could trigger decryption failures arbitrarily, throwing the entire connection away. We could try restarting the connection again, but that's difficult to model in the interface.

Why not IK?

  1. The initiator's identity claim is vulnerable to replay attack, so we can't assume anything until their first transport message, making IK pointless.
  2. IK has an ss token, which is harder to protect against timing attacks on the result of the DH operation, while es and se are a bit safer.
  3. IK hides identity more poorly than XK.

Why not NK?

Authenticating the initiatior makes the API simpler from the user's point of view, since they don't have to handle whether the message has a sender or not.

Size Limits

accept() Reply Argument

The message size limit is 2¹⁵ - 1 = 32767 bytes. The other half of the payload is reserved for ECNet metadata.

Initiator's First Message

The message size limit is 2¹⁵ - 1 = 32767 bytes. The other half of the payload is reserved for ECNet metadata.

Other Messages

50 bytes of overhead:

Since noise allows packets of at most 2¹⁶ - 1 bytes in length, the message size limit is 2¹⁶ - 1 - 50 = 65485 bytes.

Handshake Model

The XK handshake is modeled into Connection and Listener objects. The second payload is modeled as a reply parameter to accept, while the third payload is modeled as a regular message:

Handshake payloads:
connect() -> e, es, ""    -> os.pullEvent("ecnet2_request")
receive() <- e, ee, reply <- accept(reply)
send(msg) -> s, se, msg   -> receive()

Transport:
receive() <- msg <- send(msg)
send(msg) -> msg -> receive()