screen: implement Apple-DH (type 30) auth for neatvnc 0.9 compatibility
This commit is contained in:
parent
fd433d2406
commit
3224178d2d
1 changed files with 156 additions and 5 deletions
|
|
@ -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 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; }
|
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<<s)|(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() {
|
function processRfb() {
|
||||||
while (true) {
|
while (true) {
|
||||||
if (!tryStep()) break;
|
if (!tryStep()) break;
|
||||||
|
|
@ -221,18 +302,22 @@ canvas { display: block; cursor: default; }
|
||||||
if (types.indexOf(1) !== -1) prefer = 1; // plain None
|
if (types.indexOf(1) !== -1) prefer = 1; // plain None
|
||||||
else if (types.indexOf(19) !== -1) prefer = 19; // VeNCrypt
|
else if (types.indexOf(19) !== -1) prefer = 19; // VeNCrypt
|
||||||
else prefer = types[0];
|
else prefer = types[0];
|
||||||
// Only handle known-safe types; reject everything else.
|
// Prefer: 1 (None) → 19 (VeNCrypt) → 30 (Apple-DH)
|
||||||
if (prefer !== 1 && prefer !== 19) {
|
if (types.indexOf(1) !== -1) prefer = 1;
|
||||||
dbg('no supported security type in [' + Array.from(types).join(', ') + '] — need 1 (None) or 19 (VeNCrypt)', 'err');
|
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');
|
setStatus('unsupported security types: [' + Array.from(types).join(', ') + ']', 'error');
|
||||||
ws.close();
|
ws.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
dbg('→ choosing security type ' + prefer +
|
dbg('→ choosing security type ' + prefer +
|
||||||
(prefer === 1 ? ' (None)' : ' (VeNCrypt)'));
|
(prefer === 1 ? ' (None)' : prefer === 19 ? ' (VeNCrypt)' : ' (Apple-DH)'));
|
||||||
send(new Uint8Array([prefer]));
|
send(new Uint8Array([prefer]));
|
||||||
if (prefer === 1) state = 'security-result';
|
if (prefer === 1) state = 'security-result';
|
||||||
else state = 'vencrypt-version';
|
else if (prefer === 19) state = 'vencrypt-version';
|
||||||
|
else state = 'apple-dh-params';
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
case 'security-vnc-challenge': {
|
case 'security-vnc-challenge': {
|
||||||
|
|
@ -290,6 +375,72 @@ canvas { display: block; cursor: default; }
|
||||||
state = 'security-result';
|
state = 'security-result';
|
||||||
return true;
|
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': {
|
case 'security-result': {
|
||||||
const b = drainTo(4);
|
const b = drainTo(4);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue