pairlink 0.2.0 copy "pairlink: ^0.2.0" to clipboard
pairlink: ^0.2.0 copied to clipboard

Local-network pairing between two phones over TCP: an out-of-band secret, a nonce-bound HMAC handshake, replay rejection, heartbeats and takeover. Pure Dart.

pairlink #

CI License: MIT

Local-network pairing between two phones, in pure Dart. One phone listens in a wall socket, the other connects and drives reconnection; a secret shared out of band proves possession over a nonce-bound HMAC handshake, and nothing secret ever travels the wire.

PROTOCOL.md is the normative specification. Every INVARIANT (tag) in it is anchored to a symbol in lib/ or test/, and test/contract_anchors_test.dart fails when one loses its anchor.

Roles #

Phone Class Does
Outlet, stationary in the socket OutletSession listens, sends power events, refuses strangers
Panel, carried around PanelSession connects, reconnects, drives the heartbeat

Two ways to share the secret #

  • QR code (primary): the outlet phone shows a PairInvite, 128 random bits plus its own address. No discovery, nothing guessable.
  • Four-digit code (fallback): the panel phone shows a code, the user types it on the outlet phone, and the outlet phone advertises over mDNS. Weaker by design, and PairStrength says so all the way up to the UI. Guessing is capped by AttemptLimiter.

Usage #

const identity = PairIdentity(brand: 'myapp', serviceType: '_myapp._tcp', scheme: 'myapp');

// Outlet phone: generate the secret, show the invite, accept the peer.
final secret = PairSecret.generate();
final outlet = OutletSession(identity: identity, secret: secret);
final invite = PairInvite(identity: identity, endpoint: endpoint, secret: secret);
outlet.events.listen((event) { /* OutletPaired, OutletPeerLeft, ... */ });
await outlet.attach(SocketTransport(socket));

// Panel phone: scan, connect, listen for power events.
final scanned = PairInvite.tryParse(qrText, identity: identity)!;
final panel = PanelSession(identity: identity, secret: scanned.secret);
panel.events.listen((event) { /* PanelPowerEvent, PanelDisconnected, ... */ });
await panel.attach(SocketTransport(await Socket.connect(scanned.endpoint.host, scanned.endpoint.port)));

PairIdentity.brand is bound into every session key, so two applications on this protocol never share a key space; serviceType is what the platform's mDNS responder advertises; scheme is the QR payload's URI scheme. Discovery itself is the application's job: implement PairAdvertiser and PairDiscovery over the platform responder (NsdManager, Bonjour).

Testing #

dart test. The suite includes test/skeptic/, an adversarial peer implemented independently of the production code; extend hostile.dart when probing, never the code under test. The package runs under plain dart test on Linux as well as macOS and Windows, which matters: a peer that vanishes reports done on one and an error on another, and the attempt budget must not be spent by either.

Install #

dependencies:
  pairlink:
    git:
      url: https://github.com/zs-dima/pairlink.git
      ref: v0.1.0

Changelog #

CHANGELOG.md

License #

MIT

1
likes
160
points
--
downloads

Documentation

API reference

Publisher

verified publisherdmitrii.app

Weekly Downloads

Local-network pairing between two phones over TCP: an out-of-band secret, a nonce-bound HMAC handshake, replay rejection, heartbeats and takeover. Pure Dart.

Repository (GitHub)
View/report issues

Topics

#pairing #lan #tcp #hmac #mdns

License

MIT (license)

Dependencies

crypto, meta

More

Packages that depend on pairlink