OPUS OS protocols#

OPUS OS (the “OS” in the name is NOT optional) is an operating system started by kepler155c in 2016, and maintained by Anavrins as of November 2020.

OPUS OS networking using modems introduces trust and encryption, with an unusual trust system where servers need to trust clients for clients to establish connections with them. Trusting a client means knowing its password, which leads to most communications being established between devices belonging to the same player or organization.

OPUS OS starts servers for all protocols at startup by default. A device supports to up to 16384 simultaneous connections using OPUS OS protocols.

Device keys and trust#

Every device who wishes to communicate with other devices using OPUS OS protocols must have the following elements:

  • A server-wide unique numerical identifier, usually obtained using the hardware computer identifier (see os.getComputerID); we will call this identifier the computer ID, while the reference implementation names it “host” or “id”.

  • A server-wide unique 256-bit identifier (usually base16 encoded); we will call this identifier the device key, while the code names it “identifier” or “pk”.

  • A password defined for its system, which is stored hashed using SHA-256. One-time passwords can also be defined for the trust protocol.

When devices send a connection establishment request, they encrypt part of it with ChaCha20 using their device key. This has the following consequences:

  • When servers receive an connection establishment request, they only receive the ID of the sender and an encrypted payload using its device key, not the device key itself. It must therefore be able to find the device key using the ID of the sender. This is what trusting is about: beforehand, the client sends its device key and a proof that it’s managed by the same player or organization than the server (the hashed password of the server), which is learnt by the server for further connections, using a snake oil device key. See OPUS OS trust protocol for more information.

  • ChaCha20 is symmetrical, therefore any device which knows the device key of a given computer can impersonate it. However, intercepting a trust handshake isn’t enough as the device key is, in turn, encrypted using the server’s password hash. The only way to impersonate a device is to know the server’s password or somehow break into one that has learnt the device key of the device we’re trying to impersonate; therefore, a client should only share its device key with trustworthy servers.

Note that the client doesn’t need a password and the server doesn’t need a device key; therefore, trusting only needs to be done one way for a connection to be established further on.

The device key (named “identifier” on OPUS OS) and password are managed by the OPUS OS security module.

Encryption algorithms#

OPUS OS protocols have a high focus on security, providing up-to-date encryption (using ChaCha20, ECC and SHA-2) and peer-to-peer trust facilities. However, they use standard Lua pseudorandom facilities without focusing on entropy generation, which is not cryptographically secure; see Randomness and entropy for more information.

Algorithms used in OPUS OS protocols are the following:

OPUS OS PRNG for sequence numbers#

OPUS OS protocols use ChaCha20 as a PRNG. The PRNG is a seed which must be 32-bytes (256 bits) long; then, the following parameters are used:

  • Key: the given seed.

  • Nonce: (41:41:41:41:41:41:41:41:41:41:41:41).

The incoming data encrypted to give the bytes is an infinite stream of 0x41 bytes, and the counter starts at 1.

When a n-byte number is required, where n varies from 1 to 6, bytes are extracted in a little-endian fashion. For example, if n is 4, and 01 23 45 67 is picked, then 0x67452301 is returned by the PRNG.

See the OPUS OS ChaCha20 PRNG definition for reference.

Transport system#

OPUS OS network sniffer preview

Sniffed traffic from an OPUS OS network. Here, you can mainly see discovery protocol traffic, pings on an existing connection, and a connection setup on port 23.#

OPUS OS uses what it calls “sockets” with transport. These sockets are connected bidirectional communications, with built-in optional encryption and authentication.

Although the connection is initiated on a well-known port representing the application protocol, e.g. 23 for telnet, both the client and the server use ephemeral ports in the \([2 ^ {14}, 2 ^ {15} - 1]\) range as connection identifiers.

Any communication, even unencrypted, requires an ECC public/private 256-bit key pair for both the client and the server; these are unique to every connection by default.

