mirror of
https://github.com/Ylianst/MeshCentral.git
synced 2024-12-28 08:05:53 -05:00
567 lines
20 KiB
JavaScript
567 lines
20 KiB
JavaScript
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;
|
|
}
|
|
} |