eleven / ocho Public

Clone
git clone https://amadan.net/eleven/ocho

Plain git — no account needed to clone.

Download

Download this file

1// πŸ€– Validation of the restore page's #fragment payload β€” pure and
2// dependency-free so it's unit-testable in Node
3// (home/test/restore-payload.test.mjs). Three callers ask this page to run the
4// passkey ceremony, distinguished by shape:
5//
6// { ret, nonce, ephPub } an instance page: gets back ONE entry's
7// credentials, matched by ret's origin
8// { app: 1, nonce, ephPub } the native app: gets the home session,
9// returned via the FIXED custom scheme
10// { cli: 1, nonce, ephPub, port? } the eleven CLI: gets the home session,
11// returned via a loopback redirect when
12// port is set, else shown as a copy-paste
13// code (a terminal SSH'd into another box
14// can't receive a local redirect)
15//
16// Every shape carries an ephemeral public key: whatever comes back is
17// ECIES-wrapped to it, so secrets are never plaintext in a URL or on screen.
18
19// A loopback listener needs an unprivileged, real port. The CLI binds
20// 127.0.0.1:0 and reports what it got.
21export function validCLIPort(port) {
22 return Number.isInteger(port) && port >= 1024 && port <= 65535;
23}
24
25// The custom scheme an app handoff returns on. The plain scheme is the
26// shipped app; a dev alt registers a suffixed one (elevenmessenger.alt-<slug>
27// β€” ios/project.yml), and without naming itself in the payload its own
28// sign-in return would be delivered to whichever app owns the PLAIN scheme
29// (the release app, which claims and drops it). Constrained to our prefix;
30// the security stance is unchanged either way β€” the returned session is
31// ECIES-wrapped to the initiator's ephemeral key and nonce-bound, so a
32// crafted payload naming some other installed build delivers only
33// undecryptable bytes to an app that never asked.
34export function validReturnScheme(s) {
35 return typeof s === "string" && /^elevenmessenger(\.alt-[a-z0-9-]{1,32})?$/.test(s);
36}
37
38// validRestorePayload returns the payload if it's one of the three shapes
39// above (with only the expected fields trusted), else null.
40export function validRestorePayload(p) {
41 if (!p || typeof p.nonce !== "string" || typeof p.ephPub !== "string") return null;
42 if (p.app === 1) {
43 const out = { app: 1, nonce: p.nonce, ephPub: p.ephPub };
44 if (validReturnScheme(p.scheme)) out.scheme = p.scheme; // else: plain-app fallback
45 return out;
46 }
47 if (p.cli === 1) {
48 if (p.port !== undefined && !validCLIPort(p.port)) return null;
49 return { cli: 1, nonce: p.nonce, ephPub: p.ephPub, port: p.port };
50 }
51 if (typeof p.ret !== "string") return null;
52 try {
53 new URL(p.ret); // must parse β€” it's the redirect target
54 } catch {
55 return null;
56 }
57 return { ret: p.ret, nonce: p.nonce, ephPub: p.ephPub };
58}
59
60// cliReturnURL builds the loopback redirect for a CLI handoff. The host is
61// HARDCODED to 127.0.0.1 (like the app path's custom scheme): a crafted
62// payload can vary only the port, so the sealed session can only ever be
63// delivered to a listener on the user's own machine β€” and it's ciphertext to
64// everyone but the process holding the ephemeral private key.
65export function cliReturnURL(port, backB64) {
66 if (!validCLIPort(port)) throw new Error("bad CLI port");
67 // backB64 is base64url β€” URL-safe by construction, no encoding needed.
68 return `http://127.0.0.1:${port}/cb?d=${backB64}`;
69}
70