pair

package
v0.5.2-rc.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 1, 2026 License: Apache-2.0 Imports: 23 Imported by: 0

Documentation

Overview

Package pair is how two machines come to trust each other without ssh, and the encrypted tunnel they talk through afterwards.

THE WHOLE STORY IN SIX LINES:

big-machine$ codeaf serve
  this machine is reachable as  otter-lamp-42
  pair a new device with code   715 302   (valid 10 minutes)

laptop$ codeaf chat --at otter-lamp-42
  pairing with otter-lamp-42 — enter the code shown there: ______
  paired. this laptop is now a key to otter-lamp-42.

After that, `codeaf chat --at otter-lamp-42` from that laptop just opens.

── the three facts this package is built on ────────────────────────────────

FIRST: THE RELAY BROKERS THE INTRODUCTION AND IS NEVER TRUSTED WITH IT. The pairing runs a PAKE — a password-authenticated key exchange — over the six digits shown on the engine machine's own screen. Somebody in the middle who does not know those digits cannot learn the shared key, cannot guess at it offline, and gets exactly ONE guess per attempt, which the engine machine counts and cuts off. That is the property a plain shared secret over a brokered channel would not have, and it is why the code can be six digits instead of a paragraph.

SECOND: THE PAIRING PINS KEYS, AND EVERY LATER CONNECTION IS BETWEEN PINNED KEYS. The PAKE is used once, to carry two long-term public keys past the relay safely. From then on the surface dials with a Noise handshake against the key it pinned, and the machine answers only devices whose key is in its own book. The code is never used again and cannot be replayed.

THIRD: A PAIRED DEVICE IS A HAND ON THAT MACHINE'S TOOLS. It is not a read-only window and it is not a login to a website. It opens conversations on that machine, runs the tools that machine's gate allows, and spends that machine's key — the same weight as an ssh key, and the pairing screen says so in those words before anybody types a code.

── what is NOT here, said plainly ──────────────────────────────────────────

THE DEVICE KEY IS A FILE, NOT A KEYCHAIN ENTRY, AND THERE IS NO TOUCH ID. The design for this lane puts the device key in the OS keychain where there is one, so that the platform can demand a fingerprint before releasing it and codeaf never sees a biometric. That is the right design and it is not built. What is built is Keeper — the seam it goes behind — with one implementation: a file with owner-only permissions under the codeaf home directory. Every person-facing sentence in this package says "a file on this machine" because that is what it is, and the manual page says the same. A CAPABILITY THAT CANNOT WORK IS ABSENT, NOT BROKEN.

Index

Constants

View Source
const CodeAttempts = 5

CodeAttempts is how many wrong codes a single code will absorb before it is thrown away.

IT IS THE OTHER HALF OF WHAT MAKES SIX DIGITS ENOUGH. A PAKE gives an attacker one guess per exchange and no way to test a guess offline; a small cap on exchanges is what turns "one guess at a time" into "five guesses, ever". A million codes and five guesses is the whole of the arithmetic.

View Source
const CodeValidFor = 10 * time.Minute

CodeValidFor is how long a pairing code shown by `codeaf serve` is good for. It is quoted in the person-facing line, and there is exactly one of it.

View Source
const HandshakeWithin = 30 * time.Second

HandshakeWithin bounds every exchange in this package. A connection that has arrived but not finished proving who it is holds a slot, and a slot held for ever is the cheapest denial of service there is.

View Source
const PairingWeight = "" /* 187-byte string literal not displayed */

PairingWeight is what a person reads BEFORE they type a code. It is the whole security posture of this feature in one sentence, in a person's words.

A PAIRED DEVICE IS A HAND ON THAT MACHINE'S TOOLS. Saying so at the moment of the decision is not a disclaimer; it is the only moment at which the sentence can do any good.

View Source
const RelayEnv = "CODEAF_RELAY"

RelayEnv names the override, exported so that a sentence and the code that reads it spell it the same way.

Variables

View Source
var (
	// ErrNoRelay is this machine having no relay address at all.
	ErrNoRelay = errors.New("pair: no relay is configured")
	// ErrNotPaired is this device never having been let in to that machine.
	ErrNotPaired = errors.New("pair: this device is not paired with that machine")
	// ErrWrongCode is the pairing code not matching.
	ErrWrongCode = errors.New("pair: that pairing code is not the one shown there")
	// ErrNotThatMachine is a completed dial to something that does not hold
	// the key this device pinned.
	ErrNotThatMachine = errors.New("pair: that is not the machine this device paired with")
	// ErrNoAnswer is a handshake that got no reply at all.
	ErrNoAnswer = errors.New("pair: that machine did not answer the handshake")
)

