diff --git a/hive-ag3nt/assets/screen.html b/hive-ag3nt/assets/screen.html index 31c1db1..268e04b 100644 --- a/hive-ag3nt/assets/screen.html +++ b/hive-ag3nt/assets/screen.html @@ -189,6 +189,87 @@ 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; @@ -221,18 +302,22 @@ 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]; - // 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'); + // 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'); setStatus('unsupported security types: [' + Array.from(types).join(', ') + ']', 'error'); ws.close(); return false; } dbg('→ choosing security type ' + prefer + - (prefer === 1 ? ' (None)' : ' (VeNCrypt)')); + (prefer === 1 ? ' (None)' : prefer === 19 ? ' (VeNCrypt)' : ' (Apple-DH)')); send(new Uint8Array([prefer])); if (prefer === 1) state = 'security-result'; - else state = 'vencrypt-version'; + else if (prefer === 19) state = 'vencrypt-version'; + else state = 'apple-dh-params'; return true; } case 'security-vnc-challenge': { @@ -290,6 +375,72 @@ 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);