Skip to content

Protocol and Crypto

The Management Protocol channel between C3PO and an agent is an encrypted, authenticated, protobuf-framed line protocol over TCP. C3PO's implementation in c3po/c2/ is byte-for-byte compatible with the firmware's transport.

Silent handshake

When an agent connects, identity is established without ever sending the device id in plaintext (c3po/c2/handshake.py):

sequenceDiagram
    participant D as Device
    participant S as Server (C3PO)
    S->>D: base64(server_nonce[32])
    D->>S: base64(nonce12 + ChaCha(key, device_id + server_nonce))
    Note over S: Try each known key until AEAD decrypt succeeds -> identify device
    S->>D: base64(nonce12 + ChaCha(key, "OK:" + server_nonce))
  1. The server sends a random 32-byte nonce.
  2. The device replies with its device id and the server nonce, encrypted under its session key.
  3. The server tries each registered key until one decrypts the frame, which identifies the device and verifies the nonce.
  4. The server returns an authenticated acknowledgement.

Each step has a 10-second timeout. An eavesdropper without the key cannot tell which device is connecting or correlate sessions.

Per-device crypto context

Each device has its own AEAD context (c3po/c2/crypto.py:CryptoContext) derived from the device master_key with HKDF-SHA256 salted by the device id:

Property Value
Cipher ChaCha20-Poly1305 (AEAD)
Key 32 bytes, derived per device
Nonce 12 bytes, random per message
Tag 16 bytes

Wire framing

Every message is encrypted and base64-encoded, one frame per line:

base64( nonce[12] || ChaCha20-Poly1305(plaintext) || tag[16] ) + "\n"

A frame that fails authentication is dropped silently; the connection is not torn down.

Protobuf messages

The payload is a protobuf message (c2.proto). Commands go to the device, agent messages come back:

message Command {
  string device_id     = 1;
  string command_name  = 2;
  repeated string argv = 3;
  string request_id    = 4;
  bytes binary_data    = 5;   // module chunks
  uint32 binary_offset = 6;
  uint32 binary_total  = 7;
}

message AgentMessage {
  string device_id  = 1;
  AgentMsgType type = 2;   // INFO, ERROR, DATA, LOG, CMD_RESULT
  string source     = 3;   // module name
  string request_id = 4;   // correlates to Command.request_id
  bytes payload     = 5;
  bool eof          = 6;
}

request_id correlates a command with its responses; eof marks the final frame of a response.

Command dispatch path

flowchart LR
    CMD["Command"] --> ENC["serialize -> encrypt -> base64"]
    ENC -->|"TCP :2626"| RX["device RX"]
    RX --> DEC["base64 -> decrypt -> verify"]
    DEC --> PB["protobuf decode"]
    PB --> DISP["static or dynamic dispatch"]
    DISP --> RESP["msg_info / msg_result / ..."]
    RESP -->|"reverse path"| C3PO["C3PO router"]

Server internals

c3po/c2/server.py:C2Server is an asyncio TCP server. Per connection it performs the handshake, registers the device, and reads newline-delimited frames. Relevant timing:

Parameter Value Meaning
HEARTBEAT_INTERVAL_S 10 Timeout check interval
PROBE_THRESHOLD_S 1200 (20 min) Send a keep-alive probe after silence
DEVICE_TIMEOUT_S 1500 (25 min) Mark a device inactive
Command stale cleanup 120 s Drop in-flight commands with no EOF

The C2 layer never touches the UI directly; it emits Qt signals (C2Events) that the views subscribe to.

See also