The facts, so a caller can tell them apart before phrasing them.

Functions

func Busy

func Busy() error

Busy is the relay turning connections away.

func DeviceKeyPath

func DeviceKeyPath() string

DeviceKeyPath is the file the device key is in.

func DevicesList

func DevicesList(name string, paired []Paired, keeper Keeper, now time.Time) string

DevicesList is what `codeaf devices` prints on the machine that owns the work.

func Dir

func Dir() string

Dir is where everything this package writes lives, moved wholesale by CODEAF_HOME like the rest of codeaf's state.

func Lines

func Lines(name string, code *Code) string

Lines is what `codeaf serve` prints, exactly.

The wording and the spacing are the design's own, and the validity is interpolated from CodeValidFor rather than typed, because a number that appears in two places drifts.

func MachinesList

func MachinesList(known []Known, now time.Time) string

MachinesList is what `codeaf devices` prints about the machines THIS device can reach. It is the other half of the same command, because a person asking "what am I paired with" means both directions and should not have to know there are two books.

func NoAnswer

func NoAnswer(name string) error

NoAnswer is the other shape the same protection takes.

A MACHINE THAT IS NOT THE ONE THIS DEVICE PAIRED WITH CANNOT EVEN READ THE FIRST MESSAGE, so it does not answer at all — and neither does a connection that dropped. The two are one event from this side and this sentence covers both truthfully, because the fact that matters is the same either way: nothing of the conversation left this device.

func NoRelay

func NoRelay(name string) error

NoRelay is the first sentence: nothing is configured.

func NotConnected

func NotConnected(name string) error

NotConnected is the third: the relay is fine and that machine is not there.

func NotPaired

func NotPaired(name string) error

NotPaired is the fourth: this device has never been let in.

func NotThatMachine

func NotThatMachine(name string) error

NotThatMachine is the pinned key not matching. It is the sentence that says the safety property held: the connection stopped rather than opening.

func PairedLine

func PairedLine(name string) string

PairedLine is what a person reads when it worked.

func PairingPreamble

func PairingPreamble(name string) string

PairingPreamble names what is about to happen.

func PairingPrompt

func PairingPrompt(name string) string

PairingPrompt is the line the code is typed after.

func ReadCode

func ReadCode(typed string) (string, error)

ReadCode takes what a person typed and answers the six digits, or says what is wrong with it.

SPACES AND DASHES ARE THROWN AWAY, because a code read aloud gets written down with whichever separator the listener prefers and neither of them is wrong.

func Relay

func Relay() string

Relay is the address this machine uses when nothing else says otherwise. Empty means none is set up.

func RelayFile

func RelayFile() string

RelayFile is where a relay address is kept when somebody wants it to outlive a shell.

func RelayFor

func RelayFor(known Known, found bool) string

RelayFor is the address to reach one machine through: the override, then the relay that machine was paired through, then the configured one.

func RevokedLine

func RevokedLine(label string) string

RevokedLine is what a person reads when a device has been stopped.

func Stopped

func Stopped() string

Stopped is what a machine says to a device it no longer lets in. It travels inside the encryption, so it is this machine's own words and not the relay's.

func ThisMachineLabel

func ThisMachineLabel() string

ThisMachineLabel is what this device calls itself when it pairs. It is the host name, because that is the word a person already uses for their laptop, and it is only ever a label — a connection is checked against a key.

func Unreachable

func Unreachable(service string) error

Unreachable is the second: something is configured and it is not answering.

func WrongCode

func WrongCode(name string) error

WrongCode is the pairing code being wrong, said with the fact that makes it worth trying again.

Types

type Book

type Book struct {
	// contains filtered or unexported fields
}

Book is one of the two files, held against the disk.

func BookAt

func BookAt(path string) *Book

BookAt is either book, put somewhere else — which is what tests want and nothing in the product does.

func DeviceBook

func DeviceBook() *Book

DeviceBook is what this machine remembers about the devices it lets in.

func MachineBook

func MachineBook() *Book

MachineBook is what this device remembers about the machines it can reach.

func (*Book) Admit

func (b *Book) Admit(one Paired) error

Admit writes a device into this machine's book. A device that pairs twice keeps its first Since, because that is when this machine first let it in.

func (*Book) Allows

func (b *Book) Allows(publicKey []byte) (Paired, bool, error)

Allows says whether a key may open a conversation on this machine, and is the one question the connection path asks of this book.

func (*Book) Devices

func (b *Book) Devices() ([]Paired, error)

Devices is every device this machine lets in, oldest first.

func (*Book) Machine

func (b *Book) Machine(name string) (Known, bool, error)

Machine finds one by name.

func (*Book) Machines

func (b *Book) Machines() ([]Known, error)

Machines is every machine this device is paired with, oldest first.

func (*Book) Remember

func (b *Book) Remember(one Known) error

Remember writes a machine into this device's book, replacing an older pairing with the same name.

A RE-PAIRING REPLACES THE KEY, and that is not a hole: getting here means the person read a fresh code off that machine's own screen and the PAKE agreed. Refusing to replace would mean a machine that was rebuilt could never be reached again from a device that remembered its old key.

func (*Book) Revoke

func (b *Book) Revoke(label string) (Paired, error)

Revoke takes a device out of this machine's book. It matches on the label a person can see, and refuses an ambiguous one rather than guessing which of two laptops called `laptop` was meant.

func (*Book) RevokeAll

func (b *Book) RevokeAll(label string) (int, error)

RevokeAll takes every device with a label out, which is the honest answer to two laptops with the same host name.

func (*Book) Touch

func (b *Book) Touch(publicKey []byte, when time.Time)

Touch records that a device connected. It is best-effort: a connection is not refused because a note about it could not be written.

type Code

type Code struct {
	// contains filtered or unexported fields
}

Code is one live pairing code.

func NewCode

func NewCode(now time.Time) (*Code, error)

NewCode mints one. The digits come from crypto/rand and not from the clock, a counter, or anything else a watcher could follow.

func (*Code) Shown

func (c *Code) Shown() string

Shown is the code as a person reads it off the screen.

type Desk

type Desk struct {
	// Now is the clock, swapped by tests.
	Now func() time.Time
	// contains filtered or unexported fields
}

Desk is the engine machine's live pairing code: minting it, showing it, spending its attempts, and throwing it away.

THERE IS ONE CODE AT A TIME. Two live codes would mean two independent guess budgets against the same machine, and a person only ever reads one number off the screen anyway.

func (*Desk) Offer

func (d *Desk) Offer() (*Code, error)

Offer is the code to show right now, minting a fresh one when the last has expired or been spent.

func (*Desk) Retire

func (d *Desk) Retire()

Retire throws the current code away — what a successful pairing does, so that one code pairs one device.

func (*Desk) Spend

func (d *Desk) Spend() (string, error)

Spend takes one attempt off the live code and answers what the exchange should be run against. A code with nothing left is gone, and the sentence says so rather than letting somebody keep guessing.

type Device

type Device struct {
	// contains filtered or unexported fields
}

Device is this machine's identity on the relay.

func ThisDevice

func ThisDevice(keeper Keeper) (Device, error)

ThisDevice is this machine's key, made on first use.

MAKING ONE IS NOT AN EVENT A PERSON IS TOLD ABOUT, because it is not a decision they made: the first time anything needs this machine's name, the key that name comes from has to exist. What they are told about is the name, which is the part that means something.

func (Device) Name

func (d Device) Name() string

Name is what this machine would be reachable as, which is a fact about the key and not about whether anything is running.

func (Device) Private

func (d Device) Private() *ecdh.PrivateKey

Private is the key the relay's registration challenge is answered with.

func (Device) Public

func (d Device) Public() []byte

Public is the 32 bytes the other end pins.

type FileKeeper

type FileKeeper struct{ Path string }

FileKeeper keeps the key in a file only its owner can read.

func (FileKeeper) Forget

func (f FileKeeper) Forget() error

func (FileKeeper) Load

func (f FileKeeper) Load() ([]byte, bool, error)

func (FileKeeper) Save

func (f FileKeeper) Save(seed []byte) error

func (FileKeeper) Where

func (f FileKeeper) Where() string

Where is the sentence a person reads. It says a file because it is a file.

type Host

type Host struct {
	// Service is the relay's address.
	Service string
	// Device is this machine's key.
	Device Device
	// Devices is the book of devices this machine lets in.
	Devices *Book
	// Desk holds the live pairing code.
	Desk *Desk
	// Say is where a person-facing line goes — the name, the code, and one
	// line per device that arrives or is turned away. Nil says nothing.
	Say func(string)
	// Open is handed a plaintext pipe for each connection that was let in, and
	// owns it from then on: it runs the conversation and closes the pipe. THE
	// TUNNEL IS AN io.ReadWriteCloser AND NOTHING MORE, which is what lets the
	// caller hand it straight to internal/remote.
	Open func(io.ReadWriteCloser)
	// Now is the clock, swapped by tests.
	Now func() time.Time
}

Host is the machine side.

func (*Host) Run

func (h *Host) Run(ctx context.Context) error

Run holds the registration until the context is cancelled, redialling when the connection to the relay is lost.

A LOST RELAY IS NOT A LOST MACHINE. Wifi drops, a relay is restarted, a NAT forgets — none of those are reasons for the conversations on this machine to end, so this loop backs off and comes back rather than returning. It returns only when the context ends, or when the relay refuses in a way that trying again cannot fix.

