Skip to main content
Device pairing establishes trust between Spacedrive instances using cryptographic signatures and user-friendly codes. Once paired, devices can communicate securely and share data directly.

How Pairing Works

Pairing uses a 12-word code to create a secure connection between two devices. The initiator generates the code, and the joiner enters it to establish trust.

The Pairing Code

Spacedrive uses BIP39 mnemonic codes for pairing, which come in two formats:

Text Format (Local Network Only)

A 12-word BIP39 mnemonic for manual entry:
brave lion sunset river eagle mountain forest ocean thunder crystal diamond phoenix
This format:
  • Works only on the same local network (mDNS discovery)
  • Easy to read and type
  • Contains 128 bits of entropy
  • Valid for 5 minutes
  • Never reused

QR Code Format (Local + Internet)

A JSON structure that enables both local and cross-network pairing:
{
  "version": 2,
  "words": "brave lion sunset river eagle mountain forest ocean thunder crystal diamond phoenix",
  "node_id": "6jn4e7l3pzx2kqhv..."
}
This format:
  • Works across different networks and the internet
  • Includes the initiator’s node_id for pkarr discovery
  • Enables automatic relay fallback
  • Same 5-minute expiration
  • Recommended for most use cases

Security Model

The pairing protocol provides multiple security guarantees: Authentication: Devices prove their identity using Ed25519 signatures Confidentiality: All communication encrypted with session keys Integrity: Challenge-response prevents tampering Forward secrecy: New keys for each session

Choosing a Pairing Method

When to Use Text Codes

Text-based codes are best for:
  • Devices on the same local network (home, office)
  • Quick pairing without scanning QR codes
  • Situations where QR scanning is inconvenient
Limitations:
  • Only works on the same subnet
  • Cannot traverse NATs or firewalls
  • Requires both devices to be on the same physical or virtual network

When to Use QR Codes

QR codes are recommended for:
  • Pairing across different networks
  • Remote device pairing over the internet
  • Maximum reliability (falls back to relay if needed)
  • Most production use cases
Benefits:
  • Works anywhere with internet connectivity
  • Automatic relay fallback for NAT traversal
  • Faster on local networks (dual-path discovery)
  • More reliable overall

Pairing Process

For the Initiator

1

Generate Code

Call the pairing API to generate a code:
const result = await client.action("network.pair.generate", {});

// For local network pairing (manual entry)
console.log(`Share this code: ${result.code}`);

// For cross-network pairing (QR code)
console.log(`QR code data: ${result.qr_json}`);
// Contains: { version: 2, words: "...", node_id: "..." }
2

Wait for Connection

The device advertises via mDNS (local) and pkarr (internet) and waits for a joiner. The code expires after 5 minutes.Advertisement includes:
  • Session ID (via mDNS user_data)
  • Node address published to dns.iroh.link (via pkarr)
3

Verify Joiner

When a joiner connects, the initiator sends a cryptographic challenge to verify they have the correct code and own their device keys.
4

Complete Pairing

After verification, both devices exchange session keys and save the pairing relationship.

For the Joiner

1

Enter Code

Enter the code from the initiator (text or QR):
// Manual entry (local network only)
await client.action("network.pair.join", {
    code: "brave lion sunset river eagle mountain forest ocean thunder crystal diamond phoenix"
});

// QR code scan (local + internet)
await client.action("network.pair.join", {
    code: '{"version":2,"words":"brave lion sunset...","node_id":"..."}'
});

// Manual entry with node_id (enables internet pairing)
await client.action("network.pair.join", {
    code: "brave lion sunset...",
    node_id: "6jn4e7l3pzx2kqhv..."
});
2

Discover Device

The system searches for the initiator using:
  • Local network (mDNS) - Scans for matching session_id
  • Internet (pkarr/DNS) - Queries dns.iroh.link for node address (requires node_id)
  • Relay servers - Automatic fallback if direct connection fails
With QR codes, both paths run simultaneously and the first to succeed wins.
3

Prove Identity

Sign a challenge from the initiator to prove you have the code and own your device keys.
4

Save Relationship

Store the paired device information and session keys for future communication.

Technical Architecture

Protocol Messages

The pairing protocol uses four message types:
pub enum PairingMessage {
    // Joiner → Initiator: "I want to pair"
    PairingRequest {
        session_id: Uuid,
        device_info: DeviceInfo,
        public_key: Vec<u8>,
    },

    // Initiator → Joiner: "Prove you have the code"
    Challenge {
        session_id: Uuid,
        challenge: Vec<u8>,  // 32 random bytes
        device_info: DeviceInfo,
    },

    // Joiner → Initiator: "Here's my signature"
    Response {
        session_id: Uuid,
        response: Vec<u8>,   // 64-byte Ed25519 signature
        device_info: DeviceInfo,
    },

    // Initiator → Joiner: "Pairing complete"
    Complete {
        session_id: Uuid,
        success: bool,
        reason: Option<String>,
    },
}

