Skip to content

Protocol Flow

Detailed message sequences for Secure LSL communication.


Overview

This page describes the exact message sequences used in secure LSL communication, useful for implementers and security auditors.


Stream Discovery

Discovery Request

Inlets send multicast UDP queries to discover streams:

sequenceDiagram
    participant Inlet as Inlet
    participant Net as Multicast Group
    participant Outlet as Outlet

    Inlet->>Net: UDP Query<br/>"Looking for type=EEG"
    Net->>Outlet: Query forwarded
    Outlet->>Net: UDP Response<br/>StreamInfo XML + Security metadata
    Net->>Inlet: Response received

Discovery Response XML

The response includes a <security> node:

<?xml version="1.0"?>
<info>
    <name>MyEEG</name>
    <type>EEG</type>
    <channel_count>64</channel_count>
    <nominal_srate>1000</nominal_srate>
    <channel_format>float32</channel_format>
    <source_id>eeg-amp-001</source_id>
    <hostname>lab-eeg-01</hostname>
    <session_id>default</session_id>
    <uid>a1b2c3d4-e5f6-7890-abcd-ef1234567890</uid>
    <created_at>1234567890.123</created_at>
    <security>
        <enabled>true</enabled>
        <public_key>MCowBQYDK2VwAyEA...</public_key>
        <fingerprint>SHA256:70:14:e1:b5:7f:93:ae:af:...</fingerprint>
    </security>
</info>

Connection Establishment

TCP Handshake

After discovery, the inlet connects via TCP for data streaming:

sequenceDiagram
    participant Inlet as Inlet
    participant Outlet as Outlet

    Note over Inlet,Outlet: TCP Connection
    Inlet->>Outlet: TCP SYN
    Outlet->>Inlet: TCP SYN-ACK
    Inlet->>Outlet: TCP ACK

    Note over Inlet,Outlet: LSL Protocol Negotiation
    Inlet->>Outlet: GET /stream HTTP/1.1<br/>Security-Enabled: true<br/>Security-Public-Key: [base64]

    alt Both Secure
        Outlet->>Inlet: HTTP/1.1 200 OK<br/>Security-Enabled: true<br/>Security-Public-Key: [base64]
        Note over Inlet,Outlet: Key Exchange Complete
    else Security Mismatch
        Outlet->>Inlet: HTTP/1.1 403 Forbidden<br/>Security-Error: mismatch
        Note over Inlet,Outlet: Connection Terminated
    end

Request Headers

GET /stream HTTP/1.1
Host: lab-eeg-01:16574
Security-Enabled: true
Security-Public-Key: MCowBQYDK2VwAyEAx7Kp...
Security-Version: 1

Response Headers (Success)

HTTP/1.1 200 OK
Content-Type: application/x-lsl-stream
Security-Enabled: true
Security-Public-Key: MCowBQYDK2VwAyEAy8Lq...
Security-Version: 1

Response Headers (Security Mismatch)

HTTP/1.1 403 Forbidden
Security-Error: outlet_requires_security
Content-Type: text/plain

Connection refused: outlet requires security but client has no security enabled.

Key Exchange

After header exchange, both sides derive the session key:

sequenceDiagram
    participant Inlet as Inlet
    participant Outlet as Outlet

    Note over Inlet,Outlet: Both sides have exchanged public keys

    Inlet->>Inlet: Convert Ed25519 to X25519<br/>inlet_x25519_sk = ed_to_x25519(inlet_ed_sk)
    Outlet->>Outlet: Convert Ed25519 to X25519<br/>outlet_x25519_sk = ed_to_x25519(outlet_ed_sk)

    Note over Inlet,Outlet: X25519 Key Agreement
    Inlet->>Inlet: shared_secret = X25519(inlet_x25519_sk, outlet_x25519_pk)
    Outlet->>Outlet: shared_secret = X25519(outlet_x25519_sk, inlet_x25519_pk)

    Note over Inlet,Outlet: Both compute same shared_secret

    Note over Inlet,Outlet: HKDF Key Derivation
    Inlet->>Inlet: session_key = HKDF(shared_secret, "lsl-session-v1")
    Outlet->>Outlet: session_key = HKDF(shared_secret, "lsl-session-v1")

    Note over Inlet,Outlet: Both have identical session_key

HKDF Parameters

Algorithm: HKDF-SHA256
Input Key Material: 32-byte X25519 shared secret
Salt: None (zero-length)
Info: "lsl-session-v1" (UTF-8 bytes)
Output Length: 32 bytes

Data Streaming

Encrypted Packet Format

Each data packet has this wire format:

┌─────────────────────────────────────────────────────┐
│ Length (4 bytes, little-endian)                     │
├─────────────────────────────────────────────────────┤
│ Nonce (8 bytes, little-endian counter)              │
├─────────────────────────────────────────────────────┤
│ Ciphertext (variable length)                        │
│   Contains encrypted sample data                    │
├─────────────────────────────────────────────────────┤
│ Authentication Tag (16 bytes)                       │
└─────────────────────────────────────────────────────┘

