mirror of
https://github.com/Ylianst/MeshCentral.git
synced 2025-01-12 15:33:20 -05:00
1139a37338
Signed-off-by: si458 <simonsmith5521@gmail.com>
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;
|
|
}
|
|
}
|