tinyhumansai/openhuman · error · Error

[tunnel] no device keypair

Error message

[tunnel] no device keypair

What it means

TunnelTransport.sendHandshake needs the device's long-term keypair to prove identity during the XChaCha20-Poly1305 handshake. This fires when connect() reached the handshake stage without a keypair — none was supplied to the constructor (the manager passes profile.devicePrivkey) or it was never generated/loaded.

Source

Thrown at app/src/services/transport/TunnelTransport.ts:157

        this.clientEphemeralKeypair = null;
        this.cipher = null;
        this._connectPromise = null;
      });

      socket.on('connect_error', (err: Error) => {
        logErr('[tunnel] connect_error %s', err.message);
        reject(err);
        this._connectPromise = null;
      });
    });

    return this._connectPromise;
  }

  // -- handshake -------------------------------------------------------------

  private async sendHandshake(): Promise<void> {
    if (!this.deviceKeypair) throw new Error('[tunnel] no device keypair');

    const corePubkey = base64urlDecode(this.corePubkeyB64);
    const devicePubkeyB64 = base64urlEncode(this.deviceKeypair.publicKey);
    const clientEphemeral = generateKeypair();
    const clientEphemeralPubkeyB64 = base64urlEncode(clientEphemeral.publicKey);

    const payload = new TextEncoder().encode(
      JSON.stringify({
        device_pubkey: devicePubkeyB64,
        client_ephemeral_pubkey: clientEphemeralPubkeyB64,
      })
    );

    // Seal the handshake payload to the core's public key.
    const handshakeFrame = sealHandshake(corePubkey, payload);
    const frameB64 = base64urlEncode(handshakeFrame);

    log('[tunnel] sending sealed handshake frame_len=%d', handshakeFrame.length);

View on GitHub (pinned to a221052e0d)

Solutions

  1. Complete device pairing so devicePrivkey is stored with the profile (keyring/security-devices flow)
  2. When constructing TunnelTransport manually, generate/load the device keypair first and pass it in
  3. Refuse to build a kind 'tunnel' profile without devicePrivkey at validation time

Example fix

// before
const t = new TunnelTransport(url, channelId, corePubkey, token, undefined, 'pairing');
await t.connect();

// after
const devicePrivkey = await loadOrCreateDeviceKey(); // Uint8Array from keyring
const t = new TunnelTransport(url, channelId, corePubkey, token, devicePrivkey, 'pairing');
await t.connect();
Defensive patterns

Strategy: validation

Validate before calling

function tunnelProfileHasDeviceKey(p: ConnectionProfile): boolean {
  return p.kind !== 'tunnel' || !!p.devicePrivkey;
}

Prevention

When it happens

Trigger: Constructing TunnelTransport from a tunnel profile lacking devicePrivkey and calling connect(); or a code path invoking sendHandshake before device key generation completed (e.g. test harness passing only token arguments).

Common situations: Tunnel profile persisted without the device private key (pairing never fully completed); tests building TunnelTransport directly with positional args and omitting the key.

Related errors


AI-assisted analysis of tinyhumansai/openhuman@a221052e0d (2026-08-16). Data as JSON: /api/errors/efd86df7c999fa33. Report an issue: GitHub.