See the OPUS OS socket module, the OPUS OS transport network app and the OPUS OS keygen network app for reference.

Establishing connections#

Connections are established using the following process:

  • The client sends an OPEN message with its public key and the current timestamp (for avoiding replay attacks), encoded using the client’s device key (as a proof), to the well-known port from its allocated ephemeral port.

  • The server, upon reception, can answer one of the following:

    • A CONN message for notifying the client that the connection has successfully been established.

    • A NOPASS message for notifying the client that the connection could not be established because the password on the distant host wasn’t set.

    • A REJE message for notifying the client that the connection could not be established because the client wasn’t trusted by the server (see OPUS OS trust protocol), or because the timestamp at treatment time was more than 4.096 seconds away from the timestamp given in the connection establishment request.

    It does so to the client ephemeral port from the allocated server ephemeral port. A lack of such an answer means that the server is either unreachable or has no ephemeral ports available (i.e. it has reached the maximum amount of connections).

Note that hosts must have a password defined for it to accept connections from foreign hosts.

Once a socket is established between a client and a server, the server will send its messages from its allocated ephemeral port to the client’s allocated ephemeral port, and the client will send its messages from its allocated ephemeral port to the server’s allocated ephemeral port.

The following actions can occur from either one of them according to the protocol running above the socket:

  • A DISC message is sent, meaning that the device which has sent the message wants to close the socket on both ends.

  • A DATA message is sent, meaning that the device which has sent the message wants to send data, and expects an ACK message within a given duration, otherwise the socket is closed.

  • A PING message is sent, which means that the device which sent the message expects an ACK within a given duration, otherwise the socket is closed.

When the connection is closed because of a timeout between devices (for data and ping messages), a DISC message is sent.

All messages are tables containing at least the type field, which should be a case-sensitive string describing the message type.

An OPEN message is composed as follows:

  • type is set to "OPEN".

  • dhost is set to the server’s ID.

  • shost is set to the client’s ID.

  • type is set to the message type as a string.

  • t is set to a table, encrypted with ChaCha20 using the sender’s device key, containing the following fields:

    • ts: the timestamp, as milliseconds from the UNIX EPOCH in UTC, obtained using os.epoch('utc').

    • pk: the public key of the key pair used by the sender for the current connection, base16-encoded.

If the server has no password set, a NOPASS message is sent:

  • type is set to "NOPASS".

  • dhost is set to the client’s ID.

  • shost is set to the server’s ID.

Otherwise, if the connection is rejected, because the timestamp is too far away or the sender is untrusted (i.e. no device key registered in the server’s trusted clients database), a REJE message is sent, composed of the following:

  • type is set to "REJE".

  • dhost is set to the client’s ID.

  • shost is set to the server’s ID.

Otherwise, the connection can be established, and a CONN message is sent, composed of the following:

  • type is set to "CONN".

  • dhost is set to the client’s ID.

  • shost is set to the server’s ID.

  • pk is set to the public key of the server’s ECC public/private key pair for this connection, base16-encoded.

  • options is set to the connection options, either nil or a table containing the following fields:

    • ENCRYPT: is set to true if the data payloads should be encoded, or another value if they won’t.

The client expects the server’s answer to its OPEN message within 3 seconds; otherwise, the connection is considered closed because of a time out.

Sending and receiving data#

Out of the other device’s public key and the device’s private key, we generate a shared key which will be common to both sides.

Todo

This uses ECC.exchange(privKey, remotePubKey) which as I’ve been suggested is very probably Diffie-Helman. This should be verified however, and documented and sourced correctly.

Out of this shared key, two PRNGs (see OPUS OS PRNG for sequence numbers) are initialized:

  • One for client->server sequence numbers, which is seeded by pbkdf2(sharedKey, "4sseed", 1).

  • One for server->client sequence numbers, which is seeded by pbkdf2(sharedKey, "3rseed", 1).

