The PIN now surfaces in the host's web admin UI (port 3000 → Pairing), which is where users will actually read it — the pairing sheet's footer, field prompts, the tvOS keyboard title, and the wrong-PIN/failure errors all reference the console instead of the host log / --allow-pairing flag (the log mention stays in the README as the secondary path). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -146,7 +146,8 @@ signing, bundle id `io.unom.punktfunk`. Notes:
|
|||||||
7. **Trust — the full ceremony exists now (SPAKE2).** `generateIdentity()` once (persist
|
7. **Trust — the full ceremony exists now (SPAKE2).** `generateIdentity()` once (persist
|
||||||
both PEMs in the Keychain), then `pair(host:identity:pin:name:)` with the 4-digit PIN
|
both PEMs in the Keychain), then `pair(host:identity:pin:name:)` with the 4-digit PIN
|
||||||
the host prints when it ARMS pairing (`--allow-pairing`/`--require-pairing`; one PIN
|
the host prints when it ARMS pairing (`--allow-pairing`/`--require-pairing`; one PIN
|
||||||
per arming window, shown at startup — the user reads it before pairing). Returns the
|
per arming window, surfaced in the host's web console — port 3000 → Pairing — and
|
||||||
|
printed at startup; the user reads it before pairing). Returns the
|
||||||
host's VERIFIED fingerprint; persist it and pass `pinSHA256:` + `identity:` to every
|
host's VERIFIED fingerprint; persist it and pass `pinSHA256:` + `identity:` to every
|
||||||
connect. Pairing is a real PAKE: a wrong PIN gets ONE online guess (no offline
|
connect. Pairing is a real PAKE: a wrong PIN gets ONE online guess (no offline
|
||||||
dictionary attack), throwing `.wrongPIN`; a wrong-size pin throws `.invalidPin`. `PunktfunkClient` implements both flows:
|
dictionary attack), throwing `.wrongPIN`; a wrong-size pin throws `.invalidPin`. `PunktfunkClient` implements both flows:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// PIN pairing sheet. The host, started with --allow-pairing (or --require-pairing),
|
// PIN pairing sheet. The host shows the pairing PIN in its web console (port 3000 →
|
||||||
// prints a short PIN at startup ("PAIRING ARMED — enter this PIN on the client to
|
// Pairing; also printed in the host's log when armed via --allow-pairing); the user
|
||||||
// pair"); the user types it here. The ceremony is SPAKE2, so a wrong PIN buys an
|
// types it here. The ceremony is SPAKE2, so a wrong PIN buys an
|
||||||
// attacker exactly one online guess — for the user a typo just means "try again" (the
|
// attacker exactly one online guess — for the user a typo just means "try again" (the
|
||||||
// host rate-limits ceremonies to one per 2 s). Success returns the host's now-VERIFIED
|
// host rate-limits ceremonies to one per 2 s). Success returns the host's now-VERIFIED
|
||||||
// fingerprint: the caller pins it, no manual comparison needed, and the host stores this
|
// fingerprint: the caller pins it, no manual comparison needed, and the host stores this
|
||||||
@@ -44,15 +44,15 @@ struct PairSheet: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
VStack(spacing: 24) {
|
VStack(spacing: 24) {
|
||||||
Text("The host prints the PIN when pairing is armed "
|
Text("The PIN is shown in the host's web console "
|
||||||
+ "(--allow-pairing, \u{201C}PAIRING ARMED\u{201D} in its log). "
|
+ "(http://<host>:3000 → Pairing). "
|
||||||
+ "Pairing verifies both sides at once — no fingerprint comparison "
|
+ "Pairing verifies both sides at once — no fingerprint comparison "
|
||||||
+ "needed.")
|
+ "needed.")
|
||||||
.font(.callout)
|
.font(.callout)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
TVFieldRow(
|
TVFieldRow(
|
||||||
label: "PIN", value: pin, placeholder: "Shown in the host's log"
|
label: "PIN", value: pin, placeholder: "Shown in the host's web console"
|
||||||
) { editing = .pin }
|
) { editing = .pin }
|
||||||
TVFieldRow(
|
TVFieldRow(
|
||||||
label: "Device name", value: clientName, placeholder: "Apple TV"
|
label: "Device name", value: clientName, placeholder: "Apple TV"
|
||||||
@@ -83,7 +83,7 @@ struct PairSheet: View {
|
|||||||
switch field {
|
switch field {
|
||||||
case .pin:
|
case .pin:
|
||||||
TVTextEntry(
|
TVTextEntry(
|
||||||
title: "PIN (shown in the host's log)", text: pin,
|
title: "PIN (shown in the host's web console)", text: pin,
|
||||||
keyboardType: .numberPad
|
keyboardType: .numberPad
|
||||||
) {
|
) {
|
||||||
pin = $0.trimmingCharacters(in: .whitespaces)
|
pin = $0.trimmingCharacters(in: .whitespaces)
|
||||||
@@ -100,7 +100,9 @@ struct PairSheet: View {
|
|||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
Form {
|
Form {
|
||||||
Section {
|
Section {
|
||||||
TextField("PIN", text: $pin, prompt: Text("Shown in the host's log"))
|
TextField(
|
||||||
|
"PIN", text: $pin,
|
||||||
|
prompt: Text("Shown in the host's web console"))
|
||||||
.font(.system(.title3, design: .monospaced))
|
.font(.system(.title3, design: .monospaced))
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
.keyboardType(.numberPad)
|
.keyboardType(.numberPad)
|
||||||
@@ -115,8 +117,8 @@ struct PairSheet: View {
|
|||||||
Label("Pair with \(host.displayName)", systemImage: "lock.shield")
|
Label("Pair with \(host.displayName)", systemImage: "lock.shield")
|
||||||
.foregroundStyle(.tint)
|
.foregroundStyle(.tint)
|
||||||
} footer: {
|
} footer: {
|
||||||
Text("The host prints the PIN when pairing is armed "
|
Text("The PIN is shown in the host's web console "
|
||||||
+ "(--allow-pairing, \u{201C}PAIRING ARMED\u{201D} in its log). "
|
+ "(http://<host>:3000 → Pairing). "
|
||||||
+ "Pairing verifies both sides at once — no fingerprint "
|
+ "Pairing verifies both sides at once — no fingerprint "
|
||||||
+ "comparison needed.")
|
+ "comparison needed.")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
@@ -194,16 +196,16 @@ struct PairSheet: View {
|
|||||||
onPaired(fingerprint)
|
onPaired(fingerprint)
|
||||||
dismiss()
|
dismiss()
|
||||||
case .failure(PunktfunkClientError.wrongPIN):
|
case .failure(PunktfunkClientError.wrongPIN):
|
||||||
errorText = "Wrong PIN — check the host's \u{201C}PAIRING ARMED\u{201D} "
|
errorText = "Wrong PIN — check the host's web console (port 3000) "
|
||||||
+ "line and try again."
|
+ "and try again."
|
||||||
case .failure(is ClientIdentityStore.IdentityError):
|
case .failure(is ClientIdentityStore.IdentityError):
|
||||||
errorText = "Can't store this Mac's identity in the Keychain, so the "
|
errorText = "Can't store this Mac's identity in the Keychain, so the "
|
||||||
+ "pairing would not survive a relaunch. Unlock the login "
|
+ "pairing would not survive a relaunch. Unlock the login "
|
||||||
+ "keychain and try again."
|
+ "keychain and try again."
|
||||||
case .failure:
|
case .failure:
|
||||||
errorText = "Pairing failed. Is the host reachable, armed with "
|
errorText = "Pairing failed. Is the host reachable, pairing armed "
|
||||||
+ "--allow-pairing, and not mid-session? Retries are rate-limited "
|
+ "(web console → Pairing), and not mid-session? Retries are "
|
||||||
+ "to one per 2 seconds."
|
+ "rate-limited to one per 2 seconds."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user