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";