Encryption Process

sequenceDiagram
    participant App as Application
    participant LSL as liblsl
    participant Net as Network

    App->>LSL: push_sample(data)

    LSL->>LSL: Serialize sample to bytes
    LSL->>LSL: nonce = increment_counter()
    LSL->>LSL: (ciphertext, tag) = ChaCha20Poly1305_Encrypt(<br/>  key=session_key,<br/>  nonce=nonce,<br/>  plaintext=serialized_data,<br/>  aad=none<br/>)
    LSL->>LSL: packet = length || nonce || ciphertext || tag

    LSL->>Net: Send packet

Decryption Process

sequenceDiagram
    participant Net as Network
    participant LSL as liblsl
    participant App as Application

    Net->>LSL: Receive packet

    LSL->>LSL: Parse: length, nonce, ciphertext, tag
    LSL->>LSL: Verify nonce > last_seen_nonce

    alt Nonce Valid
        LSL->>LSL: plaintext = ChaCha20Poly1305_Decrypt(<br/>  key=session_key,<br/>  nonce=nonce,<br/>  ciphertext=ciphertext,<br/>  tag=tag<br/>)

        alt Decryption Success
            LSL->>LSL: Deserialize sample from plaintext
            LSL->>App: return sample
        else Decryption Failed (tampered)
            LSL->>LSL: Log security event
            LSL->>App: return error
        end
    else Nonce Invalid (replay)
        LSL->>LSL: Log replay attempt
        LSL->>App: return error
    end

Session Key Rotation

Keys are rotated every 24 hours (configurable):

sequenceDiagram
    participant Outlet as Outlet
    participant Inlet as Inlet

    Note over Outlet,Inlet: Normal streaming with Key A

    Outlet->>Outlet: Rotation timer expires
    Outlet->>Outlet: Generate new ephemeral X25519 keypair
    Outlet->>Inlet: KEY_ROTATION message<br/>new_public_key: [base64]

    Note over Outlet,Inlet: Grace period begins (60 seconds)
    Outlet->>Outlet: Derive Key B from new exchange
    Inlet->>Inlet: Derive Key B from new exchange

    Note over Outlet,Inlet: Inlet accepts Key A or Key B
    Outlet->>Inlet: Data encrypted with Key B

    Note over Outlet,Inlet: Grace period ends
    Outlet->>Outlet: Discard Key A
    Inlet->>Inlet: Discard Key A

    Note over Outlet,Inlet: Continue with Key B only

Rotation Message Format

┌─────────────────────────────────────────────────────┐
│ Message Type: KEY_ROTATION (0x01)                   │
├─────────────────────────────────────────────────────┤
│ New Public Key (32 bytes)                           │
├─────────────────────────────────────────────────────┤
│ Timestamp (8 bytes)                                 │
├─────────────────────────────────────────────────────┤
│ Signature (64 bytes, Ed25519 over above fields)     │
└─────────────────────────────────────────────────────┘

Error Cases

Security Mismatch

sequenceDiagram
    participant SecureInlet as Secure Inlet
    participant InsecureOutlet as Insecure Outlet

    SecureInlet->>InsecureOutlet: GET /stream<br/>Security-Enabled: true

    InsecureOutlet->>SecureInlet: HTTP/1.1 403 Forbidden<br/>Security-Error: outlet_not_secure

    SecureInlet->>SecureInlet: Log error<br/>"Connection refused: outlet does not have security enabled"

Authentication Failure

sequenceDiagram
    participant Attacker as Attacker
    participant Outlet as Legitimate Outlet

    Attacker->>Outlet: GET /stream<br/>Security-Enabled: true<br/>Security-Public-Key: [fake_key]

    Outlet->>Outlet: Derive session key with fake_key

    Note over Attacker,Outlet: Data streaming begins

    Attacker->>Attacker: Cannot derive same session key<br/>(doesn't have private key for fake_key)

    Outlet->>Attacker: Encrypted data

    Attacker->>Attacker: Decryption fails<br/>(wrong session key)

Nonce Management

Counter-Based Nonces

Initial nonce: 0x0000000000000000
After packet 1: 0x0000000000000001
After packet 2: 0x0000000000000002
...
Maximum: 0xFFFFFFFFFFFFFFFF (18 quintillion packets)

Replay Window

The inlet maintains a sliding window for out-of-order tolerance:

Window size: 64 packets
Current highest nonce: N

Accept if:
  - nonce > N (new highest)
  - nonce > N - 64 AND nonce not in seen_set

Reject if:
  - nonce <= N - 64 (too old)
  - nonce in seen_set (replay)

Next Steps