These PRNGs will be used to generate sequence numbers in the \([0, 2 ^ {40} - 1]\) range in a predictable manner from both sides.

Note

The key pairs used for the connection are the only variable data used for generating the PRNG seeds; therefore, they must be unique to every connection for these sequence numbers to be unpredictable by any attacker.

If a protocol chooses a fixed keypair for any connection using the OPUS OS transport protocol, an attacker can save a great number out of one connection, then after an unencrypted connection is established, hijack the connection by using these sequence numbers on any side, taking advantage of the fact that data messages are not acknowledged for.

When sending data from one device to the other, a DATA message is sent, composed of the following fields:

  • type is set to "DATA".

  • seq is set to the next number in the PRNG for sent sequence numbers, which is then consumed.

  • data is set to the data, either encrypted or not depending on the options set by the server on connection establishment.

If encryption for the connection is enabled, a ChaCha20 symmetric key is also generated using pbkdf2(sharedKey, "1enc", 1), and is used to encrypt data on one end and decrypt the data on the other end.

Upon receiving such a data message, the sequence number is matched with the predicted reception sequence number. If the numbers match, the next reception sequence number is processed and the data is decrypted if necessary, and made available for the application to query; otherwise, the message is dismissed.

Warning

Note that there is no acknowledgment for data messages. Therefore:

  • You cannot know if the connection has timed out without sending a ping; that, however, does not guarantee that previous data messages have been successfully received (two causes of connection disruption can be weather changes impacting wireless message receiving range or turtles moving out of range).

  • Messages are supposed to effectively arrive, and in the same order they were sent. If messages A then B then C are sent, and B then A then C are received, then B will be dismissed because of an out-of-sequence error, A will be accepted, and C and following messages will be dismissed because of out-of-sequence errors.

  • Out-of-sequence errors are silent, therefore an intelligent host cannot detect if the connection is blocked in an out-of-sequence state without an understanding of the application protocol, because pings do not use sequence number generators.

Any corrupted connection should be reset using this transport protocol, by closing it using a DISC message and re-opening it.

Managing timeouts and disconnections#

When a read is attempted at by any side, and no timeout has been specified by the application, every 5 seconds of waiting for data, a ping handshake is started.

Note

If the reference implementation, when the socket user sets a custom timeout, no ping is emitted, and a timeout does not break the connection.

When such a handshake happens, if no data within a given timeout or the socket user explicitely ran a ping request, a PING message is emitted, composed of the following fields:

  • type is set to "PING".

  • seq is set to any sequence number; the reference implementation uses -1 for this field.

Upon receiving such a message, the other device must reply with an ACK message, composed of the following fields:

  • type is set to "ACK".

  • seq must be a copy of the seq member in the corresponding PING message.

If no such ACK message is received within the ping delay, which is 3 seconds, the connection is considered broken and a disconnection is requested.

When such a timeout happens, or when the socket user purposefully closes the connection, a DISC message is sent, composed of the following fields:

  • type is set to "DISC".

  • seq is set to the next up data sequence number (?).

OPUS OS discovery protocol#

OPUS OS network application preview

The OPUS OS network application, presenting devices detected through announces on the discovery channel.#

The discovery protocol is the protocol with which devices announce themselves on wireless modems. Devices implementing this protocol send a message periodically on modem channel 999, a table which may or may not contain the following keys (depending on the device type):

  • label: the label of the device, determined at setup time.

  • uptime: the uptime, in seconds (as an integer).

  • group: the network group (unused).

  • fuel: the current fuel level (for turtles).

  • status: the status as a string (e.g. "Swimming").

  • inv: the inventory of the device (or the player).

  • slotIndex: the currently selected slot of the inventory.

See the OPUS OS discovery protocol server implementation for reference.

OPUS OS trust protocol#

OPUS OS network application trust failure preview

The trust pop-up of the OPUS OS network application, presenting a password prompt, and failing because the password is incorrect.#

