code wiki / (root) / nx_connect_e2e_client.nx

nx_connect_e2e_client.nx source

↩ module page · 86 lines · 11847 B

1// nx_connect_e2e_client.nx -- the FULL-E2E CONNECT client: key AGREEMENT + sealed send + decrypting 2// inbox, all client-side, all sovereign crypto compiled to WASM. This is the page that escapes the 3// zero-JS security ceiling AND closes the two rungs the 2026-07-23 honest grade left open: 4// (a) the conversation key is no longer typed -- it is AGREED via X25519 (RFC 7748; the shipped wasm 5// is KAT-proven in nx_wasm_vm: nx_x25519_wasm_vm_gate + nx_connect_keyx_vm_gate 5/5 DH agreement); 6// (b) the recipient has a DECRYPT view -- the inbox fetches ciphertext routed by the server's shared 7// content-blind plane and decrypts it in the browser with the same agreed key. 8// The cipher is sovereign ChaCha20 (nx_media_crypt_wasm; RFC 8439 bit-exact in-VM, gate 3/3). The JS is 9// the ACCEPTED minimum last-mile: instantiate wasm + marshal bytes + DOM. ALL crypto is in the wasm. 10// 11// HONEST SCOPE (stated on the page too): the name->pubkey directory is first-come/latest-wins with NO 12// identity binding yet (classic TOFU gap; OPAQUE-bound identities = the next rung), and private keys 13// live in this browser's localStorage (per-name, so one browser can demo both ends). 14// nx_connect_e2e_client <chacha.wasm> <x25519.wasm> <out.html> 15// ce_emit_html(b64c, b64clen, b64x, b64xlen, dst) is the pure emitter (gate-testable without real wasm). 16// license_tier: ORIGINAL 17import "nx_syscalls.nx" 18import "nx_base64.nx" 19const K_MAGIC_524288: i64 = 524288 20 21func ce_puts(dst: *u8, off: i64, s: *u8) -> i64 { var o: i64=off; var i: i64=0; while s[i]!=(0 as u8){dst[o]=s[i]; o=o+1; i=i+1} return o } 22func ce_putb(dst: *u8, off: i64, b: *u8, n: i64) -> i64 { var i: i64=0; while i<n { dst[off+i]=b[i]; i=i+1 } return off+n } 23 24// emit the full client HTML, embedding BOTH base64 wasms. Returns byte length. PURE (no syscalls). 25func ce_emit_html(b64c: *u8, b64clen: i64, b64x: *u8, b64xlen: i64, dst: *u8) -> i64 { 26 var o: i64 = 0 27 o = ce_puts(dst, o, "<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><title>Connect &mdash; end-to-end (X25519 agreed keys, client-side sovereign crypto)</title>" as *u8) 28 o = ce_puts(dst, o, "<style>body{font-family:system-ui,sans-serif;max-width:680px;margin:4vh auto;padding:0 20px;background:#0b0e15;color:#e8ecf5}h1{font-size:1.4rem}h2{font-size:1.05rem;margin-top:28px;border-top:1px solid #252d40;padding-top:18px}textarea,input{width:100%;box-sizing:border-box;background:#141a29;color:#e8ecf5;border:1px solid #252d40;border-radius:8px;padding:10px;font:inherit;margin:6px 0}button{background:#7aa2ff;color:#0b0e15;border:0;border-radius:8px;padding:10px 18px;font-weight:700;cursor:pointer;margin:4px 6px 4px 0}.mono{font-family:ui-monospace,monospace;font-size:.85rem;color:#7aa2ff;word-break:break-all}.note{color:#98a2b8;font-size:.85rem}.ok{color:#7dd8a0}.bad{color:#ff9d9d}ul{padding-left:18px}li{margin:8px 0}.who{color:#98a2b8;font-size:.8rem}</style></head><body>" as *u8) 29 o = ce_puts(dst, o, "<main id=\"main\"><h1>Connect &mdash; end-to-end</h1><p class=\"note\">Keys are <b>agreed, never typed and never sent</b>: your browser generates an X25519 keypair (RFC 7748, sovereign wasm, KAT-proven), publishes only the <i>public</i> half, and derives a shared conversation key with your peer. Messages are sealed with sovereign ChaCha20 (RFC 8439, bit-exact) <b>before</b> they leave this page. The server stores names, public keys and ciphertext &mdash; it can never read a word.</p>" as *u8) 30 o = ce_puts(dst, o, "<h2>1 &middot; Your identity</h2><label>Your name (a&ndash;z, 0&ndash;9, _)</label><input id=\"me\" value=\"\" placeholder=\"alice\"><button id=\"mkid\">Create / load identity &amp; publish public key</button><p id=\"ido\" class=\"mono\">(no identity yet)</p><p class=\"note\">The private key never leaves this browser (localStorage, per name). The directory is first-come/latest-wins with no identity binding yet &mdash; that is a stated TOFU gap, not a hidden one; OPAQUE-bound identities are the next rung.</p>" as *u8) 31 o = ce_puts(dst, o, "<h2>2 &middot; Send sealed</h2><label>To (their name)</label><input id=\"peer\" value=\"\" placeholder=\"bob\"><label>Message (max 120 chars)</label><textarea id=\"m\" rows=\"3\" maxlength=\"120\">meet me at the library saturday</textarea><button id=\"send\">Agree key &amp; send ciphertext</button><p id=\"out\" class=\"mono\">(ciphertext appears here)</p>" as *u8) 32 o = ce_puts(dst, o, "<h2>3 &middot; Your sealed inbox &mdash; decrypted here, never on the server</h2><button id=\"rx\">Fetch &amp; decrypt my messages</button><ul id=\"inbox\"></ul><p class=\"note\">Each message arrives as ciphertext plus the sender&rsquo;s public key; this page re-derives the same X25519 shared key and decrypts locally. The server&rsquo;s copy stays opaque forever &mdash; see it at <a href=\"/connect/secure\">/connect/secure</a>.</p>" as *u8) 33 o = ce_puts(dst, o, "<script>\nconst BC='" as *u8) 34 o = ce_putb(dst, o, b64c, b64clen) 35 o = ce_puts(dst, o, "';\nconst BX='" as *u8) 36 o = ce_putb(dst, o, b64x, b64xlen) 37 o = ce_puts(dst, o, "';\n" as *u8) 38 o = ce_puts(dst, o, "const ub=s=>Uint8Array.from(atob(s),c=>c.charCodeAt(0));\n" as *u8) 39 o = ce_puts(dst, o, "const hx=b=>{let s='';for(let i=0;i<b.length;i++)s+=b[i].toString(16).padStart(2,'0');return s;};\n" as *u8) 40 o = ce_puts(dst, o, "const uh=s=>{const b=new Uint8Array(s.length/2);for(let i=0;i<b.length;i++)b[i]=parseInt(s.substr(i*2,2),16);return b;};\n" as *u8) 41 o = ce_puts(dst, o, "Promise.all([WebAssembly.instantiate(ub(BC)),WebAssembly.instantiate(ub(BX))]).then(([rc,rx])=>{\n" as *u8) 42 o = ce_puts(dst, o, "const exC=rc.instance.exports,exX=rx.instance.exports;\n" as *u8) 43 o = ce_puts(dst, o, "const memC=()=>new Uint8Array(exC.memory.buffer),memX=()=>new Uint8Array(exX.memory.buffer);\n" as *u8) 44 // X25519: scalar@0, point@64, scratch@256 (same layout the VM gates prove), out@2048 45 o = ce_puts(dst, o, "function x25519(scalar,point){const M=memX();M.set(scalar,0);M.set(point,64);exX.nx_x25519_scalarmult(0n,64n,256n,2048n);return memX().slice(2048,2080);}\n" as *u8) 46 o = ce_puts(dst, o, "const BASE9=new Uint8Array(32);BASE9[0]=9;\n" as *u8) 47 o = ce_puts(dst, o, "const nameOk=s=>/^[a-z0-9_]{1,24}$/.test(s);\n" as *u8) 48 o = ce_puts(dst, o, "function myName(){return document.getElementById('me').value.trim().toLowerCase();}\n" as *u8) 49 o = ce_puts(dst, o, "function myPriv(n){let h=localStorage.getItem('nxe2e_priv_'+n);if(!h){const r=new Uint8Array(32);crypto.getRandomValues(r);h=hx(r);localStorage.setItem('nxe2e_priv_'+n,h);}return uh(h);}\n" as *u8) 50 // ChaCha20 seal/open: key from 64-hex shared secret, nonce from seed 'connect1' + per-message seq 51 o = ce_puts(dst, o, "function ccrypt(keyHex,seq,data){const M=memC();const kOff=256,nOff=320,sOff=384,dOff=512;for(let i=0;i<64;i++)M[sOff+i]=keyHex.charCodeAt(i);exC.mcw_key(BigInt(sOff),64n,BigInt(kOff));const sd='connect1';for(let i=0;i<8;i++)M[sOff+i]=sd.charCodeAt(i);exC.mcw_nonce(BigInt(sOff),BigInt(seq),BigInt(nOff));memC().set(data,dOff);exC.mcw_xform(BigInt(kOff),BigInt(nOff),0n,BigInt(dOff),BigInt(data.length));return memC().slice(dOff,dOff+data.length);}\n" as *u8) 52 o = ce_puts(dst, o, "function sharedKey(privBytes,peerPubHex){return hx(x25519(privBytes,uh(peerPubHex)));}\n" as *u8) 53 // identity: keygen + publish public half 54 o = ce_puts(dst, o, "document.getElementById('mkid').onclick=async function(){const n=myName();const el=document.getElementById('ido');if(!nameOk(n)){el.textContent='name must be a-z 0-9 _ (1..24)';el.className='mono bad';return;}const pub=hx(x25519(myPriv(n),BASE9));const r=await fetch('/connect/pubkey',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:'n='+n+'&p='+pub});const j=await r.json();el.className=j.ok?'mono ok':'mono bad';el.textContent=(j.ok?'identity live. your PUBLIC key (the only half that ever leaves): ':'directory refused: r=')+(j.ok?pub:j.r);};\n" as *u8) 55 // send: fetch peer pub -> agree -> seal -> POST ciphertext only 56 o = ce_puts(dst, o, "document.getElementById('send').onclick=async function(){const n=myName(),to=document.getElementById('peer').value.trim().toLowerCase();const el=document.getElementById('out');if(!nameOk(n)||!nameOk(to)){el.textContent='set your identity (1) and a valid peer name first';el.className='mono bad';return;}const kr=await fetch('/connect/pubkeys');const kj=await kr.json();const peer=kj.keys.find(k=>k.n===to);if(!peer){el.textContent='no published key for \\''+to+'\\' -- they must create their identity first (a message nobody can decrypt is refused)';el.className='mono bad';return;}const key=sharedKey(myPriv(n),peer.p);const seq=Date.now();const pt=new TextEncoder().encode(document.getElementById('m').value);const ct=hx(ccrypt(key,seq,pt));const r=await fetch('/connect/seal',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:'from='+n+'&to='+to+'&seq='+seq+'&ct='+ct});const j=await r.json();el.className=j.ok?'mono ok':'mono bad';el.textContent=j.ok?('sealed & routed. only this ciphertext left your browser: '+ct):('server refused: r='+j.r);};\n" as *u8) 57 // inbox: fetch routed ciphertext -> re-derive the SAME shared key -> decrypt locally 58 o = ce_puts(dst, o, "document.getElementById('rx').onclick=async function(){const n=myName();const ul=document.getElementById('inbox');ul.textContent='';if(!nameOk(n)){const li=document.createElement('li');li.textContent='set your identity (1) first';li.className='bad';ul.appendChild(li);return;}const r=await fetch('/connect/sealed?u='+n);const j=await r.json();if(!j.msgs.length){const li=document.createElement('li');li.className='note';li.textContent='no sealed messages for '+n+' yet.';ul.appendChild(li);return;}const priv=myPriv(n);for(const m of j.msgs){const li=document.createElement('li');if(!m.p){li.textContent='(sender key unpublished -- cannot derive)';li.className='bad';ul.appendChild(li);continue;}const key=sharedKey(priv,m.p);const pt=ccrypt(key,m.s,uh(m.ct));const who=document.createElement('div');who.className='who';who.textContent='from '+m.f+' -- wire carried only: '+m.ct.slice(0,48)+(m.ct.length>48?'...':'');const tx=document.createElement('div');tx.textContent=new TextDecoder().decode(pt);li.appendChild(who);li.appendChild(tx);ul.appendChild(li);}};\n" as *u8) 59 o = ce_puts(dst, o, "}).catch(e=>{document.getElementById('ido').textContent='wasm load error: '+e.message;});\n</script></main></body></html>\n" as *u8) 60 return o 61} 62 63func main(argc: i64, argv: *i64) -> i64 { 64 if argc < 4 { sys_write(2, "usage: nx_connect_e2e_client <chacha.wasm> <x25519.wasm> <out.html>\n" as *u8, 68); return 2 } 65 let cpath: *u8 = argv[1] as *u8 66 let xpath: *u8 = argv[2] as *u8 67 let hpath: *u8 = argv[3] as *u8 68 let clen_p: *i64 = sys_mmap(8) as *i64 69 let cbytes: *u8 = sys_read_file(cpath, clen_p) 70 if (cbytes as i64) == 0 { sys_write(2, "read chacha wasm failed\n" as *u8, 24); return 3 } 71 let xlen_p: *i64 = sys_mmap(8) as *i64 72 let xbytes: *u8 = sys_read_file(xpath, xlen_p) 73 if (xbytes as i64) == 0 { sys_write(2, "read x25519 wasm failed\n" as *u8, 24); return 3 } 74 let b64c: *u8 = sys_mmap(clen_p[0]*2 + 64) 75 let b64clen: i64 = b64_encode(cbytes, clen_p[0], b64c) 76 let b64x: *u8 = sys_mmap(xlen_p[0]*2 + 64) 77 let b64xlen: i64 = b64_encode(xbytes, xlen_p[0], b64x) 78 let dst: *u8 = sys_mmap(K_MAGIC_524288) 79 let n: i64 = ce_emit_html(b64c, b64clen, b64x, b64xlen, dst) 80 let fd: i64 = sys_openat_wr(hpath, 0x1a4) 81 if fd < 0 { sys_write(2, "open html failed\n" as *u8, 17); return 4 } 82 sys_write(fd, dst, n) 83 sys_close(fd) 84 sys_write(1, "wrote FULL-E2E connect client html\n" as *u8, 35) 85 return 0 86}