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.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 = 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 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 = (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.send(clientPublicKey); // 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.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 = 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.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 + 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]; 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; } }