The trust protocol is the protocol that allows an untrusted client to be trusted by a server, knowing its password; see Device keys and trust.

For this, both the server and the client use a “snake oil” device key, hardcoded within the reference implementation (which is only 200-bit long):

01c3ba27fe01383a03a1785276d99df27c3edcef68fbf231ca

Once the connection is established using the OPUS OS transport protocol (see Transport system), using this device key for both the client and the server, the client sends the following table to the server:

  • pk: the client’s 256-bit identifier.

  • dh: the client’s computer ID.

Before being sent, this message is encrypted with ChaCha20, using the SHA-256 hash of the server’s password or one-time password (if it was set). The server then answers the following table:

  • success: either true if the trust was accepted, which means that the client can now communicate with the server using other protocols, or nil (or else) if the trust was not accepted.

  • msg: the status message, either informing of the success or the error that occurred.

See the OPUS OS trust protocol server implementation and the OPUS OS trust protocol client implementation for reference.

OPUS OS SNMP protocol#

SNMP in OPUS OS is a protocol to have basic control over some devices. It is inspired from Simple Network Management Protocol (hence the port number), see opus-snmp.txt.

It uses modem channel 161, on which the client establishes an OPUS OS connection; see Transport system. Once the connection is established, the client sends a request as a table containing at least the type field, which defines the message type as a string. In case of unknown message type, the request is ignored.

The message types are the following:

"reboot"

The server is asked to reboot immediately.

"shutdown"

The server is asked to shutdown immediately.

"ping"

The server answers with the string "pong".

"script"

The server is asked to execute a script as a separate process. The script is provided in the request using the args key, as a string (loaded as text).

The server does not answer to such a request.

"scriptEx"

The server is asked to execute a script synchronously. The script is provided in the request using the args key, as a string (loaded as text).

The server answers to such a request with the following:

  • If an error has occurred in the given script, a sequence composed of false followed by the error message is returned.

  • Otherwise, a sequence composed of the returned arguments by the script is returned.

It is recommended to scripts executed through this command to return a first argument equal to true, to distinguish with error cases described in the first case.

"gps"

The server is asked to provide its current position. If such a position is found, the point is returned as a sequence of three cartesian coordinates {x, y, z}; otherwise, no answer is given.

"info"

The server is asked to provide some information about itself. The server answers with a table with the following fields (those that may not be filled in are marked with “opt.”):

  • id: the device’s unique identifier, as a number.

  • label: the device’s label, as a string.

  • uptime: the device’s uptime, as a number of seconds.

  • fuel (opt.): the fuel level.

  • status (opt).: the current status.

See the OPUS OS snmp protocol server implementation for reference.

OPUS OS telnet protocol#

OPUS OS telnet tunnel

The native shell of another device opened through the network application.#

It uses modem channel 22 (encrypted) and 23 (unencrypted).

Todo

Describe the ssh protocol, its role, how it is organized.

See the OPUS OS telnet protocol server implementation and the OPUS OS telnet protocol client implementation for reference.

OPUS OS VNC protocol#

OPUS OS PAIN application through two VNC tunnels

The PAIN application through a VNC client opened on a turtle, itself opened through a VNC client on the main device. Interactions are slow but functional at this stage. Note that the final host’s resolution has been modified by the VNC server app.#

It uses modem channels 5900 (unencrypted) and 5901 (encrypted).

Todo

Describe the VNC and SVNC protocols, their role, how they are organized.

See the OPUS OS VNC protocol server implementation and the OPUS OS VNC protocol client implementation for reference.

OPUS OS samba protocol#

OPUS OS files application on a network mount

The files application on a netfs mount, which corresponds to the native samba client.#

The OPUS OS samba protocol is an RPC protocol specialized in filesystem operations, by proxying the global _G.fs module with special treatment of file handles (as modem messages don’t carry objects).

