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; } }