Skip to content

Commit

Permalink
Support random setup codes and saved verifiers (#730)
Browse files Browse the repository at this point in the history
Supports the split and transient pair-setup flags. 

Co-authored-by: Supereg <mail@anderl-bauer.de>
  • Loading branch information
samuelthomas2774 and Supereg committed Apr 28, 2021
1 parent 7cf7397 commit e54f2ba
Show file tree
Hide file tree
Showing 14 changed files with 8,163 additions and 126 deletions.
1 change: 1 addition & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"prefer-arrow-callback": "warn",
"max-len": ["warn", 150],
"object-curly-spacing": ["error", "always"],
"operator-linebreak": ["error"],

"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-use-before-define": ["error", {"classes": false}],
Expand Down
7,771 changes: 7,770 additions & 1 deletion package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@
"README.md",
"LICENSE",
"dist",
"!dist/accessories",
"!dist/scripts",
"@types"
],
"dependencies": {
Expand All @@ -69,9 +71,9 @@
"@types/node": "^10.17.58",
"@typescript-eslint/eslint-plugin": "^4.17.0",
"@typescript-eslint/parser": "^4.17.0",
"eslint": "^7.24.0",
"commander": "^6.2.1",
"escape-html": "^1.0.3",
"eslint": "^7.24.0",
"jest": "^26.6.3",
"rimraf": "^3.0.2",
"semver": "^7.3.5",
Expand Down
3 changes: 1 addition & 2 deletions src/BridgedCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import path from 'path';

import storage from 'node-persist';

import { Accessory, AccessoryEventTypes, AccessoryLoader, Bridge, Categories, uuid, VoidCallback } from './';
import { Accessory, AccessoryEventTypes, AccessoryLoader, Bridge, Categories, uuid, generateSetupCode, VoidCallback } from './';

console.log("HAP-NodeJS starting...");

Expand Down Expand Up @@ -34,7 +34,6 @@ accessories.forEach((accessory: Accessory) => {
bridge.publish({
username: "CC:22:3D:E3:CE:F6",
port: 51826,
pincode: "031-45-154",
category: Categories.BRIDGE
});

Expand Down
8 changes: 2 additions & 6 deletions src/Core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import path from 'path';

import storage from 'node-persist';

import { AccessoryLoader } from './';
import { AccessoryLoader, generateSetupCode } from './';
import { NodeCallback } from './types';

console.log("HAP-NodeJS starting...");

Expand All @@ -28,11 +29,6 @@ accessories.forEach((accessory) => {
throw new Error("Username not found on accessory '" + accessory.displayName +
"'. Core.js requires all accessories to define a unique 'username' property.");

// @ts-ignore
if (!accessory.pincode)
throw new Error("Pincode not found on accessory '" + accessory.displayName +
"'. Core.js requires all accessories to define a 'pincode' property.");

// publish this Accessory on the local network
accessory.publish({
port: targetPort++,
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export * from './lib/controller';

export * from './lib/util/clone';
export * from './lib/util/once';
export * from './lib/util/setupcode';
export * from './lib/util/setupid'; // TODO make `generateSetupUri` over the accessory and remove this global export
export * from './lib/util/tlv';
export * from './lib/util/hapStatusError';
export * from './lib/util/color-utils';
Expand Down
101 changes: 49 additions & 52 deletions src/lib/Accessory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
Nullable,
VoidCallback,
WithUUID,
NodeCallback,
} from '../types';
import { Advertiser, AdvertiserEvent, BonjourHAPAdvertiser, CiaoAdvertiser } from './Advertiser';
// noinspection JSDeprecatedSymbols
Expand Down Expand Up @@ -60,11 +61,12 @@ import {
IdentifyCallback,
ListPairingsCallback,
PairCallback,
PairIdentity,
ReadCharacteristicsCallback,
RemovePairingCallback,
ResourceRequestCallback,
TLVErrorCode,
WriteCharacteristicsCallback
WriteCharacteristicsCallback,
} from './HAPServer';
import { AccessoryInfo, PermissionTypes } from './model/AccessoryInfo';
import { ControllerStorage } from "./model/ControllerStorage";
Expand All @@ -75,7 +77,10 @@ import { EventName, HAPConnection, HAPUsername } from "./util/eventedhttp";
import { formatOutgoingCharacteristicValue } from "./util/request-util";
import * as uuid from "./util/uuid";
import { toShortForm } from "./util/uuid";
import { generateSetupId, generateSetupUri } from './util/setupid';
import { generateSetupCode } from './util/setupcode';
import Timeout = NodeJS.Timeout;
import { Identity } from 'fast-srp-hap';

const debug = createDebug('HAP-NodeJS:Accessory');
const MAX_ACCESSORIES = 149; // Maximum number of bridged accessories per bridge.
Expand Down Expand Up @@ -178,7 +183,9 @@ export type CharacteristicEvents = Record<string, any>;

export interface PublishInfo {
username: MacAddress;
pincode: HAPPincode;
pincode?: HAPPincode
| ((callback: NodeCallback<string>, connection: HAPConnection) => void)
| {salt: Buffer; verifier: Buffer};
/**
* Specify the category for the HomeKit accessory.
* The category is used only in the mdns advertisement and specifies the devices type
Expand Down Expand Up @@ -293,6 +300,8 @@ const enum WriteRequestState {
TIMED_WRITE_REJECTED
}

export type SetupCode = Identity & {setupcode: string | null; setupuri: string | null};

// noinspection JSUnusedGlobalSymbols
/**
* @deprecated Use AccessoryEventTypes instead
Expand All @@ -310,6 +319,8 @@ export const enum AccessoryEventTypes {
*/
IDENTIFY = "identify",
LISTENING = "listening",
PAIR_SETUP_STARTED = 'pair-setup-started',
PAIR_SETUP_FINISHED = 'pair-setup-finished',
SERVICE_CONFIGURATION_CHANGE = "service-configurationChange",
/**
* Emitted after a change in the value of one of the provided Service's Characteristics.
Expand All @@ -328,6 +339,9 @@ export declare interface Accessory {
on(event: "service-configurationChange", listener: (change: ServiceConfigurationChange) => void): this;
on(event: "service-characteristic-change", listener: (change: AccessoryCharacteristicChange) => void): this;

on(event: "pair-setup-started", listener: (setupcode: SetupCode, connection: HAPConnection) => void): this;
on(event: "pair-setup-finished", listener: (err: Error | null, clientUsername: string | null, connection: HAPConnection) => void): this;

on(event: "paired", listener: () => void): this;
on(event: "unpaired", listener: () => void): this;

Expand All @@ -340,6 +354,9 @@ export declare interface Accessory {
emit(event: "service-configurationChange", change: ServiceConfigurationChange): boolean;
emit(event: "service-characteristic-change", change: AccessoryCharacteristicChange): boolean;

emit(event: "pair-setup-started", setupcode: SetupCode, connection: HAPConnection): boolean;
emit(event: "pair-setup-finished", err: Error | null, clientUsername: string | null, connection: HAPConnection): boolean;

emit(event: "paired"): boolean;
emit(event: "unpaired"): boolean;

Expand Down Expand Up @@ -880,39 +897,6 @@ export class Accessory extends EventEmitter {
});
}

setupURI(): string {
if (this._setupURI) {
return this._setupURI;
}

const buffer = Buffer.alloc(8);
const setupCode = this._accessoryInfo && parseInt(this._accessoryInfo.pincode.replace(/-/g, ''), 10);

let value_low = setupCode!;
const value_high = this._accessoryInfo && this._accessoryInfo.category >> 1;

value_low |= 1 << 28; // Supports IP;

buffer.writeUInt32BE(value_low, 4);

if (this._accessoryInfo && this._accessoryInfo.category & 1) {
buffer[4] = buffer[4] | 1 << 7;
}

buffer.writeUInt32BE(value_high!, 0);

let encodedPayload = (buffer.readUInt32BE(4) + (buffer.readUInt32BE(0) * Math.pow(2, 32))).toString(36).toUpperCase();

if (encodedPayload.length != 9) {
for (let i = 0; i <= 9 - encodedPayload.length; i++) {
encodedPayload = "0" + encodedPayload;
}
}

this._setupURI = "X-HM://" + encodedPayload + this._setupID;
return this._setupURI;
}

/**
* This method is called right before the accessory is published. It should be used to check for common
* mistakes in Accessory structured, which may lead to HomeKit rejecting the accessory when pairing.
Expand Down Expand Up @@ -1080,8 +1064,9 @@ export class Accessory extends EventEmitter {
* @param allowInsecureRequest - Will allow unencrypted and unauthenticated access to the http server
* @param {string} info.username - The "username" (formatted as a MAC address - like "CC:22:3D:E3:CE:F6") of
* this Accessory. Must be globally unique from all Accessories on your local network.
* @param {string} info.pincode - The 8-digit pincode for clients to use when pairing this Accessory. Must be formatted
* as a string like "031-45-154".
* @param {string|function} info.pincode - The 8-digit pincode for clients to use when pairing this Accessory. Must
* be formatted as a string like "031-45-154". You can also provide a function that
* generates a random setup code and presents it to the user.
* @param {string} info.category - One of the values of the Accessory.Category enum, like Accessory.Category.SWITCH.
* This is a hint to iOS clients about what "type" of Accessory this represents, so
* that for instance an appropriate icon can be drawn for the user while adding a
Expand Down Expand Up @@ -1132,7 +1117,7 @@ export class Accessory extends EventEmitter {
if (info.setupID) {
this._setupID = info.setupID;
} else if (this._accessoryInfo.setupID === undefined || this._accessoryInfo.setupID === "") {
this._setupID = Accessory._generateSetupID();
this._setupID = generateSetupId();
} else {
this._setupID = this._accessoryInfo.setupID;
}
Expand All @@ -1143,7 +1128,7 @@ export class Accessory extends EventEmitter {
this._accessoryInfo.displayName = this.displayName;
this._accessoryInfo.model = this.getService(Service.AccessoryInformation)!.getCharacteristic(Characteristic.Model).value as string;
this._accessoryInfo.category = info.category || Categories.OTHER;
this._accessoryInfo.pincode = info.pincode;
this._accessoryInfo.pincode = info.pincode && typeof info.pincode !== 'function' ? info.pincode : null;
this._accessoryInfo.save();

// create our IdentifierCache so we can provide clients with stable aid/iid's
Expand Down Expand Up @@ -1218,6 +1203,8 @@ export class Accessory extends EventEmitter {
this._server.allowInsecureRequest = !!allowInsecureRequest;
this._server.on(HAPServerEventTypes.LISTENING, this.onListening.bind(this));
this._server.on(HAPServerEventTypes.IDENTIFY, this.identificationRequest.bind(this, false));
this._server.on(HAPServerEventTypes.PAIR_SETUP_STARTED, this._handlePairSetupStarted.bind(this));
this._server.on(HAPServerEventTypes.PAIR_SETUP_FINISHED, this._handlePairSetupFinished.bind(this));
this._server.on(HAPServerEventTypes.PAIR, this.handleInitialPairSetupFinished.bind(this));
this._server.on(HAPServerEventTypes.ADD_PAIRING, this.handleAddPairing.bind(this));
this._server.on(HAPServerEventTypes.REMOVE_PAIRING, this.handleRemovePairing.bind(this));
Expand All @@ -1228,6 +1215,12 @@ export class Accessory extends EventEmitter {
this._server.on(HAPServerEventTypes.CONNECTION_CLOSED, this.handleHAPConnectionClosed.bind(this));
this._server.on(HAPServerEventTypes.REQUEST_RESOURCE, this.handleResource.bind(this));

if (typeof info.pincode === 'function') {
this._server.on(HAPServerEventTypes.GENERATE_SETUP_CODE, info.pincode);
} else if (typeof info.pincode !== 'string' && typeof info.pincode !== 'object') {
this._server.on(HAPServerEventTypes.GENERATE_SETUP_CODE, generateSetupCode);
}

this._server.listen(info.port, parsed.serverAddress);
}

Expand Down Expand Up @@ -1294,6 +1287,23 @@ export class Accessory extends EventEmitter {
this.emit(AccessoryEventTypes.LISTENING, port, hostname);
}

/** Called when starting the pair setup process after a setup code has been generated */
private _handlePairSetupStarted(i: PairIdentity, connection: HAPConnection) {
if (this.listenerCount(AccessoryEventTypes.PAIR_SETUP_STARTED)) {
if (!this._setupID) this._setupID = generateSetupId();
const setupuri = i.setupcode ? generateSetupUri(i.setupcode, this._setupID, this.category) : null;
this.emit(AccessoryEventTypes.PAIR_SETUP_STARTED, {...i, setupuri}, connection);
} else if (!this._accessoryInfo!.pincode) {
// If we're using random setup codes and there's nothing listening for the setup code print it to the console
console.log('[%s] Received pair request from %s', this.displayName, connection.remoteAddress, i.setupcode);
}
}

/** Called when the pair setup process has finished with the error or paired client username */
private _handlePairSetupFinished(err: Nullable<Error>, clientUsername: Nullable<string>, connection: HAPConnection) {
this.emit(AccessoryEventTypes.PAIR_SETUP_FINISHED, err, clientUsername, connection);
}

private handleInitialPairSetupFinished(username: string, publicKey: Buffer, callback: PairCallback): void {
debug("[%s] Paired with client %s", this.displayName, username);

Expand Down Expand Up @@ -1917,19 +1927,6 @@ export class Accessory extends EventEmitter {
});
}

private static _generateSetupID(): string {
const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
const max = chars.length;
let setupID = '';

for (let i = 0; i < 4; i++) {
const index = Math.floor(Math.random() * max)
setupID += chars.charAt(index);
}

return setupID;
}

// serialization and deserialization functions, mainly designed for homebridge to create a json copy to store on disk
public static serialize(accessory: Accessory): SerializedAccessory {
const json: SerializedAccessory = {
Expand Down
14 changes: 4 additions & 10 deletions src/lib/Advertiser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import ciao, {
import { InterfaceName, IPAddress } from "@homebridge/ciao/lib/NetworkManager";
import assert from "assert";
import bonjour, { BonjourHAP, BonjourHAPService, MulticastOptions } from "bonjour-hap";
import crypto from 'crypto';
import { EventEmitter } from "events";
import { AccessoryInfo } from './model/AccessoryInfo';
import { AccessoryInfo } from "./model/AccessoryInfo";
import { generateSetupHash } from "./util/setupid";

/**
* This enum lists all bitmasks for all known status flags.
Expand Down Expand Up @@ -101,7 +101,7 @@ export class CiaoAdvertiser extends EventEmitter implements Advertiser {
constructor(accessoryInfo: AccessoryInfo, responderOptions?: MDNSServerOptions, serviceOptions?: ServiceNetworkOptions) {
super();
this.accessoryInfo = accessoryInfo;
this.setupHash = CiaoAdvertiser.computeSetupHash(accessoryInfo);
this.setupHash = generateSetupHash(this.accessoryInfo.username, this.accessoryInfo.setupID);

this.responder = ciao.getResponder({
...responderOptions
Expand Down Expand Up @@ -157,12 +157,6 @@ export class CiaoAdvertiser extends EventEmitter implements Advertiser {
};
}

static computeSetupHash(accessoryInfo: AccessoryInfo): string {
const hash = crypto.createHash('sha512');
hash.update(accessoryInfo.setupID + accessoryInfo.username.toUpperCase());
return hash.digest().slice(0, 4).toString('base64');
}

public static ff(...flags: PairingFeatureFlag[]): number {
let value = 0;
flags.forEach(flag => value |= flag);
Expand Down Expand Up @@ -195,7 +189,7 @@ export class BonjourHAPAdvertiser extends EventEmitter implements Advertiser {
constructor(accessoryInfo: AccessoryInfo, responderOptions?: MulticastOptions, serviceOptions?: ServiceNetworkOptions) {
super();
this.accessoryInfo = accessoryInfo;
this.setupHash = CiaoAdvertiser.computeSetupHash(accessoryInfo);
this.setupHash = generateSetupHash(this.accessoryInfo.username, this.accessoryInfo.setupID);
this.serviceOptions = serviceOptions;

this.bonjour = bonjour(responderOptions);
Expand Down
Loading

0 comments on commit e54f2ba

Please sign in to comment.