Messages using this protocol are carried on modem port 139. Once the connection is setup unencrypted, the client can send samba requests as tables containing the following fields:

  • fn: the name of the function to execute, as a string.

  • args: the arguments to the function, as a sequence.

The servere always answers with a table containing the following field:

  • response: the response, depending on the executed function.

The following functions are available, based on the functions described in the CraftOS fs API:

"open"

Open a file handle on the given path. Takes the following parameters:

  • path: the path to the file to open, as a string.

  • mode: the mode to open the file with, as a string; either r to get a read handle, w to get a write handle, or a to get a write handle with the cursor placed at the end of the file (keeping contents). If b is added to the end, the file will be opened in binary mode; otherwise, it is opened in text mode.

Returns a file handle as a number (see fileOp), or nil if opening the file has failed.

"fileOp"

Run a command on the given file handle. The arguments of this command are the following:

  • vfh: the virtual file handle identifier, as a number.

  • op: the operation to carry out on this file handle, as a string.

In case of an invalid file handle or operation, nil will be returned.

The possible operations using this command are the following:

"close"

Close the file handle, which won’t be usable again. Returns nil.

"write"

Write a string of characters to the file. Takes a value parameter as a string, which corresponds to the value to write. Returns nil.

"writeLine"

Writes a string of characters to the file, followed by a new line character. Takes a value parameter as a string, which corresponds to the value to write. Returns nil.

"flush"

Save the current file without closing it. Returns nil.

"read"

Reads a number of characters from the file. Takes an optional count parameter as a number, corresponding to the number of characters to read, and defaulting to 1. Returns the read characters as a string, or nil if at the end of the file.

"readLine"

Reads a line from the file. Takes an optional withTrailing parameter as a boolean, corresponding to whether to include the newline characters with the returned string, and defaulting to false. Returns the read line as a string, or nil if at the end of the file.

"readAll"

Reads the remainder of the file. Returns the remaining content of the file as a string, or nil if at the end of the file.

"copy"

Copies a file or directory (recursively) to a new path; any parent directories are created as needed. Takes the following arguments:

  • path: the path to the file or directory to copy.

  • dest: the path to the destination file or directory.

"isDir"

Checks whether the specified path corresponds to a directory. Takes the following argument:

  • path: the path to check.

Returns either true if the element at the given path is a directory, or false otherwise.

"isReadOnly"

Checks whether the specified path is read-only. Takes the following argument:

  • path: the path to check.

Returns either true if the given path is read-only, or false otherwise.

Todo

Describe the other commands used in the fs module. The functions to describe are the following:

  • complete

  • getSize

  • find

  • move

  • open

See the OPUS OS samba protocol server implementation and the OPUS OS samba protocol fs driver for reference.

OPUS OS proxy protocol#

../../_images/opus-proxy-bpmn.png

The OPUS OS proxy protocol is a synchronous RPC protocol for executing procedures from a trusted device. It uses modem channel 188, and OPUS OS transport; see Transport system.

Once the connection is setup, the client starts by selecting an API, by sending a string correspondong to the API. For example, sending "turtle" means we want to select the turtle API. Sub-API selection (e.g. a.b) is possible by splitting the higher-level and lower-level API name by a solidus, e.g. a/b.

The server answers with a sequence corresponding to the method names in the given API, e.g. {"forward", "back", "turnLeft", "turnRight", ...} if the turtle API was selected. From now on, the server is set up and ready to execute commands.

Then, for each call the client wants to make, it emits a sequence containing the method name followed by the arguments, e.g. {"myfunction", "arg1", 2, true} for imitating a local call selectedapi.myfunction("arg1", 2, true).

If the method is undefined or not a function, the connection is closed by the server (resulting in an error on the server side). Otherwise, the result arguments are returned as a sequence, e.g. {"one", 2, false}.

See the OPUS OS proxy protocol server implementation for reference, and multiMiner hijacking for an example (turtle “hijacking” by proxying the turtle module).