hap-nodejs
Version:
HAP-NodeJS is a Node.js implementation of HomeKit Accessory Server.
276 lines • 11.9 kB
JavaScript
"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