8.8 KiB
Spacedrive v2: Device Pairing Protocol
The Spacedrive v2 device pairing system is a secure, robust protocol for establishing a trusted, end-to-end encrypted connection between two devices, regardless of whether they are on the same local network or across the internet. It is built upon the unified libp2p networking stack and uses a combination of modern cryptographic principles and user-friendly codes to deliver a seamless pairing experience.
Overview
The primary goal of the pairing system is to create a secure relationship between two devices, allowing them to communicate directly, exchange session keys, and perform operations like file transfer and synchronization. This is achieved through a challenge-response handshake initiated by a user-friendly 12-word BIP39 code.
Key Features
- Cryptographic Security: Pairing is secured using an Ed25519 challenge-response signature verification, ensuring that only the intended device can complete the process. All transport-level communication is encrypted using the Noise Protocol.
- Dual-Discovery Mechanism: The system uses a unified discovery approach. It queries the Kademlia Distributed Hash Table (DHT) for remote discovery (across different networks) while simultaneously listening for local peers via mDNS. The first successful discovery method is used, providing both speed on local networks and reachability over the internet.
- User-Friendly Pairing Codes: Instead of complex hashes, the system generates a 12-word BIP39 mnemonic code. This code is easy for users to read and type, yet contains enough entropy to securely identify a pairing session.
- State Machine Architecture: The entire pairing process is managed by a robust state machine within the
PairingProtocolHandler, which tracks each session from initiation to completion or failure. - Automatic Device Registration: Upon successful pairing, devices are automatically added to each other's
DeviceRegistry, and persistent, encrypted session keys are established for all future communication.
Core Components
The pairing system is a collaboration between several key components in the new architecture:
Core(src/lib.rs): Provides the high-level public API for initiating pairing. Thestart_pairing_as_initiator()andstart_pairing_as_joiner()methods are the entry points for the entire process.PairingProtocolHandler(src/infrastructure/networking/protocols/pairing/mod.rs): This is the heart of the pairing system. It acts as a state machine, managing active pairing sessions, generating cryptographic challenges, verifying responses, and orchestrating the entire protocol flow.PairingCode&PairingSession(types.rs): These structs define the data model for pairing.PairingCode: Represents the 12-word code and the cryptographic secret from which the session ID is derived.PairingSession: Tracks the state (PairingStateenum) of a single pairing attempt, including remote device info and derived keys.
DeviceRegistry(device/registry.rs): The central registry for device state. During pairing, it maps an ephemeralsession_idto a permanentdevice_idand stores the finalSessionKeysupon completion.UnifiedBehaviour(core/behavior.rs): The unified libp2p behavior that enables discovery. Its Kademlia DHT component is used to publish and query pairing advertisements, while the mDNS component listens for local peers.
The Pairing Flow
The pairing process involves two roles: the Initiator (who generates the code) and the Joiner (who uses the code).
Initiator Flow (e.g., Alice)
- Initiation: A user triggers the pairing process, calling
core.start_pairing_as_initiator(). - Code Generation: A cryptographically secure
PairingCodeis generated. A uniquesession_idis derived from this code's entropy. - DHT Advertisement:
- The Initiator gathers its public
DeviceInfo(name, OS, etc.) and its external network addresses (e.g.,/ip4/1.2.3.4/tcp/5678). - This information is packaged into a
PairingAdvertisement. - The advertisement is published to the Kademlia DHT. The DHT
RecordKeyis thesession_id, making it discoverable by the Joiner.
- The Initiator gathers its public
- Waiting State: The Initiator's
PairingSessionenters theWaitingForConnectionstate. It now listens for incoming connections from any peer. - Challenge Issuance:
- When a Joiner connects and sends a
PairingRequestmessage containing its public key andDeviceInfo, the Initiator'sPairingProtocolHandlerreceives it. - The handler generates a random 32-byte cryptographic
challenge. - It sends this
challengeback to the Joiner in aChallengemessage. The session state transitions toChallengeReceived.
- When a Joiner connects and sends a
- Response Verification:
- The Initiator receives a
Responsemessage from the Joiner. This message contains the original challenge signed with the Joiner's private device key. - The
PairingSecuritymodule is used to verify the signature against the Joiner's public key (received in step 5).
- The Initiator receives a
- Completion:
- If the signature is valid, the pairing is successful.
- The Initiator derives the shared session keys.
- It updates its
DeviceRegistryto mark the Joiner as a trusted, paired device. - It sends a final
Completemessage to the Joiner. The session is nowCompleted.
Joiner Flow (e.g., Bob)
- Code Entry: The user enters the 12-word
PairingCodeprovided by the Initiator. - Session ID Extraction: The
PairingCodeis parsed to deterministically reconstruct the samesession_idthe Initiator created. - Unified Discovery:
- The
core.start_pairing_as_joiner()method is called. - The
NetworkingCoreimmediately begins querying the DHT using thesession_idto find the Initiator'sPairingAdvertisement. - Simultaneously, the mDNS service listens for the Initiator on the local network.
- The
- Connection:
- Once the Initiator's address is discovered (via DHT or mDNS), the system establishes a direct TCP connection.
- Sending Request:
- As soon as the connection is established, the Joiner sends a
PairingRequestmessage. This message includes its ownDeviceInfoand, crucially, its public key.
- As soon as the connection is established, the Joiner sends a
- Signing Challenge:
- The Joiner receives the
Challengemessage from the Initiator. - It uses its private
NetworkIdentitykey to sign the 32-byte challenge. - It sends the resulting 64-byte signature back in a
Responsemessage.
- The Joiner receives the
- Completion:
- The Joiner receives the final
Completemessage from the Initiator. - It derives the same shared session keys.
- It updates its
DeviceRegistryto add the Initiator as a trusted, paired device. The connection is now fully authenticated and encrypted for all future communication.
- The Joiner receives the final
Security Model
The pairing protocol is designed with security as a primary concern.
- Transport Encryption: All communication, from the very first connection attempt, is encrypted using the Noise Protocol, which provides forward secrecy.
- Cryptographic Authentication: The identity of the joining device is verified using an Ed25519 digital signature. The challenge-response mechanism prevents replay attacks and ensures that the device joining is the one that possesses the private key corresponding to the public key it presented.
- Session Key Derivation: Once authenticated, the shared secret from the pairing code is used as input to a Key Derivation Function (HKDF). This generates strong, unique symmetric keys for sending and receiving data between the two devices, ensuring all subsequent communication is confidential and authenticated.
- Ephemeral & Discoverable Session ID: The
session_idused for DHT discovery is derived from the pairing code but is not the secret itself. This allows the session to be publicly discoverable for a short period without exposing any sensitive information. The codes and sessions expire after 5-10 minutes to limit the window of opportunity for attacks.
Implementation Details
The core of the logic is implemented in the PairingProtocolHandler, which uses a PairingState enum to manage the lifecycle of each PairingSession.
// from src/infrastructure/networking/protocols/pairing/messages.rs
pub enum PairingMessage {
PairingRequest {
session_id: Uuid,
device_info: DeviceInfo,
public_key: Vec<u8>,
},
Challenge {
session_id: Uuid,
challenge: Vec<u8>,
device_info: DeviceInfo,
},
Response {
session_id: Uuid,
response: Vec<u8>,
device_info: DeviceInfo,
},
Complete {
session_id: Uuid,
success: bool,
reason: Option<String>,
},
}
This message-passing design, combined with a robust state machine, ensures that the pairing process is reliable and secure from start to finish.