code wiki / (root) / nx_connect_device_client.nx

nx_connect_device_client.nx source

↩ module page · 111 lines · 14989 B

1// nx_connect_device_client.nx -- CONNECT, PRIVACY-FIRST / DATA-ON-THE-ENDPOINT. The evolution of the E2E 2// client (nx_connect_e2e_client): the user's PRIVATE data -- their contact list and their whole message 3// history -- lives in THIS BROWSER, encrypted at rest, and NEVER on our server. Our server only ever sees 4// two things: a user's PUBLIC key (public by definition) and CIPHERTEXT in transit to a named recipient. 5// There is nothing private on our systems to sniff, subpoena, or leak. This is the Berners-Lee/Solid model 6// (you own your data) made concrete with our own sovereign crypto. 7// 8// HOW (all crypto is our own wasm, KAT-proven; no third-party libraries, no new wat chain): 9// - Identity: an X25519 keypair per handle; the PRIVATE half stays in localStorage, never sent (RFC 7748, 10// nx_x25519.wasm; DH agreement proven in nx_connect_keyx_vm_gate 5/5). 11// - Device vault: {contacts, messages} serialised to JSON and sealed with sovereign ChaCha20 (RFC 8439, 12// nx_media_crypt.wasm 3/3) under a VAULT KEY derived on-device from the private identity key, with a 13// fresh random nonce each save (no keystream reuse). The vault ciphertext lives in localStorage -- so 14// even the browser's own storage-at-rest holds only ciphertext; only the running page (with the key 15// derived from the private key) can read it. 16// - Messaging: a per-conversation key is AGREED via X25519 with the peer's published key; the message is 17// ChaCha20-sealed and only the ciphertext is POSTed for delivery; on sync, fetched ciphertext is 18// decrypted in-browser and the PLAINTEXT is written into the device vault (encrypted at rest), never 19// back to the server. 20// 21// HONEST SCOPE (stated on the page): the delivery relay still holds in-transit ciphertext until it is 22// fetched (delete-on-delivery + a sender-held outbox are the next rungs); the pubkey directory is TOFU 23// (identity binding is a separate rung); localStorage is the store (IndexedDB/OPFS is the scale rung). 24// nx_connect_device_client <chacha.wasm> <x25519.wasm> <out.html> 25// cd_emit_html(b64c,b64clen,b64x,b64xlen,dst) is the pure emitter (gate-testable without real wasm). 26// license_tier: ORIGINAL 27import "nx_syscalls.nx" 28import "nx_base64.nx" 29const K_MAGIC_524288: i64 = 524288 30 31func cd_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 } 32func cd_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 } 33 34func cd_emit_html(b64c: *u8, b64clen: i64, b64x: *u8, b64xlen: i64, dst: *u8) -> i64 { 35 var o: i64 = 0 36 o = cd_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; your data lives on your device</title>" as *u8) 37 o = cd_puts(dst, o, "<style>body{font-family:system-ui,sans-serif;max-width:700px;margin:4vh auto;padding:0 20px;background:#0b0e15;color:#e8ecf5}h1{font-size:1.4rem}h2{font-size:1.05rem;margin-top:26px;border-top:1px solid #252d40;padding-top:16px}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:9px 16px;font-weight:700;cursor:pointer;margin:4px 6px 4px 0}.mono{font-family:ui-monospace,monospace;font-size:.82rem;color:#7aa2ff;word-break:break-all}.note{color:#98a2b8;font-size:.85rem}.ok{color:#7dd8a0}.bad{color:#ff9d9d}ul{padding-left:0;list-style:none}li{margin:8px 0;padding:10px;border:1px solid #252d40;border-radius:8px}.who{color:#98a2b8;font-size:.8rem}.pill{display:inline-block;font-size:.72rem;font-weight:700;padding:2px 8px;border-radius:99px;background:#16351f;color:#7dd8a0}.pill.no{background:#3a1720;color:#ff9d9d}</style></head><body>" as *u8) 38 o = cd_puts(dst, o, "<main id=\"main\"><h1>Connect &mdash; on your device</h1><p class=\"note\">Your <b>contacts and your entire message history live in this browser</b>, encrypted with a key only this device holds. Our server never receives them &mdash; it only ever sees your <i>public</i> key and ciphertext in transit. There is nothing private on our systems to sniff.</p>" as *u8) 39 o = cd_puts(dst, o, "<h2>1 &middot; This device&rsquo;s identity</h2><label>Your handle (a&ndash;z, 0&ndash;9, _)</label><input id=\"me\" value=\"\" placeholder=\"alice\"><button id=\"mkid\">Open this device</button><p id=\"ido\" class=\"mono\">(no identity yet)</p>" as *u8) 40 o = cd_puts(dst, o, "<h2>2 &middot; Contacts (stored only here)</h2><label>Add by handle</label><input id=\"peer\" value=\"\" placeholder=\"bob\"><button id=\"addc\">Add contact</button><ul id=\"contacts\"></ul>" as *u8) 41 o = cd_puts(dst, o, "<h2>3 &middot; Send &amp; sync</h2><label>To</label><input id=\"to\" value=\"\" placeholder=\"bob\"><label>Message</label><textarea id=\"m\" rows=\"2\" maxlength=\"120\">meet me at the library saturday</textarea><button id=\"send\">Seal &amp; send</button><button id=\"sync\">Sync my inbox</button><p id=\"out\" class=\"mono\"></p>" as *u8) 42 o = cd_puts(dst, o, "<h2>4 &middot; Conversations (decrypted only here)</h2><ul id=\"log\"></ul></main>" as *u8) 43 o = cd_puts(dst, o, "<script>\nconst BC='" as *u8) 44 o = cd_putb(dst, o, b64c, b64clen) 45 o = cd_puts(dst, o, "';\nconst BX='" as *u8) 46 o = cd_putb(dst, o, b64x, b64xlen) 47 o = cd_puts(dst, o, "';\n" as *u8) 48 o = cd_puts(dst, o, "const ub=s=>Uint8Array.from(atob(s),c=>c.charCodeAt(0));\n" as *u8) 49 o = cd_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) 50 o = cd_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) 51 o = cd_puts(dst, o, "Promise.all([WebAssembly.instantiate(ub(BC)),WebAssembly.instantiate(ub(BX))]).then(([rc,rx])=>{\n" as *u8) 52 o = cd_puts(dst, o, "const exC=rc.instance.exports,exX=rx.instance.exports;\n" as *u8) 53 o = cd_puts(dst, o, "const memC=()=>new Uint8Array(exC.memory.buffer),memX=()=>new Uint8Array(exX.memory.buffer);\n" as *u8) 54 o = cd_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) 55 o = cd_puts(dst, o, "const BASE9=new Uint8Array(32);BASE9[0]=9;\n" as *u8) 56 o = cd_puts(dst, o, "const nameOk=s=>/^[a-z0-9_]{1,24}$/.test(s);\n" as *u8) 57 o = cd_puts(dst, o, "let ME='',PRIV=null,VKEYSET=false;\n" as *u8) 58 // ChaCha20 with an explicit key (from hex) + explicit 12-byte nonce buffer. 59 o = cd_puts(dst, o, "function cc(keyHex,nonce,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));M.set(nonce,nOff);M.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) 60 // the DEVICE VAULT KEY is derived on-device from the private identity key (never leaves this browser). 61 o = cd_puts(dst, o, "function vkeyHex(){return hx(PRIV);}\n" as *u8) // 64 hex chars of the 32-byte priv; cc() re-derives a 32-byte key from it 62 // vault = one localStorage blob per handle: randnonce_hex ':' ciphertext_hex of JSON({contacts,messages}). 63 o = cd_puts(dst, o, "function vaultSave(v){const pt=new TextEncoder().encode(JSON.stringify(v));const nonce=new Uint8Array(12);crypto.getRandomValues(nonce);const ct=cc(vkeyHex(),nonce,pt);localStorage.setItem('nxvault_'+ME,hx(nonce)+':'+hx(ct));}\n" as *u8) 64 o = cd_puts(dst, o, "function vaultLoad(){const raw=localStorage.getItem('nxvault_'+ME);if(!raw)return{contacts:[],messages:[]};const p=raw.split(':');const pt=cc(vkeyHex(),uh(p[0]),uh(p[1]));try{return JSON.parse(new TextDecoder().decode(pt));}catch(e){return{contacts:[],messages:[]};}}\n" as *u8) 65 o = cd_puts(dst, o, "function convKey(peerPubHex){return hx(x25519(PRIV,uh(peerPubHex)));}\n" as *u8) 66 // ccSeq = the WIRE cipher (agreed key + mcw_nonce(seed='connect1',seq)) -- both ends derive the same nonce 67 // from the sequence number, so a peer decrypts what we seal. Separate from cc() which uses an explicit 68 // random nonce for the at-rest device vault (no keystream reuse across saves). 69 o = cd_puts(dst, o, "function ccSeq(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));M.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) 70 o = cd_puts(dst, o, "const $=id=>document.getElementById(id);\n" as *u8) 71 // OPEN THIS DEVICE: load/generate identity, publish PUBLIC key only, load the device vault, render. 72 o = cd_puts(dst, o, "$('mkid').onclick=async function(){const n=$('me').value.trim().toLowerCase();const el=$('ido');if(!nameOk(n)){el.textContent='handle must be a-z 0-9 _ (1..24)';el.className='mono bad';return;}ME=n;let h=localStorage.getItem('nxid_priv_'+n);if(!h){const r=new Uint8Array(32);crypto.getRandomValues(r);h=hx(r);localStorage.setItem('nxid_priv_'+n,h);}PRIV=uh(h);const pub=hx(x25519(PRIV,BASE9));await fetch('/connect/pubkey',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:'n='+n+'&p='+pub});el.className='mono ok';el.textContent='device open for @'+n+' \\u2014 your data is loaded from THIS browser. public key (the only half that ever leaves): '+pub;render();};\n" as *u8) 73 // ADD CONTACT: fetch the public directory, store {handle,pub} in the DEVICE VAULT. Nothing is sent to us. 74 o = cd_puts(dst, o, "$('addc').onclick=async function(){if(!ME){return;}const to=$('peer').value.trim().toLowerCase();if(!nameOk(to))return;const kr=await fetch('/connect/pubkeys');const kj=await kr.json();const peer=kj.keys.find(k=>k.n===to);if(!peer){$('out').textContent='no published key for '+to+' yet';$('out').className='mono bad';return;}const v=vaultLoad();if(!v.contacts.find(c=>c.h===to))v.contacts.push({h:to,p:peer.p});vaultSave(v);render();};\n" as *u8) 75 // SEND: agree key with the contact, seal, POST ciphertext only, and record the sent message in the vault. 76 o = cd_puts(dst, o, "$('send').onclick=async function(){if(!ME)return;const to=$('to').value.trim().toLowerCase();const v=vaultLoad();const c=v.contacts.find(x=>x.h===to);if(!c){$('out').textContent='add '+to+' as a contact first';$('out').className='mono bad';return;}const key=convKey(c.p);const seq=Date.now()>>>0;const pt=new TextEncoder().encode($('m').value);const ct=hx(ccSeq(key,seq,pt));await fetch('/connect/seal',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:'from='+ME+'&to='+to+'&seq='+seq+'&ct='+ct});v.messages.push({peer:to,dir:'out',seq:seq,text:$('m').value});vaultSave(v);$('out').className='mono ok';$('out').textContent='sealed & sent. only this ciphertext left your device: '+ct;render();};\n" as *u8) 77 // SYNC: fetch ciphertext addressed to me, decrypt in-browser, write PLAINTEXT into the device vault only. 78 o = cd_puts(dst, o, "$('sync').onclick=async function(){if(!ME)return;const r=await fetch('/connect/sealed?u='+ME);const j=await r.json();const v=vaultLoad();let added=0;const acks=[];for(const m of j.msgs){if(!m.p)continue;if(v.messages.find(x=>x.dir==='in'&&x.peer===m.f&&x.seq===m.s)){acks.push(m);continue;}const key=convKey(m.p);const pt=new TextDecoder().decode(ccSeq(key,m.s,uh(m.ct)));v.messages.push({peer:m.f,dir:'in',seq:m.s,text:pt});if(!v.contacts.find(c=>c.h===m.f))v.contacts.push({h:m.f,p:m.p});acks.push(m);added++;}vaultSave(v);" as *u8) 79 // DELETE-ON-DELIVERY: now that the vault holds each message, ACK it so the server tombstones it and 80 // retains nothing servable. The device vault is the durable copy from here on. 81 o = cd_puts(dst, o, "for(const m of acks){await fetch('/connect/ack',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:'from='+m.f+'&to='+ME+'&seq='+m.s});}$('out').className='mono ok';$('out').textContent='synced '+added+' new message(s) into this device\\u2019s vault; the server was told to delete its copies (delete-on-delivery)';render();};\n" as *u8) 82 // RENDER: everything shown is read + decrypted from the DEVICE VAULT (localStorage), never from a server view. 83 o = cd_puts(dst, o, "function render(){if(!ME)return;const v=vaultLoad();const cu=$('contacts');cu.textContent='';for(const c of v.contacts){const li=document.createElement('li');const nm=document.createElement('b');nm.textContent='@'+c.h;const pill=document.createElement('span');pill.className='pill';pill.textContent='e2e key';li.appendChild(nm);li.appendChild(document.createTextNode(' '));li.appendChild(pill);cu.appendChild(li);}const lg=$('log');lg.textContent='';const ms=v.messages.slice().sort((a,b)=>a.seq-b.seq);for(const m of ms){const li=document.createElement('li');const w=document.createElement('div');w.className='who';w.textContent=(m.dir==='out'?'you \\u2192 @'+m.peer:'@'+m.peer+' \\u2192 you');const tx=document.createElement('div');tx.textContent=m.text;li.appendChild(w);li.appendChild(tx);lg.appendChild(li);}}\n" as *u8) 84 o = cd_puts(dst, o, "}).catch(e=>{$('ido').textContent='wasm load error: '+e.message;$('ido').className='mono bad';});\n</script></body></html>\n" as *u8) 85 return o 86} 87 88func main(argc: i64, argv: *i64) -> i64 { 89 if argc < 4 { sys_write(2, "usage: nx_connect_device_client <chacha.wasm> <x25519.wasm> <out.html>\n" as *u8, 70); return 2 } 90 let cpath: *u8 = argv[1] as *u8 91 let xpath: *u8 = argv[2] as *u8 92 let hpath: *u8 = argv[3] as *u8 93 let clen_p: *i64 = sys_mmap(8) as *i64 94 let cbytes: *u8 = sys_read_file(cpath, clen_p) 95 if (cbytes as i64) == 0 { sys_write(2, "read chacha wasm failed\n" as *u8, 24); return 3 } 96 let xlen_p: *i64 = sys_mmap(8) as *i64 97 let xbytes: *u8 = sys_read_file(xpath, xlen_p) 98 if (xbytes as i64) == 0 { sys_write(2, "read x25519 wasm failed\n" as *u8, 24); return 3 } 99 let b64c: *u8 = sys_mmap(clen_p[0]*2 + 64) 100 let b64clen: i64 = b64_encode(cbytes, clen_p[0], b64c) 101 let b64x: *u8 = sys_mmap(xlen_p[0]*2 + 64) 102 let b64xlen: i64 = b64_encode(xbytes, xlen_p[0], b64x) 103 let dst: *u8 = sys_mmap(K_MAGIC_524288) 104 let n: i64 = cd_emit_html(b64c, b64clen, b64x, b64xlen, dst) 105 let fd: i64 = sys_openat_wr(hpath, 0x1a4) 106 if fd < 0 { sys_write(2, "open html failed\n" as *u8, 17); return 4 } 107 sys_write(fd, dst, n) 108 sys_close(fd) 109 sys_write(1, "wrote device-first connect client html\n" as *u8, 39) 110 return 0 111}