UNPKG

11.9 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3exports.AccessoryInfo = exports.PermissionTypes = void 0;
4const tslib_1 = require("tslib");
5const assert_1 = tslib_1.__importDefault(require("assert"));
6const crypto_1 = tslib_1.__importDefault(require("crypto"));
7const tweetnacl_1 = tslib_1.__importDefault(require("tweetnacl"));
8const util_1 = tslib_1.__importDefault(require("util"));
9const eventedhttp_1 = require("../util/eventedhttp");
10const HAPStorage_1 = require("./HAPStorage");
11function getVersion() {
12 // eslint-disable-next-line @typescript-eslint/no-var-requires
13 const packageJson = require("../../../package.json");
14 return packageJson.version;
15}
16/**
17 * @group Model
18 */
19var PermissionTypes;
20(function (PermissionTypes) {
21 // noinspection JSUnusedGlobalSymbols
22 PermissionTypes[PermissionTypes["USER"] = 0] = "USER";
23 PermissionTypes[PermissionTypes["ADMIN"] = 1] = "ADMIN";
24})(PermissionTypes || (exports.PermissionTypes = PermissionTypes = {}));
25/**
26 * AccessoryInfo is a model class containing a subset of Accessory data relevant to the internal HAP server,
27 * such as encryption keys and username. It is persisted to disk.
28 * @group Model
29 */
30class AccessoryInfo {
31 static deviceIdPattern = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/;
32 username;
33 displayName;
34 model; // this property is currently not saved to disk
35 category;
36 pincode;
37 signSk;
38 signPk;
39 pairedClients;
40 pairedAdminClients;
41 configVersion = 1;
42 configHash;
43 setupID;
44 lastFirmwareVersion = "";
45 constructor(username) {
46 this.username = username;
47 this.displayName = "";
48 this.model = "";
49 this.category = 1 /* Categories.OTHER */;
50 this.pincode = "";
51 this.signSk = Buffer.alloc(0);
52 this.signPk = Buffer.alloc(0);
53 this.pairedClients = {};
54 this.pairedAdminClients = 0;
55 this.configHash = "";
56 this.setupID = "";
57 }
58 /**
59 * Add a paired client to memory.
60 * @param {HAPUsername} username
61 * @param {Buffer} publicKey
62 * @param {PermissionTypes} permission
63 */
64 addPairedClient(username, publicKey, permission) {
65 this.pairedClients[username] = {
66 username: username,
67 publicKey: publicKey,
68 permission: permission,
69 };
70 if (permission === 1 /* PermissionTypes.ADMIN */) {
71 this.pairedAdminClients++;
72 }
73 }
74 updatePermission(username, permission) {
75 const pairingInformation = this.pairedClients[username];
76 if (pairingInformation) {
77 const oldPermission = pairingInformation.permission;
78 pairingInformation.permission = permission;
79 if (oldPermission === 1 /* PermissionTypes.ADMIN */ && permission !== 1 /* PermissionTypes.ADMIN */) {
80 this.pairedAdminClients--;
81 }
82 else if (oldPermission !== 1 /* PermissionTypes.ADMIN */ && permission === 1 /* PermissionTypes.ADMIN */) {
83 this.pairedAdminClients++;
84 }
85 }
86 }
87 listPairings() {
88 const array = [];
89 for (const pairingInformation of Object.values(this.pairedClients)) {
90 array.push(pairingInformation);
91 }
92 return array;
93 }
94 /**
95 * Remove a paired client from memory.
96 * @param connection - the session of the connection initiated the removal of the pairing
97 * @param {string} username
98 */
99 removePairedClient(connection, username) {
100 this._removePairedClient0(connection, username);
101 if (this.pairedAdminClients === 0) { // if we don't have any admin clients left paired it is required to kill all normal clients
102 for (const username0 of Object.keys(this.pairedClients)) {
103 this._removePairedClient0(connection, username0);
104 }
105 }
106 }
107 _removePairedClient0(connection, username) {
108 if (this.pairedClients[username] && this.pairedClients[username].permission === 1 /* PermissionTypes.ADMIN */) {
109 this.pairedAdminClients--;
110 }
111 delete this.pairedClients[username];
112 eventedhttp_1.EventedHTTPServer.destroyExistingConnectionsAfterUnpair(connection, username);
113 }
114 /**
115 * Check if username is paired
116 * @param username
117 */
118 isPaired(username) {
119 return !!this.pairedClients[username];
120 }
121 hasAdminPermissions(username) {
122 if (!username) {
123 return false;
124 }
125 const pairingInformation = this.pairedClients[username];
126 return !!pairingInformation && pairingInformation.permission === 1 /* PermissionTypes.ADMIN */;
127 }
128 // Gets the public key for a paired client as a Buffer, or falsy value if not paired.
129 getClientPublicKey(username) {
130 const pairingInformation = this.pairedClients[username];
131 if (pairingInformation) {
132 return pairingInformation.publicKey;
133 }
134 else {
135 return undefined;
136 }
137 }
138 // Returns a boolean indicating whether this accessory has been paired with a client.
139 paired = () => {
140 return Object.keys(this.pairedClients).length > 0; // if we have any paired clients, we're paired.
141 };
142 /**
143 * Checks based on the current accessory configuration if the current configuration number needs to be incremented.
144 * Additionally, if desired, it checks if the firmware version was incremented (aka the HAP-NodeJS) version did grow.
145 *
146 * @param configuration - The current accessory configuration.
147 * @param checkFirmwareIncrement
148 * @returns True if the current configuration number was incremented and thus a new TXT must be advertised.
149 */
150 checkForCurrentConfigurationNumberIncrement(configuration, checkFirmwareIncrement) {
151 const shasum = crypto_1.default.createHash("sha1");
152 shasum.update(JSON.stringify(configuration));
153 const configHash = shasum.digest("hex");
154 let changed = false;
155 if (configHash !== this.configHash) {
156 this.configVersion++;
157 this.configHash = configHash;
158 this.ensureConfigVersionBounds();
159 changed = true;
160 }
161 if (checkFirmwareIncrement) {
162 const version = getVersion();
163 if (this.lastFirmwareVersion !== version) {
164 // we only check if it is different and not only if it is incremented
165 // HomeKit spec prohibits firmware downgrades, but with hap-nodejs it's possible lol
166 this.lastFirmwareVersion = version;
167 changed = true;
168 }
169 }
170 if (changed) {
171 this.save();
172 }
173 return changed;
174 }
175 getConfigVersion() {
176 return this.configVersion;
177 }
178 ensureConfigVersionBounds() {
179 // current configuration number must be in the range of 1-65535 and wrap to 1 when it overflows
180 this.configVersion = this.configVersion % (0xFFFF + 1);
181 if (this.configVersion === 0) {
182 this.configVersion = 1;
183 }
184 }
185 save() {
186 const saved = {
187 displayName: this.displayName,
188 category: this.category,
189 pincode: this.pincode,
190 signSk: this.signSk.toString("hex"),
191 signPk: this.signPk.toString("hex"),
192 pairedClients: {},
193 // moving permissions into an extra object, so there is nothing to migrate from old files.
194 // if the legacy node-persist storage should be upgraded some time, it would be reasonable to combine the storage
195 // of public keys (pairedClients object) and permissions.
196 pairedClientsPermission: {},
197 configVersion: this.configVersion,
198 configHash: this.configHash,
199 setupID: this.setupID,
200 lastFirmwareVersion: this.lastFirmwareVersion,
201 };
202 for (const [username, pairingInformation] of Object.entries(this.pairedClients)) {
203 // @ts-expect-error: missing typing, object instead of Record
204 saved.pairedClients[username] = pairingInformation.publicKey.toString("hex");
205 // @ts-expect-error: missing typing, object instead of Record
206 saved.pairedClientsPermission[username] = pairingInformation.permission;
207 }
208 const key = AccessoryInfo.persistKey(this.username);
209 HAPStorage_1.HAPStorage.storage().setItemSync(key, saved);
210 }
211 // Gets a key for storing this AccessoryInfo in the filesystem, like "AccessoryInfo.CC223DE3CEF3.json"
212 static persistKey(username) {
213 return util_1.default.format("AccessoryInfo.%s.json", username.replace(/:/g, "").toUpperCase());
214 }
215 static create(username) {
216 AccessoryInfo.assertValidUsername(username);
217 const accessoryInfo = new AccessoryInfo(username);
218 accessoryInfo.lastFirmwareVersion = getVersion();
219 // Create a new unique key pair for this accessory.
220 const keyPair = tweetnacl_1.default.sign.keyPair();
221 accessoryInfo.signSk = Buffer.from(keyPair.secretKey);
222 accessoryInfo.signPk = Buffer.from(keyPair.publicKey);
223 return accessoryInfo;
224 }
225 static load(username) {
226 AccessoryInfo.assertValidUsername(username);
227 const key = AccessoryInfo.persistKey(username);
228 const saved = HAPStorage_1.HAPStorage.storage().getItem(key);
229 if (saved) {
230 const info = new AccessoryInfo(username);
231 info.displayName = saved.displayName || "";
232 info.category = saved.category || "";
233 info.pincode = saved.pincode || "";
234 info.signSk = Buffer.from(saved.signSk || "", "hex");
235 info.signPk = Buffer.from(saved.signPk || "", "hex");
236 info.pairedClients = {};
237 for (const username of Object.keys(saved.pairedClients || {})) {
238 const publicKey = saved.pairedClients[username];
239 let permission = saved.pairedClientsPermission ? saved.pairedClientsPermission[username] : undefined;
240 if (permission === undefined) {
241 permission = 1 /* PermissionTypes.ADMIN */;
242 } // defaulting to admin permissions is the only suitable solution, there is no way to recover permissions
243 info.pairedClients[username] = {
244 username: username,
245 publicKey: Buffer.from(publicKey, "hex"),
246 permission: permission,
247 };
248 if (permission === 1 /* PermissionTypes.ADMIN */) {
249 info.pairedAdminClients++;
250 }
251 }
252 info.configVersion = saved.configVersion || 1;
253 info.configHash = saved.configHash || "";
254 info.setupID = saved.setupID || "";
255 info.lastFirmwareVersion = saved.lastFirmwareVersion || getVersion();
256 info.ensureConfigVersionBounds();
257 return info;
258 }
259 else {
260 return null;
261 }
262 }
263 static remove(username) {
264 const key = AccessoryInfo.persistKey(username);
265 HAPStorage_1.HAPStorage.storage().removeItemSync(key);
266 }
267 static assertValidUsername(username) {
268 assert_1.default.ok(AccessoryInfo.deviceIdPattern.test(username), "The supplied username (" + username + ") is not valid " +
269 "(expected a format like 'XX:XX:XX:XX:XX:XX' with XX being a valid hexadecimal string). " +
270 "Note that, if you had this accessory already paired with the invalid username, you will need to repair " +
271 "the accessory and reconfigure your services in the Home app. " +
272 "Using an invalid username will lead to unexpected behaviour.");
273 }
274}
275exports.AccessoryInfo = AccessoryInfo;
276//# sourceMappingURL=AccessoryInfo.js.map
\No newline at end of file