Device Pairing#
Transfer an encryption identity from an existing device to a new one, using the PDS as a relay. Both devices are authenticated to the same DID.
Pair Request (new device)#
sequenceDiagram
participant User
participant CLI as CLI (new device)
participant Crypto
participant PDS
User->>CLI: opake pair request
CLI->>CLI: Verify no local identity exists
CLI->>Crypto: generate_ephemeral_keypair()
Crypto-->>CLI: { public_key, private_key }
CLI->>PDS: createRecord(pairRequest)<br/>{ ephemeralKey, algo: "x25519" }
PDS-->>CLI: { uri, cid }
CLI->>User: Fingerprint: a1:b2:c3:d4:e5:f6:g7:h8
CLI->>User: Run `opake pair approve` on existing device
loop Poll every 3s
CLI->>PDS: listRecords(pairResponse)
PDS-->>CLI: records[]
CLI->>CLI: Filter by response.request == our request URI
end
Note over CLI: Response found — see "Receive" below
The ephemeral private key stays in memory. The fingerprint (first 8 bytes of the public key, hex-encoded) is displayed for out-of-band verification.
Pair Approve (existing device)#
sequenceDiagram
participant User
participant CLI as CLI (existing device)
participant Crypto
participant PDS
User->>CLI: opake pair approve
CLI->>CLI: Load local identity
CLI->>PDS: listRecords(pairRequest)
PDS-->>CLI: Pending requests
CLI->>User: [1] 2026-03-06T14:00:00Z — fingerprint: a1:b2:c3:d4:...
User-->>CLI: 1
CLI->>Crypto: generate_content_key()
Crypto-->>CLI: K (256-bit AES key)
CLI->>CLI: Serialize identity → JSON bytes
CLI->>Crypto: encrypt_blob(K, identity_json)
Crypto-->>CLI: { ciphertext, nonce }
CLI->>Crypto: wrap_key(K, ephemeral_pubkey, did)
Crypto-->>CLI: wrappedKey
CLI->>PDS: createRecord(pairResponse)<br/>{ request, wrappedKey, ciphertext, nonce }
PDS-->>CLI: { uri, cid }
CLI->>User: Identity sent.
The identity payload includes the X25519 encryption keypair, Ed25519 signing keypair, and the DID — everything needed to operate as that account.
Receive (new device, after poll succeeds)#
sequenceDiagram
participant CLI as CLI (new device)
participant Crypto
participant PDS
Note over CLI: Poll found a matching pairResponse
CLI->>Crypto: unwrap_key(wrappedKey, ephemeral_private_key)
Crypto-->>CLI: K (content key)
CLI->>Crypto: decrypt_blob(K, ciphertext, nonce)
Crypto-->>CLI: identity JSON bytes
CLI->>CLI: Deserialize → Identity
CLI->>PDS: getRecord(publicKey/self)
PDS-->>CLI: Published public key
CLI->>CLI: Verify identity's public key == published key
CLI->>CLI: Save identity.json (0600)
CLI->>PDS: deleteRecord(pairRequest)
CLI->>PDS: deleteRecord(pairResponse)
CLI->>CLI: Pairing complete
The verification step guards against a corrupted or tampered response — the derived public key must match what's already published on the PDS.
Login Detection#
When opake login runs on a device without a local identity, it checks for an existing publicKey/self record on the PDS before generating a new keypair:
sequenceDiagram
participant CLI
participant PDS
CLI->>PDS: getRecord(publicKey/self)
alt No published key (new user)
CLI->>CLI: Generate identity, publish key
else Published key exists, no local identity
CLI->>CLI: Save session only
CLI->>CLI: Print: "Run opake pair request"
else Published key exists, local identity matches
CLI->>CLI: Proceed normally
end
This prevents accidental key overwrites that would break encryption on the existing device.
Security Properties#
- Ephemeral key exchange — the DH keypair exists only in memory during the pairing session. No long-term secret is exposed in the PDS records.
- Visual SAS — key fingerprints are displayed for comparison but not programmatically enforced. True zero-trust verification is a follow-up.
- Same encryption as documents — the identity payload uses AES-256-GCM + x25519-hkdf-a256kw, the same primitives as file encryption. No new crypto.
- Record cleanup — both pairing records are deleted after transfer. Stale request cleanup is tracked separately.