mirror of
https://git.proxmox.com/git/mirror_novnc
synced 2025-04-30 14:52:07 +00:00

Callers shouldn't be poking around directly in to the send queue, but should use accessor functions like for the read queue.
313 lines
12 KiB
JavaScript
313 lines
12 KiB
JavaScript
import { encodeUTF8 } from './util/strings.js';
|
|
import EventTargetMixin from './util/eventtarget.js';
|
|
import legacyCrypto from './crypto/crypto.js';
|
|
|
|
class RA2Cipher {
|
|
constructor() {
|
|
this._cipher = null;
|
|
this._counter = new Uint8Array(16);
|
|
}
|
|
|
|
async setKey(key) {
|
|
this._cipher = await legacyCrypto.importKey(
|
|
"raw", key, { name: "AES-EAX" }, false, ["encrypt, decrypt"]);
|
|
}
|
|
|
|
async makeMessage(message) {
|
|
const ad = new Uint8Array([(message.length & 0xff00) >>> 8, message.length & 0xff]);
|
|
const encrypted = await legacyCrypto.encrypt({
|
|
name: "AES-EAX",
|
|
iv: this._counter,
|
|
additionalData: ad,
|
|
}, this._cipher, message);
|
|
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) {
|
|
const ad = new Uint8Array([(length & 0xff00) >>> 8, length & 0xff]);
|
|
const res = await legacyCrypto.decrypt({
|
|
name: "AES-EAX",
|
|
iv: this._counter,
|
|
additionalData: ad,
|
|
}, this._cipher, encrypted);
|
|
for (let i = 0; i < 16 && this._counter[i]++ === 255; i++);
|
|
return res;
|
|
}
|
|
}
|
|
|
|
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.rQpeekBytes(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 = await legacyCrypto.importKey(
|
|
"raw", { n: serverN, e: serverE }, { name: "RSA-PKCS1-v1_5" }, false, ["encrypt"]);
|
|
const serverPublickey = new Uint8Array(4 + serverKeyBytes * 2);
|
|
serverPublickey.set(serverKeyLengthBuffer);
|
|
serverPublickey.set(serverN, 4);
|
|
serverPublickey.set(serverE, 4 + serverKeyBytes);
|
|
|
|
// verify server public key
|
|
let approveKey = this._waitApproveKeyAsync();
|
|
this.dispatchEvent(new CustomEvent("serververification", {
|
|
detail: { type: "RSA", publickey: serverPublickey }
|
|
}));
|
|
await approveKey;
|
|
|
|
// 2: Send client public key
|
|
const clientKeyLength = 2048;
|
|
const clientKeyBytes = Math.ceil(clientKeyLength / 8);
|
|
const clientRSACipher = (await legacyCrypto.generateKey({
|
|
name: "RSA-PKCS1-v1_5",
|
|
modulusLength: clientKeyLength,
|
|
publicExponent: new Uint8Array([1, 0, 1]),
|
|
}, true, ["encrypt"])).privateKey;
|
|
const clientExportedRSAKey = await legacyCrypto.exportKey("raw", clientRSACipher);
|
|
const clientN = clientExportedRSAKey.n;
|
|
const clientE = clientExportedRSAKey.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.sQpushBytes(clientPublicKey);
|
|
this._sock.flush();
|
|
|
|
// 3: Send client random
|
|
const clientRandom = new Uint8Array(16);
|
|
window.crypto.getRandomValues(clientRandom);
|
|
const clientEncryptedRandom = await legacyCrypto.encrypt(
|
|
{ name: "RSA-PKCS1-v1_5" }, serverRSACipher, clientRandom);
|
|
const clientRandomMessage = new Uint8Array(2 + serverKeyBytes);
|
|
clientRandomMessage[0] = (serverKeyBytes & 0xff00) >>> 8;
|
|
clientRandomMessage[1] = serverKeyBytes & 0xff;
|
|
clientRandomMessage.set(clientEncryptedRandom, 2);
|
|
this._sock.sQpushBytes(clientRandomMessage);
|
|
this._sock.flush();
|
|
|
|
// 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 = await legacyCrypto.decrypt(
|
|
{ name: "RSA-PKCS1-v1_5" }, clientRSACipher, 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.sQpushBytes(await clientCipher.makeMessage(clientHash));
|
|
this._sock.flush();
|
|
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 + 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 + 16)));
|
|
if (subtype === null) {
|
|
throw new Error("RA2: failed to authenticate the message");
|
|
}
|
|
subtype = subtype[0];
|
|
let waitCredentials = this._waitCredentialsAsync(subtype);
|
|
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 waitCredentials;
|
|
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.sQpushBytes(await clientCipher.makeMessage(credentials));
|
|
this._sock.flush();
|
|
}
|
|
|
|
get hasStarted() {
|
|
return this._hasStarted;
|
|
}
|
|
|
|
set hasStarted(s) {
|
|
this._hasStarted = s;
|
|
}
|
|
}
|