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