State Machine

The PairingProtocolHandler manages session state:
pub enum PairingState {
    // Initiator states
    WaitingForConnection,    // Code generated, waiting
    ChallengeIssued,        // Sent challenge to joiner

    // Joiner states
    Connecting,             // Looking for initiator
    ChallengeReceived,      // Got challenge, signing

    // Terminal states
    Completed,              // Success!
    Failed(String),         // Something went wrong
}

Session Management

Each pairing attempt creates a session:
pub struct PairingSession {
    session_id: Uuid,           // Derived from code
    state: PairingState,        // Current state
    remote_device: Option<DeviceInfo>,
    created_at: SystemTime,
    expires_at: SystemTime,     // 5 minutes later
}
Sessions expire after 5 minutes. Users must complete pairing within this time window.

Discovery Mechanisms

Devices find each other through multiple methods, depending on the pairing code format:

Local Network (mDNS)

On the same network, devices discover each other instantly using multicast DNS:
// Initiator broadcasts session_id via user_data
endpoint.set_user_data_for_discovery(Some(session_id));

// Joiner listens for matching session_id
discovery_stream.filter(|item| {
    item.node_info().data.user_data() == session_id
});
How it works:
  • Initiator includes session_id in mDNS broadcasts
  • Joiner scans local network for matching session_id
  • Typically connects in 1-3 seconds
  • Only works on the same subnet

Internet (Pkarr/DNS)

For pairing across networks, Spacedrive uses pkarr to publish and resolve node addresses via DNS:
// Automatic pkarr publishing (done by Iroh)
.add_discovery(PkarrPublisher::n0_dns())  // Publish to dns.iroh.link
.add_discovery(DnsDiscovery::n0_dns())    // Resolve from dns.iroh.link

// Joiner queries by node_id
let node_addr = NodeAddr::new(node_id);  // Pkarr resolves in background
endpoint.connect(node_addr, PAIRING_ALPN).await?;
How it works:
  • Initiator automatically publishes its address to dns.iroh.link via pkarr
  • Record includes relay_url and any direct addresses
  • Joiner queries dns.iroh.link with the node_id from QR code
  • Pkarr returns all connection options (relay + direct)
  • Takes 5-15 seconds including DNS resolution
Pkarr uses DNS-based discovery backed by the Mainline DHT. It’s more reliable than traditional DHT for NAT traversal and works globally.

Dual-Path Discovery

When using QR codes (with node_id), Spacedrive races both discovery methods:
tokio::select! {
    result = try_mdns_discovery(session_id) => {
        // Fast path: local network
    }
    result = try_relay_discovery(node_id) => {
        // Reliable path: internet via pkarr
    }
}
// First to succeed wins, other is canceled
This approach optimizes for speed on local networks while ensuring reliability across the internet.

Relay Servers

When direct connection fails, devices automatically connect through relay servers:
// Relay mode configured at startup
.relay_mode(RelayMode::Default)  // Uses n0's production relays

// Automatic relay fallback during connection
endpoint.connect(node_addr, PAIRING_ALPN).await?;  // Tries direct, then relay
Current Configuration:
  • Uses n0’s default relay servers (North America, Europe, Asia-Pacific)
  • Relay URLs discovered automatically via pkarr
  • Custom relay support coming soon (configurable per-node)
Relay servers only forward encrypted QUIC traffic. They cannot decrypt your data or compromise security.

Cryptographic Details

Challenge-Response Authentication

The challenge-response prevents replay attacks and verifies device identity:
// Initiator generates challenge
let challenge = rand::thread_rng().gen::<[u8; 32]>();

// Joiner signs challenge
let signature = signing_key.sign(&challenge);

// Initiator verifies signature
let valid = verifying_key.verify(&challenge, &signature).is_ok();

Key Derivation

Session keys are derived from the pairing code and device identities:
// Derive shared secret from pairing code
let shared_secret = hkdf::extract(
    &pairing_code.secret,
    &[initiator_id, joiner_id].concat()
);

// Generate session keys
let (tx_key, rx_key) = hkdf::expand(
    &shared_secret,
    b"spacedrive-session-keys",
    64
);

Pkarr Implementation

Spacedrive uses pkarr for decentralized node address resolution:
// Automatic publishing (initiator)
let endpoint = Endpoint::builder()
    .add_discovery(PkarrPublisher::n0_dns())  // Publishes to dns.iroh.link
    .bind().await?;

// Automatic resolution (joiner)
let endpoint = Endpoint::builder()
    .add_discovery(DnsDiscovery::n0_dns())    // Resolves from dns.iroh.link
    .bind().await?;

// Discovery happens automatically during connection
endpoint.connect(NodeAddr::new(node_id), PAIRING_ALPN).await?;
How Pkarr Works:
  • Uses DNS TXT records backed by the Mainline DHT
  • Records include relay URL and direct addresses
  • Automatic publishing every time the node’s address changes
  • TTL-based caching for performance
  • No manual DHT interaction required

