main
1import { encodeUTF8 } from './util/strings.js';
2import EventTargetMixin from './util/eventtarget.js';
3import legacyCrypto from './crypto/crypto.js';
4
5class RA2Cipher {
6 constructor() {
7 this._cipher = null;
8 this._counter = new Uint8Array(16);
9 }
10
11 async setKey(key) {
12 this._cipher = await legacyCrypto.importKey(
13 "raw", key, { name: "AES-EAX" }, false, ["encrypt, decrypt"]);
14 }
15
16 async makeMessage(message) {
17 const ad = new Uint8Array([(message.length & 0xff00) >>> 8, message.length & 0xff]);
18 const encrypted = await legacyCrypto.encrypt({
19 name: "AES-EAX",
20 iv: this._counter,
21 additionalData: ad,
22 }, this._cipher, message);
23 for (let i = 0; i < 16 && this._counter[i]++ === 255; i++);
24 const res = new Uint8Array(message.length + 2 + 16);
25 res.set(ad);
26 res.set(encrypted, 2);
27 return res;
28 }
29
30 async receiveMessage(length, encrypted) {
31 const ad = new Uint8Array([(length & 0xff00) >>> 8, length & 0xff]);
32 const res = await legacyCrypto.decrypt({
33 name: "AES-EAX",
34 iv: this._counter,
35 additionalData: ad,
36 }, this._cipher, encrypted);
37 for (let i = 0; i < 16 && this._counter[i]++ === 255; i++);
38 return res;
39 }
40}
41
42export default class RSAAESAuthenticationState extends EventTargetMixin {
43 constructor(sock, getCredentials) {
44 super();
45 this._hasStarted = false;
46 this._checkSock = null;
47 this._checkCredentials = null;
48 this._approveServerResolve = null;
49 this._sockReject = null;
50 this._credentialsReject = null;
51 this._approveServerReject = null;
52 this._sock = sock;
53 this._getCredentials = getCredentials;
54 }
55
56 _waitSockAsync(len) {
57 return new Promise((resolve, reject) => {
58 const hasData = () => !this._sock.rQwait('RA2', len);
59 if (hasData()) {
60 resolve();
61 } else {
62 this._checkSock = () => {
63 if (hasData()) {
64 resolve();
65 this._checkSock = null;
66 this._sockReject = null;
67 }
68 };
69 this._sockReject = reject;
70 }
71 });
72 }
73
74 _waitApproveKeyAsync() {
75 return new Promise((resolve, reject) => {
76 this._approveServerResolve = resolve;
77 this._approveServerReject = reject;
78 });
79 }
80
81 _waitCredentialsAsync(subtype) {
82 const hasCredentials = () => {
83 if (subtype === 1 && this._getCredentials().username !== undefined &&
84 this._getCredentials().password !== undefined) {
85 return true;
86 } else if (subtype === 2 && this._getCredentials().password !== undefined) {
87 return true;
88 }
89 return false;
90 };
91 return new Promise((resolve, reject) => {
92 if (hasCredentials()) {
93 resolve();
94 } else {
95 this._checkCredentials = () => {
96 if (hasCredentials()) {
97 resolve();
98 this._checkCredentials = null;
99 this._credentialsReject = null;
100 }
101 };
102 this._credentialsReject = reject;
103 }
104 });
105 }
106
107 checkInternalEvents() {
108 if (this._checkSock !== null) {
109 this._checkSock();
110 }
111 if (this._checkCredentials !== null) {
112 this._checkCredentials();
113 }
114 }
115
116 approveServer() {
117 if (this._approveServerResolve !== null) {
118 this._approveServerResolve();
119 this._approveServerResolve = null;
120 }
121 }
122
123 disconnect() {
124 if (this._sockReject !== null) {
125 this._sockReject(new Error("disconnect normally"));
126 this._sockReject = null;
127 }
128 if (this._credentialsReject !== null) {
129 this._credentialsReject(new Error("disconnect normally"));
130 this._credentialsReject = null;
131 }
132 if (this._approveServerReject !== null) {
133 this._approveServerReject(new Error("disconnect normally"));
134 this._approveServerReject = null;
135 }
136 }
137
138 async negotiateRA2neAuthAsync() {
139 this._hasStarted = true;
140 // 1: Receive server public key
141 await this._waitSockAsync(4);
142 const serverKeyLengthBuffer = this._sock.rQpeekBytes(4);
143 const serverKeyLength = this._sock.rQshift32();
144 if (serverKeyLength < 1024) {
145 throw new Error("RA2: server public key is too short: " + serverKeyLength);
146 } else if (serverKeyLength > 8192) {
147 throw new Error("RA2: server public key is too long: " + serverKeyLength);
148 }
149 const serverKeyBytes = Math.ceil(serverKeyLength / 8);
150 await this._waitSockAsync(serverKeyBytes * 2);
151 const serverN = this._sock.rQshiftBytes(serverKeyBytes);
152 const serverE = this._sock.rQshiftBytes(serverKeyBytes);
153 const serverRSACipher = await legacyCrypto.importKey(
154 "raw", { n: serverN, e: serverE }, { name: "RSA-PKCS1-v1_5" }, false, ["encrypt"]);
155 const serverPublickey = new Uint8Array(4 + serverKeyBytes * 2);
156 serverPublickey.set(serverKeyLengthBuffer);
157 serverPublickey.set(serverN, 4);
158 serverPublickey.set(serverE, 4 + serverKeyBytes);
159
160 // verify server public key
161 let approveKey = this._waitApproveKeyAsync();
162 this.dispatchEvent(new CustomEvent("serververification", {
163 detail: { type: "RSA", publickey: serverPublickey }
164 }));
165 await approveKey;
166
167 // 2: Send client public key
168 const clientKeyLength = 2048;
169 const clientKeyBytes = Math.ceil(clientKeyLength / 8);
170 const clientRSACipher = (await legacyCrypto.generateKey({
171 name: "RSA-PKCS1-v1_5",
172 modulusLength: clientKeyLength,
173 publicExponent: new Uint8Array([1, 0, 1]),
174 }, true, ["encrypt"])).privateKey;
175 const clientExportedRSAKey = await legacyCrypto.exportKey("raw", clientRSACipher);
176 const clientN = clientExportedRSAKey.n;
177 const clientE = clientExportedRSAKey.e;
178 const clientPublicKey = new Uint8Array(4 + clientKeyBytes * 2);
179 clientPublicKey[0] = (clientKeyLength & 0xff000000) >>> 24;
180 clientPublicKey[1] = (clientKeyLength & 0xff0000) >>> 16;
181 clientPublicKey[2] = (clientKeyLength & 0xff00) >>> 8;
182 clientPublicKey[3] = clientKeyLength & 0xff;
183 clientPublicKey.set(clientN, 4);
184 clientPublicKey.set(clientE, 4 + clientKeyBytes);
185 this._sock.sQpushBytes(clientPublicKey);
186 this._sock.flush();
187
188 // 3: Send client random
189 const clientRandom = new Uint8Array(16);
190 window.crypto.getRandomValues(clientRandom);
191 const clientEncryptedRandom = await legacyCrypto.encrypt(
192 { name: "RSA-PKCS1-v1_5" }, serverRSACipher, clientRandom);
193 const clientRandomMessage = new Uint8Array(2 + serverKeyBytes);
194 clientRandomMessage[0] = (serverKeyBytes & 0xff00) >>> 8;
195 clientRandomMessage[1] = serverKeyBytes & 0xff;
196 clientRandomMessage.set(clientEncryptedRandom, 2);
197 this._sock.sQpushBytes(clientRandomMessage);
198 this._sock.flush();
199
200 // 4: Receive server random
201 await this._waitSockAsync(2);
202 if (this._sock.rQshift16() !== clientKeyBytes) {
203 throw new Error("RA2: wrong encrypted message length");
204 }
205 const serverEncryptedRandom = this._sock.rQshiftBytes(clientKeyBytes);
206 const serverRandom = await legacyCrypto.decrypt(
207 { name: "RSA-PKCS1-v1_5" }, clientRSACipher, serverEncryptedRandom);
208 if (serverRandom === null || serverRandom.length !== 16) {
209 throw new Error("RA2: corrupted server encrypted random");
210 }
211
212 // 5: Compute session keys and set ciphers
213 let clientSessionKey = new Uint8Array(32);
214 let serverSessionKey = new Uint8Array(32);
215 clientSessionKey.set(serverRandom);
216 clientSessionKey.set(clientRandom, 16);
217 serverSessionKey.set(clientRandom);
218 serverSessionKey.set(serverRandom, 16);
219 clientSessionKey = await window.crypto.subtle.digest("SHA-1", clientSessionKey);
220 clientSessionKey = new Uint8Array(clientSessionKey).slice(0, 16);
221 serverSessionKey = await window.crypto.subtle.digest("SHA-1", serverSessionKey);
222 serverSessionKey = new Uint8Array(serverSessionKey).slice(0, 16);
223 const clientCipher = new RA2Cipher();
224 await clientCipher.setKey(clientSessionKey);
225 const serverCipher = new RA2Cipher();
226 await serverCipher.setKey(serverSessionKey);
227
228 // 6: Compute and exchange hashes
229 let serverHash = new Uint8Array(8 + serverKeyBytes * 2 + clientKeyBytes * 2);
230 let clientHash = new Uint8Array(8 + serverKeyBytes * 2 + clientKeyBytes * 2);
231 serverHash.set(serverPublickey);
232 serverHash.set(clientPublicKey, 4 + serverKeyBytes * 2);
233 clientHash.set(clientPublicKey);
234 clientHash.set(serverPublickey, 4 + clientKeyBytes * 2);
235 serverHash = await window.crypto.subtle.digest("SHA-1", serverHash);
236 clientHash = await window.crypto.subtle.digest("SHA-1", clientHash);
237 serverHash = new Uint8Array(serverHash);
238 clientHash = new Uint8Array(clientHash);
239 this._sock.sQpushBytes(await clientCipher.makeMessage(clientHash));
240 this._sock.flush();
241 await this._waitSockAsync(2 + 20 + 16);
242 if (this._sock.rQshift16() !== 20) {
243 throw new Error("RA2: wrong server hash");
244 }
245 const serverHashReceived = await serverCipher.receiveMessage(
246 20, this._sock.rQshiftBytes(20 + 16));
247 if (serverHashReceived === null) {
248 throw new Error("RA2: failed to authenticate the message");
249 }
250 for (let i = 0; i < 20; i++) {
251 if (serverHashReceived[i] !== serverHash[i]) {
252 throw new Error("RA2: wrong server hash");
253 }
254 }
255
256 // 7: Receive subtype
257 await this._waitSockAsync(2 + 1 + 16);
258 if (this._sock.rQshift16() !== 1) {
259 throw new Error("RA2: wrong subtype");
260 }
261 let subtype = (await serverCipher.receiveMessage(
262 1, this._sock.rQshiftBytes(1 + 16)));
263 if (subtype === null) {
264 throw new Error("RA2: failed to authenticate the message");
265 }
266 subtype = subtype[0];
267 let waitCredentials = this._waitCredentialsAsync(subtype);
268 if (subtype === 1) {
269 if (this._getCredentials().username === undefined ||
270 this._getCredentials().password === undefined) {
271 this.dispatchEvent(new CustomEvent(
272 "credentialsrequired",
273 { detail: { types: ["username", "password"] } }));
274 }
275 } else if (subtype === 2) {
276 if (this._getCredentials().password === undefined) {
277 this.dispatchEvent(new CustomEvent(
278 "credentialsrequired",
279 { detail: { types: ["password"] } }));
280 }
281 } else {
282 throw new Error("RA2: wrong subtype");
283 }
284 await waitCredentials;
285 let username;
286 if (subtype === 1) {
287 username = encodeUTF8(this._getCredentials().username).slice(0, 255);
288 } else {
289 username = "";
290 }
291 const password = encodeUTF8(this._getCredentials().password).slice(0, 255);
292 const credentials = new Uint8Array(username.length + password.length + 2);
293 credentials[0] = username.length;
294 credentials[username.length + 1] = password.length;
295 for (let i = 0; i < username.length; i++) {
296 credentials[i + 1] = username.charCodeAt(i);
297 }
298 for (let i = 0; i < password.length; i++) {
299 credentials[username.length + 2 + i] = password.charCodeAt(i);
300 }
301 this._sock.sQpushBytes(await clientCipher.makeMessage(credentials));
302 this._sock.flush();
303 }
304
305 get hasStarted() {
306 return this._hasStarted;
307 }
308
309 set hasStarted(s) {
310 this._hasStarted = s;
311 }
312}