Skip to content

Pairing ​

ts
import { PairSetup, PairVerify } from '@corewire/core/pairing';

AirPlay and Companion Link pair with the HomeKit Accessory Protocol (HAP): pair-setup with SRP and a PIN (M1 to M6), and pair-verify with Curve25519 on every connect (M1 to M4). @corewire/core/pairing implements both sides and knows no protocol. A protocol hands it a PairingTransport that carries the TLV8 payloads.

You only need this page when you add a protocol or build a test peer. pair() and connect() of the protocol packages run pairing for you.

The transport ​

A PairingTransport is a function that sends one payload and resolves the reply. step says which message it is (m1, m3 or m5), for protocols that frame each step differently.

ts
import type { PairingTransport } from '@corewire/core/pairing';

declare function post(path: string, body: Buffer): Promise<Buffer>;

const transport: PairingTransport = async payload => await post('/pair-setup', payload);

Pair-setup ​

ts
import { PairSetup, type PairingTransport } from '@corewire/core/pairing';
import { Context } from '@corewire/core/transport';

declare const transport: PairingTransport;

const setup = new PairSetup(new Context('Living-Room.local'), transport);

await setup.start();
const credentials = await setup.pin(async () => await askForPin());

console.log(credentials.accessoryIdentifier);

start() generates the controller's long-term Ed25519 key pair. pin(askPin) runs M1 to M6 and calls askPin between M1 and M2, when the device shows its PIN. It resolves AccessoryCredentials with the accessory identifier and long-term public key, plus this controller's pairing id and key pair. M5 carries the controller name that the device lists with its pairings. That is the name of the context identity, or CoreWire without one.

The steps are public too (m1() to m6()) for a caller that has to wait for the PIN between M1 and M2 across calls, as PinPairing does.

transient() runs M1 to M4 with the fixed transient PIN (TRANSIENT_PAIRING_PIN) and resolves AccessoryKeys that hold for this connection only. A HomePod accepts it.

PairingOptions.useAes switches the encrypted sub-TLVs to AES-128-CTR for legacy devices.

Pair-verify ​

ts
import { PairVerify, type AccessoryCredentials, type PairingTransport } from '@corewire/core/pairing';
import { Context } from '@corewire/core/transport';

declare const transport: PairingTransport;
declare const credentials: AccessoryCredentials;

const verify = new PairVerify(new Context('Living-Room.local'), transport);
const keys = await verify.start(credentials);

console.log(keys.sharedSecret.byteLength);

start(credentials) resolves AccessoryKeys with the shared secret. Each protocol derives its own channel keys from it with HKDF. It throws AuthenticationError when the accessory's identity does not match the credentials. deriveControlKeys(sharedSecret) derives the HAP control channel keys.

The accessory side ​

PairSetupServer and PairVerifyServer answer pair-setup and pair-verify as an accessory would. generateAccessoryIdentity() creates an identity for them. Use them to test a controller, or to build a peer that sits between a controller and a device.

ts
import { generateAccessoryIdentity, PairSetup, PairSetupServer } from '@corewire/core/pairing';
import { Context } from '@corewire/core/transport';

const context = new Context('test');
const identity = generateAccessoryIdentity();
const server = new PairSetupServer(context, identity, '1234');

const client = new PairSetup(context, payload => server.handle(payload));
await client.start();

const credentials = await client.pin(async () => '1234');
console.log(credentials.accessoryIdentifier === identity.identifier, server.controller?.identifier);

handle(request) takes one request payload and resolves the reply. PairVerifyServer takes a ControllerResolver that maps a pairing id to the controller's long-term public key.

CoreWire is released under the MIT license.