Transport Security

All pairing communication uses encrypted channels:
  1. QUIC encryption: TLS 1.3 at transport layer
  2. Application encryption: Additional layer using session keys
  3. Perfect forward secrecy: New keys each session

Error Handling

Common Errors

pub enum PairingError {
    // User errors
    InvalidCode,            // Wrong or malformed code
    CodeExpired,           // Took too long

    // Network errors
    DeviceNotFound,        // Can't find initiator
    ConnectionFailed,      // Network issues

    // Security errors
    InvalidSignature,      // Challenge verification failed
    UntrustedDevice,       // Device key mismatch

    // State errors
    SessionNotFound,       // Unknown session ID
    InvalidState,          // Wrong state transition
}

Recovery Strategies

Invalid code: Check spelling, ensure correct code Connection failed: Check network, firewall settings Timeout: Generate new code and try again Signature failed: Restart both applications

Implementation Guide

Starting Pairing (Initiator)

// High-level API
pub async fn start_pairing_as_initiator(
    &self
) -> Result<PairingCode> {
    // Generate secure code
    let code = PairingCode::generate();
    let session_id = code.derive_session_id();

    // Create session
    let session = PairingSession::new_initiator(session_id);
    self.sessions.insert(session_id, session);

    // Advertise on network
    self.advertise_pairing(session_id).await?;

    Ok(code)
}

Joining Pairing (Joiner)

// High-level API
pub async fn start_pairing_as_joiner(
    &self,
    code: &str
) -> Result<()> {
    // Parse and validate code
    let pairing_code = PairingCode::from_str(code)?;
    let session_id = pairing_code.derive_session_id();

    // Create session
    let session = PairingSession::new_joiner(session_id);
    self.sessions.insert(session_id, session);

    // Find and connect to initiator
    let initiator = self.discover_initiator(session_id).await?;
    self.connect_and_pair(initiator, session_id).await?;

    Ok(())
}

Handling Protocol Messages

impl PairingProtocolHandler {
    async fn handle_message(
        &mut self,
        msg: PairingMessage,
        peer_id: PeerId,
    ) -> Result<()> {
        match msg {
            PairingMessage::PairingRequest { .. } => {
                self.handle_pairing_request(..);
            }
            PairingMessage::Challenge { .. } => {
                self.handle_challenge(..);
            }
            PairingMessage::Response { .. } => {
                self.handle_response(..);
            }
            PairingMessage::Complete { .. } => {
                self.handle_complete(..);
            }
        }
    }
}

Testing Pairing

Unit Tests

#[test]
fn test_pairing_code_generation() {
    let code = PairingCode::generate();
    assert_eq!(code.words.len(), 12);
    assert!(code.is_valid());
}

#[test]
fn test_challenge_response() {
    let (signing_key, verifying_key) = generate_keypair();
    let challenge = generate_challenge();

    let signature = signing_key.sign(&challenge);
    assert!(verifying_key.verify(&challenge, &signature).is_ok());
}

Integration Tests

#[tokio::test]
async fn test_full_pairing_flow() {
    // Start initiator
    let code = initiator.start_pairing_as_initiator().await?;

    // Join with code
    joiner.start_pairing_as_joiner(&code.to_string()).await?;

    // Verify both paired
    assert!(initiator.is_paired_with(joiner.device_id()));
    assert!(joiner.is_paired_with(initiator.device_id()));
}

Best Practices

For Users

  1. Prefer QR codes: Use QR codes for reliability across any network
  2. Share codes securely: Use encrypted messaging or voice calls for text codes
  3. Complete quickly: Codes expire in 5 minutes
  4. Verify device names: Check the paired device is correct
  5. One code at a time: Cancel old attempts before starting new ones
  6. Check network connectivity: For cross-network pairing, ensure internet access

For Developers

  1. Handle all states: Account for every possible state transition
  2. Clean up sessions: Remove expired sessions promptly
  3. Log failures: Record why pairing failed for debugging
  4. Test edge cases: Network failures, timeouts, wrong codes

Troubleshooting

Pairing Fails Immediately

Check:
  • Both devices have network connectivity
  • Firewalls allow Spacedrive traffic
  • System time is roughly correct (within 5 minutes)

Cannot Find Device

For text-based codes:
  • Ensure both devices are on the same local network
  • Check that mDNS is not blocked by firewalls
  • Text codes only work locally - use QR codes for cross-network pairing
For QR codes:
  • Ensure both devices have internet connectivity
  • Check that the node_id is included in the QR code
  • Verify dns.iroh.link is accessible (not blocked by corporate firewalls)
  • Try generating a fresh code

Code Invalid or Expired

Solutions:
  • Double-check spelling of all 12 words
  • Ensure code was entered within 5 minutes
  • Generate new code if expired
  • Check for typos in word order