1 | "use strict";
|
2 | Object.defineProperty(exports, "__esModule", { value: true });
|
3 | exports.HAPServer = exports.HAPServerEventTypes = exports.HAPPairingHTTPCode = exports.HAPHTTPCode = exports.HAPStatus = exports.TLVErrorCode = void 0;
|
4 | exports.IsKnownHAPStatusError = IsKnownHAPStatusError;
|
5 | const tslib_1 = require("tslib");
|
6 | const crypto_1 = tslib_1.__importDefault(require("crypto"));
|
7 | const debug_1 = tslib_1.__importDefault(require("debug"));
|
8 | const events_1 = require("events");
|
9 | const fast_srp_hap_1 = require("fast-srp-hap");
|
10 | const tweetnacl_1 = tslib_1.__importDefault(require("tweetnacl"));
|
11 | const url_1 = require("url");
|
12 | const internal_types_1 = require("../internal-types");
|
13 | const eventedhttp_1 = require("./util/eventedhttp");
|
14 | const hapCrypto = tslib_1.__importStar(require("./util/hapCrypto"));
|
15 | const once_1 = require("./util/once");
|
16 | const tlv = tslib_1.__importStar(require("./util/tlv"));
|
17 | const debug = (0, debug_1.default)("HAP-NodeJS:HAPServer");
|
18 |
|
19 |
|
20 |
|
21 |
|
22 |
|
23 | var TLVErrorCode;
|
24 | (function (TLVErrorCode) {
|
25 |
|
26 | TLVErrorCode[TLVErrorCode["UNKNOWN"] = 1] = "UNKNOWN";
|
27 | TLVErrorCode[TLVErrorCode["INVALID_REQUEST"] = 2] = "INVALID_REQUEST";
|
28 |
|
29 | TLVErrorCode[TLVErrorCode["AUTHENTICATION"] = 2] = "AUTHENTICATION";
|
30 | TLVErrorCode[TLVErrorCode["BACKOFF"] = 3] = "BACKOFF";
|
31 | TLVErrorCode[TLVErrorCode["MAX_PEERS"] = 4] = "MAX_PEERS";
|
32 | TLVErrorCode[TLVErrorCode["MAX_TRIES"] = 5] = "MAX_TRIES";
|
33 | TLVErrorCode[TLVErrorCode["UNAVAILABLE"] = 6] = "UNAVAILABLE";
|
34 | TLVErrorCode[TLVErrorCode["BUSY"] = 7] = "BUSY";
|
35 | })(TLVErrorCode || (exports.TLVErrorCode = TLVErrorCode = {}));
|
36 |
|
37 |
|
38 |
|
39 | var HAPStatus;
|
40 | (function (HAPStatus) {
|
41 |
|
42 | |
43 |
|
44 |
|
45 | HAPStatus[HAPStatus["SUCCESS"] = 0] = "SUCCESS";
|
46 | |
47 |
|
48 |
|
49 | HAPStatus[HAPStatus["INSUFFICIENT_PRIVILEGES"] = -70401] = "INSUFFICIENT_PRIVILEGES";
|
50 | |
51 |
|
52 |
|
53 | HAPStatus[HAPStatus["SERVICE_COMMUNICATION_FAILURE"] = -70402] = "SERVICE_COMMUNICATION_FAILURE";
|
54 | |
55 |
|
56 |
|
57 | HAPStatus[HAPStatus["RESOURCE_BUSY"] = -70403] = "RESOURCE_BUSY";
|
58 | |
59 |
|
60 |
|
61 | HAPStatus[HAPStatus["READ_ONLY_CHARACTERISTIC"] = -70404] = "READ_ONLY_CHARACTERISTIC";
|
62 | |
63 |
|
64 |
|
65 | HAPStatus[HAPStatus["WRITE_ONLY_CHARACTERISTIC"] = -70405] = "WRITE_ONLY_CHARACTERISTIC";
|
66 | |
67 |
|
68 |
|
69 | HAPStatus[HAPStatus["NOTIFICATION_NOT_SUPPORTED"] = -70406] = "NOTIFICATION_NOT_SUPPORTED";
|
70 | |
71 |
|
72 |
|
73 | HAPStatus[HAPStatus["OUT_OF_RESOURCE"] = -70407] = "OUT_OF_RESOURCE";
|
74 | |
75 |
|
76 |
|
77 | HAPStatus[HAPStatus["OPERATION_TIMED_OUT"] = -70408] = "OPERATION_TIMED_OUT";
|
78 | |
79 |
|
80 |
|
81 | HAPStatus[HAPStatus["RESOURCE_DOES_NOT_EXIST"] = -70409] = "RESOURCE_DOES_NOT_EXIST";
|
82 | |
83 |
|
84 |
|
85 | HAPStatus[HAPStatus["INVALID_VALUE_IN_REQUEST"] = -70410] = "INVALID_VALUE_IN_REQUEST";
|
86 | |
87 |
|
88 |
|
89 | HAPStatus[HAPStatus["INSUFFICIENT_AUTHORIZATION"] = -70411] = "INSUFFICIENT_AUTHORIZATION";
|
90 | |
91 |
|
92 |
|
93 | HAPStatus[HAPStatus["NOT_ALLOWED_IN_CURRENT_STATE"] = -70412] = "NOT_ALLOWED_IN_CURRENT_STATE";
|
94 |
|
95 | })(HAPStatus || (exports.HAPStatus = HAPStatus = {}));
|
96 |
|
97 |
|
98 |
|
99 |
|
100 |
|
101 | function IsKnownHAPStatusError(status) {
|
102 | return (
|
103 |
|
104 | status >= -70412 &&
|
105 |
|
106 | status <= -70401 );
|
107 | }
|
108 |
|
109 |
|
110 |
|
111 |
|
112 |
|
113 |
|
114 |
|
115 |
|
116 |
|
117 |
|
118 | var HAPHTTPCode;
|
119 | (function (HAPHTTPCode) {
|
120 |
|
121 | HAPHTTPCode[HAPHTTPCode["OK"] = 200] = "OK";
|
122 | HAPHTTPCode[HAPHTTPCode["NO_CONTENT"] = 204] = "NO_CONTENT";
|
123 | HAPHTTPCode[HAPHTTPCode["MULTI_STATUS"] = 207] = "MULTI_STATUS";
|
124 |
|
125 | HAPHTTPCode[HAPHTTPCode["BAD_REQUEST"] = 400] = "BAD_REQUEST";
|
126 | HAPHTTPCode[HAPHTTPCode["NOT_FOUND"] = 404] = "NOT_FOUND";
|
127 | HAPHTTPCode[HAPHTTPCode["UNPROCESSABLE_ENTITY"] = 422] = "UNPROCESSABLE_ENTITY";
|
128 |
|
129 | HAPHTTPCode[HAPHTTPCode["INTERNAL_SERVER_ERROR"] = 500] = "INTERNAL_SERVER_ERROR";
|
130 | HAPHTTPCode[HAPHTTPCode["SERVICE_UNAVAILABLE"] = 503] = "SERVICE_UNAVAILABLE";
|
131 | })(HAPHTTPCode || (exports.HAPHTTPCode = HAPHTTPCode = {}));
|
132 |
|
133 |
|
134 |
|
135 |
|
136 |
|
137 |
|
138 | var HAPPairingHTTPCode;
|
139 | (function (HAPPairingHTTPCode) {
|
140 |
|
141 | HAPPairingHTTPCode[HAPPairingHTTPCode["OK"] = 200] = "OK";
|
142 | HAPPairingHTTPCode[HAPPairingHTTPCode["BAD_REQUEST"] = 400] = "BAD_REQUEST";
|
143 | HAPPairingHTTPCode[HAPPairingHTTPCode["METHOD_NOT_ALLOWED"] = 405] = "METHOD_NOT_ALLOWED";
|
144 | HAPPairingHTTPCode[HAPPairingHTTPCode["TOO_MANY_REQUESTS"] = 429] = "TOO_MANY_REQUESTS";
|
145 | HAPPairingHTTPCode[HAPPairingHTTPCode["CONNECTION_AUTHORIZATION_REQUIRED"] = 470] = "CONNECTION_AUTHORIZATION_REQUIRED";
|
146 | HAPPairingHTTPCode[HAPPairingHTTPCode["INTERNAL_SERVER_ERROR"] = 500] = "INTERNAL_SERVER_ERROR";
|
147 | })(HAPPairingHTTPCode || (exports.HAPPairingHTTPCode = HAPPairingHTTPCode = {}));
|
148 |
|
149 |
|
150 |
|
151 | var HAPServerEventTypes;
|
152 | (function (HAPServerEventTypes) {
|
153 | |
154 |
|
155 |
|
156 | HAPServerEventTypes["LISTENING"] = "listening";
|
157 | |
158 |
|
159 |
|
160 |
|
161 | HAPServerEventTypes["IDENTIFY"] = "identify";
|
162 | HAPServerEventTypes["ADD_PAIRING"] = "add-pairing";
|
163 | HAPServerEventTypes["REMOVE_PAIRING"] = "remove-pairing";
|
164 | HAPServerEventTypes["LIST_PAIRINGS"] = "list-pairings";
|
165 | |
166 |
|
167 |
|
168 |
|
169 |
|
170 | HAPServerEventTypes["PAIR"] = "pair";
|
171 | |
172 |
|
173 |
|
174 |
|
175 |
|
176 |
|
177 | HAPServerEventTypes["ACCESSORIES"] = "accessories";
|
178 | |
179 |
|
180 |
|
181 |
|
182 |
|
183 |
|
184 | HAPServerEventTypes["GET_CHARACTERISTICS"] = "get-characteristics";
|
185 | |
186 |
|
187 |
|
188 |
|
189 |
|
190 |
|
191 | HAPServerEventTypes["SET_CHARACTERISTICS"] = "set-characteristics";
|
192 | HAPServerEventTypes["REQUEST_RESOURCE"] = "request-resource";
|
193 | HAPServerEventTypes["CONNECTION_CLOSED"] = "connection-closed";
|
194 | })(HAPServerEventTypes || (exports.HAPServerEventTypes = HAPServerEventTypes = {}));
|
195 |
|
196 |
|
197 |
|
198 |
|
199 |
|
200 |
|
201 |
|
202 |
|
203 |
|
204 |
|
205 |
|
206 |
|
207 |
|
208 |
|
209 |
|
210 |
|
211 |
|
212 |
|
213 |
|
214 |
|
215 |
|
216 |
|
217 | class HAPServer extends events_1.EventEmitter {
|
218 | accessoryInfo;
|
219 | httpServer;
|
220 | unsuccessfulPairAttempts = 0;
|
221 | allowInsecureRequest;
|
222 | constructor(accessoryInfo) {
|
223 | super();
|
224 | this.accessoryInfo = accessoryInfo;
|
225 | this.allowInsecureRequest = false;
|
226 |
|
227 | this.httpServer = new eventedhttp_1.EventedHTTPServer();
|
228 | this.httpServer.on("listening" , this.onListening.bind(this));
|
229 | this.httpServer.on("request" , this.handleRequestOnHAPConnection.bind(this));
|
230 | this.httpServer.on("connection-closed" , this.handleConnectionClosed.bind(this));
|
231 | }
|
232 | listen(port = 0, host) {
|
233 | if (host === "::") {
|
234 |
|
235 |
|
236 | host = undefined;
|
237 | }
|
238 | this.httpServer.listen(port, host);
|
239 | }
|
240 | stop() {
|
241 | this.httpServer.stop();
|
242 | }
|
243 | destroy() {
|
244 | this.stop();
|
245 | this.removeAllListeners();
|
246 | }
|
247 | |
248 |
|
249 |
|
250 |
|
251 |
|
252 |
|
253 |
|
254 |
|
255 |
|
256 |
|
257 |
|
258 | sendEventNotifications(aid, iid, value, originator, immediateDelivery) {
|
259 | try {
|
260 | this.httpServer.broadcastEvent(aid, iid, value, originator, immediateDelivery);
|
261 | }
|
262 | catch (error) {
|
263 | console.warn("[" + this.accessoryInfo.username + "] Error when sending event notifications: " + error.message);
|
264 | }
|
265 | }
|
266 | onListening(port, hostname) {
|
267 | this.emit("listening" , port, hostname);
|
268 | }
|
269 |
|
270 | handleRequestOnHAPConnection(connection, request, response) {
|
271 | debug("[%s] HAP Request: %s %s", this.accessoryInfo.username, request.method, request.url);
|
272 | const buffers = [];
|
273 | request.on("data", data => buffers.push(data));
|
274 | request.on("end", () => {
|
275 | const url = new url_1.URL(request.url, "http://hap-nodejs.local");
|
276 | const handler = this.getHandler(url);
|
277 | if (!handler) {
|
278 | debug("[%s] WARNING: Handler for %s not implemented", this.accessoryInfo.username, request.url);
|
279 | response.writeHead(404 , { "Content-Type": "application/hap+json" });
|
280 | response.end(JSON.stringify({ status: -70409 }));
|
281 | }
|
282 | else {
|
283 | const data = Buffer.concat(buffers);
|
284 | try {
|
285 | handler(connection, url, request, data, response);
|
286 | }
|
287 | catch (error) {
|
288 | debug("[%s] Error executing route handler: %s", this.accessoryInfo.username, error.stack);
|
289 | response.writeHead(500 , { "Content-Type": "application/hap+json" });
|
290 | response.end(JSON.stringify({ status: -70403 }));
|
291 | }
|
292 | }
|
293 | });
|
294 | }
|
295 | handleConnectionClosed(connection) {
|
296 | this.emit("connection-closed" , connection);
|
297 | }
|
298 | getHandler(url) {
|
299 | switch (url.pathname.toLowerCase()) {
|
300 | case "/identify":
|
301 | return this.handleIdentifyRequest.bind(this);
|
302 | case "/pair-setup":
|
303 | return this.handlePairSetup.bind(this);
|
304 | case "/pair-verify":
|
305 | return this.handlePairVerify.bind(this);
|
306 | case "/pairings":
|
307 | return this.handlePairings.bind(this);
|
308 | case "/accessories":
|
309 | return this.handleAccessories.bind(this);
|
310 | case "/characteristics":
|
311 | return this.handleCharacteristics.bind(this);
|
312 | case "/prepare":
|
313 | return this.handlePrepareWrite.bind(this);
|
314 | case "/resource":
|
315 | return this.handleResource.bind(this);
|
316 | default:
|
317 | return undefined;
|
318 | }
|
319 | }
|
320 | |
321 |
|
322 |
|
323 | handleIdentifyRequest(connection, url, request, data, response) {
|
324 |
|
325 | if (this.accessoryInfo.paired() && !this.allowInsecureRequest) {
|
326 | response.writeHead(400 , { "Content-Type": "application/hap+json" });
|
327 | response.end(JSON.stringify({ status: -70401 }));
|
328 | return;
|
329 | }
|
330 | this.emit("identify" , (0, once_1.once)(err => {
|
331 | if (!err) {
|
332 | debug("[%s] Identification success", this.accessoryInfo.username);
|
333 | response.writeHead(204 );
|
334 | response.end();
|
335 | }
|
336 | else {
|
337 | debug("[%s] Identification error: %s", this.accessoryInfo.username, err.message);
|
338 | response.writeHead(500 , { "Content-Type": "application/hap+json" });
|
339 | response.end(JSON.stringify({ status: -70403 }));
|
340 | }
|
341 | }));
|
342 | }
|
343 | handlePairSetup(connection, url, request, data, response) {
|
344 |
|
345 | if (!this.allowInsecureRequest && this.accessoryInfo.paired()) {
|
346 | response.writeHead(200 , { "Content-Type": "application/pairing+tlv8" });
|
347 | response.end(tlv.encode(6 , 2 , 7 , 6 ));
|
348 | return;
|
349 | }
|
350 | if (this.unsuccessfulPairAttempts > 100) {
|
351 | debug("[%s] Reached maximum amount of unsuccessful pair attempts!", this.accessoryInfo.username);
|
352 | response.writeHead(200 , { "Content-Type": "application/pairing+tlv8" });
|
353 | response.end(tlv.encode(6 , 2 , 7 , 5 ));
|
354 | return;
|
355 | }
|
356 | const tlvData = tlv.decode(data);
|
357 | const sequence = tlvData[6 ][0];
|
358 | if (sequence === 1 ) {
|
359 | this.handlePairSetupM1(connection, request, response);
|
360 | }
|
361 | else if (sequence === 3 && connection._pairSetupState === 2 ) {
|
362 | this.handlePairSetupM3(connection, request, response, tlvData);
|
363 | }
|
364 | else if (sequence === 5 && connection._pairSetupState === 4 ) {
|
365 | this.handlePairSetupM5(connection, request, response, tlvData);
|
366 | }
|
367 | else {
|
368 |
|
369 | response.writeHead(400 , { "Content-Type": "application/pairing+tlv8" });
|
370 | response.end(tlv.encode(6 , sequence + 1, 7 , 1 ));
|
371 | return;
|
372 | }
|
373 | }
|
374 | handlePairSetupM1(connection, request, response) {
|
375 | debug("[%s] Pair step 1/5", this.accessoryInfo.username);
|
376 | const salt = crypto_1.default.randomBytes(16);
|
377 | const srpParams = fast_srp_hap_1.SRP.params.hap;
|
378 | fast_srp_hap_1.SRP.genKey(32).then(key => {
|
379 |
|
380 | const srpServer = new fast_srp_hap_1.SrpServer(srpParams, salt, Buffer.from("Pair-Setup"), Buffer.from(this.accessoryInfo.pincode), key);
|
381 | const srpB = srpServer.computeB();
|
382 |
|
383 | connection.srpServer = srpServer;
|
384 | response.writeHead(200 , { "Content-Type": "application/pairing+tlv8" });
|
385 | response.end(tlv.encode(6 , 2 , 2 , salt, 3 , srpB));
|
386 | connection._pairSetupState = 2 ;
|
387 | }).catch(error => {
|
388 | debug("[%s] Error occurred when generating srp key: %s", this.accessoryInfo.username, error.message);
|
389 | response.writeHead(200 , { "Content-Type": "application/pairing+tlv8" });
|
390 | response.end(tlv.encode(6 , 2 , 7 , 1 ));
|
391 | return;
|
392 | });
|
393 | }
|
394 | handlePairSetupM3(connection, request, response, tlvData) {
|
395 | debug("[%s] Pair step 2/5", this.accessoryInfo.username);
|
396 | const A = tlvData[3 ];
|
397 | const M1 = tlvData[4 ];
|
398 |
|
399 | const srpServer = connection.srpServer;
|
400 | srpServer.setA(A);
|
401 | try {
|
402 | srpServer.checkM1(M1);
|
403 | }
|
404 | catch (err) {
|
405 |
|
406 | this.unsuccessfulPairAttempts++;
|
407 | debug("[%s] Error while checking pincode: %s", this.accessoryInfo.username, err.message);
|
408 | response.writeHead(200 , { "Content-Type": "application/pairing+tlv8" });
|
409 | response.end(tlv.encode(6 , 4 , 7 , 2 ));
|
410 | connection._pairSetupState = undefined;
|
411 | return;
|
412 | }
|
413 |
|
414 | const M2 = srpServer.computeM2();
|
415 | response.writeHead(200 , { "Content-Type": "application/pairing+tlv8" });
|
416 | response.end(tlv.encode(6 , 4 , 4 , M2));
|
417 | connection._pairSetupState = 4 ;
|
418 | }
|
419 | handlePairSetupM5(connection, request, response, tlvData) {
|
420 | debug("[%s] Pair step 3/5", this.accessoryInfo.username);
|
421 |
|
422 | const srpServer = connection.srpServer;
|
423 | const encryptedData = tlvData[5 ];
|
424 | const messageData = Buffer.alloc(encryptedData.length - 16);
|
425 | const authTagData = Buffer.alloc(16);
|
426 | encryptedData.copy(messageData, 0, 0, encryptedData.length - 16);
|
427 | encryptedData.copy(authTagData, 0, encryptedData.length - 16, encryptedData.length);
|
428 | const S_private = srpServer.computeK();
|
429 | const encSalt = Buffer.from("Pair-Setup-Encrypt-Salt");
|
430 | const encInfo = Buffer.from("Pair-Setup-Encrypt-Info");
|
431 | const outputKey = hapCrypto.HKDF("sha512", encSalt, S_private, encInfo, 32);
|
432 | let plaintext;
|
433 | try {
|
434 | plaintext = hapCrypto.chacha20_poly1305_decryptAndVerify(outputKey, Buffer.from("PS-Msg05"), null, messageData, authTagData);
|
435 | }
|
436 | catch (error) {
|
437 | debug("[%s] Error while decrypting and verifying M5 subTlv: %s", this.accessoryInfo.username);
|
438 | response.writeHead(200 , { "Content-Type": "application/pairing+tlv8" });
|
439 | response.end(tlv.encode(6 , 4 , 7 , 2 ));
|
440 | connection._pairSetupState = undefined;
|
441 | return;
|
442 | }
|
443 |
|
444 | const M5Packet = tlv.decode(plaintext);
|
445 | const clientUsername = M5Packet[1 ];
|
446 | const clientLTPK = M5Packet[3 ];
|
447 | const clientProof = M5Packet[10 ];
|
448 | this.handlePairSetupM5_2(connection, request, response, clientUsername, clientLTPK, clientProof, outputKey);
|
449 | }
|
450 |
|
451 | handlePairSetupM5_2(connection, request, response, clientUsername, clientLTPK, clientProof, hkdfEncKey) {
|
452 | debug("[%s] Pair step 4/5", this.accessoryInfo.username);
|
453 | const S_private = connection.srpServer.computeK();
|
454 | const controllerSalt = Buffer.from("Pair-Setup-Controller-Sign-Salt");
|
455 | const controllerInfo = Buffer.from("Pair-Setup-Controller-Sign-Info");
|
456 | const outputKey = hapCrypto.HKDF("sha512", controllerSalt, S_private, controllerInfo, 32);
|
457 | const completeData = Buffer.concat([outputKey, clientUsername, clientLTPK]);
|
458 | if (!tweetnacl_1.default.sign.detached.verify(completeData, clientProof, clientLTPK)) {
|
459 | debug("[%s] Invalid signature", this.accessoryInfo.username);
|
460 | response.writeHead(200 , { "Content-Type": "application/pairing+tlv8" });
|
461 | response.end(tlv.encode(6 , 6 , 7 , 2 ));
|
462 | connection._pairSetupState = undefined;
|
463 | return;
|
464 | }
|
465 | this.handlePairSetupM5_3(connection, request, response, clientUsername, clientLTPK, hkdfEncKey);
|
466 | }
|
467 |
|
468 | handlePairSetupM5_3(connection, request, response, clientUsername, clientLTPK, hkdfEncKey) {
|
469 | debug("[%s] Pair step 5/5", this.accessoryInfo.username);
|
470 | const S_private = connection.srpServer.computeK();
|
471 | const accessorySalt = Buffer.from("Pair-Setup-Accessory-Sign-Salt");
|
472 | const accessoryInfo = Buffer.from("Pair-Setup-Accessory-Sign-Info");
|
473 | const outputKey = hapCrypto.HKDF("sha512", accessorySalt, S_private, accessoryInfo, 32);
|
474 | const serverLTPK = this.accessoryInfo.signPk;
|
475 | const usernameData = Buffer.from(this.accessoryInfo.username);
|
476 | const material = Buffer.concat([outputKey, usernameData, serverLTPK]);
|
477 | const privateKey = Buffer.from(this.accessoryInfo.signSk);
|
478 | const serverProof = tweetnacl_1.default.sign.detached(material, privateKey);
|
479 | const message = tlv.encode(1 , usernameData, 3 , serverLTPK, 10 , serverProof);
|
480 | const encrypted = hapCrypto.chacha20_poly1305_encryptAndSeal(hkdfEncKey, Buffer.from("PS-Msg06"), null, message);
|
481 |
|
482 | this.emit("pair" , clientUsername.toString(), clientLTPK, (0, once_1.once)(err => {
|
483 | if (err) {
|
484 | debug("[%s] Error adding pairing info: %s", this.accessoryInfo.username, err.message);
|
485 | response.writeHead(200 , { "Content-Type": "application/pairing+tlv8" });
|
486 | response.end(tlv.encode(6 , 6 , 7 , 1 ));
|
487 | connection._pairSetupState = undefined;
|
488 | return;
|
489 | }
|
490 |
|
491 | response.writeHead(200 , { "Content-Type": "application/pairing+tlv8" });
|
492 | response.end(tlv.encode(6 , 6 , 5 , Buffer.concat([encrypted.ciphertext, encrypted.authTag])));
|
493 | connection._pairSetupState = undefined;
|
494 | }));
|
495 | }
|
496 | handlePairVerify(connection, url, request, data, response) {
|
497 | const tlvData = tlv.decode(data);
|
498 | const sequence = tlvData[6 ][0];
|
499 | if (sequence === 1 ) {
|
500 | this.handlePairVerifyM1(connection, request, response, tlvData);
|
501 | }
|
502 | else if (sequence === 3 && connection._pairVerifyState === 2 ) {
|
503 | this.handlePairVerifyM3(connection, request, response, tlvData);
|
504 | }
|
505 | else {
|
506 |
|
507 | response.writeHead(400 , { "Content-Type": "application/pairing+tlv8" });
|
508 | response.end(tlv.encode(6 , sequence + 1, 7 , 1 ));
|
509 | return;
|
510 | }
|
511 | }
|
512 | handlePairVerifyM1(connection, request, response, tlvData) {
|
513 | debug("[%s] Pair verify step 1/2", this.accessoryInfo.username);
|
514 | const clientPublicKey = tlvData[3 ];
|
515 |
|
516 | const keyPair = hapCrypto.generateCurve25519KeyPair();
|
517 | const secretKey = Buffer.from(keyPair.secretKey);
|
518 | const publicKey = Buffer.from(keyPair.publicKey);
|
519 | const sharedSec = Buffer.from(hapCrypto.generateCurve25519SharedSecKey(secretKey, clientPublicKey));
|
520 | const usernameData = Buffer.from(this.accessoryInfo.username);
|
521 | const material = Buffer.concat([publicKey, usernameData, clientPublicKey]);
|
522 | const privateKey = Buffer.from(this.accessoryInfo.signSk);
|
523 | const serverProof = tweetnacl_1.default.sign.detached(material, privateKey);
|
524 | const encSalt = Buffer.from("Pair-Verify-Encrypt-Salt");
|
525 | const encInfo = Buffer.from("Pair-Verify-Encrypt-Info");
|
526 | const outputKey = hapCrypto.HKDF("sha512", encSalt, sharedSec, encInfo, 32).slice(0, 32);
|
527 | connection.encryption = new eventedhttp_1.HAPEncryption(clientPublicKey, secretKey, publicKey, sharedSec, outputKey);
|
528 |
|
529 | const message = tlv.encode(1 , usernameData, 10 , serverProof);
|
530 | const encrypted = hapCrypto.chacha20_poly1305_encryptAndSeal(outputKey, Buffer.from("PV-Msg02"), null, message);
|
531 | response.writeHead(200 , { "Content-Type": "application/pairing+tlv8" });
|
532 | response.end(tlv.encode(6 , 2 , 5 , Buffer.concat([encrypted.ciphertext, encrypted.authTag]), 3 , publicKey));
|
533 | connection._pairVerifyState = 2 ;
|
534 | }
|
535 | handlePairVerifyM3(connection, request, response, objects) {
|
536 | debug("[%s] Pair verify step 2/2", this.accessoryInfo.username);
|
537 | const encryptedData = objects[5 ];
|
538 | const messageData = Buffer.alloc(encryptedData.length - 16);
|
539 | const authTagData = Buffer.alloc(16);
|
540 | encryptedData.copy(messageData, 0, 0, encryptedData.length - 16);
|
541 | encryptedData.copy(authTagData, 0, encryptedData.length - 16, encryptedData.length);
|
542 |
|
543 | const enc = connection.encryption;
|
544 | let plaintext;
|
545 | try {
|
546 | plaintext = hapCrypto.chacha20_poly1305_decryptAndVerify(enc.hkdfPairEncryptionKey, Buffer.from("PV-Msg03"), null, messageData, authTagData);
|
547 | }
|
548 | catch (error) {
|
549 | debug("[%s] M3: Failed to decrypt and/or verify", this.accessoryInfo.username);
|
550 | response.writeHead(200 , { "Content-Type": "application/pairing+tlv8" });
|
551 | response.end(tlv.encode(6 , 4 , 7 , 2 ));
|
552 | connection._pairVerifyState = undefined;
|
553 | return;
|
554 | }
|
555 | const decoded = tlv.decode(plaintext);
|
556 | const clientUsername = decoded[1 ];
|
557 | const proof = decoded[10 ];
|
558 | const material = Buffer.concat([enc.clientPublicKey, clientUsername, enc.publicKey]);
|
559 |
|
560 | const clientPublicKey = this.accessoryInfo.getClientPublicKey(clientUsername.toString());
|
561 |
|
562 |
|
563 | if (!clientPublicKey) {
|
564 | debug("[%s] Client %s attempting to verify, but we are not paired; rejecting client", this.accessoryInfo.username, clientUsername);
|
565 | response.writeHead(200 , { "Content-Type": "application/pairing+tlv8" });
|
566 | response.end(tlv.encode(6 , 4 , 7 , 2 ));
|
567 | connection._pairVerifyState = undefined;
|
568 | return;
|
569 | }
|
570 | if (!tweetnacl_1.default.sign.detached.verify(material, proof, clientPublicKey)) {
|
571 | debug("[%s] Client %s provided an invalid signature", this.accessoryInfo.username, clientUsername);
|
572 | response.writeHead(200 , { "Content-Type": "application/pairing+tlv8" });
|
573 | response.end(tlv.encode(6 , 4 , 7 , 2 ));
|
574 | connection._pairVerifyState = undefined;
|
575 | return;
|
576 | }
|
577 | debug("[%s] Client %s verification complete", this.accessoryInfo.username, clientUsername);
|
578 | response.writeHead(200 , { "Content-Type": "application/pairing+tlv8" });
|
579 | response.end(tlv.encode(6 , 4 ));
|
580 |
|
581 |
|
582 |
|
583 | const encSalt = Buffer.from("Control-Salt");
|
584 | const infoRead = Buffer.from("Control-Read-Encryption-Key");
|
585 | const infoWrite = Buffer.from("Control-Write-Encryption-Key");
|
586 | enc.accessoryToControllerKey = hapCrypto.HKDF("sha512", encSalt, enc.sharedSecret, infoRead, 32);
|
587 | enc.controllerToAccessoryKey = hapCrypto.HKDF("sha512", encSalt, enc.sharedSecret, infoWrite, 32);
|
588 |
|
589 | connection.connectionAuthenticated(clientUsername.toString());
|
590 | connection._pairVerifyState = undefined;
|
591 | }
|
592 | handlePairings(connection, url, request, data, response) {
|
593 |
|
594 | if (!this.allowInsecureRequest && !connection.isAuthenticated()) {
|
595 | response.writeHead(470 , { "Content-Type": "application/hap+json" });
|
596 | response.end(JSON.stringify({ status: -70401 }));
|
597 | return;
|
598 | }
|
599 | const objects = tlv.decode(data);
|
600 | const method = objects[0 ][0];
|
601 | const state = objects[6 ][0];
|
602 | if (state !== 1 ) {
|
603 | return;
|
604 | }
|
605 | if (method === 3 ) {
|
606 | const identifier = objects[1 ].toString();
|
607 | const publicKey = objects[3 ];
|
608 | const permissions = objects[11 ][0];
|
609 | this.emit("add-pairing" , connection, identifier, publicKey, permissions, (0, once_1.once)((error) => {
|
610 | if (error > 0) {
|
611 | debug("[%s] Pairings: failed ADD_PAIRING with code %d", this.accessoryInfo.username, error);
|
612 | response.writeHead(200 , { "Content-Type": "application/pairing+tlv8" });
|
613 | response.end(tlv.encode(6 , 2 , 7 , error));
|
614 | return;
|
615 | }
|
616 | response.writeHead(200 , { "Content-Type": "application/pairing+tlv8" });
|
617 | response.end(tlv.encode(6 , 2 ));
|
618 | debug("[%s] Pairings: successfully executed ADD_PAIRING", this.accessoryInfo.username);
|
619 | }));
|
620 | }
|
621 | else if (method === 4 ) {
|
622 | const identifier = objects[1 ].toString();
|
623 | this.emit("remove-pairing" , connection, identifier, (0, once_1.once)((error) => {
|
624 | if (error > 0) {
|
625 | debug("[%s] Pairings: failed REMOVE_PAIRING with code %d", this.accessoryInfo.username, error);
|
626 | response.writeHead(200 , { "Content-Type": "application/pairing+tlv8" });
|
627 | response.end(tlv.encode(6 , 2 , 7 , error));
|
628 | return;
|
629 | }
|
630 | response.writeHead(200 , { "Content-Type": "application/pairing+tlv8" });
|
631 | response.end(tlv.encode(6 , 2 ));
|
632 | debug("[%s] Pairings: successfully executed REMOVE_PAIRING", this.accessoryInfo.username);
|
633 | }));
|
634 | }
|
635 | else if (method === 5 ) {
|
636 | this.emit("list-pairings" , connection, (0, once_1.once)((error, data) => {
|
637 | if (error > 0) {
|
638 | debug("[%s] Pairings: failed LIST_PAIRINGS with code %d", this.accessoryInfo.username, error);
|
639 | response.writeHead(200 , { "Content-Type": "application/pairing+tlv8" });
|
640 | response.end(tlv.encode(6 , 2 , 7 , error));
|
641 | return;
|
642 | }
|
643 |
|
644 | const tlvList = [];
|
645 | data.forEach((value, index) => {
|
646 | if (index > 0) {
|
647 | tlvList.push(255 , Buffer.alloc(0));
|
648 | }
|
649 | tlvList.push(1 , value.username, 3 , value.publicKey, 11 , value.permission);
|
650 | });
|
651 | const list = tlv.encode(6 , 2 , ...tlvList);
|
652 | response.writeHead(200 , { "Content-Type": "application/pairing+tlv8" });
|
653 | response.end(list);
|
654 | debug("[%s] Pairings: successfully executed LIST_PAIRINGS", this.accessoryInfo.username);
|
655 | }));
|
656 | }
|
657 | }
|
658 | handleAccessories(connection, url, request, data, response) {
|
659 | if (!this.allowInsecureRequest && !connection.isAuthenticated()) {
|
660 | response.writeHead(470 , { "Content-Type": "application/hap+json" });
|
661 | response.end(JSON.stringify({ status: -70401 }));
|
662 | return;
|
663 | }
|
664 |
|
665 | this.emit("accessories" , connection, (0, once_1.once)((error, result) => {
|
666 | if (error) {
|
667 | response.writeHead(error.httpCode, { "Content-Type": "application/hap+json" });
|
668 | response.end(JSON.stringify({ status: error.status }));
|
669 | }
|
670 | else {
|
671 | response.writeHead(200 , { "Content-Type": "application/hap+json" });
|
672 | response.end(JSON.stringify(result));
|
673 | }
|
674 | }));
|
675 | }
|
676 | handleCharacteristics(connection, url, request, data, response) {
|
677 | if (!this.allowInsecureRequest && !connection.isAuthenticated()) {
|
678 | response.writeHead(470 , { "Content-Type": "application/hap+json" });
|
679 | response.end(JSON.stringify({ status: -70401 }));
|
680 | return;
|
681 | }
|
682 | if (request.method === "GET") {
|
683 | const searchParams = url.searchParams;
|
684 | const idParam = searchParams.get("id");
|
685 | if (!idParam) {
|
686 | response.writeHead(400 , { "Content-Type": "application/hap+json" });
|
687 | response.end(JSON.stringify({ status: -70410 }));
|
688 | return;
|
689 | }
|
690 | const ids = [];
|
691 | for (const entry of idParam.split(",")) {
|
692 | const split = entry.split(".");
|
693 | ids.push({
|
694 | aid: parseInt(split[0], 10),
|
695 | iid: parseInt(split[1], 10),
|
696 | });
|
697 | }
|
698 | const readRequest = {
|
699 | ids: ids,
|
700 | includeMeta: (0, internal_types_1.consideredTrue)(searchParams.get("meta")),
|
701 | includePerms: (0, internal_types_1.consideredTrue)(searchParams.get("perms")),
|
702 | includeType: (0, internal_types_1.consideredTrue)(searchParams.get("type")),
|
703 | includeEvent: (0, internal_types_1.consideredTrue)(searchParams.get("ev")),
|
704 | };
|
705 | this.emit("get-characteristics" , connection, readRequest, (0, once_1.once)((error, readResponse) => {
|
706 | if (error) {
|
707 | response.writeHead(error.httpCode, { "Content-Type": "application/hap+json" });
|
708 | response.end(JSON.stringify({ status: error.status }));
|
709 | return;
|
710 | }
|
711 | const characteristics = readResponse.characteristics;
|
712 | let errorOccurred = false;
|
713 | for (const data of characteristics) {
|
714 | if (data.status) {
|
715 | errorOccurred = true;
|
716 | break;
|
717 | }
|
718 | }
|
719 | if (errorOccurred) {
|
720 | for (const data of characteristics) {
|
721 | if (!data.status) {
|
722 | data.status = 0 ;
|
723 | }
|
724 | }
|
725 | }
|
726 |
|
727 | response.writeHead(errorOccurred ? 207 : 200 , { "Content-Type": "application/hap+json" });
|
728 | response.end(JSON.stringify({ characteristics: characteristics }));
|
729 | }));
|
730 | }
|
731 | else if (request.method === "PUT") {
|
732 | if (!connection.isAuthenticated()) {
|
733 | if (!request.headers || (request.headers && request.headers.authorization !== this.accessoryInfo.pincode)) {
|
734 | response.writeHead(470 , { "Content-Type": "application/hap+json" });
|
735 | response.end(JSON.stringify({ status: -70401 }));
|
736 | return;
|
737 | }
|
738 | }
|
739 | if (data.length === 0) {
|
740 | response.writeHead(400 , { "Content-Type": "application/hap+json" });
|
741 | response.end(JSON.stringify({ status: -70410 }));
|
742 | return;
|
743 | }
|
744 | const writeRequest = JSON.parse(data.toString("utf8"));
|
745 | this.emit("set-characteristics" , connection, writeRequest, (0, once_1.once)((error, writeResponse) => {
|
746 | if (error) {
|
747 | response.writeHead(error.httpCode, { "Content-Type": "application/hap+json" });
|
748 | response.end(JSON.stringify({ status: error.status }));
|
749 | return;
|
750 | }
|
751 | const characteristics = writeResponse.characteristics;
|
752 | let multiStatus = false;
|
753 | for (const data of characteristics) {
|
754 | if (data.status || data.value !== undefined) {
|
755 |
|
756 | multiStatus = true;
|
757 | break;
|
758 | }
|
759 | }
|
760 | if (multiStatus) {
|
761 |
|
762 | response.writeHead(207 , { "Content-Type": "application/hap+json" });
|
763 | response.end(JSON.stringify({ characteristics: characteristics }));
|
764 | }
|
765 | else {
|
766 |
|
767 | response.writeHead(204 );
|
768 | response.end();
|
769 | }
|
770 | }));
|
771 | }
|
772 | else {
|
773 | response.writeHead(400 , { "Content-Type": "application/hap+json" });
|
774 | response.end(JSON.stringify({ status: -70410 }));
|
775 | }
|
776 | }
|
777 | handlePrepareWrite(connection, url, request, data, response) {
|
778 | if (!this.allowInsecureRequest && !connection.isAuthenticated()) {
|
779 | response.writeHead(470 , { "Content-Type": "application/hap+json" });
|
780 | response.end(JSON.stringify({ status: -70401 }));
|
781 | return;
|
782 | }
|
783 | if (request.method === "PUT") {
|
784 | if (data.length === 0) {
|
785 | response.writeHead(400 , { "Content-Type": "application/hap+json" });
|
786 | response.end(JSON.stringify({ status: -70410 }));
|
787 | return;
|
788 | }
|
789 | const prepareRequest = JSON.parse(data.toString());
|
790 | if (prepareRequest.pid && prepareRequest.ttl) {
|
791 | debug("[%s] Received prepare write request with pid %d and ttl %d", this.accessoryInfo.username, prepareRequest.pid, prepareRequest.ttl);
|
792 | if (connection.timedWriteTimeout) {
|
793 | clearTimeout(connection.timedWriteTimeout);
|
794 | }
|
795 | connection.timedWritePid = prepareRequest.pid;
|
796 | connection.timedWriteTimeout = setTimeout(() => {
|
797 | debug("[%s] Timed write request timed out for pid %d", this.accessoryInfo.username, prepareRequest.pid);
|
798 | connection.timedWritePid = undefined;
|
799 | connection.timedWriteTimeout = undefined;
|
800 | }, prepareRequest.ttl);
|
801 | response.writeHead(200 , { "Content-Type": "application/hap+json" });
|
802 | response.end(JSON.stringify({ status: 0 }));
|
803 | return;
|
804 | }
|
805 | else {
|
806 | response.writeHead(400 , { "Content-Type": "application/hap+json" });
|
807 | response.end(JSON.stringify({ status: -70410 }));
|
808 | }
|
809 | }
|
810 | else {
|
811 | response.writeHead(400 , { "Content-Type": "application/hap+json" });
|
812 | response.end(JSON.stringify({ status: -70410 }));
|
813 | }
|
814 | }
|
815 | handleResource(connection, url, request, data, response) {
|
816 | if (!connection.isAuthenticated()) {
|
817 | if (!(this.allowInsecureRequest && request.headers && request.headers.authorization === this.accessoryInfo.pincode)) {
|
818 | response.writeHead(470 , { "Content-Type": "application/hap+json" });
|
819 | response.end(JSON.stringify({ status: -70401 }));
|
820 | return;
|
821 | }
|
822 | }
|
823 | if (request.method === "POST") {
|
824 | if (data.length === 0) {
|
825 | response.writeHead(400 , { "Content-Type": "application/hap+json" });
|
826 | response.end(JSON.stringify({ status: -70410 }));
|
827 | return;
|
828 | }
|
829 | const resourceRequest = JSON.parse(data.toString());
|
830 |
|
831 | this.emit("request-resource" , resourceRequest, (0, once_1.once)((error, resource) => {
|
832 | if (error) {
|
833 | response.writeHead(error.httpCode, { "Content-Type": "application/hap+json" });
|
834 | response.end(JSON.stringify({ status: error.status }));
|
835 | }
|
836 | else {
|
837 | response.writeHead(200 , { "Content-Type": "image/jpeg" });
|
838 | response.end(resource);
|
839 | }
|
840 | }));
|
841 | }
|
842 | else {
|
843 | response.writeHead(400 , { "Content-Type": "application/hap+json" });
|
844 | response.end(JSON.stringify({ status: -70410 }));
|
845 | }
|
846 | }
|
847 | }
|
848 | exports.HAPServer = HAPServer;
|
849 |
|
\ | No newline at end of file |