UNPKG

hap-nodejs

Version:

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

276 lines 11.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.AccessoryInfo = exports.PermissionTypes = void 0; const tslib_1 = require("tslib"); const assert_1 = tslib_1.__importDefault(require("assert")); const crypto_1 = tslib_1.__importDefault(require("crypto")); const tweetnacl_1 = tslib_1.__importDefault(require("tweetnacl")); const util_1 = tslib_1.__importDefault(require("util")); const eventedhttp_1 = require("../util/eventedhttp"); const HAPStorage_1 = require("./HAPStorage"); function getVersion() { // eslint-disable-next-line @typescript-eslint/no-var-requires const packageJson = require("../../../package.json"); return packageJson.version; } /** * @group Model */ var PermissionTypes; (function (PermissionTypes) { // noinspection JSUnusedGlobalSymbols PermissionTypes[PermissionTypes["USER"] = 0] = "USER"; PermissionTypes[PermissionTypes["ADMIN"] = 1] = "ADMIN"; })(PermissionTypes || (exports.PermissionTypes = PermissionTypes = {})); /** * AccessoryInfo is a model class containing a subset of Accessory data relevant to the internal HAP server, * such as encryption keys and username. It is persisted to disk. * @group Model */ class AccessoryInfo { static deviceIdPattern = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/; username; displayName; model; // this property is currently not saved to disk category; pincode; signSk; signPk; pairedClients; pairedAdminClients; configVersion = 1; configHash; setupID; lastFirmwareVersion = ""; constructor(username) { this.username = username; this.displayName = ""; this.model = ""; this.category = 1 /* Categories.OTHER */; this.pincode = ""; this.signSk = Buffer.alloc(0); this.signPk = Buffer.alloc(0); this.pairedClients = {}; this.pairedAdminClients = 0; this.configHash = ""; this.setupID = ""; } /** * Add a paired client to memory. * @param {HAPUsername} username * @param {Buffer} publicKey * @param {PermissionTypes} permission */ addPairedClient(username, publicKey, permission) { this.pairedClients[username] = { username: username, publicKey: publicKey, permission: permission, }; if (permission === 1 /* PermissionTypes.ADMIN */) { this.pairedAdminClients++; } } updatePermission(username, permission) { const pairingInformation = this.pairedClients[username]; if (pairingInformation) { const oldPermission = pairingInformation.permission; pairingInformation.permission = permission; if (oldPermission === 1 /* PermissionTypes.ADMIN */ && permission !== 1 /* PermissionTypes.ADMIN */) { this.pairedAdminClients--; } else if (oldPermission !== 1 /* PermissionTypes.ADMIN */ && permission === 1 /* PermissionTypes.ADMIN */) { this.pairedAdminClients++; } } } listPairings() { const array = []; for (const pairingInformation of Object.values(this.pairedClients)) { array.push(pairingInformation); } return array; } /** * Remove a paired client from memory. * @param connection - the session of the connection initiated the removal of the pairing * @param {string} username */ removePairedClient(connection, username) { this._removePairedClient0(connection, username); if (this.pairedAdminClients === 0) { // if we don't have any admin clients left paired it is required to kill all normal clients for (const username0 of Object.keys(this.pairedClients)) { this._removePairedClient0(connection, username0); } } } _removePairedClient0(connection, username) { if (this.pairedClients[username] && this.pairedClients[username].permission === 1 /* PermissionTypes.ADMIN */) { this.pairedAdminClients--; } delete this.pairedClients[username]; eventedhttp_1.EventedHTTPServer.destroyExistingConnectionsAfterUnpair(connection, username); } /** * Check if username is paired * @param username */ isPaired(username) { return !!this.pairedClients[username]; } hasAdminPermissions(username) { if (!username) { return false; } const pairingInformation = this.pairedClients[username]; return !!pairingInformation && pairingInformation.permission === 1 /* PermissionTypes.ADMIN */; } // Gets the public key for a paired client as a Buffer, or falsy value if not paired. getClientPublicKey(username) { const pairingInformation = this.pairedClients[username]; if (pairingInformation) { return pairingInformation.publicKey; } else { return undefined; } } // Returns a boolean indicating whether this accessory has been paired with a client. paired = () => { return Object.keys(this.pairedClients).length > 0; // if we have any paired clients, we're paired. }; /** * Checks based on the current accessory configuration if the current configuration number needs to be incremented. * Additionally, if desired, it checks if the firmware version was incremented (aka the HAP-NodeJS) version did grow. * * @param configuration - The current accessory configuration. * @param checkFirmwareIncrement * @returns True if the current configuration number was incremented and thus a new TXT must be advertised. */ checkForCurrentConfigurationNumberIncrement(configuration, checkFirmwareIncrement) { const shasum = crypto_1.default.createHash("sha1"); shasum.update(JSON.stringify(configuration)); const configHash = shasum.digest("hex"); let changed = false; if (configHash !== this.configHash) { this.configVersion++; this.configHash = configHash; this.ensureConfigVersionBounds(); changed = true; } if (checkFirmwareIncrement) { const version = getVersion(); if (this.lastFirmwareVersion !== version) { // we only check if it is different and not only if it is incremented // HomeKit spec prohibits firmware downgrades, but with hap-nodejs it's possible lol this.lastFirmwareVersion = version; changed = true; } } if (changed) { this.save(); } return changed; } getConfigVersion() { return this.configVersion; } ensureConfigVersionBounds() { // current configuration number must be in the range of 1-65535 and wrap to 1 when it overflows this.configVersion = this.configVersion % (0xFFFF + 1); if (this.configVersion === 0) { this.configVersion = 1; } } save() { const saved = { displayName: this.displayName, category: this.category, pincode: this.pincode, signSk: this.signSk.toString("hex"), signPk: this.signPk.toString("hex"), pairedClients: {}, // moving permissions into an extra object, so there is nothing to migrate from old files. // if the legacy node-persist storage should be upgraded some time, it would be reasonable to combine the storage // of public keys (pairedClients object) and permissions. pairedClientsPermission: {}, configVersion: this.configVersion, configHash: this.configHash, setupID: this.setupID, lastFirmwareVersion: this.lastFirmwareVersion, }; for (const [username, pairingInformation] of Object.entries(this.pairedClients)) { // @ts-expect-error: missing typing, object instead of Record saved.pairedClients[username] = pairingInformation.publicKey.toString("hex"); // @ts-expect-error: missing typing, object instead of Record saved.pairedClientsPermission[username] = pairingInformation.permission; } const key = AccessoryInfo.persistKey(this.username); HAPStorage_1.HAPStorage.storage().setItemSync(key, saved); } // Gets a key for storing this AccessoryInfo in the filesystem, like "AccessoryInfo.CC223DE3CEF3.json" static persistKey(username) { return util_1.default.format("AccessoryInfo.%s.json", username.replace(/:/g, "").toUpperCase()); } static create(username) { AccessoryInfo.assertValidUsername(username); const accessoryInfo = new AccessoryInfo(username); accessoryInfo.lastFirmwareVersion = getVersion(); // Create a new unique key pair for this accessory. const keyPair = tweetnacl_1.default.sign.keyPair(); accessoryInfo.signSk = Buffer.from(keyPair.secretKey); accessoryInfo.signPk = Buffer.from(keyPair.publicKey); return accessoryInfo; } static load(username) { AccessoryInfo.assertValidUsername(username); const key = AccessoryInfo.persistKey(username); const saved = HAPStorage_1.HAPStorage.storage().getItem(key); if (saved) { const info = new AccessoryInfo(username); info.displayName = saved.displayName || ""; info.category = saved.category || ""; info.pincode = saved.pincode || ""; info.signSk = Buffer.from(saved.signSk || "", "hex"); info.signPk = Buffer.from(saved.signPk || "", "hex"); info.pairedClients = {}; for (const username of Object.keys(saved.pairedClients || {})) { const publicKey = saved.pairedClients[username]; let permission = saved.pairedClientsPermission ? saved.pairedClientsPermission[username] : undefined; if (permission === undefined) { permission = 1 /* PermissionTypes.ADMIN */; } // defaulting to admin permissions is the only suitable solution, there is no way to recover permissions info.pairedClients[username] = { username: username, publicKey: Buffer.from(publicKey, "hex"), permission: permission, }; if (permission === 1 /* PermissionTypes.ADMIN */) { info.pairedAdminClients++; } } info.configVersion = saved.configVersion || 1; info.configHash = saved.configHash || ""; info.setupID = saved.setupID || ""; info.lastFirmwareVersion = saved.lastFirmwareVersion || getVersion(); info.ensureConfigVersionBounds(); return info; } else { return null; } } static remove(username) { const key = AccessoryInfo.persistKey(username); HAPStorage_1.HAPStorage.storage().removeItemSync(key); } static assertValidUsername(username) { assert_1.default.ok(AccessoryInfo.deviceIdPattern.test(username), "The supplied username (" + username + ") is not valid " + "(expected a format like 'XX:XX:XX:XX:XX:XX' with XX being a valid hexadecimal string). " + "Note that, if you had this accessory already paired with the invalid username, you will need to repair " + "the accessory and reconfigure your services in the Home app. " + "Using an invalid username will lead to unexpected behaviour."); } } exports.AccessoryInfo = AccessoryInfo; //# sourceMappingURL=AccessoryInfo.js.map