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
- Architecture Overview - High-level design
- Cryptographic Design - Algorithm details
- Security Model - Threat analysis