diff --git a/.eslintrc b/.eslintrc index a53bb402..aaf53366 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,10 +1,11 @@ { "env": { "browser": true, - "es6": true + "es2020": true }, "parserOptions": { - "sourceType": "module" + "sourceType": "module", + "ecmaVersion": 11 }, "extends": "eslint:recommended", "rules": { diff --git a/app/styles/base.css b/app/styles/base.css index adad4150..052398a7 100644 --- a/app/styles/base.css +++ b/app/styles/base.css @@ -841,6 +841,25 @@ select:active { height: 1.3em; } +/* ---------------------------------------- + * Key verification Dialog + * ---------------------------------------- + */ + +#noVNC_verify_server_dlg { + position: relative; + + transform: translateY(-50px); +} +#noVNC_verify_server_dlg.noVNC_open { + transform: translateY(0); +} +#noVNC_verify_server_dlg ul { + list-style: none; + margin: 0px; + padding: 0px; +} + /* ---------------------------------------- * Password Dialog * ---------------------------------------- diff --git a/app/ui.js b/app/ui.js index cb6a9fda..ce9d7f91 100644 --- a/app/ui.js +++ b/app/ui.js @@ -316,6 +316,10 @@ const UI = { document.getElementById("noVNC_cancel_reconnect_button") .addEventListener('click', UI.cancelReconnect); + document.getElementById("noVNC_approve_server_button") + .addEventListener('click', UI.approveServer); + document.getElementById("noVNC_reject_server_button") + .addEventListener('click', UI.rejectServer); document.getElementById("noVNC_credentials_button") .addEventListener('click', UI.setCredentials); }, @@ -1030,6 +1034,7 @@ const UI = { credentials: { password: password } }); UI.rfb.addEventListener("connect", UI.connectFinished); UI.rfb.addEventListener("disconnect", UI.disconnectFinished); + UI.rfb.addEventListener("serververification", UI.serverVerify); UI.rfb.addEventListener("credentialsrequired", UI.credentials); UI.rfb.addEventListener("securityfailure", UI.securityFailed); UI.rfb.addEventListener("capabilities", UI.updatePowerButton); @@ -1152,6 +1157,37 @@ const UI = { /* ------^------- * /CONNECTION * ============== + * SERVER VERIFY + * ------v------*/ + + async serverVerify(e) { + const type = e.detail.type; + if (type === 'RSA') { + const publickey = e.detail.publickey; + let fingerprint = await window.crypto.subtle.digest("SHA-1", publickey); + // The same fingerprint format as RealVNC + fingerprint = Array.from(new Uint8Array(fingerprint).slice(0, 8)).map( + x => x.toString(16).padStart(2, '0')).join('-'); + document.getElementById('noVNC_verify_server_dlg').classList.add('noVNC_open'); + document.getElementById('noVNC_fingerprint').innerHTML = fingerprint; + } + }, + + approveServer(e) { + e.preventDefault(); + document.getElementById('noVNC_verify_server_dlg').classList.remove('noVNC_open'); + UI.rfb.approveServer(); + }, + + rejectServer(e) { + e.preventDefault(); + document.getElementById('noVNC_verify_server_dlg').classList.remove('noVNC_open'); + UI.disconnect(); + }, + +/* ------^------- + * /SERVER VERIFY + * ============== * PASSWORD * ------v------*/ diff --git a/core/ra2.js b/core/ra2.js new file mode 100644 index 00000000..81a8a895 --- /dev/null +++ b/core/ra2.js @@ -0,0 +1,567 @@ +import Base64 from './base64.js'; +import { encodeUTF8 } from './util/strings.js'; +import EventTargetMixin from './util/eventtarget.js'; + +export class AESEAXCipher { + constructor() { + this._rawKey = null; + this._ctrKey = null; + this._cbcKey = null; + this._zeroBlock = new Uint8Array(16); + this._prefixBlock0 = this._zeroBlock; + this._prefixBlock1 = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); + this._prefixBlock2 = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]); + } + + async _encryptBlock(block) { + const encrypted = await window.crypto.subtle.encrypt({ + name: "AES-CBC", + iv: this._zeroBlock, + }, this._cbcKey, block); + return new Uint8Array(encrypted).slice(0, 16); + } + + async _initCMAC() { + const k1 = await this._encryptBlock(this._zeroBlock); + const k2 = new Uint8Array(16); + const v = k1[0] >>> 6; + for (let i = 0; i < 15; i++) { + k2[i] = (k1[i + 1] >> 6) | (k1[i] << 2); + k1[i] = (k1[i + 1] >> 7) | (k1[i] << 1); + } + const lut = [0x0, 0x87, 0x0e, 0x89]; + k2[14] ^= v >>> 1; + k2[15] = (k1[15] << 2) ^ lut[v]; + k1[15] = (k1[15] << 1) ^ lut[v >> 1]; + this._k1 = k1; + this._k2 = k2; + } + + async _encryptCTR(data, counter) { + const encrypted = await window.crypto.subtle.encrypt({ + "name": "AES-CTR", + counter: counter, + length: 128 + }, this._ctrKey, data); + return new Uint8Array(encrypted); + } + + async _decryptCTR(data, counter) { + const decrypted = await window.crypto.subtle.decrypt({ + "name": "AES-CTR", + counter: counter, + length: 128 + }, this._ctrKey, data); + return new Uint8Array(decrypted); + } + + async _computeCMAC(data, prefixBlock) { + if (prefixBlock.length !== 16) { + return null; + } + const n = Math.floor(data.length / 16); + const m = Math.ceil(data.length / 16); + const r = data.length - n * 16; + const cbcData = new Uint8Array((m + 1) * 16); + cbcData.set(prefixBlock); + cbcData.set(data, 16); + if (r === 0) { + for (let i = 0; i < 16; i++) { + cbcData[n * 16 + i] ^= this._k1[i]; + } + } else { + cbcData[(n + 1) * 16 + r] = 0x80; + for (let i = 0; i < 16; i++) { + cbcData[(n + 1) * 16 + i] ^= this._k2[i]; + } + } + let cbcEncrypted = await window.crypto.subtle.encrypt({ + name: "AES-CBC", + iv: this._zeroBlock, + }, this._cbcKey, cbcData); + + cbcEncrypted = new Uint8Array(cbcEncrypted); + const mac = cbcEncrypted.slice(cbcEncrypted.length - 32, cbcEncrypted.length - 16); + return mac; + } + + async setKey(key) { + this._rawKey = key; + this._ctrKey = await window.crypto.subtle.importKey( + "raw", key, {"name": "AES-CTR"}, false, ["encrypt", "decrypt"]); + this._cbcKey = await window.crypto.subtle.importKey( + "raw", key, {"name": "AES-CBC"}, false, ["encrypt", "decrypt"]); + await this._initCMAC(); + } + + async encrypt(message, associatedData, nonce) { + const nCMAC = await this._computeCMAC(nonce, this._prefixBlock0); + const encrypted = await this._encryptCTR(message, nCMAC); + const adCMAC = await this._computeCMAC(associatedData, this._prefixBlock1); + const mac = await this._computeCMAC(encrypted, this._prefixBlock2); + for (let i = 0; i < 16; i++) { + mac[i] ^= nCMAC[i] ^ adCMAC[i]; + } + const res = new Uint8Array(16 + encrypted.length); + res.set(encrypted); + res.set(mac, encrypted.length); + return res; + } + + async decrypt(encrypted, associatedData, nonce, mac) { + const nCMAC = await this._computeCMAC(nonce, this._prefixBlock0); + const adCMAC = await this._computeCMAC(associatedData, this._prefixBlock1); + const computedMac = await this._computeCMAC(encrypted, this._prefixBlock2); + for (let i = 0; i < 16; i++) { + computedMac[i] ^= nCMAC[i] ^ adCMAC[i]; + } + if (computedMac.length !== mac.length) { + return null; + } + for (let i = 0; i < mac.length; i++) { + if (computedMac[i] !== mac[i]) { + return null; + } + } + const res = await this._decryptCTR(encrypted, nCMAC); + return res; + } +} + +export class RA2Cipher { + constructor() { + this._cipher = new AESEAXCipher(); + this._counter = new Uint8Array(16); + } + + async setKey(key) { + await this._cipher.setKey(key); + } + + async makeMessage(message) { + const ad = new Uint8Array([(message.length & 0xff00) >>> 8, message.length & 0xff]); + const encrypted = await this._cipher.encrypt(message, ad, this._counter); + for (let i = 0; i < 16 && this._counter[i]++ === 255; i++); + const res = new Uint8Array(message.length + 2 + 16); + res.set(ad); + res.set(encrypted, 2); + return res; + } + + async receiveMessage(length, encrypted, mac) { + const ad = new Uint8Array([(length & 0xff00) >>> 8, length & 0xff]); + const res = await this._cipher.decrypt(encrypted, ad, this._counter, mac); + for (let i = 0; i < 16 && this._counter[i]++ === 255; i++); + return res; + } +} + +export class RSACipher { + constructor(keyLength) { + this._key = null; + this._keyLength = keyLength; + this._keyBytes = Math.ceil(keyLength / 8); + this._n = null; + this._e = null; + this._d = null; + this._nBigInt = null; + this._eBigInt = null; + this._dBigInt = null; + } + + _base64urlDecode(data) { + data = data.replace(/-/g, "+").replace(/_/g, "/"); + data = data.padEnd(Math.ceil(data.length / 4) * 4, "="); + return Base64.decode(data); + } + + _u8ArrayToBigInt(arr) { + let hex = '0x'; + for (let i = 0; i < arr.length; i++) { + hex += arr[i].toString(16).padStart(2, '0'); + } + return BigInt(hex); + } + + _padArray(arr, length) { + const res = new Uint8Array(length); + res.set(arr, length - arr.length); + return res; + } + + _bigIntToU8Array(bigint, padLength=0) { + let hex = bigint.toString(16); + if (padLength === 0) { + padLength = Math.ceil(hex.length / 2) * 2; + } + hex = hex.padStart(padLength * 2, '0'); + const length = hex.length / 2; + const arr = new Uint8Array(length); + for (let i = 0; i < length; i++) { + arr[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16); + } + return arr; + } + + _modPow(b, e, m) { + if (m === 1n) { + return 0; + } + let r = 1n; + b = b % m; + while (e > 0) { + if (e % 2n === 1n) { + r = (r * b) % m; + } + e = e / 2n; + b = (b * b) % m; + } + return r; + } + + async generateKey() { + this._key = await window.crypto.subtle.generateKey( + { + name: "RSA-OAEP", + modulusLength: this._keyLength, + publicExponent: new Uint8Array([0x01, 0x00, 0x01]), + hash: {name: "SHA-256"}, + }, + true, ["encrypt", "decrypt"]); + const privateKey = await window.crypto.subtle.exportKey("jwk", this._key.privateKey); + this._n = this._padArray(this._base64urlDecode(privateKey.n), this._keyBytes); + this._nBigInt = this._u8ArrayToBigInt(this._n); + this._e = this._padArray(this._base64urlDecode(privateKey.e), this._keyBytes); + this._eBigInt = this._u8ArrayToBigInt(this._e); + this._d = this._padArray(this._base64urlDecode(privateKey.d), this._keyBytes); + this._dBigInt = this._u8ArrayToBigInt(this._d); + } + + setPublicKey(n, e) { + if (n.length !== this._keyBytes || e.length !== this._keyBytes) { + return; + } + this._n = new Uint8Array(this._keyBytes); + this._e = new Uint8Array(this._keyBytes); + this._n.set(n); + this._e.set(e); + this._nBigInt = this._u8ArrayToBigInt(this._n); + this._eBigInt = this._u8ArrayToBigInt(this._e); + } + + encrypt(message) { + if (message.length > this._keyBytes - 11) { + return null; + } + const ps = new Uint8Array(this._keyBytes - message.length - 3); + window.crypto.getRandomValues(ps); + for (let i = 0; i < ps.length; i++) { + ps[i] = Math.floor(ps[i] * 254 / 255 + 1); + } + const em = new Uint8Array(this._keyBytes); + em[1] = 0x02; + em.set(ps, 2); + em.set(message, ps.length + 3); + const emBigInt = this._u8ArrayToBigInt(em); + const c = this._modPow(emBigInt, this._eBigInt, this._nBigInt); + return this._bigIntToU8Array(c, this._keyBytes); + } + + decrypt(message) { + if (message.length !== this._keyBytes) { + return null; + } + const msgBigInt = this._u8ArrayToBigInt(message); + const emBigInt = this._modPow(msgBigInt, this._dBigInt, this._nBigInt); + const em = this._bigIntToU8Array(emBigInt, this._keyBytes); + if (em[0] !== 0x00 || em[1] !== 0x02) { + return null; + } + let i = 2; + for (; i < em.length; i++) { + if (em[i] === 0x00) { + break; + } + } + if (i === em.length) { + return null; + } + return em.slice(i + 1, em.length); + } + + get keyLength() { + return this._keyLength; + } + + get n() { + return this._n; + } + + get e() { + return this._e; + } + + get d() { + return this._d; + } +} + +export default class RSAAESAuthenticationState extends EventTargetMixin { + constructor(sock, getCredentials) { + super(); + this._hasStarted = false; + this._checkSock = null; + this._checkCredentials = null; + this._approveServerResolve = null; + this._sockReject = null; + this._credentialsReject = null; + this._approveServerReject = null; + this._sock = sock; + this._getCredentials = getCredentials; + } + + _waitSockAsync(len) { + return new Promise((resolve, reject) => { + const hasData = () => !this._sock.rQwait('RA2', len); + if (hasData()) { + resolve(); + } else { + this._checkSock = () => { + if (hasData()) { + resolve(); + this._checkSock = null; + this._sockReject = null; + } + }; + this._sockReject = reject; + } + }); + } + + _waitApproveKeyAsync() { + return new Promise((resolve, reject) => { + this._approveServerResolve = resolve; + this._approveServerReject = reject; + }); + } + + _waitCredentialsAsync(subtype) { + const hasCredentials = () => { + if (subtype === 1 && this._getCredentials().username !== undefined && + this._getCredentials().password !== undefined) { + return true; + } else if (subtype === 2 && this._getCredentials().password !== undefined) { + return true; + } + return false; + }; + return new Promise((resolve, reject) => { + if (hasCredentials()) { + resolve(); + } else { + this._checkCredentials = () => { + if (hasCredentials()) { + resolve(); + this._checkCredentials = null; + this._credentialsReject = null; + } + }; + this._credentialsReject = reject; + } + }); + } + + checkInternalEvents() { + if (this._checkSock !== null) { + this._checkSock(); + } + if (this._checkCredentials !== null) { + this._checkCredentials(); + } + } + + approveServer() { + if (this._approveServerResolve !== null) { + this._approveServerResolve(); + this._approveServerResolve = null; + } + } + + disconnect() { + if (this._sockReject !== null) { + this._sockReject(new Error("disconnect normally")); + this._sockReject = null; + } + if (this._credentialsReject !== null) { + this._credentialsReject(new Error("disconnect normally")); + this._credentialsReject = null; + } + if (this._approveServerReject !== null) { + this._approveServerReject(new Error("disconnect normally")); + this._approveServerReject = null; + } + } + + async negotiateRA2neAuthAsync() { + this._hasStarted = true; + // 1: Receive server public key + await this._waitSockAsync(4); + const serverKeyLengthBuffer = this._sock.rQslice(0, 4); + const serverKeyLength = this._sock.rQshift32(); + if (serverKeyLength < 1024) { + throw new Error("RA2: server public key is too short: " + serverKeyLength); + } else if (serverKeyLength > 8192) { + throw new Error("RA2: server public key is too long: " + serverKeyLength); + } + const serverKeyBytes = Math.ceil(serverKeyLength / 8); + await this._waitSockAsync(serverKeyBytes * 2); + const serverN = this._sock.rQshiftBytes(serverKeyBytes); + const serverE = this._sock.rQshiftBytes(serverKeyBytes); + const serverRSACipher = new RSACipher(serverKeyLength); + serverRSACipher.setPublicKey(serverN, serverE); + const serverPublickey = new Uint8Array(4 + serverKeyBytes * 2); + serverPublickey.set(serverKeyLengthBuffer); + serverPublickey.set(serverN, 4); + serverPublickey.set(serverE, 4 + serverKeyBytes); + + // verify server public key + this.dispatchEvent(new CustomEvent("serververification", { + detail: { type: "RSA", publickey: serverPublickey } + })); + await this._waitApproveKeyAsync(); + + // 2: Send client public key + const clientKeyLength = 2048; + const clientKeyBytes = Math.ceil(clientKeyLength / 8); + const clientRSACipher = new RSACipher(clientKeyLength); + await clientRSACipher.generateKey(); + const clientN = clientRSACipher.n; + const clientE = clientRSACipher.e; + const clientPublicKey = new Uint8Array(4 + clientKeyBytes * 2); + clientPublicKey[0] = (clientKeyLength & 0xff000000) >>> 24; + clientPublicKey[1] = (clientKeyLength & 0xff0000) >>> 16; + clientPublicKey[2] = (clientKeyLength & 0xff00) >>> 8; + clientPublicKey[3] = clientKeyLength & 0xff; + clientPublicKey.set(clientN, 4); + clientPublicKey.set(clientE, 4 + clientKeyBytes); + this._sock.send(clientPublicKey); + + // 3: Send client random + const clientRandom = new Uint8Array(16); + window.crypto.getRandomValues(clientRandom); + const clientEncryptedRandom = serverRSACipher.encrypt(clientRandom); + const clientRandomMessage = new Uint8Array(2 + serverKeyBytes); + clientRandomMessage[0] = (serverKeyBytes & 0xff00) >>> 8; + clientRandomMessage[1] = serverKeyBytes & 0xff; + clientRandomMessage.set(clientEncryptedRandom, 2); + this._sock.send(clientRandomMessage); + + // 4: Receive server random + await this._waitSockAsync(2); + if (this._sock.rQshift16() !== clientKeyBytes) { + throw new Error("RA2: wrong encrypted message length"); + } + const serverEncryptedRandom = this._sock.rQshiftBytes(clientKeyBytes); + const serverRandom = clientRSACipher.decrypt(serverEncryptedRandom); + if (serverRandom === null || serverRandom.length !== 16) { + throw new Error("RA2: corrupted server encrypted random"); + } + + // 5: Compute session keys and set ciphers + let clientSessionKey = new Uint8Array(32); + let serverSessionKey = new Uint8Array(32); + clientSessionKey.set(serverRandom); + clientSessionKey.set(clientRandom, 16); + serverSessionKey.set(clientRandom); + serverSessionKey.set(serverRandom, 16); + clientSessionKey = await window.crypto.subtle.digest("SHA-1", clientSessionKey); + clientSessionKey = new Uint8Array(clientSessionKey).slice(0, 16); + serverSessionKey = await window.crypto.subtle.digest("SHA-1", serverSessionKey); + serverSessionKey = new Uint8Array(serverSessionKey).slice(0, 16); + const clientCipher = new RA2Cipher(); + await clientCipher.setKey(clientSessionKey); + const serverCipher = new RA2Cipher(); + await serverCipher.setKey(serverSessionKey); + + // 6: Compute and exchange hashes + let serverHash = new Uint8Array(8 + serverKeyBytes * 2 + clientKeyBytes * 2); + let clientHash = new Uint8Array(8 + serverKeyBytes * 2 + clientKeyBytes * 2); + serverHash.set(serverPublickey); + serverHash.set(clientPublicKey, 4 + serverKeyBytes * 2); + clientHash.set(clientPublicKey); + clientHash.set(serverPublickey, 4 + clientKeyBytes * 2); + serverHash = await window.crypto.subtle.digest("SHA-1", serverHash); + clientHash = await window.crypto.subtle.digest("SHA-1", clientHash); + serverHash = new Uint8Array(serverHash); + clientHash = new Uint8Array(clientHash); + this._sock.send(await clientCipher.makeMessage(clientHash)); + await this._waitSockAsync(2 + 20 + 16); + if (this._sock.rQshift16() !== 20) { + throw new Error("RA2: wrong server hash"); + } + const serverHashReceived = await serverCipher.receiveMessage( + 20, this._sock.rQshiftBytes(20), this._sock.rQshiftBytes(16)); + if (serverHashReceived === null) { + throw new Error("RA2: failed to authenticate the message"); + } + for (let i = 0; i < 20; i++) { + if (serverHashReceived[i] !== serverHash[i]) { + throw new Error("RA2: wrong server hash"); + } + } + + // 7: Receive subtype + await this._waitSockAsync(2 + 1 + 16); + if (this._sock.rQshift16() !== 1) { + throw new Error("RA2: wrong subtype"); + } + let subtype = (await serverCipher.receiveMessage( + 1, this._sock.rQshiftBytes(1), this._sock.rQshiftBytes(16))); + if (subtype === null) { + throw new Error("RA2: failed to authenticate the message"); + } + subtype = subtype[0]; + if (subtype === 1) { + if (this._getCredentials().username === undefined || + this._getCredentials().password === undefined) { + this.dispatchEvent(new CustomEvent( + "credentialsrequired", + { detail: { types: ["username", "password"] } })); + } + } else if (subtype === 2) { + if (this._getCredentials().password === undefined) { + this.dispatchEvent(new CustomEvent( + "credentialsrequired", + { detail: { types: ["password"] } })); + } + } else { + throw new Error("RA2: wrong subtype"); + } + await this._waitCredentialsAsync(subtype); + let username; + if (subtype === 1) { + username = encodeUTF8(this._getCredentials().username).slice(0, 255); + } else { + username = ""; + } + const password = encodeUTF8(this._getCredentials().password).slice(0, 255); + const credentials = new Uint8Array(username.length + password.length + 2); + credentials[0] = username.length; + credentials[username.length + 1] = password.length; + for (let i = 0; i < username.length; i++) { + credentials[i + 1] = username.charCodeAt(i); + } + for (let i = 0; i < password.length; i++) { + credentials[username.length + 2 + i] = password.charCodeAt(i); + } + this._sock.send(await clientCipher.makeMessage(credentials)); + } + + get hasStarted() { + return this._hasStarted; + } + + set hasStarted(s) { + this._hasStarted = s; + } +} \ No newline at end of file diff --git a/core/rfb.js b/core/rfb.js index 970e03ff..860a85b2 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -25,6 +25,7 @@ import DES from "./des.js"; import KeyTable from "./input/keysym.js"; import XtScancode from "./input/xtscancodes.js"; import { encodings } from "./encodings.js"; +import RSAAESAuthenticationState from "./ra2.js"; import RawDecoder from "./decoders/raw.js"; import CopyRectDecoder from "./decoders/copyrect.js"; @@ -100,6 +101,7 @@ export default class RFB extends EventTargetMixin { this._rfbInitState = ''; this._rfbAuthScheme = -1; this._rfbCleanDisconnect = true; + this._rfbRSAAESAuthenticationState = null; // Server capabilities this._rfbVersion = 0; @@ -178,6 +180,8 @@ export default class RFB extends EventTargetMixin { handleMouse: this._handleMouse.bind(this), handleWheel: this._handleWheel.bind(this), handleGesture: this._handleGesture.bind(this), + handleRSAAESCredentialsRequired: this._handleRSAAESCredentialsRequired.bind(this), + handleRSAAESServerVerification: this._handleRSAAESServerVerification.bind(this), }; // main setup @@ -378,6 +382,15 @@ export default class RFB extends EventTargetMixin { this._sock.off('error'); this._sock.off('message'); this._sock.off('open'); + if (this._rfbRSAAESAuthenticationState !== null) { + this._rfbRSAAESAuthenticationState.disconnect(); + } + } + + approveServer() { + if (this._rfbRSAAESAuthenticationState !== null) { + this._rfbRSAAESAuthenticationState.approveServer(); + } } sendCredentials(creds) { @@ -1335,6 +1348,8 @@ export default class RFB extends EventTargetMixin { this._rfbAuthScheme = 22; // XVP } else if (types.includes(16)) { this._rfbAuthScheme = 16; // Tight + } else if (types.includes(6)) { + this._rfbAuthScheme = 6; // RA2ne Auth } else if (types.includes(2)) { this._rfbAuthScheme = 2; // VNC Auth } else if (types.includes(19)) { @@ -1652,6 +1667,44 @@ export default class RFB extends EventTargetMixin { return this._fail("No supported sub-auth types!"); } + _handleRSAAESCredentialsRequired(event) { + this.dispatchEvent(event); + } + + _handleRSAAESServerVerification(event) { + this.dispatchEvent(event); + } + + _negotiateRA2neAuth() { + if (this._rfbRSAAESAuthenticationState === null) { + this._rfbRSAAESAuthenticationState = new RSAAESAuthenticationState(this._sock, () => this._rfbCredentials); + this._rfbRSAAESAuthenticationState.addEventListener( + "serververification", this._eventHandlers.handleRSAAESServerVerification); + this._rfbRSAAESAuthenticationState.addEventListener( + "credentialsrequired", this._eventHandlers.handleRSAAESCredentialsRequired); + } + this._rfbRSAAESAuthenticationState.checkInternalEvents(); + if (!this._rfbRSAAESAuthenticationState.hasStarted) { + this._rfbRSAAESAuthenticationState.negotiateRA2neAuthAsync() + .catch((e) => { + if (e.message !== "disconnect normally") { + this._fail(e.message); + } + }).then(() => { + this.dispatchEvent(new CustomEvent('securityresult')); + this._rfbInitState = "SecurityResult"; + this._initMsg(); + }).finally(() => { + this._rfbRSAAESAuthenticationState.removeEventListener( + "serververification", this._eventHandlers.handleRSAAESServerVerification); + this._rfbRSAAESAuthenticationState.removeEventListener( + "credentialsrequired", this._eventHandlers.handleRSAAESCredentialsRequired); + this._rfbRSAAESAuthenticationState = null; + }); + } + return false; + } + _negotiateAuthentication() { switch (this._rfbAuthScheme) { case 1: // no auth @@ -1677,6 +1730,9 @@ export default class RFB extends EventTargetMixin { case 129: // TightVNC UNIX Security Type return this._negotiateTightUnixAuth(); + case 6: // RA2ne Security Type + return this._negotiateRA2neAuth(); + default: return this._fail("Unsupported auth scheme (scheme: " + this._rfbAuthScheme + ")"); diff --git a/tests/test.ra2.js b/tests/test.ra2.js new file mode 100644 index 00000000..cc505b1f --- /dev/null +++ b/tests/test.ra2.js @@ -0,0 +1,357 @@ +const expect = chai.expect; + +import RFB from '../core/rfb.js'; + +import FakeWebSocket from './fake.websocket.js'; + +function fakeGetRandomValues(arr) { + if (arr.length === 16) { + arr.set(new Uint8Array([ + 0x1c, 0x08, 0xfe, 0x21, 0x78, 0xef, 0x4e, 0xf9, + 0x3f, 0x05, 0xec, 0xea, 0xd4, 0x6b, 0xa5, 0xd5, + ])); + } else { + arr.set(new Uint8Array([ + 0xee, 0xe2, 0xf1, 0x5a, 0x3c, 0xa7, 0xbe, 0x95, + 0x6f, 0x2a, 0x75, 0xfd, 0x62, 0x01, 0xcb, 0xbf, + 0x43, 0x74, 0xca, 0x47, 0x4d, 0xfb, 0x0f, 0xcf, + 0x3a, 0x6d, 0x55, 0x6b, 0x59, 0x3a, 0xf6, 0x87, + 0xcb, 0x03, 0xb7, 0x28, 0x35, 0x7b, 0x15, 0x8e, + 0xb6, 0xc8, 0x8f, 0x2d, 0x5e, 0x7b, 0x1c, 0x9a, + 0x32, 0x55, 0xe7, 0x64, 0x36, 0x25, 0x7b, 0xa3, + 0xe9, 0x4f, 0x6f, 0x97, 0xdc, 0xa4, 0xd4, 0x62, + 0x6d, 0x7f, 0xab, 0x02, 0x6b, 0x13, 0x56, 0x69, + 0xfb, 0xd0, 0xd4, 0x13, 0x76, 0xcd, 0x0d, 0xd0, + 0x1f, 0xd1, 0x0c, 0x63, 0x3a, 0x34, 0x20, 0x6c, + 0xbb, 0x60, 0x45, 0x82, 0x23, 0xfd, 0x7c, 0x77, + 0x6d, 0xcc, 0x5e, 0xaa, 0xc3, 0x0c, 0x43, 0xb7, + 0x8d, 0xc0, 0x27, 0x6e, 0xeb, 0x1d, 0x6c, 0x5f, + 0xd8, 0x1c, 0x3c, 0x1c, 0x60, 0x2e, 0x82, 0x15, + 0xfd, 0x2e, 0x5f, 0x3a, 0x15, 0x53, 0x14, 0x70, + 0x4f, 0xe1, 0x65, 0x68, 0x35, 0x6d, 0xc7, 0x64, + 0xdb, 0xdd, 0x09, 0x31, 0x4f, 0x7b, 0x6d, 0x6c, + 0x77, 0x59, 0x5e, 0x1e, 0xfa, 0x4b, 0x06, 0x14, + 0xbe, 0xdc, 0x9c, 0x3d, 0x7b, 0xed, 0xf3, 0x2b, + 0x19, 0x26, 0x11, 0x8e, 0x3f, 0xab, 0x73, 0x9a, + 0x0a, 0x3a, 0xaa, 0x85, 0x06, 0xd5, 0xca, 0x3f, + 0xc3, 0xe2, 0x33, 0x7f, 0x97, 0x74, 0x98, 0x8f, + 0x2f, 0xa5, 0xfc, 0x7e, 0xb1, 0x77, 0x71, 0x58, + 0xf0, 0xbc, 0x04, 0x59, 0xbb, 0xb4, 0xc6, 0xcc, + 0x0f, 0x06, 0xcd, 0xa2, 0xd5, 0x01, 0x2f, 0xb2, + 0x22, 0x0b, 0xfc, 0x1e, 0x59, 0x9f, 0xd3, 0x4f, + 0x30, 0x95, 0xc6, 0x80, 0x0f, 0x69, 0xf3, 0x4a, + 0xd4, 0x36, 0xb6, 0x5a, 0x0b, 0x16, 0x0d, 0x81, + 0x31, 0xb0, 0x69, 0xd4, 0x4e, + ])); + } +} + +async function fakeGeneratekey() { + let key = JSON.parse('{"alg":"RSA-OAEP-256","d":"B7QR2yI8sXjo8vQhJpX9odqqR\ +6wIuPrTM1B1JJEKVeSrr7OYcc1FRJ52Vap9LIAU-ezigs9QDvWMxknB8motLnG69Wck37nt9_z4s8l\ +FQp0nROA-oaR92HW34KNL1b2fEVWGI0N86h730MvTJC5O2cmKeMezIG-oNqbbfFyP8AW-WLdDlgZm1\ +1-FjzhbVpb0Bc7nRSgBPSV-EY6Sl-LuglxDx4LaTdQW7QE_WXoRUt-GYGfTseuFQQK5WeoyX3yBtQy\ +dpauW6rrgyWdtP4hDFIoZsX6w1i-UMWMMwlIB5FdnUSi26igVGADGpV_vGMP36bv-EHp0bY-Qp0gpI\ +fLfgQ","dp":"Z1v5UceFfV2bhmbG19eGYb30jFxqoRBq36PKNY7IunMs1keYy0FpLbyGhtgMZ1Ymm\ +c8wEzGYsCPEP-ykcun_rlyu7YxmcnyC9YQqTqLyqvO-7rUqDvk9TMfdqWFP6heADRhKZmEbmcau6_m\ +2MwwK9kOkMKWvpqp8_TpJMnAH7zE","dq":"OBacRE15aY3NtCR4cvP5os3sT70JbDdDLHT3IHZM6r\ +E35CYNpLDia2chm_wnMcYvKFW9zC2ajRZ15i9c_VXQzS7ZlTaQYBFyMt7kVhxMEMFsPv1crD6t3uEI\ +j0LNuNYyy0jkon_LPZKQFK654CiL-L2YaNXOH4HbHP02dWeVQIE","e":"AQAB","ext":true,"ke\ +y_ops":["decrypt"],"kty":"RSA","n":"m1c92ZFk9ZI6l_O4YFiNxbv0Ng94SB3yThy1P_mcqr\ +GDQkRiGVdcTxAk38T9PgLztmspF-6U5TAHO-gSmmW88AC9m6f1Mspps6r7zl-M_OG-TwvGzf3BDz8z\ +Eg1FPbZV7whO1M4TCAZ0PqwG7qCc6nK1WiAhaKrSpzuPdL1igfNBsX7qu5wgw4ZTTGSLbVC_LfULQ5\ +FADgFTRXUSaxm1F8C_Lwy6a2e4nTcXilmtN2IHUjHegzm-Tq2HizmR3ARdWJpESYIW5-AXoiqj29tD\ +rqCmu2WPkB2psVp83IzZfaQNQzjNfvA8GpimkcDCkP5VMRrtKCcG4ZAFnO-A3NBX_Q","p":"2Q_lN\ +L7vCOBzAppYzCZo3WSh0hX-MOZyPUznks5U2TjmfdNZoL6_FJRiGyyLvwSiZFdEAAvpAyESFfFigng\ +AqMLSf448nPg15VUGj533CotsEM0WpoEr1JCgqdUbgDAfJQIBcwOmegBqd7lWm7uzEnRCvouB70ybk\ +JfpdprhkVE","q":"tzTt-F3g2u_3Ctj26Ho9iN_wC_W0lXGzslLt5nLmss8JqdLoDDrijjU-gjeRh\ +7lgiuHdUc3dorfFKbaMNOjoW3QKqt9oZ1JM0HKeRw0X2PnWW_0WK6DK5ASWDTXbMq2sUZqJvYEyL74\ +H2Zrt0RPAux7XQLEVgND6ROdXnMJ70O0","qi":"qfl4cXQkz4BNqa2De0-PfdU-8d1w3onnaGqx1D\ +s2fHzD_SJ4cNghn2TksoT9Qo64b3pUjH9igi2pyEjomk6D12N6FG0e10u7vFKv3W5YqUOgTpYdbcWH\ +dZ2qZWJU0XQZIrF8jLGTOO4GYP6_9sJ5R7Wk_0MdqQy8qvixWD4zLcY"}'); + key = await window.crypto.subtle.importKey("jwk", key, { + name: "RSA-OAEP", + hash: {name: "SHA-256"} + }, true, ["decrypt"]); + return {privateKey: key}; +} + +const receiveData = new Uint8Array([ + // server public key + 0x00, 0x00, 0x08, 0x00, 0xac, 0x1a, 0xbc, 0x42, + 0x8a, 0x2a, 0x69, 0x65, 0x54, 0xf8, 0x9a, 0xe6, + 0x43, 0xaa, 0xf7, 0x27, 0xf6, 0x2a, 0xf8, 0x8f, + 0x36, 0xd4, 0xae, 0x54, 0x0f, 0x16, 0x28, 0x08, + 0xc2, 0x5b, 0xca, 0x23, 0xdc, 0x27, 0x88, 0x1a, + 0x12, 0x82, 0xa8, 0x54, 0xea, 0x00, 0x99, 0x8d, + 0x02, 0x1d, 0x77, 0x4a, 0xeb, 0xd0, 0x93, 0x40, + 0x79, 0x86, 0xcb, 0x37, 0xd4, 0xb2, 0xc7, 0xcd, + 0x93, 0xe1, 0x00, 0x4d, 0x86, 0xff, 0x97, 0x33, + 0x0c, 0xad, 0x51, 0x47, 0x45, 0x85, 0x56, 0x07, + 0x65, 0x21, 0x7c, 0x57, 0x6d, 0x68, 0x7d, 0xd7, + 0x00, 0x43, 0x0c, 0x9d, 0x3b, 0xa1, 0x5a, 0x11, + 0xed, 0x51, 0x77, 0xf9, 0xd1, 0x5b, 0x33, 0xd7, + 0x1a, 0xeb, 0x65, 0x57, 0xc0, 0x01, 0x51, 0xff, + 0x9b, 0x82, 0xb3, 0xeb, 0x82, 0xc2, 0x1f, 0xca, + 0x47, 0xc0, 0x6a, 0x09, 0xe0, 0xf7, 0xda, 0x39, + 0x85, 0x12, 0xe7, 0x45, 0x8d, 0xb4, 0x1a, 0xda, + 0xcb, 0x86, 0x58, 0x52, 0x37, 0x66, 0x9d, 0x8a, + 0xce, 0xf2, 0x18, 0x78, 0x7d, 0x7f, 0xf0, 0x07, + 0x94, 0x8e, 0x6b, 0x17, 0xd9, 0x00, 0x2a, 0x3a, + 0xb9, 0xd4, 0x77, 0xde, 0x70, 0x85, 0xc4, 0x3a, + 0x62, 0x10, 0x02, 0xee, 0xba, 0xd8, 0xc0, 0x62, + 0xd0, 0x8e, 0xc1, 0x98, 0x19, 0x8e, 0x39, 0x0f, + 0x3e, 0x1d, 0x61, 0xb1, 0x93, 0x13, 0x59, 0x39, + 0xcb, 0x96, 0xf2, 0x17, 0xc9, 0xe1, 0x41, 0xd3, + 0x20, 0xdd, 0x62, 0x5e, 0x7d, 0x53, 0xd6, 0xb7, + 0x1d, 0xfe, 0x02, 0x18, 0x1f, 0xe0, 0xef, 0x3d, + 0x94, 0xe3, 0x0a, 0x9c, 0x59, 0x54, 0xd8, 0x98, + 0x16, 0x9c, 0x31, 0xda, 0x41, 0x0f, 0x2e, 0x71, + 0x68, 0xe0, 0xa2, 0x62, 0x3e, 0xe5, 0x25, 0x31, + 0xcf, 0xfc, 0x67, 0x63, 0xc3, 0xb0, 0xda, 0x3f, + 0x7b, 0x59, 0xbe, 0x7e, 0x9e, 0xa8, 0xd0, 0x01, + 0x4f, 0x43, 0x7f, 0x8d, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x01, + // server random + 0x01, 0x00, 0x5b, 0x58, 0x2a, 0x96, 0x2d, 0xbb, + 0x88, 0xec, 0xc3, 0x54, 0x00, 0xf3, 0xbb, 0xbe, + 0x17, 0xa3, 0x84, 0xd3, 0xef, 0xd8, 0x4a, 0x31, + 0x09, 0x20, 0xdd, 0xbc, 0x16, 0x9d, 0xc9, 0x5b, + 0x99, 0x62, 0x86, 0xfe, 0x0b, 0x28, 0x4b, 0xfe, + 0x5b, 0x56, 0x2d, 0xcb, 0x6e, 0x6f, 0xec, 0xf0, + 0x53, 0x0c, 0x33, 0x84, 0x93, 0xc9, 0xbf, 0x79, + 0xde, 0xb3, 0xb9, 0x29, 0x60, 0x78, 0xde, 0xe6, + 0x1d, 0xa7, 0x89, 0x48, 0x3f, 0xd1, 0x58, 0x66, + 0x27, 0x9c, 0xd4, 0x6e, 0x72, 0x9c, 0x6e, 0x4a, + 0xc0, 0x69, 0x79, 0x6f, 0x79, 0x0f, 0x13, 0xc4, + 0x20, 0xcf, 0xa6, 0xbb, 0xce, 0x18, 0x6d, 0xd5, + 0x9e, 0xd9, 0x67, 0xbe, 0x61, 0x43, 0x67, 0x11, + 0x76, 0x2f, 0xfd, 0x78, 0x75, 0x2b, 0x89, 0x35, + 0xdd, 0x0f, 0x13, 0x7f, 0xee, 0x78, 0xad, 0x32, + 0x56, 0x21, 0x81, 0x08, 0x1f, 0xcf, 0x4c, 0x29, + 0xa3, 0xeb, 0x89, 0x2d, 0xbe, 0xba, 0x8d, 0xe4, + 0x69, 0x28, 0xba, 0x53, 0x82, 0xce, 0x5c, 0xf6, + 0x5e, 0x5e, 0xa5, 0xb3, 0x88, 0xd8, 0x3d, 0xab, + 0xf4, 0x24, 0x9e, 0x3f, 0x04, 0xaf, 0xdc, 0x48, + 0x90, 0x53, 0x37, 0xe6, 0x82, 0x1d, 0xe0, 0x15, + 0x91, 0xa1, 0xc6, 0xa9, 0x54, 0xe5, 0x2a, 0xb5, + 0x64, 0x2d, 0x93, 0xc0, 0xc0, 0xe1, 0x0f, 0x6a, + 0x4b, 0xdb, 0x77, 0xf8, 0x4a, 0x0f, 0x83, 0x36, + 0xdd, 0x5e, 0x1e, 0xdd, 0x39, 0x65, 0xa2, 0x11, + 0xc2, 0xcf, 0x56, 0x1e, 0xa1, 0x29, 0xae, 0x11, + 0x9f, 0x3a, 0x82, 0xc7, 0xbd, 0x89, 0x6e, 0x59, + 0xb8, 0x59, 0x17, 0xcb, 0x65, 0xa0, 0x4b, 0x4d, + 0xbe, 0x33, 0x32, 0x85, 0x9c, 0xca, 0x5e, 0x95, + 0xc2, 0x5a, 0xd0, 0xc9, 0x8b, 0xf1, 0xf5, 0x14, + 0xcf, 0x76, 0x80, 0xc2, 0x24, 0x0a, 0x39, 0x7e, + 0x60, 0x64, 0xce, 0xd9, 0xb8, 0xad, 0x24, 0xa8, + 0xdf, 0xcb, + // server hash + 0x00, 0x14, 0x39, 0x30, 0x66, 0xb5, 0x66, 0x8a, + 0xcd, 0xb9, 0xda, 0xe0, 0xde, 0xcb, 0xf6, 0x47, + 0x5f, 0x54, 0x66, 0xe0, 0xbc, 0x49, 0x37, 0x01, + 0xf2, 0x9e, 0xef, 0xcc, 0xcd, 0x4d, 0x6c, 0x0e, + 0xc6, 0xab, 0x28, 0xd4, 0x7b, 0x13, + // subtype + 0x00, 0x01, 0x30, 0x2a, 0xc3, 0x0b, 0xc2, 0x1c, + 0xeb, 0x02, 0x44, 0x92, 0x5d, 0xfd, 0xf9, 0xa7, + 0x94, 0xd0, 0x19, +]); + +const sendData = new Uint8Array([ + // client public key + 0x00, 0x00, 0x08, 0x00, 0x9b, 0x57, 0x3d, 0xd9, + 0x91, 0x64, 0xf5, 0x92, 0x3a, 0x97, 0xf3, 0xb8, + 0x60, 0x58, 0x8d, 0xc5, 0xbb, 0xf4, 0x36, 0x0f, + 0x78, 0x48, 0x1d, 0xf2, 0x4e, 0x1c, 0xb5, 0x3f, + 0xf9, 0x9c, 0xaa, 0xb1, 0x83, 0x42, 0x44, 0x62, + 0x19, 0x57, 0x5c, 0x4f, 0x10, 0x24, 0xdf, 0xc4, + 0xfd, 0x3e, 0x02, 0xf3, 0xb6, 0x6b, 0x29, 0x17, + 0xee, 0x94, 0xe5, 0x30, 0x07, 0x3b, 0xe8, 0x12, + 0x9a, 0x65, 0xbc, 0xf0, 0x00, 0xbd, 0x9b, 0xa7, + 0xf5, 0x32, 0xca, 0x69, 0xb3, 0xaa, 0xfb, 0xce, + 0x5f, 0x8c, 0xfc, 0xe1, 0xbe, 0x4f, 0x0b, 0xc6, + 0xcd, 0xfd, 0xc1, 0x0f, 0x3f, 0x33, 0x12, 0x0d, + 0x45, 0x3d, 0xb6, 0x55, 0xef, 0x08, 0x4e, 0xd4, + 0xce, 0x13, 0x08, 0x06, 0x74, 0x3e, 0xac, 0x06, + 0xee, 0xa0, 0x9c, 0xea, 0x72, 0xb5, 0x5a, 0x20, + 0x21, 0x68, 0xaa, 0xd2, 0xa7, 0x3b, 0x8f, 0x74, + 0xbd, 0x62, 0x81, 0xf3, 0x41, 0xb1, 0x7e, 0xea, + 0xbb, 0x9c, 0x20, 0xc3, 0x86, 0x53, 0x4c, 0x64, + 0x8b, 0x6d, 0x50, 0xbf, 0x2d, 0xf5, 0x0b, 0x43, + 0x91, 0x40, 0x0e, 0x01, 0x53, 0x45, 0x75, 0x12, + 0x6b, 0x19, 0xb5, 0x17, 0xc0, 0xbf, 0x2f, 0x0c, + 0xba, 0x6b, 0x67, 0xb8, 0x9d, 0x37, 0x17, 0x8a, + 0x59, 0xad, 0x37, 0x62, 0x07, 0x52, 0x31, 0xde, + 0x83, 0x39, 0xbe, 0x4e, 0xad, 0x87, 0x8b, 0x39, + 0x91, 0xdc, 0x04, 0x5d, 0x58, 0x9a, 0x44, 0x49, + 0x82, 0x16, 0xe7, 0xe0, 0x17, 0xa2, 0x2a, 0xa3, + 0xdb, 0xdb, 0x43, 0xae, 0xa0, 0xa6, 0xbb, 0x65, + 0x8f, 0x90, 0x1d, 0xa9, 0xb1, 0x5a, 0x7c, 0xdc, + 0x8c, 0xd9, 0x7d, 0xa4, 0x0d, 0x43, 0x38, 0xcd, + 0x7e, 0xf0, 0x3c, 0x1a, 0x98, 0xa6, 0x91, 0xc0, + 0xc2, 0x90, 0xfe, 0x55, 0x31, 0x1a, 0xed, 0x28, + 0x27, 0x06, 0xe1, 0x90, 0x05, 0x9c, 0xef, 0x80, + 0xdc, 0xd0, 0x57, 0xfd, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x01, + // client random + 0x01, 0x00, 0x84, 0x7f, 0x26, 0x54, 0x74, 0xf6, + 0x47, 0xaf, 0x33, 0x64, 0x0d, 0xa6, 0xe5, 0x30, + 0xba, 0xe6, 0xe4, 0x8e, 0x50, 0x40, 0x71, 0x1c, + 0x0e, 0x06, 0x63, 0xf5, 0x07, 0x2a, 0x26, 0x68, + 0xd6, 0xcf, 0xa6, 0x80, 0x84, 0x5e, 0x64, 0xd4, + 0x5e, 0x62, 0x31, 0xfe, 0x44, 0x51, 0x0b, 0x7c, + 0x4d, 0x55, 0xc5, 0x4a, 0x7e, 0x0d, 0x4d, 0x9b, + 0x84, 0xb4, 0x32, 0x2b, 0x4d, 0x8a, 0x34, 0x8d, + 0xc8, 0xcf, 0x19, 0x3b, 0x64, 0x82, 0x27, 0x9e, + 0xa7, 0x70, 0x2a, 0xc1, 0xb8, 0xf3, 0x6a, 0x3a, + 0xf2, 0x75, 0x6e, 0x1d, 0xeb, 0xb6, 0x70, 0x7a, + 0x15, 0x18, 0x38, 0x00, 0xb4, 0x4f, 0x55, 0xb5, + 0xd8, 0x03, 0x4e, 0xb8, 0x53, 0xff, 0x80, 0x62, + 0xf1, 0x9d, 0x27, 0xe8, 0x2a, 0x3d, 0x98, 0x19, + 0x32, 0x09, 0x7e, 0x9a, 0xb0, 0xc7, 0x46, 0x23, + 0x10, 0x85, 0x35, 0x00, 0x96, 0xce, 0xb3, 0x2c, + 0x84, 0x8d, 0xf4, 0x9e, 0xa8, 0x42, 0x67, 0xed, + 0x09, 0xa6, 0x09, 0x97, 0xb3, 0x64, 0x26, 0xfb, + 0x71, 0x11, 0x9b, 0x3f, 0xbb, 0x57, 0xb8, 0x5b, + 0x2e, 0xc5, 0x2d, 0x8c, 0x5c, 0xf7, 0xef, 0x27, + 0x25, 0x88, 0x42, 0x45, 0x43, 0xa4, 0xe7, 0xde, + 0xea, 0xf9, 0x15, 0x7b, 0x5d, 0x66, 0x24, 0xce, + 0xf7, 0xc8, 0x2f, 0xc5, 0xc0, 0x3d, 0xcd, 0xf2, + 0x62, 0xfc, 0x1a, 0x5e, 0xec, 0xff, 0xf1, 0x1b, + 0xc8, 0xdb, 0xc1, 0x0f, 0x54, 0x66, 0x9e, 0xfd, + 0x99, 0x9b, 0x23, 0x70, 0x62, 0x37, 0x80, 0xad, + 0x91, 0x6b, 0x84, 0x85, 0x6a, 0x4c, 0x80, 0x9e, + 0x60, 0x8a, 0x93, 0xa3, 0xc8, 0x8e, 0xc4, 0x4b, + 0x4d, 0xb4, 0x8e, 0x3e, 0xaf, 0xce, 0xcd, 0x83, + 0xe5, 0x21, 0x90, 0x95, 0x20, 0x3c, 0x82, 0xb4, + 0x7c, 0xab, 0x63, 0x9c, 0xae, 0xc3, 0xc9, 0x71, + 0x1a, 0xec, 0x34, 0x18, 0x47, 0xec, 0x5c, 0x4d, + 0xed, 0x84, + // client hash + 0x00, 0x14, 0x9c, 0x91, 0x9e, 0x76, 0xcf, 0x1e, + 0x66, 0x87, 0x5e, 0x29, 0xf1, 0x13, 0x80, 0xea, + 0x7d, 0xec, 0xae, 0xf9, 0x60, 0x01, 0xd3, 0x6f, + 0xb7, 0x9e, 0xb2, 0xcd, 0x2d, 0xc8, 0xf8, 0x84, + 0xb2, 0x9f, 0xc3, 0x7e, 0xb4, 0xbe, + // credentials + 0x00, 0x08, 0x9d, 0xc8, 0x3a, 0xb8, 0x80, 0x4f, + 0xe3, 0x52, 0xdb, 0x62, 0x9e, 0x97, 0x64, 0x82, + 0xa8, 0xa1, 0x6b, 0x7e, 0x4d, 0x68, 0x8c, 0x29, + 0x91, 0x38, +]); + +describe('RA2 handshake', function () { + let sock; + let rfb; + let sentData; + + before(() => { + FakeWebSocket.replace(); + sinon.stub(window.crypto, "getRandomValues").callsFake(fakeGetRandomValues); + sinon.stub(window.crypto.subtle, "generateKey").callsFake(fakeGeneratekey); + }); + after(() => { + FakeWebSocket.restore(); + window.crypto.getRandomValues.restore(); + window.crypto.subtle.generateKey.restore(); + }); + + it('should fire the serververification event', function (done) { + sentData = new Uint8Array(); + rfb = new RFB(document.createElement('div'), "ws://example.com"); + sock = rfb._sock; + sock.send = (data) => { + let res = new Uint8Array(sentData.length + data.length); + res.set(sentData); + res.set(data, sentData.length); + sentData = res; + }; + rfb._rfbInitState = "Security"; + rfb._rfbVersion = 3.8; + sock._websocket._receiveData(new Uint8Array([1, 6])); + rfb.addEventListener("serververification", (e) => { + expect(e.detail.publickey).to.eql(receiveData.slice(0, 516)); + done(); + }); + sock._websocket._receiveData(receiveData); + }); + + it('should handle approveServer and fire the credentialsrequired event', function (done) { + rfb.addEventListener("credentialsrequired", (e) => { + expect(e.detail.types).to.eql(["password"]); + done(); + }); + rfb.approveServer(); + }); + + it('should match sendData after sending credentials', function (done) { + rfb.addEventListener("securityresult", (event) => { + expect(sentData.slice(1)).to.eql(sendData); + done(); + }); + rfb.sendCredentials({ "password": "123456" }); + }); +}); diff --git a/vnc.html b/vnc.html index c678c2a2..7656d277 100644 --- a/vnc.html +++ b/vnc.html @@ -274,6 +274,25 @@ + +