type Keeper

type Keeper interface {
	// Where is the person-facing answer to "where is that key kept", in a
	// person's words rather than a path.
	Where() string
	// Load answers the key, or false when this machine has never made one.
	Load() ([]byte, bool, error)
	// Save writes a newly made key.
	Save(seed []byte) error
	// Forget removes it. The machine loses its name and every pairing that
	// named it, which is why nothing calls this except a person who asked.
	Forget() error
}

Keeper is where a device key lives. It is an interface because THIS IS THE SEAM THE OS KEYCHAIN GOES BEHIND — on a Mac the key should be a keychain item whose release the platform can gate on Touch ID, so that a fingerprint unlocks a connection and codeaf never sees a biometric.

THAT IS NOT BUILT. The only implementation in this build is FileKeeper, and OpenKeeper returns it on every platform. Nothing in this package pretends otherwise: Keeper.Where is the sentence a person is shown, and today it always says the key is a file.

func OpenKeeper

func OpenKeeper() Keeper

OpenKeeper is where this build keeps a device key.

IT IS THE FILE KEEPER ON EVERY PLATFORM, INCLUDING MACS. See Keeper.

type Known

type Known struct {
	// Name is what a person types after --at.
	Name string `json:"name"`
	// Key is the machine's long-term public key, base64 raw-url. THIS IS THE
	// PINNED THING: a connection is to this key, and the name is only how the
	// relay finds it.
	Key string `json:"key"`
	// Service is the relay this machine was paired through, so a device that
	// has used two relays reaches each machine through the right one.
	Service string `json:"service"`
	// Since is when the pairing happened.
	Since time.Time `json:"since"`
}

Known is one machine this device has paired with, as the surface remembers it.

func Machines

func Machines() ([]Known, error)

Machines is every machine this device can reach, for `codeaf devices` on a surface and for a door that wants to say which names it knows.

type Paired

type Paired struct {
	// Label is what the device called itself when it paired — its host name.
	// It is a convenience for the person reading `codeaf devices` and is
	// NEVER what a connection is checked against.
	Label string `json:"label"`
	// Key is the device's long-term public key, base64 raw-url. This is what a
	// connection is checked against.
	Key string `json:"key"`
	// Since is when it paired, and Seen is when it last opened a connection.
	Since time.Time `json:"since"`
	Seen  time.Time `json:"seen,omitempty"`
}

Paired is one device this machine has let in, as the engine remembers it.

type Reach

type Reach struct {
	// Name is what the person typed after --at.
	Name string
	// Device is this device's key.
	Device Device
	// Machines is this device's book of machines it has paired with.
	Machines *Book
	// Label is what this device calls itself; see [ThisMachineLabel].
	Label string
	// AskCode is asked for the pairing code when this device is not yet paired
	// with the machine. NIL MEANS THIS DOOR CANNOT ASK — a headless run, a
	// script — and an unpaired machine is then refused with [NotPaired]
	// rather than hanging on a prompt nobody is there to answer.
	AskCode func(name string) (string, error)
	// Say is where the pairing's own lines go. Nil says nothing.
	Say func(string)
	// Now is the clock, swapped by tests.
	Now func() time.Time
}

Reach is one attempt to open a connection to a named machine.

func (Reach) Open

func (r Reach) Open(ctx context.Context) (*Tunnel, error)

Open answers a live tunnel to the machine, pairing first if it has to.

EVERY ROAD OUT OF HERE IS ONE SENTENCE NAMING WHAT IS WRONG. There are four ways this fails that a person cannot tell apart by looking, and errors.go keeps one sentence for each.

type Tunnel

type Tunnel struct {
	// contains filtered or unexported fields
}

Tunnel is one encrypted connection.

func (*Tunnel) Close

func (t *Tunnel) Close() error

func (*Tunnel) Peer

func (t *Tunnel) Peer() []byte

Peer is the key on the other end, proved by the handshake.

func (*Tunnel) Read

func (t *Tunnel) Read(b []byte) (int, error)

Read hands back plaintext. A record arrives whole and is drained across as many Reads as the caller needs, which is what makes this an ordinary stream rather than a message queue.

func (*Tunnel) Write

func (t *Tunnel) Write(b []byte) (int, error)

Write encrypts and sends. A write longer than one record is cut up, because the caller above knows nothing about records and must not have to.

Directories

Path Synopsis
Package cpace implements the CPace password authenticated key exchange (PAKE) instantiated with the ristretto255 group.
Package cpace implements the CPace password authenticated key exchange (PAKE) instantiated with the ristretto255 group.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL