UNPKG

hap-nodejs

Version:

HAP-NodeJS is a Node.js implementation of HomeKit Accessory Server.

826 lines 51.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.HAPServer = exports.HAPServerEventTypes = exports.HAPPairingHTTPCode = exports.HAPHTTPCode = exports.HAPStatus = exports.TLVErrorCode = void 0; exports.IsKnownHAPStatusError = IsKnownHAPStatusError; const tslib_1 = require("tslib"); const crypto_1 = tslib_1.__importDefault(require("crypto")); const debug_1 = tslib_1.__importDefault(require("debug")); const events_1 = require("events"); const fast_srp_hap_1 = require("fast-srp-hap"); const tweetnacl_1 = tslib_1.__importDefault(require("tweetnacl")); const url_1 = require("url"); const internal_types_1 = require("../internal-types"); const eventedhttp_1 = require("./util/eventedhttp"); const hapCrypto = tslib_1.__importStar(require("./util/hapCrypto")); const once_1 = require("./util/once"); const tlv = tslib_1.__importStar(require("./util/tlv")); const debug = (0, debug_1.default)("HAP-NodeJS:HAPServer"); /** * TLV error codes for the `TLVValues.ERROR_CODE` field. * * @group HAP Accessory Server */ var TLVErrorCode; (function (TLVErrorCode) { // noinspection JSUnusedGlobalSymbols TLVErrorCode[TLVErrorCode["UNKNOWN"] = 1] = "UNKNOWN"; TLVErrorCode[TLVErrorCode["INVALID_REQUEST"] = 2] = "INVALID_REQUEST"; // eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values TLVErrorCode[TLVErrorCode["AUTHENTICATION"] = 2] = "AUTHENTICATION"; TLVErrorCode[TLVErrorCode["BACKOFF"] = 3] = "BACKOFF"; TLVErrorCode[TLVErrorCode["MAX_PEERS"] = 4] = "MAX_PEERS"; TLVErrorCode[TLVErrorCode["MAX_TRIES"] = 5] = "MAX_TRIES"; TLVErrorCode[TLVErrorCode["UNAVAILABLE"] = 6] = "UNAVAILABLE"; TLVErrorCode[TLVErrorCode["BUSY"] = 7] = "BUSY"; // cannot accept pairing request at this time })(TLVErrorCode || (exports.TLVErrorCode = TLVErrorCode = {})); /** * @group HAP Accessory Server */ var HAPStatus; (function (HAPStatus) { // noinspection JSUnusedGlobalSymbols /** * Success of the request. */ HAPStatus[HAPStatus["SUCCESS"] = 0] = "SUCCESS"; /** * The request was rejected due to insufficient privileges. */ HAPStatus[HAPStatus["INSUFFICIENT_PRIVILEGES"] = -70401] = "INSUFFICIENT_PRIVILEGES"; /** * Operation failed due to some communication failure with the characteristic. */ HAPStatus[HAPStatus["SERVICE_COMMUNICATION_FAILURE"] = -70402] = "SERVICE_COMMUNICATION_FAILURE"; /** * The resource is busy. Try again. */ HAPStatus[HAPStatus["RESOURCE_BUSY"] = -70403] = "RESOURCE_BUSY"; /** * Cannot write a read-only characteristic ({@link Perms.PAIRED_WRITE} not defined). */ HAPStatus[HAPStatus["READ_ONLY_CHARACTERISTIC"] = -70404] = "READ_ONLY_CHARACTERISTIC"; /** * Cannot read from a write-only characteristic ({@link Perms.PAIRED_READ} not defined). */ HAPStatus[HAPStatus["WRITE_ONLY_CHARACTERISTIC"] = -70405] = "WRITE_ONLY_CHARACTERISTIC"; /** * Event notifications are not supported for the requested characteristic ({@link Perms.NOTIFY} not defined). */ HAPStatus[HAPStatus["NOTIFICATION_NOT_SUPPORTED"] = -70406] = "NOTIFICATION_NOT_SUPPORTED"; /** * The device is out of resources to process the request. */ HAPStatus[HAPStatus["OUT_OF_RESOURCE"] = -70407] = "OUT_OF_RESOURCE"; /** * The operation timed out. */ HAPStatus[HAPStatus["OPERATION_TIMED_OUT"] = -70408] = "OPERATION_TIMED_OUT"; /** * The given resource does not exist. */ HAPStatus[HAPStatus["RESOURCE_DOES_NOT_EXIST"] = -70409] = "RESOURCE_DOES_NOT_EXIST"; /** * Received an invalid value in the given request for the given characteristic. */ HAPStatus[HAPStatus["INVALID_VALUE_IN_REQUEST"] = -70410] = "INVALID_VALUE_IN_REQUEST"; /** * Insufficient authorization. */ HAPStatus[HAPStatus["INSUFFICIENT_AUTHORIZATION"] = -70411] = "INSUFFICIENT_AUTHORIZATION"; /** * Operation not allowed in the current state. */ HAPStatus[HAPStatus["NOT_ALLOWED_IN_CURRENT_STATE"] = -70412] = "NOT_ALLOWED_IN_CURRENT_STATE"; // when adding new status codes, remember to update bounds in IsKnownHAPStatusError below })(HAPStatus || (exports.HAPStatus = HAPStatus = {})); /** * Determines if the given status code is a known {@link HAPStatus} error code. * * @group HAP Accessory Server */ function IsKnownHAPStatusError(status) { return ( // Lower bound (most negative error code) status >= -70412 /* HAPStatus.NOT_ALLOWED_IN_CURRENT_STATE */ && // Upper bound (negative error code closest to zero) status <= -70401 /* HAPStatus.INSUFFICIENT_PRIVILEGES */); } /** * Those status codes are the one listed as appropriate for the HAP spec! * * When the response is a client error 4xx or server error 5xx, the response * must include a status {@link HAPStatus} property. * * When the response is a MULTI_STATUS EVERY entry in the characteristics property MUST include a status property (even success). * * @group HAP Accessory Server */ var HAPHTTPCode; (function (HAPHTTPCode) { // noinspection JSUnusedGlobalSymbols HAPHTTPCode[HAPHTTPCode["OK"] = 200] = "OK"; HAPHTTPCode[HAPHTTPCode["NO_CONTENT"] = 204] = "NO_CONTENT"; HAPHTTPCode[HAPHTTPCode["MULTI_STATUS"] = 207] = "MULTI_STATUS"; // client error HAPHTTPCode[HAPHTTPCode["BAD_REQUEST"] = 400] = "BAD_REQUEST"; HAPHTTPCode[HAPHTTPCode["NOT_FOUND"] = 404] = "NOT_FOUND"; HAPHTTPCode[HAPHTTPCode["UNPROCESSABLE_ENTITY"] = 422] = "UNPROCESSABLE_ENTITY"; // server error HAPHTTPCode[HAPHTTPCode["INTERNAL_SERVER_ERROR"] = 500] = "INTERNAL_SERVER_ERROR"; HAPHTTPCode[HAPHTTPCode["SERVICE_UNAVAILABLE"] = 503] = "SERVICE_UNAVAILABLE"; })(HAPHTTPCode || (exports.HAPHTTPCode = HAPHTTPCode = {})); /** * When in a request is made to the pairing endpoints, and mime type is 'application/pairing+tlv8' * one should use the below status codes. * * @group HAP Accessory Server */ var HAPPairingHTTPCode; (function (HAPPairingHTTPCode) { // noinspection JSUnusedGlobalSymbols HAPPairingHTTPCode[HAPPairingHTTPCode["OK"] = 200] = "OK"; HAPPairingHTTPCode[HAPPairingHTTPCode["BAD_REQUEST"] = 400] = "BAD_REQUEST"; HAPPairingHTTPCode[HAPPairingHTTPCode["METHOD_NOT_ALLOWED"] = 405] = "METHOD_NOT_ALLOWED"; HAPPairingHTTPCode[HAPPairingHTTPCode["TOO_MANY_REQUESTS"] = 429] = "TOO_MANY_REQUESTS"; HAPPairingHTTPCode[HAPPairingHTTPCode["CONNECTION_AUTHORIZATION_REQUIRED"] = 470] = "CONNECTION_AUTHORIZATION_REQUIRED"; HAPPairingHTTPCode[HAPPairingHTTPCode["INTERNAL_SERVER_ERROR"] = 500] = "INTERNAL_SERVER_ERROR"; })(HAPPairingHTTPCode || (exports.HAPPairingHTTPCode = HAPPairingHTTPCode = {})); /** * @group HAP Accessory Server */ var HAPServerEventTypes; (function (HAPServerEventTypes) { /** * Emitted when the server is fully set up and ready to receive connections. */ HAPServerEventTypes["LISTENING"] = "listening"; /** * Emitted when a client wishes for this server to identify itself before pairing. You must call the * callback to respond to the client with success. */ HAPServerEventTypes["IDENTIFY"] = "identify"; HAPServerEventTypes["ADD_PAIRING"] = "add-pairing"; HAPServerEventTypes["REMOVE_PAIRING"] = "remove-pairing"; HAPServerEventTypes["LIST_PAIRINGS"] = "list-pairings"; /** * This event is emitted when a client completes the "pairing" process and exchanges encryption keys. * Note that this does not mean the "Add Accessory" process in iOS has completed. * You must call the callback to complete the process. */ HAPServerEventTypes["PAIR"] = "pair"; /** * This event is emitted when a client requests the complete representation of Accessory data for * this Accessory (for instance, what services, characteristics, etc. are supported) and any bridged * Accessories in the case of a Bridge Accessory. The listener must call the provided callback function * when the accessory data is ready. We will automatically JSON.stringify the data. */ HAPServerEventTypes["ACCESSORIES"] = "accessories"; /** * This event is emitted when a client wishes to retrieve the current value of one or more characteristics. * The listener must call the provided callback function when the values are ready. iOS clients can typically * wait up to 10 seconds for this call to return. We will automatically JSON.stringify the data (which must * be an array) and wrap it in an object with a top-level "characteristics" property. */ HAPServerEventTypes["GET_CHARACTERISTICS"] = "get-characteristics"; /** * This event is emitted when a client wishes to set the current value of one or more characteristics and/or * subscribe to one or more events. The 'events' param is an initially-empty object, associated with the current * connection, on which you may store event registration keys for later processing. The listener must call * the provided callback when the request has been processed. */ HAPServerEventTypes["SET_CHARACTERISTICS"] = "set-characteristics"; HAPServerEventTypes["REQUEST_RESOURCE"] = "request-resource"; HAPServerEventTypes["CONNECTION_CLOSED"] = "connection-closed"; })(HAPServerEventTypes || (exports.HAPServerEventTypes = HAPServerEventTypes = {})); /** * The actual HAP server that iOS devices talk to. * * Notes * ----- * It turns out that the IP-based version of HomeKit's HAP protocol operates over a sort of pseudo-HTTP. * Accessories are meant to host a TCP socket server that initially behaves exactly as an HTTP/1.1 server. * So iOS devices will open up a long-lived connection to this server and begin issuing HTTP requests. * So far, this conforms with HTTP/1.1 Keepalive. However, after the "pairing" process is complete, the * connection is expected to be "upgraded" to support full-packet encryption of both HTTP headers and data. * This encryption is NOT SSL. It is a customized ChaCha20+Poly1305 encryption layer. * * Additionally, this "HTTP Server" supports sending "event" responses at any time without warning. The iOS * device simply keeps the connection open after it's finished with HTTP request/response traffic, and while * the connection is open, the server can elect to issue "EVENT/1.0 200 OK" HTTP-style responses. These are * typically sent to inform the iOS device of a characteristic change for the accessory (like "Door was Unlocked"). * * See {@link EventedHTTPServer} for more detail on the implementation of this protocol. * * @group HAP Accessory Server */ // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging class HAPServer extends events_1.EventEmitter { accessoryInfo; httpServer; unsuccessfulPairAttempts = 0; // after 100 unsuccessful attempts the server won't accept any further attempts. Will currently be reset on a reboot allowInsecureRequest; constructor(accessoryInfo) { super(); this.accessoryInfo = accessoryInfo; this.allowInsecureRequest = false; // internal server that does all the actual communication this.httpServer = new eventedhttp_1.EventedHTTPServer(); this.httpServer.on("listening" /* EventedHTTPServerEvent.LISTENING */, this.onListening.bind(this)); this.httpServer.on("request" /* EventedHTTPServerEvent.REQUEST */, this.handleRequestOnHAPConnection.bind(this)); this.httpServer.on("connection-closed" /* EventedHTTPServerEvent.CONNECTION_CLOSED */, this.handleConnectionClosed.bind(this)); } listen(port = 0, host) { if (host === "::") { // this will work around "EAFNOSUPPORT: address family not supported" errors // on systems where IPv6 is not supported/enabled, we just use the node default then by supplying undefined host = undefined; } this.httpServer.listen(port, host); } stop() { this.httpServer.stop(); } destroy() { this.stop(); this.removeAllListeners(); } /** * Send an even notification for given characteristic and changed value to all connected clients. * If `originator` is specified, the given {@link HAPConnection} will be excluded from the broadcast. * * @param aid - The accessory id of the updated characteristic. * @param iid - The instance id of the updated characteristic. * @param value - The newly set value of the characteristic. * @param originator - If specified, the connection will not get an event message. * @param immediateDelivery - The HAP spec requires some characteristics to be delivery immediately. * Namely, for the {@link Characteristic.ButtonEvent} and the {@link Characteristic.ProgrammableSwitchEvent} characteristics. */ sendEventNotifications(aid, iid, value, originator, immediateDelivery) { try { this.httpServer.broadcastEvent(aid, iid, value, originator, immediateDelivery); } catch (error) { console.warn("[" + this.accessoryInfo.username + "] Error when sending event notifications: " + error.message); } } onListening(port, hostname) { this.emit("listening" /* HAPServerEventTypes.LISTENING */, port, hostname); } // Called when an HTTP request was detected. handleRequestOnHAPConnection(connection, request, response) { debug("[%s] HAP Request: %s %s", this.accessoryInfo.username, request.method, request.url); const buffers = []; request.on("data", data => buffers.push(data)); request.on("end", () => { const url = new url_1.URL(request.url, "http://hap-nodejs.local"); // parse the url (query strings etc) const handler = this.getHandler(url); if (!handler) { debug("[%s] WARNING: Handler for %s not implemented", this.accessoryInfo.username, request.url); response.writeHead(404 /* HAPHTTPCode.NOT_FOUND */, { "Content-Type": "application/hap+json" /* HAPMimeTypes.HAP_JSON */ }); response.end(JSON.stringify({ status: -70409 /* HAPStatus.RESOURCE_DOES_NOT_EXIST */ })); } else { const data = Buffer.concat(buffers); try { handler(connection, url, request, data, response); } catch (error) { debug("[%s] Error executing route handler: %s", this.accessoryInfo.username, error.stack); response.writeHead(500 /* HAPHTTPCode.INTERNAL_SERVER_ERROR */, { "Content-Type": "application/hap+json" /* HAPMimeTypes.HAP_JSON */ }); response.end(JSON.stringify({ status: -70403 /* HAPStatus.RESOURCE_BUSY */ })); // resource busy try again, does somehow fit? } } }); } handleConnectionClosed(connection) { this.emit("connection-closed" /* HAPServerEventTypes.CONNECTION_CLOSED */, connection); } getHandler(url) { switch (url.pathname.toLowerCase()) { case "/identify": return this.handleIdentifyRequest.bind(this); case "/pair-setup": return this.handlePairSetup.bind(this); case "/pair-verify": return this.handlePairVerify.bind(this); case "/pairings": return this.handlePairings.bind(this); case "/accessories": return this.handleAccessories.bind(this); case "/characteristics": return this.handleCharacteristics.bind(this); case "/prepare": return this.handlePrepareWrite.bind(this); case "/resource": return this.handleResource.bind(this); default: return undefined; } } /** * UNPAIRED Accessory identification. */ handleIdentifyRequest(connection, url, request, data, response) { // POST body is empty if (this.accessoryInfo.paired() && !this.allowInsecureRequest) { response.writeHead(400 /* HAPHTTPCode.BAD_REQUEST */, { "Content-Type": "application/hap+json" /* HAPMimeTypes.HAP_JSON */ }); response.end(JSON.stringify({ status: -70401 /* HAPStatus.INSUFFICIENT_PRIVILEGES */ })); return; } this.emit("identify" /* HAPServerEventTypes.IDENTIFY */, (0, once_1.once)(err => { if (!err) { debug("[%s] Identification success", this.accessoryInfo.username); response.writeHead(204 /* HAPHTTPCode.NO_CONTENT */); response.end(); } else { debug("[%s] Identification error: %s", this.accessoryInfo.username, err.message); response.writeHead(500 /* HAPHTTPCode.INTERNAL_SERVER_ERROR */, { "Content-Type": "application/hap+json" /* HAPMimeTypes.HAP_JSON */ }); response.end(JSON.stringify({ status: -70403 /* HAPStatus.RESOURCE_BUSY */ })); } })); } handlePairSetup(connection, url, request, data, response) { // Can only be directly paired with one iOS device if (!this.allowInsecureRequest && this.accessoryInfo.paired()) { response.writeHead(200 /* HAPPairingHTTPCode.OK */, { "Content-Type": "application/pairing+tlv8" }); response.end(tlv.encode(6 /* TLVValues.STATE */, 2 /* PairingStates.M2 */, 7 /* TLVValues.ERROR_CODE */, 6 /* TLVErrorCode.UNAVAILABLE */)); return; } if (this.unsuccessfulPairAttempts > 100) { debug("[%s] Reached maximum amount of unsuccessful pair attempts!", this.accessoryInfo.username); response.writeHead(200 /* HAPPairingHTTPCode.OK */, { "Content-Type": "application/pairing+tlv8" }); response.end(tlv.encode(6 /* TLVValues.STATE */, 2 /* PairingStates.M2 */, 7 /* TLVValues.ERROR_CODE */, 5 /* TLVErrorCode.MAX_TRIES */)); return; } const tlvData = tlv.decode(data); const sequence = tlvData[6 /* TLVValues.SEQUENCE_NUM */][0]; // value is single byte with sequence number if (sequence === 1 /* PairingStates.M1 */) { this.handlePairSetupM1(connection, request, response); } else if (sequence === 3 /* PairingStates.M3 */ && connection._pairSetupState === 2 /* PairingStates.M2 */) { this.handlePairSetupM3(connection, request, response, tlvData); } else if (sequence === 5 /* PairingStates.M5 */ && connection._pairSetupState === 4 /* PairingStates.M4 */) { this.handlePairSetupM5(connection, request, response, tlvData); } else { // Invalid state/sequence number response.writeHead(400 /* HAPPairingHTTPCode.BAD_REQUEST */, { "Content-Type": "application/pairing+tlv8" }); response.end(tlv.encode(6 /* TLVValues.STATE */, sequence + 1, 7 /* TLVValues.ERROR_CODE */, 1 /* TLVErrorCode.UNKNOWN */)); return; } } handlePairSetupM1(connection, request, response) { debug("[%s] Pair step 1/5", this.accessoryInfo.username); const salt = crypto_1.default.randomBytes(16); const srpParams = fast_srp_hap_1.SRP.params.hap; fast_srp_hap_1.SRP.genKey(32).then(key => { // create a new SRP server const srpServer = new fast_srp_hap_1.SrpServer(srpParams, salt, Buffer.from("Pair-Setup"), Buffer.from(this.accessoryInfo.pincode), key); const srpB = srpServer.computeB(); // attach it to the current TCP session connection.srpServer = srpServer; response.writeHead(200 /* HAPPairingHTTPCode.OK */, { "Content-Type": "application/pairing+tlv8" }); response.end(tlv.encode(6 /* TLVValues.SEQUENCE_NUM */, 2 /* PairingStates.M2 */, 2 /* TLVValues.SALT */, salt, 3 /* TLVValues.PUBLIC_KEY */, srpB)); connection._pairSetupState = 2 /* PairingStates.M2 */; }).catch(error => { debug("[%s] Error occurred when generating srp key: %s", this.accessoryInfo.username, error.message); response.writeHead(200 /* HAPPairingHTTPCode.OK */, { "Content-Type": "application/pairing+tlv8" }); response.end(tlv.encode(6 /* TLVValues.STATE */, 2 /* PairingStates.M2 */, 7 /* TLVValues.ERROR_CODE */, 1 /* TLVErrorCode.UNKNOWN */)); return; }); } handlePairSetupM3(connection, request, response, tlvData) { debug("[%s] Pair step 2/5", this.accessoryInfo.username); const A = tlvData[3 /* TLVValues.PUBLIC_KEY */]; // "A is a public key that exists only for a single login session." const M1 = tlvData[4 /* TLVValues.PASSWORD_PROOF */]; // "M1 is the proof that you actually know your own password." // pull the SRP server we created in stepOne out of the current session const srpServer = connection.srpServer; srpServer.setA(A); try { srpServer.checkM1(M1); } catch (err) { // most likely the client supplied an incorrect pincode. this.unsuccessfulPairAttempts++; debug("[%s] Error while checking pincode: %s", this.accessoryInfo.username, err.message); response.writeHead(200 /* HAPPairingHTTPCode.OK */, { "Content-Type": "application/pairing+tlv8" }); response.end(tlv.encode(6 /* TLVValues.SEQUENCE_NUM */, 4 /* PairingStates.M4 */, 7 /* TLVValues.ERROR_CODE */, 2 /* TLVErrorCode.AUTHENTICATION */)); connection._pairSetupState = undefined; return; } // "M2 is the proof that the server actually knows your password." const M2 = srpServer.computeM2(); response.writeHead(200 /* HAPPairingHTTPCode.OK */, { "Content-Type": "application/pairing+tlv8" }); response.end(tlv.encode(6 /* TLVValues.SEQUENCE_NUM */, 4 /* PairingStates.M4 */, 4 /* TLVValues.PASSWORD_PROOF */, M2)); connection._pairSetupState = 4 /* PairingStates.M4 */; } handlePairSetupM5(connection, request, response, tlvData) { debug("[%s] Pair step 3/5", this.accessoryInfo.username); // pull the SRP server we created in stepOne out of the current session const srpServer = connection.srpServer; const encryptedData = tlvData[5 /* TLVValues.ENCRYPTED_DATA */]; const messageData = Buffer.alloc(encryptedData.length - 16); const authTagData = Buffer.alloc(16); encryptedData.copy(messageData, 0, 0, encryptedData.length - 16); encryptedData.copy(authTagData, 0, encryptedData.length - 16, encryptedData.length); const S_private = srpServer.computeK(); const encSalt = Buffer.from("Pair-Setup-Encrypt-Salt"); const encInfo = Buffer.from("Pair-Setup-Encrypt-Info"); const outputKey = hapCrypto.HKDF("sha512", encSalt, S_private, encInfo, 32); let plaintext; try { plaintext = hapCrypto.chacha20_poly1305_decryptAndVerify(outputKey, Buffer.from("PS-Msg05"), null, messageData, authTagData); } catch (error) { debug("[%s] Error while decrypting and verifying M5 subTlv: %s", this.accessoryInfo.username); response.writeHead(200 /* HAPPairingHTTPCode.OK */, { "Content-Type": "application/pairing+tlv8" }); response.end(tlv.encode(6 /* TLVValues.SEQUENCE_NUM */, 4 /* PairingStates.M4 */, 7 /* TLVValues.ERROR_CODE */, 2 /* TLVErrorCode.AUTHENTICATION */)); connection._pairSetupState = undefined; return; } // decode the client payload and pass it on to the next step const M5Packet = tlv.decode(plaintext); const clientUsername = M5Packet[1 /* TLVValues.USERNAME */]; const clientLTPK = M5Packet[3 /* TLVValues.PUBLIC_KEY */]; const clientProof = M5Packet[10 /* TLVValues.PROOF */]; this.handlePairSetupM5_2(connection, request, response, clientUsername, clientLTPK, clientProof, outputKey); } // M5-2 handlePairSetupM5_2(connection, request, response, clientUsername, clientLTPK, clientProof, hkdfEncKey) { debug("[%s] Pair step 4/5", this.accessoryInfo.username); const S_private = connection.srpServer.computeK(); const controllerSalt = Buffer.from("Pair-Setup-Controller-Sign-Salt"); const controllerInfo = Buffer.from("Pair-Setup-Controller-Sign-Info"); const outputKey = hapCrypto.HKDF("sha512", controllerSalt, S_private, controllerInfo, 32); const completeData = Buffer.concat([outputKey, clientUsername, clientLTPK]); if (!tweetnacl_1.default.sign.detached.verify(completeData, clientProof, clientLTPK)) { debug("[%s] Invalid signature", this.accessoryInfo.username); response.writeHead(200 /* HAPPairingHTTPCode.OK */, { "Content-Type": "application/pairing+tlv8" }); response.end(tlv.encode(6 /* TLVValues.SEQUENCE_NUM */, 6 /* PairingStates.M6 */, 7 /* TLVValues.ERROR_CODE */, 2 /* TLVErrorCode.AUTHENTICATION */)); connection._pairSetupState = undefined; return; } this.handlePairSetupM5_3(connection, request, response, clientUsername, clientLTPK, hkdfEncKey); } // M5 - F + M6 handlePairSetupM5_3(connection, request, response, clientUsername, clientLTPK, hkdfEncKey) { debug("[%s] Pair step 5/5", this.accessoryInfo.username); const S_private = connection.srpServer.computeK(); const accessorySalt = Buffer.from("Pair-Setup-Accessory-Sign-Salt"); const accessoryInfo = Buffer.from("Pair-Setup-Accessory-Sign-Info"); const outputKey = hapCrypto.HKDF("sha512", accessorySalt, S_private, accessoryInfo, 32); const serverLTPK = this.accessoryInfo.signPk; const usernameData = Buffer.from(this.accessoryInfo.username); const material = Buffer.concat([outputKey, usernameData, serverLTPK]); const privateKey = Buffer.from(this.accessoryInfo.signSk); const serverProof = tweetnacl_1.default.sign.detached(material, privateKey); const message = tlv.encode(1 /* TLVValues.USERNAME */, usernameData, 3 /* TLVValues.PUBLIC_KEY */, serverLTPK, 10 /* TLVValues.PROOF */, serverProof); const encrypted = hapCrypto.chacha20_poly1305_encryptAndSeal(hkdfEncKey, Buffer.from("PS-Msg06"), null, message); // finally, notify listeners that we have been paired with a client this.emit("pair" /* HAPServerEventTypes.PAIR */, clientUsername.toString(), clientLTPK, (0, once_1.once)(err => { if (err) { debug("[%s] Error adding pairing info: %s", this.accessoryInfo.username, err.message); response.writeHead(200 /* HAPPairingHTTPCode.OK */, { "Content-Type": "application/pairing+tlv8" }); response.end(tlv.encode(6 /* TLVValues.SEQUENCE_NUM */, 6 /* PairingStates.M6 */, 7 /* TLVValues.ERROR_CODE */, 1 /* TLVErrorCode.UNKNOWN */)); connection._pairSetupState = undefined; return; } // send final pairing response to client response.writeHead(200 /* HAPPairingHTTPCode.OK */, { "Content-Type": "application/pairing+tlv8" }); response.end(tlv.encode(6 /* TLVValues.SEQUENCE_NUM */, 6 /* PairingStates.M6 */, 5 /* TLVValues.ENCRYPTED_DATA */, Buffer.concat([encrypted.ciphertext, encrypted.authTag]))); connection._pairSetupState = undefined; })); } handlePairVerify(connection, url, request, data, response) { const tlvData = tlv.decode(data); const sequence = tlvData[6 /* TLVValues.SEQUENCE_NUM */][0]; // value is single byte with sequence number if (sequence === 1 /* PairingStates.M1 */) { this.handlePairVerifyM1(connection, request, response, tlvData); } else if (sequence === 3 /* PairingStates.M3 */ && connection._pairVerifyState === 2 /* PairingStates.M2 */) { this.handlePairVerifyM3(connection, request, response, tlvData); } else { // Invalid state/sequence number response.writeHead(400 /* HAPPairingHTTPCode.BAD_REQUEST */, { "Content-Type": "application/pairing+tlv8" }); response.end(tlv.encode(6 /* TLVValues.STATE */, sequence + 1, 7 /* TLVValues.ERROR_CODE */, 1 /* TLVErrorCode.UNKNOWN */)); return; } } handlePairVerifyM1(connection, request, response, tlvData) { debug("[%s] Pair verify step 1/2", this.accessoryInfo.username); const clientPublicKey = tlvData[3 /* TLVValues.PUBLIC_KEY */]; // Buffer // generate new encryption keys for this session const keyPair = hapCrypto.generateCurve25519KeyPair(); const secretKey = Buffer.from(keyPair.secretKey); const publicKey = Buffer.from(keyPair.publicKey); const sharedSec = Buffer.from(hapCrypto.generateCurve25519SharedSecKey(secretKey, clientPublicKey)); const usernameData = Buffer.from(this.accessoryInfo.username); const material = Buffer.concat([publicKey, usernameData, clientPublicKey]); const privateKey = Buffer.from(this.accessoryInfo.signSk); const serverProof = tweetnacl_1.default.sign.detached(material, privateKey); const encSalt = Buffer.from("Pair-Verify-Encrypt-Salt"); const encInfo = Buffer.from("Pair-Verify-Encrypt-Info"); const outputKey = hapCrypto.HKDF("sha512", encSalt, sharedSec, encInfo, 32).slice(0, 32); connection.encryption = new eventedhttp_1.HAPEncryption(clientPublicKey, secretKey, publicKey, sharedSec, outputKey); // compose the response data in TLV format const message = tlv.encode(1 /* TLVValues.USERNAME */, usernameData, 10 /* TLVValues.PROOF */, serverProof); const encrypted = hapCrypto.chacha20_poly1305_encryptAndSeal(outputKey, Buffer.from("PV-Msg02"), null, message); response.writeHead(200 /* HAPPairingHTTPCode.OK */, { "Content-Type": "application/pairing+tlv8" }); response.end(tlv.encode(6 /* TLVValues.SEQUENCE_NUM */, 2 /* PairingStates.M2 */, 5 /* TLVValues.ENCRYPTED_DATA */, Buffer.concat([encrypted.ciphertext, encrypted.authTag]), 3 /* TLVValues.PUBLIC_KEY */, publicKey)); connection._pairVerifyState = 2 /* PairingStates.M2 */; } handlePairVerifyM3(connection, request, response, objects) { debug("[%s] Pair verify step 2/2", this.accessoryInfo.username); const encryptedData = objects[5 /* TLVValues.ENCRYPTED_DATA */]; const messageData = Buffer.alloc(encryptedData.length - 16); const authTagData = Buffer.alloc(16); encryptedData.copy(messageData, 0, 0, encryptedData.length - 16); encryptedData.copy(authTagData, 0, encryptedData.length - 16, encryptedData.length); // instance of HAPEncryption (created in handlePairVerifyStepOne) const enc = connection.encryption; let plaintext; try { plaintext = hapCrypto.chacha20_poly1305_decryptAndVerify(enc.hkdfPairEncryptionKey, Buffer.from("PV-Msg03"), null, messageData, authTagData); } catch (error) { debug("[%s] M3: Failed to decrypt and/or verify", this.accessoryInfo.username); response.writeHead(200 /* HAPPairingHTTPCode.OK */, { "Content-Type": "application/pairing+tlv8" }); response.end(tlv.encode(6 /* TLVValues.STATE */, 4 /* PairingStates.M4 */, 7 /* TLVValues.ERROR_CODE */, 2 /* TLVErrorCode.AUTHENTICATION */)); connection._pairVerifyState = undefined; return; } const decoded = tlv.decode(plaintext); const clientUsername = decoded[1 /* TLVValues.USERNAME */]; const proof = decoded[10 /* TLVValues.PROOF */]; const material = Buffer.concat([enc.clientPublicKey, clientUsername, enc.publicKey]); // since we're paired, we should have the public key stored for this client const clientPublicKey = this.accessoryInfo.getClientPublicKey(clientUsername.toString()); // if we're not actually paired, then there's nothing to verify - this client thinks it's paired with us, but we // disagree. Respond with invalid request (seems to match HomeKit Accessory Simulator behavior) if (!clientPublicKey) { debug("[%s] Client %s attempting to verify, but we are not paired; rejecting client", this.accessoryInfo.username, clientUsername); response.writeHead(200 /* HAPPairingHTTPCode.OK */, { "Content-Type": "application/pairing+tlv8" }); response.end(tlv.encode(6 /* TLVValues.STATE */, 4 /* PairingStates.M4 */, 7 /* TLVValues.ERROR_CODE */, 2 /* TLVErrorCode.AUTHENTICATION */)); connection._pairVerifyState = undefined; return; } if (!tweetnacl_1.default.sign.detached.verify(material, proof, clientPublicKey)) { debug("[%s] Client %s provided an invalid signature", this.accessoryInfo.username, clientUsername); response.writeHead(200 /* HAPPairingHTTPCode.OK */, { "Content-Type": "application/pairing+tlv8" }); response.end(tlv.encode(6 /* TLVValues.STATE */, 4 /* PairingStates.M4 */, 7 /* TLVValues.ERROR_CODE */, 2 /* TLVErrorCode.AUTHENTICATION */)); connection._pairVerifyState = undefined; return; } debug("[%s] Client %s verification complete", this.accessoryInfo.username, clientUsername); response.writeHead(200 /* HAPPairingHTTPCode.OK */, { "Content-Type": "application/pairing+tlv8" }); response.end(tlv.encode(6 /* TLVValues.SEQUENCE_NUM */, 4 /* PairingStates.M4 */)); // now that the client has been verified, we must "upgrade" our pseudo-HTTP connection to include // TCP-level encryption. We'll do this by adding some more encryption vars to the session, and using them // in future calls to onEncrypt, onDecrypt. const encSalt = Buffer.from("Control-Salt"); const infoRead = Buffer.from("Control-Read-Encryption-Key"); const infoWrite = Buffer.from("Control-Write-Encryption-Key"); enc.accessoryToControllerKey = hapCrypto.HKDF("sha512", encSalt, enc.sharedSecret, infoRead, 32); enc.controllerToAccessoryKey = hapCrypto.HKDF("sha512", encSalt, enc.sharedSecret, infoWrite, 32); // Our connection is now completely setup. We now want to subscribe this connection to special connection.connectionAuthenticated(clientUsername.toString()); connection._pairVerifyState = undefined; } handlePairings(connection, url, request, data, response) { // Only accept /pairing request if there is a secure session if (!this.allowInsecureRequest && !connection.isAuthenticated()) { response.writeHead(470 /* HAPPairingHTTPCode.CONNECTION_AUTHORIZATION_REQUIRED */, { "Content-Type": "application/hap+json" /* HAPMimeTypes.HAP_JSON */ }); response.end(JSON.stringify({ status: -70401 /* HAPStatus.INSUFFICIENT_PRIVILEGES */ })); return; } const objects = tlv.decode(data); const method = objects[0 /* TLVValues.METHOD */][0]; // value is single byte with request type const state = objects[6 /* TLVValues.STATE */][0]; if (state !== 1 /* PairingStates.M1 */) { return; } if (method === 3 /* PairMethods.ADD_PAIRING */) { const identifier = objects[1 /* TLVValues.IDENTIFIER */].toString(); const publicKey = objects[3 /* TLVValues.PUBLIC_KEY */]; const permissions = objects[11 /* TLVValues.PERMISSIONS */][0]; this.emit("add-pairing" /* HAPServerEventTypes.ADD_PAIRING */, connection, identifier, publicKey, permissions, (0, once_1.once)((error) => { if (error > 0) { debug("[%s] Pairings: failed ADD_PAIRING with code %d", this.accessoryInfo.username, error); response.writeHead(200 /* HAPPairingHTTPCode.OK */, { "Content-Type": "application/pairing+tlv8" }); response.end(tlv.encode(6 /* TLVValues.STATE */, 2 /* PairingStates.M2 */, 7 /* TLVValues.ERROR_CODE */, error)); return; } response.writeHead(200 /* HAPPairingHTTPCode.OK */, { "Content-Type": "application/pairing+tlv8" }); response.end(tlv.encode(6 /* TLVValues.STATE */, 2 /* PairingStates.M2 */)); debug("[%s] Pairings: successfully executed ADD_PAIRING", this.accessoryInfo.username); })); } else if (method === 4 /* PairMethods.REMOVE_PAIRING */) { const identifier = objects[1 /* TLVValues.IDENTIFIER */].toString(); this.emit("remove-pairing" /* HAPServerEventTypes.REMOVE_PAIRING */, connection, identifier, (0, once_1.once)((error) => { if (error > 0) { debug("[%s] Pairings: failed REMOVE_PAIRING with code %d", this.accessoryInfo.username, error); response.writeHead(200 /* HAPPairingHTTPCode.OK */, { "Content-Type": "application/pairing+tlv8" }); response.end(tlv.encode(6 /* TLVValues.STATE */, 2 /* PairingStates.M2 */, 7 /* TLVValues.ERROR_CODE */, error)); return; } response.writeHead(200 /* HAPPairingHTTPCode.OK */, { "Content-Type": "application/pairing+tlv8" }); response.end(tlv.encode(6 /* TLVValues.STATE */, 2 /* PairingStates.M2 */)); debug("[%s] Pairings: successfully executed REMOVE_PAIRING", this.accessoryInfo.username); })); } else if (method === 5 /* PairMethods.LIST_PAIRINGS */) { this.emit("list-pairings" /* HAPServerEventTypes.LIST_PAIRINGS */, connection, (0, once_1.once)((error, data) => { if (error > 0) { debug("[%s] Pairings: failed LIST_PAIRINGS with code %d", this.accessoryInfo.username, error); response.writeHead(200 /* HAPPairingHTTPCode.OK */, { "Content-Type": "application/pairing+tlv8" }); response.end(tlv.encode(6 /* TLVValues.STATE */, 2 /* PairingStates.M2 */, 7 /* TLVValues.ERROR_CODE */, error)); return; } // eslint-disable-next-line @typescript-eslint/no-explicit-any const tlvList = []; data.forEach((value, index) => { if (index > 0) { tlvList.push(255 /* TLVValues.SEPARATOR */, Buffer.alloc(0)); } tlvList.push(1 /* TLVValues.IDENTIFIER */, value.username, 3 /* TLVValues.PUBLIC_KEY */, value.publicKey, 11 /* TLVValues.PERMISSIONS */, value.permission); }); const list = tlv.encode(6 /* TLVValues.STATE */, 2 /* PairingStates.M2 */, ...tlvList); response.writeHead(200 /* HAPPairingHTTPCode.OK */, { "Content-Type": "application/pairing+tlv8" /* HAPMimeTypes.PAIRING_TLV8 */ }); response.end(list); debug("[%s] Pairings: successfully executed LIST_PAIRINGS", this.accessoryInfo.username); })); } } handleAccessories(connection, url, request, data, response) { if (!this.allowInsecureRequest && !connection.isAuthenticated()) { response.writeHead(470 /* HAPPairingHTTPCode.CONNECTION_AUTHORIZATION_REQUIRED */, { "Content-Type": "application/hap+json" /* HAPMimeTypes.HAP_JSON */ }); response.end(JSON.stringify({ status: -70401 /* HAPStatus.INSUFFICIENT_PRIVILEGES */ })); return; } // call out to listeners to retrieve the latest accessories JSON this.emit("accessories" /* HAPServerEventTypes.ACCESSORIES */, connection, (0, once_1.once)((error, result) => { if (error) { response.writeHead(error.httpCode, { "Content-Type": "application/hap+json" /* HAPMimeTypes.HAP_JSON */ }); response.end(JSON.stringify({ status: error.status })); } else { response.writeHead(200 /* HAPHTTPCode.OK */, { "Content-Type": "application/hap+json" /* HAPMimeTypes.HAP_JSON */ }); response.end(JSON.stringify(result)); } })); } handleCharacteristics(connection, url, request, data, response) { if (!this.allowInsecureRequest && !connection.isAuthenticated()) { response.writeHead(470 /* HAPPairingHTTPCode.CONNECTION_AUTHORIZATION_REQUIRED */, { "Content-Type": "application/hap+json" /* HAPMimeTypes.HAP_JSON */ }); response.end(JSON.stringify({ status: -70401 /* HAPStatus.INSUFFICIENT_PRIVILEGES */ })); return; } if (request.method === "GET") { const searchParams = url.searchParams; const idParam = searchParams.get("id"); if (!idParam) { response.writeHead(400 /* HAPHTTPCode.BAD_REQUEST */, { "Content-Type": "application/hap+json" /* HAPMimeTypes.HAP_JSON */ }); response.end(JSON.stringify({ status: -70410 /* HAPStatus.INVALID_VALUE_IN_REQUEST */ })); return; } const ids = []; for (const entry of idParam.split(",")) { // ["1.9","2.14"] const split = entry.split("."); // ["1","9"] ids.push({ aid: parseInt(split[0], 10), // accessory id iid: parseInt(split[1], 10), // (characteristic) instance id }); } const readRequest = { ids: ids, includeMeta: (0, internal_types_1.consideredTrue)(searchParams.get("meta")), includePerms: (0, internal_types_1.consideredTrue)(searchParams.get("perms")), includeType: (0, internal_types_1.consideredTrue)(searchParams.get("type")), includeEvent: (0, internal_types_1.consideredTrue)(searchParams.get("ev")), }; this.emit("get-characteristics" /* HAPServerEventTypes.GET_CHARACTERISTICS */, connection, readRequest, (0, once_1.once)((error, readResponse) => { if (error) { response.writeHead(error.httpCode, { "Content-Type": "application/hap+json" /* HAPMimeTypes.HAP_JSON */ }); response.end(JSON.stringify({ status: error.status })); return; } const characteristics = readResponse.characteristics; let errorOccurred = false; // determine if we send a 207 Multi-Status for (const data of characteristics) { if (data.status) { errorOccurred = true; break; } } if (errorOccurred) { // on a 207 Multi-Status EVERY characteristic MUST include a status property for (const data of characteristics) { if (!data.status) { // a status is undefined if the request was successful data.status = 0 /* HAPStatus.SUCCESS */; // a value of zero indicates success } } } // 207 "multi-status" is returned when an error occurs reading a characteristic. otherwise 200 is returned response.writeHead(errorOccurred ? 207 /* HAPHTTPCode.MULTI_STATUS */ : 200 /* HAPHTTPCode.OK */, { "Content-Type": "application/hap+json" /* HAPMimeTypes.HAP_JSON */ }); response.end(JSON.stringify({ characteristics: characteristics })); })); } else if (request.method === "PUT") { if (!connection.isAuthenticated()) { if (!request.headers || (request.headers && request.headers.authorization !== this.accessoryInfo.pincode)) { response.writeHead(470 /* HAPPairingHTTPCode.CONNECTION_AUTHORIZATION_REQUIRED */, { "Content-Type": "application/hap+json" /* HAPMimeTypes.HAP_JSON */ }); response.end(JSON.stringify({ status: -70401 /* HAPStatus.INSUFFICIENT_PRIVILEGES */ })); return; } } if (data.length === 0) { response.writeHead(400 /* HAPHTTPCode.BAD_REQUEST */, { "Content-Type": "application/hap+json" /* HAPMimeTypes.HAP_JSON */ }); response.end(JSON.stringify({ status: -70410 /* HAPStatus.INVALID_VALUE_IN_REQUEST */ })); return; } const writeRequest = JSON.parse(data.toString("utf8")); this.emit("set-characteristics" /* HAPServerEventTypes.SET_CHARACTERISTICS */, connection, writeRequest, (0, once_1.once)((error, writeResponse) => { if (error) { response.writeHead(error.httpCode, { "Content-Type": "application/hap+json" /* HAPMimeTypes.HAP_JSON */ }); response.end(JSON.stringify({ status: error.status })); return; } const characteristics = writeResponse.characteristics; let multiStatus = false; for (const data of characteristics) { if (data.status || data.value !== undefined) { // also send multiStatus on write response requests multiStatus = true; break; } } if (multiStatus) { // 207 is "multi-status" since HomeKit may be setting multiple things and any one can fail independently response.writeHead(207 /* HAPHTTPCode.MULTI_STATUS */, { "Content-Type": "application/hap+json" /* HAPMimeTypes.HAP_JSON */ }); response.end(JSON.stringify({ characteristics: characteristics })); } else { // if everything went fine send 204 no content response response.writeHead(204 /* HAPHTTPCode.NO_CONTENT */); response.end(); } })); } else { response.writeHead(400 /* HAPHTTPCode.BAD_REQUEST */, { "Content-Type": "application/hap+json" /* HAPMimeTypes.HAP_JSON */ }); // method not allowed response.end(JSON.stringify({ status: -70410 /* HAPStatus.INVALID_VALUE_IN_REQUEST */ })); } } handlePrepareWrite(connection, url, request, data, response) { if (!this.allowInsecureRequest && !connection.isAuthenticated()) { response.writeHead(470 /* HAPPairingHTTPCode.CONNECTION_AUTHORIZATION_REQUIRED */, { "Content-Type": "application/hap+json" /* HAPMimeTypes.HAP_JSON */ }); response.end(JSON.stringify({ status: -70401 /* HAPStatus.INSUFFICIENT_PRIVILEGES */ })); return; } if (request.method === "PUT") { if (data.length === 0) { response.writeHead(400 /* HAPHTTPCode.BAD_REQUEST */, { "Content-Type": "application/hap+json" /* HAPMimeTypes.HAP_JSON */ }); response.end(JSON.stringify({ status: -70410 /* HAPStatus.INVALID_VALUE_IN_REQUEST */ })); return; } const prepareRequest = JSON.parse(data.toString()); if (prepareRequest.pid && prepareRequest.ttl) { debug("[%s] Received prepare write request with pid %d and ttl %d", this.accessoryInfo.username, prepareRequest.pid, prepareRequest.ttl); if (connection.timedWriteTimeout) { // clear any currently existing timeouts clearTimeout(connection.timedWriteTimeout); } connection.timedWritePid = prepareRequest.pid; connection.timedWriteTimeout = setTimeout(() => { debug("[%s] Timed write request timed out for pid %d", this.accessoryInfo.username, prepareRequest.pid); connection.timedWritePid = undefined; connection.timedWriteTimeout = undefined; }, prepareRequest.ttl); response.writeHead(200 /* HAPHTTPCode.OK */, { "Content-Type": "application/hap+json" /* HAPMimeTypes.HAP_JSON */ }); response.end(JSON.stringify({ status: 0 /* HAPStatus.SUCCESS */ })); return; } else { response.writeHead(400 /* HAPHTTPCode.BAD_REQUEST */, { "Content-Type": "application/hap+json" /* HAPMimeTypes.HAP_JSON */ }); response.end(JSON.stringify({ status: -70410 /* HAPStatus.INVALID_VALUE_IN_REQUEST */ })); } } else { response.writeHead(400 /* HAPHTTPCode.BAD_REQUEST */, { "Content-Type": "application/hap+json" /* HAPMimeTypes.HAP_JSON */ }); response.end(JSON.stringify({ status: -70410 /* HAPStatus.INVALID_VALUE_IN_REQUEST */ })); } } handleResource(connection, url, request, data, response) { if (!connection.isAuthenticated()) { if (!(this.allowInsecureRequest && request.headers && request.headers.authorization === this.accessoryInfo.pincode)) { response.writeHead(470 /* HAPPairingHTTPCode.CONNECTION_AUTHORIZATION_REQUIRED */, { "Content-Type": "application/hap+json" /* HAPMimeTypes.HAP_JSON */ }); response.end(JSON.stringify({ status: -70401 /* HAPStatus.INSUFFICIENT_PRIVILEGES */ })); return; } } if (request.method === "POST") { if (data.length === 0) { response.writeHead(400 /* HAPHTTPCode.BAD_REQUEST */, { "Content-Type": "application/hap+json" /* HAPMimeTypes.HAP_JSON */ }); response.end(JSON.string