diff --git a/hive-ag3nt/assets/screen.html b/hive-ag3nt/assets/screen.html index 268e04be..31c1db13 100644 --- a/hive-ag3nt/assets/screen.html +++ b/hive-ag3nt/assets/screen.html @@ -189,87 +189,6 @@ canvas { display: block; cursor: default; } function u32be(b, o) { return ((b[o]<<24)|(b[o+1]<<16)|(b[o+2]<<8)|b[o+3])>>>0; } function u16be(b, o) { return ((b[o]<<8)|b[o+1])>>>0; } - // ── Apple-DH (security type 30) helpers ───────────────────────────────── - // Protocol (from neatvnc apple-dh.c): - // Server → client: generator(2) + key_size(2) + prime[key_size] + server_pub[key_size] - // Client → server: client_pub[key_size] + aes128ecb(MD5(shared_secret), creds[128]) - // After SecurityResult=0: normal plaintext VNC (no session encryption) - // - // BigInt mod-pow — handles 2048-bit DH arithmetic. - function modpow(base, exp, mod) { - let r = 1n; - base = base % mod; - while (exp > 0n) { - if (exp & 1n) r = r * base % mod; - exp >>= 1n; - base = base * base % mod; - } - return r; - } - function bytesToBigInt(b) { - let n = 0n; - for (const byte of b) n = (n << 8n) | BigInt(byte); - return n; - } - function bigIntToBytes(n, len) { - const out = new Uint8Array(len); - for (let i = len - 1; i >= 0; i--) { out[i] = Number(n & 0xffn); n >>= 8n; } - return out; - } - - // Compact MD5 — needed because Web Crypto doesn't expose MD5. - // Based on the RFC 1321 reference implementation, minified. - function md5(data) { - const b = data instanceof Uint8Array ? data : new Uint8Array(data); - const len = b.length; - // pad message - const padLen = ((len + 8) >>> 6 << 4) + 16; - const m = new Uint32Array(padLen); - for (let i = 0; i < len; i++) m[i>>2] |= b[i] << ((i&3)*8); - m[len>>2] |= 0x80 << ((len&3)*8); - m[padLen-2] = len*8; - const T = new Int32Array(64); - for (let i = 0; i < 64; i++) T[i] = (Math.abs(Math.sin(i+1)) * 0x100000000)|0; - let [a, b2, c, d] = [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476]; - const S = [7,12,17,22, 5,9,14,20, 4,11,16,23, 6,10,15,21]; - function add(x,y){return (x+y)|0;} - function r(v,s){return (v<>>(32-s));} - for (let i = 0; i < padLen; i += 16) { - let [aa,bb,cc,dd] = [a,b2,c,d]; - for (let j = 0; j < 64; j++) { - let [f, g] = j<16 ? [(bb&cc)|((~bb)&dd), j] - : j<32 ? [(dd&bb)|((~dd)&cc), (5*j+1)%16] - : j<48 ? [bb^cc^dd, (3*j+5)%16] - : [cc^(bb|(~dd)), (7*j)%16]; - f = add(add(aa, f), add(m[i+g], T[j])); - [aa,dd,cc,bb] = [dd, cc, bb, add(bb, r(f, S[(j%4)+((j>>2&3)*4)]))]; - } - [a,b2,c,d] = [add(a,aa), add(b2,bb), add(c,cc), add(d,dd)]; - } - const out = new Uint8Array(16); - [a,b2,c,d].forEach((x,i) => { - out[i*4]=(x)&0xff; out[i*4+1]=(x>>8)&0xff; - out[i*4+2]=(x>>16)&0xff; out[i*4+3]=(x>>24)&0xff; - }); - return out; - } - - // AES-128-ECB encrypt 128 bytes using Web Crypto AES-CBC (null IV per block = ECB). - async function aes128ecb(key16, data128) { - const keyObj = await crypto.subtle.importKey('raw', key16, {name:'AES-CBC'}, false, ['encrypt']); - const out = new Uint8Array(128); - const iv = new Uint8Array(16); // zeroed IV → ECB mode for single blocks - for (let i = 0; i < 8; i++) { - const enc = await crypto.subtle.encrypt({name:'AES-CBC',iv}, keyObj, data128.slice(i*16,(i+1)*16)); - out.set(new Uint8Array(enc,0,16), i*16); - } - return out; - } - - // Apple-DH state — stored between async continuations. - let appleDhState = null; - // ──────────────────────────────────────────────────────────────────────── - function processRfb() { while (true) { if (!tryStep()) break; @@ -302,22 +221,18 @@ canvas { display: block; cursor: default; } if (types.indexOf(1) !== -1) prefer = 1; // plain None else if (types.indexOf(19) !== -1) prefer = 19; // VeNCrypt else prefer = types[0]; - // Prefer: 1 (None) → 19 (VeNCrypt) → 30 (Apple-DH) - if (types.indexOf(1) !== -1) prefer = 1; - else if (types.indexOf(19) !== -1) prefer = 19; - else if (types.indexOf(30) !== -1) prefer = 30; - else { - dbg('no supported type in [' + Array.from(types).join(', ') + '] — need 1, 19, or 30', 'err'); + // Only handle known-safe types; reject everything else. + if (prefer !== 1 && prefer !== 19) { + dbg('no supported security type in [' + Array.from(types).join(', ') + '] — need 1 (None) or 19 (VeNCrypt)', 'err'); setStatus('unsupported security types: [' + Array.from(types).join(', ') + ']', 'error'); ws.close(); return false; } dbg('→ choosing security type ' + prefer + - (prefer === 1 ? ' (None)' : prefer === 19 ? ' (VeNCrypt)' : ' (Apple-DH)')); + (prefer === 1 ? ' (None)' : ' (VeNCrypt)')); send(new Uint8Array([prefer])); if (prefer === 1) state = 'security-result'; - else if (prefer === 19) state = 'vencrypt-version'; - else state = 'apple-dh-params'; + else state = 'vencrypt-version'; return true; } case 'security-vnc-challenge': { @@ -375,72 +290,6 @@ canvas { display: block; cursor: default; } state = 'security-result'; return true; } - // ── Apple-DH (type 30) ──────────────────────────────────────────── - // Server sends: generator(2 BE) + key_size(2 BE) + prime[key_size] + - // server_pub[key_size] - // Client sends: client_pub[key_size] + aes128ecb(MD5(shared), creds[128]) - // No session encryption after auth — plain RFB follows. - case 'apple-dh-params': { - const hdr = drainTo(4); - if (!hdr) return false; - const generator = u16be(hdr, 0); - const keySize = u16be(hdr, 2); - dbg('← Apple-DH: generator=' + generator + ' key_size=' + keySize); - const rest = drainTo(keySize * 2); - if (!rest) { chunks.unshift(hdr); totalBytes += 4; return false; } - const prime = rest.slice(0, keySize); - const serverPub = rest.slice(keySize); - dbg('← Apple-DH prime[0:4]=' + hex(prime.slice(0,4)) + - ' server_pub[0:4]=' + hex(serverPub.slice(0,4))); - - // Async DH computation — pause state machine, resume when done. - appleDhState = { generator, keySize, prime, serverPub }; - state = 'apple-dh-wait'; - (async () => { - try { - const p = bytesToBigInt(appleDhState.prime); - const g = BigInt(appleDhState.generator); - const ks = appleDhState.keySize; - - // Generate client private key: random ks bytes, then mod p - const privBytes = crypto.getRandomValues(new Uint8Array(ks)); - const priv = bytesToBigInt(privBytes) % p; - - // Client public key = g ^ priv mod p - const clientPub = modpow(g, priv, p); - const clientPubBytes = bigIntToBytes(clientPub, ks); - - // Shared secret = server_pub ^ priv mod p - const serverPubInt = bytesToBigInt(appleDhState.serverPub); - const shared = modpow(serverPubInt, priv, p); - const sharedBytes = bigIntToBytes(shared, ks); - - // AES key = MD5(shared_secret) - const aesKey = md5(sharedBytes); - dbg('Apple-DH: shared MD5 key=' + hex(aesKey)); - - // Credentials: 64 bytes username (zeroed) + 64 bytes password (zeroed) - const creds = new Uint8Array(128); // empty username + empty password - const encCreds = await aes128ecb(aesKey, creds); - - // Send: client_pub + encrypted_creds - const response = new Uint8Array(ks + 128); - response.set(clientPubBytes, 0); - response.set(encCreds, ks); - send(response); - dbg('→ Apple-DH response sent (' + response.length + ' bytes)', 'ok'); - - state = 'security-result'; - processRfb(); // resume state machine - } catch(e) { - setStatus('Apple-DH error: ' + e.message, 'error'); - } - })(); - return false; // pause — async resumes - } - case 'apple-dh-wait': - // Async handshake in progress — don't consume bytes - return false; // ────────────────────────────────────────────────────────────────── case 'security-result': { const b = drainTo(4); diff --git a/nix/templates/weston-vnc.nix b/nix/templates/weston-vnc.nix index e17b1515..d89a4913 100644 --- a/nix/templates/weston-vnc.nix +++ b/nix/templates/weston-vnc.nix @@ -57,6 +57,33 @@ }; config = lib.mkIf config.hyperhive.gui.enable { + # Build weston linked against a neatvnc without TLS/auth support. + # neatvnc ≥ 0.9 compiled with gnutls always advertises RSA-AES-256 + # (type 129), RSA-AES (type 5), and Apple-DH (type 30) security types + # regardless of the weston.ini auth-method setting — because + # nvnc_has_auth() returns true at the C level, causing weston to call + # nvnc_enable_auth() unconditionally. The in-browser RFB client has no + # RSA key and cannot complete these handshakes. + # + # The fix: pass `-Dtls=disabled` to neatvnc's meson build (the option + # name from neatvnc's meson_options.txt that guards gnutls + the entire + # auth module). With TLS disabled, nvnc_has_auth() returns false, weston + # skips nvnc_enable_auth(), and neatvnc advertises only type 1 (None). + # gnutls stays in buildInputs so pkg-config resolution doesn't error; + # the meson flag overrides the feature to "disabled" at configure time. + nixpkgs.overlays = [ + (_final: prev: { + weston = prev.weston.override { + neatvnc = prev.neatvnc.overrideAttrs (old: { + mesonFlags = (old.mesonFlags or []) ++ [ + "-Dtls=disabled" + "-Dnettle=disabled" + ]; + }); + }; + }) + ]; + systemd.services.weston = { description = "Weston Wayland compositor (VNC backend)"; after = [ "network.target" ]; @@ -104,17 +131,14 @@ ${pkgs.coreutils}/bin/printf '{"vnc_port":%d,"auth":"none"}\n' \ "$VNC_PORT" > /etc/hyperhive/gui.json || true - # neatvnc ≥ 0.9 advertises RSA-AES and Apple-DH security types - # when auth is compiled in. The browser client handles Apple-DH - # (type 30) with empty credentials (weston auth-method=none - # accepts any credentials). --disable-transport-layer-security - # prevents the VeNCrypt TLS wrapper over the VNC-over-WebSocket - # relay; plain auth types are used directly. + # neatvnc is built without gnutls (see nixpkgs.overlays above), + # so nvnc_has_auth() returns false and weston skips auth setup + # entirely — neatvnc advertises only security type 1 (None). + # No weston.ini or --disable-transport-layer-security needed. exec ${pkgs.weston}/bin/weston \ --backend=vnc-backend.so \ --renderer=pixman \ - --port="$VNC_PORT" \ - --disable-transport-layer-security + --port="$VNC_PORT" ''; Restart = "on-failure"; RestartSec = "5s";