homebridge-plugin-wrapper
Version:
Wrapper for Homebridge and NodeJS-HAP with reduced dependencies that allows to intercept plugin values and also send to them
307 lines • 13.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.AccessoryInfo = exports.PermissionTypes = void 0;
var tslib_1 = require("tslib");
var assert_1 = (0, tslib_1.__importDefault)(require("assert"));
var crypto_1 = (0, tslib_1.__importDefault)(require("crypto"));
var tweetnacl_1 = (0, tslib_1.__importDefault)(require("tweetnacl"));
var util_1 = (0, tslib_1.__importDefault)(require("util"));
var eventedhttp_1 = require("../util/eventedhttp");
var HAPStorage_1 = require("./HAPStorage");
function getVersion() {
// eslint-disable-next-line @typescript-eslint/no-var-requires
var packageJson = require("../../../package.json");
return packageJson.version;
}
var PermissionTypes;
(function (PermissionTypes) {
// noinspection JSUnusedGlobalSymbols
PermissionTypes[PermissionTypes["USER"] = 0] = "USER";
PermissionTypes[PermissionTypes["ADMIN"] = 1] = "ADMIN";
})(PermissionTypes = exports.PermissionTypes || (exports.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.
*/
var AccessoryInfo = /** @class */ (function () {
function AccessoryInfo(username) {
var _this = this;
this.configVersion = 1;
this.lastFirmwareVersion = "";
// Returns a boolean indicating whether this accessory has been paired with a client.
this.paired = function () {
return Object.keys(_this.pairedClients).length > 0; // if we have any paired clients, we're paired.
};
this.username = username;
this.displayName = "";
this.model = "";
this.category = 1 /* 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
*/
AccessoryInfo.prototype.addPairedClient = function (username, publicKey, permission) {
this.pairedClients[username] = {
username: username,
publicKey: publicKey,
permission: permission,
};
if (permission === 1 /* ADMIN */) {
this.pairedAdminClients++;
}
};
AccessoryInfo.prototype.updatePermission = function (username, permission) {
var pairingInformation = this.pairedClients[username];
if (pairingInformation) {
var oldPermission = pairingInformation.permission;
pairingInformation.permission = permission;
if (oldPermission === 1 /* ADMIN */ && permission !== 1 /* ADMIN */) {
this.pairedAdminClients--;
}
else if (oldPermission !== 1 /* ADMIN */ && permission === 1 /* ADMIN */) {
this.pairedAdminClients++;
}
}
};
AccessoryInfo.prototype.listPairings = function () {
var e_1, _a;
var array = [];
try {
for (var _b = (0, tslib_1.__values)(Object.values(this.pairedClients)), _c = _b.next(); !_c.done; _c = _b.next()) {
var pairingInformation = _c.value;
array.push(pairingInformation);
}
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
try {
if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
}
finally { if (e_1) throw e_1.error; }
}
return array;
};
/**
* Remove a paired client from memory.
* @param connection - the session of the connection initiated the removal of the pairing
* @param {string} username
*/
AccessoryInfo.prototype.removePairedClient = function (connection, username) {
var e_2, _a;
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
try {
for (var _b = (0, tslib_1.__values)(Object.keys(this.pairedClients)), _c = _b.next(); !_c.done; _c = _b.next()) {
var username0 = _c.value;
this._removePairedClient0(connection, username0);
}
}
catch (e_2_1) { e_2 = { error: e_2_1 }; }
finally {
try {
if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
}
finally { if (e_2) throw e_2.error; }
}
}
};
AccessoryInfo.prototype._removePairedClient0 = function (connection, username) {
if (this.pairedClients[username] && this.pairedClients[username].permission === 1 /* ADMIN */) {
this.pairedAdminClients--;
}
delete this.pairedClients[username];
eventedhttp_1.EventedHTTPServer.destroyExistingConnectionsAfterUnpair(connection, username);
};
/**
* Check if username is paired
* @param username
*/
AccessoryInfo.prototype.isPaired = function (username) {
return !!this.pairedClients[username];
};
AccessoryInfo.prototype.hasAdminPermissions = function (username) {
if (!username) {
return false;
}
var pairingInformation = this.pairedClients[username];
return !!pairingInformation && pairingInformation.permission === 1 /* ADMIN */;
};
// Gets the public key for a paired client as a Buffer, or falsy value if not paired.
AccessoryInfo.prototype.getClientPublicKey = function (username) {
var pairingInformation = this.pairedClients[username];
if (pairingInformation) {
return pairingInformation.publicKey;
}
else {
return undefined;
}
};
/**
* 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.
*/
AccessoryInfo.prototype.checkForCurrentConfigurationNumberIncrement = function (configuration, checkFirmwareIncrement) {
var shasum = crypto_1.default.createHash("sha1");
shasum.update(JSON.stringify(configuration));
var configHash = shasum.digest("hex");
var changed = false;
if (configHash !== this.configHash) {
this.configVersion++;
this.configHash = configHash;
this.ensureConfigVersionBounds();
changed = true;
}
if (checkFirmwareIncrement) {
var 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;
};
AccessoryInfo.prototype.getConfigVersion = function () {
return this.configVersion;
};
AccessoryInfo.prototype.ensureConfigVersionBounds = function () {
// 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;
}
};
AccessoryInfo.prototype.save = function () {
var e_3, _a;
var 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,
};
try {
for (var _b = (0, tslib_1.__values)(Object.entries(this.pairedClients)), _c = _b.next(); !_c.done; _c = _b.next()) {
var _d = (0, tslib_1.__read)(_c.value, 2), username = _d[0], pairingInformation = _d[1];
// @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;
}
}
catch (e_3_1) { e_3 = { error: e_3_1 }; }
finally {
try {
if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
}
finally { if (e_3) throw e_3.error; }
}
var 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"
AccessoryInfo.persistKey = function (username) {
return util_1.default.format("AccessoryInfo.%s.json", username.replace(/:/g, "").toUpperCase());
};
AccessoryInfo.create = function (username) {
AccessoryInfo.assertValidUsername(username);
var accessoryInfo = new AccessoryInfo(username);
accessoryInfo.lastFirmwareVersion = getVersion();
// Create a new unique key pair for this accessory.
var keyPair = tweetnacl_1.default.sign.keyPair();
accessoryInfo.signSk = Buffer.from(keyPair.secretKey);
accessoryInfo.signPk = Buffer.from(keyPair.publicKey);
return accessoryInfo;
};
AccessoryInfo.load = function (username) {
var e_4, _a;
AccessoryInfo.assertValidUsername(username);
var key = AccessoryInfo.persistKey(username);
var saved = HAPStorage_1.HAPStorage.storage().getItem(key);
if (saved) {
var 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 = {};
try {
for (var _b = (0, tslib_1.__values)(Object.keys(saved.pairedClients || {})), _c = _b.next(); !_c.done; _c = _b.next()) {
var username_1 = _c.value;
var publicKey = saved.pairedClients[username_1];
var permission = saved.pairedClientsPermission ? saved.pairedClientsPermission[username_1] : undefined;
if (permission === undefined) {
permission = 1 /* ADMIN */;
} // defaulting to admin permissions is the only suitable solution, there is no way to recover permissions
info.pairedClients[username_1] = {
username: username_1,
publicKey: Buffer.from(publicKey, "hex"),
permission: permission,
};
if (permission === 1 /* ADMIN */) {
info.pairedAdminClients++;
}
}
}
catch (e_4_1) { e_4 = { error: e_4_1 }; }
finally {
try {
if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
}
finally { if (e_4) throw e_4.error; }
}
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;
}
};
AccessoryInfo.remove = function (username) {
var key = AccessoryInfo.persistKey(username);
HAPStorage_1.HAPStorage.storage().removeItemSync(key);
};
AccessoryInfo.assertValidUsername = function (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.");
};
AccessoryInfo.deviceIdPattern = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/;
return AccessoryInfo;
}());
exports.AccessoryInfo = AccessoryInfo;
//# sourceMappingURL=AccessoryInfo.js.map