LogTapJoinToken

class LogTapJoinToken(val code: String, val issuedAt: Instant, val ttl: Duration)

A short-lived join code that authorizes a debugger peer to pull the log tap.

The device shows this code — printed once to the platform log (Xcode console / logcat / stdout) or surfaced in an app debug UI — and the operator types it into the puller. The code itself never crosses the wire: the puller proves knowledge of it by returning HMAC-SHA256(code, nonce) to the device's challenge (see TokenGatedSeam).

The token is valid only within ttl of issuedAt, and is reusable for repeated pulls or reconnects inside that window (so a LogTapClient that reconnects re-admits seamlessly). Outside the window a proof is refused.

code is secret material: toString redacts it, and it must never be logged except at the single deliberate issuance print.

Entropy ↔ TTL: why the window is short (and must stay short)

The code carries about 2⁴⁰ of entropy (8 characters over a 32-symbol alphabet). On the plaintext (ws://) wire that this tap targets, the code's secrecy is bounded in time, not just in size. Two offline-cracking paths exist within the accepted honest-seam threat model:

  • a passive eavesdropper captures a Challenge nonce and the matching Proof tag and grinds candidate codes offline against HMAC-SHA256(candidate, nonce);

  • in the role-inverted topology (the prover hosts), an attacker sends the hosting prover a Challenge purely to harvest a HMAC(code, nonce) tag to grind.

On the eavesdropper path the nonce is the verifier's own fresh random value, so the grind cannot start until the tag is captured and the attacker only wins by cracking the code before the token expires — a recovered code is useless once isValid returns false. The short ttl is therefore a load-bearing security control on that path, not a UX knob: it is what keeps a ~2⁴⁰ offline search from being worthwhile.

The harvest path is weaker, and the ttl does not bound it (#1865). There the nonce is chosen by the attacker, who can pick a fixed value and precompute the whole ~2⁴⁰ table before the token is minted, making recovery on harvest a lookup. Enforcing the nonce's width (#1820) makes the challenge well-formed, not fresh, and does not change that. The only control on this path today is TokenGatedSeam's first-challenger binding, which makes the harvest a race the attacker must win rather than a certainty. Closing it properly needs prover-contributed freshness in the Proof frame — a wire change, tracked in #1865.

WARNING — do not raise DEFAULT_TTL. Widening the window trades directly against the code's entropy: every extra minute is extra offline-cracking budget on the plaintext wire. The 5-minute default is long enough to read and type the code and short enough to bound the search. If a longer-lived tap is genuinely needed, add entropy (a longer code) or move to an encrypted transport — never just lengthen the TTL.

Constructors

Link copied to clipboard
constructor(code: String, issuedAt: Instant, ttl: Duration)

Types

Link copied to clipboard
object Companion

Properties

Link copied to clipboard
Link copied to clipboard
Link copied to clipboard

Functions

Link copied to clipboard

True while now falls within [issuedAt] .. [issuedAt] + [ttl] (inclusive).

Link copied to clipboard
open override fun toString(): String

Redacted — never exposes code.