// 🤖 Validation of the restore page's #fragment payload — pure and // dependency-free so it's unit-testable in Node // (home/test/restore-payload.test.mjs). Three callers ask this page to run the // passkey ceremony, distinguished by shape: // // { ret, nonce, ephPub } an instance page: gets back ONE entry's // credentials, matched by ret's origin // { app: 1, nonce, ephPub } the native app: gets the home session, // returned via the FIXED custom scheme // { cli: 1, nonce, ephPub, port? } the eleven CLI: gets the home session, // returned via a loopback redirect when // port is set, else shown as a copy-paste // code (a terminal SSH'd into another box // can't receive a local redirect) // // Every shape carries an ephemeral public key: whatever comes back is // ECIES-wrapped to it, so secrets are never plaintext in a URL or on screen. // A loopback listener needs an unprivileged, real port. The CLI binds // 127.0.0.1:0 and reports what it got. export function validCLIPort(port) { return Number.isInteger(port) && port >= 1024 && port <= 65535; } // The custom scheme an app handoff returns on. The plain scheme is the // shipped app; a dev alt registers a suffixed one (elevenmessenger.alt- // — ios/project.yml), and without naming itself in the payload its own // sign-in return would be delivered to whichever app owns the PLAIN scheme // (the release app, which claims and drops it). Constrained to our prefix; // the security stance is unchanged either way — the returned session is // ECIES-wrapped to the initiator's ephemeral key and nonce-bound, so a // crafted payload naming some other installed build delivers only // undecryptable bytes to an app that never asked. export function validReturnScheme(s) { return typeof s === "string" && /^elevenmessenger(\.alt-[a-z0-9-]{1,32})?$/.test(s); } // validRestorePayload returns the payload if it's one of the three shapes // above (with only the expected fields trusted), else null. export function validRestorePayload(p) { if (!p || typeof p.nonce !== "string" || typeof p.ephPub !== "string") return null; if (p.app === 1) { const out = { app: 1, nonce: p.nonce, ephPub: p.ephPub }; if (validReturnScheme(p.scheme)) out.scheme = p.scheme; // else: plain-app fallback return out; } if (p.cli === 1) { if (p.port !== undefined && !validCLIPort(p.port)) return null; return { cli: 1, nonce: p.nonce, ephPub: p.ephPub, port: p.port }; } if (typeof p.ret !== "string") return null; try { new URL(p.ret); // must parse — it's the redirect target } catch { return null; } return { ret: p.ret, nonce: p.nonce, ephPub: p.ephPub }; } // cliReturnURL builds the loopback redirect for a CLI handoff. The host is // HARDCODED to 127.0.0.1 (like the app path's custom scheme): a crafted // payload can vary only the port, so the sealed session can only ever be // delivered to a listener on the user's own machine — and it's ciphertext to // everyone but the process holding the ephemeral private key. export function cliReturnURL(port, backB64) { if (!validCLIPort(port)) throw new Error("bad CLI port"); // backB64 is base64url — URL-safe by construction, no encoding needed. return `http://127.0.0.1:${port}/cb?d=${backB64}`; }