Skip to content

Commit

Permalink
Support using a random setup code
Browse files Browse the repository at this point in the history
  • Loading branch information
samuelthomas2774 committed Oct 27, 2019
1 parent 322baca commit a6a4810
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 18 deletions.
21 changes: 13 additions & 8 deletions src/lib/Accessory.ts
Expand Up @@ -96,7 +96,7 @@ export type CharacteristicEvents = Record<string, any>;

export interface PublishInfo {
username: string;
pincode: string;
pincode: string | ((callback: NodeCallback<string>, session: Session) => void);
category?: Categories;
setupID?: string;
port?: number;
Expand Down Expand Up @@ -449,15 +449,15 @@ export class Accessory extends EventEmitter<Events> {
}
}

setupURI = () => {
if (this._setupURI) {
setupURI = (pincode?: string) => {
if (this._setupURI && !pincode) {
return this._setupURI;
}

var buffer = bufferShim.alloc(8);
var setupCode = this._accessoryInfo && parseInt(this._accessoryInfo.pincode.replace(/-/g, ''), 10);
var setupCode = parseInt(((this._accessoryInfo && this._accessoryInfo.pincode) || pincode)!.replace(/-/g, ''), 10);

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

value_low |= 1 << 28; // Supports IP;
Expand Down Expand Up @@ -591,8 +591,9 @@ export class Accessory extends EventEmitter<Events> {
* @param {Object} info - Required info for publishing.
* @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 @@ -621,7 +622,7 @@ export class Accessory extends EventEmitter<Events> {
// make sure we have up-to-date values in AccessoryInfo, then save it in case they changed (or if we just created it)
this._accessoryInfo.displayName = this.displayName;
this._accessoryInfo.category = info.category || Categories.OTHER;
this._accessoryInfo.pincode = info.pincode;
this._accessoryInfo.pincode = typeof info.pincode === 'string' ? info.pincode : null;
this._accessoryInfo.save();

// if (this._isBridge) {
Expand Down Expand Up @@ -684,6 +685,10 @@ export class Accessory extends EventEmitter<Events> {
this._server.on(HAPServerEventTypes.SESSION_CLOSE, this._handleSessionClose);
this._server.on(HAPServerEventTypes.REQUEST_RESOURCE, this._handleResource);

if (typeof info.pincode === 'function') {
this._server.on(HAPServerEventTypes.GENERATE_SETUP_CODE, info.pincode);
}

const targetPort = info.port || 0;
this._server.listen(targetPort);
}
Expand Down
72 changes: 63 additions & 9 deletions src/lib/HAPServer.ts
Expand Up @@ -41,6 +41,7 @@ export enum TLVValues {
PERMISSIONS = 0x0B, // None (0x00): regular user, 0x01: Admin (able to add/remove/list pairings)
FRAGMENT_DATA = 0x0C,
FRAGMENT_LAST = 0x0D,
FLAGS = 0x13,
SEPARATOR = 0x0FF // Zero-length TLV that separates different TLVs in a list.
}

Expand Down Expand Up @@ -74,6 +75,11 @@ export enum Codes {
BUSY = 0x07 // cannot accept pairing request at this time
}

export enum PairingFlags {
TRANSIENT = 0x10,
SPLIT = 0x01000000
}

// Status codes for underlying HAP calls
export enum Status {
SUCCESS = 0,
Expand Down Expand Up @@ -109,6 +115,7 @@ export type HapRequest = {
export enum HAPServerEventTypes {
IDENTIFY = "identify",
LISTENING = "listening",
GENERATE_SETUP_CODE = 'generate-setup-code',
PAIR = 'pair',
ADD_PAIRING = 'add-pairing',
REMOVE_PAIRING = 'remove_pairing',
Expand All @@ -123,6 +130,7 @@ export enum HAPServerEventTypes {
export type Events = {
[HAPServerEventTypes.IDENTIFY]: (cb: VoidCallback) => void;
[HAPServerEventTypes.LISTENING]: (port: number) => void;
[HAPServerEventTypes.GENERATE_SETUP_CODE]: (callback: NodeCallback<string>, session: Session) => void;
[HAPServerEventTypes.PAIR]: (clientUsername: string, clientLTPK: Buffer, cb: VoidCallback) => void;
[HAPServerEventTypes.ADD_PAIRING]: (controller: Session, username: string, publicKey: Buffer, permission: number, callback: PairingsCallback<void>) => void;
[HAPServerEventTypes.REMOVE_PAIRING]: (controller: Session, username: string, callback: PairingsCallback<void>) => void;
Expand Down Expand Up @@ -171,6 +179,10 @@ export type Events = {
* @event 'identify' => function(callback(err)) { }
* Emitted when a client wishes for this server to identify itself before pairing. You must call the
* callback to respond to the client with success.
*
* @event 'generate-setup-code' => function(callback(err, pincode), session) {}
* Emitted when a client starts the pairing process. You must call the callback with the setup code to use and
* display the code to the user. You don't have to listen to this event if you provide a static setup code.
*
* @event 'pair' => function(username, publicKey, callback(err)) { }
* This event is emitted when a client completes the "pairing" process and exchanges encryption keys.
Expand Down Expand Up @@ -225,6 +237,7 @@ export class HAPServer extends EventEmitter<Events> {

/** Session currently trying to pair with the server */
_pairing: Session | null = null;
_setupCode: string | null = null;

allowInsecureRequest: boolean;
_keepAliveTimerID: NodeJS.Timeout;
Expand Down Expand Up @@ -404,26 +417,67 @@ export class HAPServer extends EventEmitter<Events> {
var objects = tlv.decode(requestData);
var sequence = objects[TLVValues.SEQUENCE_NUM][0]; // value is single byte with sequence number
if (sequence == 0x01)
this._handlePairStepOne(request, response, session);
this._handlePairStepOne(request, response, session, objects);
else if (sequence == 0x03)
this._handlePairStepTwo(request, response, session, objects);
else if (sequence == 0x05)
this._handlePairStepThree(request, response, session, objects);
}

// M1 + M2
_handlePairStepOne = (request: IncomingMessage, response: ServerResponse, session: Session) => {
_handlePairStepOne = (request: IncomingMessage, response: ServerResponse, session: Session, objects: Record<number, Buffer>) => {
debug("[%s] Pair step 1/5", this.accessoryInfo.username);
var salt = crypto.randomBytes(16);
var srpParams = srp.params["3072"];
srp.genKey(32, (error: Error, key: Buffer) => {
// create a new SRP server
var srpServer = new srp.Server(srpParams, bufferShim.from(salt), bufferShim.from("Pair-Setup"), bufferShim.from(this.accessoryInfo.pincode), key);
var srpB = srpServer.computeB();
// attach it to the current TCP session
session.srpServer = srpServer;

if (objects[TLVValues.METHOD][0] !== Methods.PAIR_SETUP) {
response.writeHead(200, {"Content-Type": "application/pairing+tlv8"});
response.end(tlv.encode(TLVValues.SEQUENCE_NUM, 0x02, TLVValues.SALT, salt, TLVValues.PUBLIC_KEY, srpB));
response.end(tlv.encode(TLVValues.STATE, States.M2, TLVValues.ERROR_CODE, Codes.UNAVAILABLE));
return;
}

const flags = objects[TLVValues.FLAGS];
const decodedFlags = flags && flags.length === 4 ? tlv.readUInt32(objects[TLVValues.FLAGS]) : null;
const transient = !decodedFlags || decodedFlags === (PairingFlags.TRANSIENT & PairingFlags.SPLIT);
const split = decodedFlags === PairingFlags.SPLIT;

srp.genKey(32, (error: Error, key: Buffer) => {
if (transient && this.listenerCount(HAPServerEventTypes.GENERATE_SETUP_CODE)) {
this.emit(HAPServerEventTypes.GENERATE_SETUP_CODE, once((err: Error | null | undefined, pincode: string) => {
if (err) {
response.writeHead(200, {"Content-Type": "application/pairing+tlv8"});
response.end(tlv.encode(TLVValues.STATE, States.M2, TLVValues.ERROR_CODE, Codes.UNAVAILABLE));
this._pairing = null;
return;
}

this._setupCode = pincode;
// create a new SRP server
var srpServer = new srp.Server(srpParams, bufferShim.from(salt), bufferShim.from("Pair-Setup"), bufferShim.from(pincode), key);
var srpB = srpServer.computeB();
// attach it to the current TCP session
session.srpServer = srpServer;
response.writeHead(200, {"Content-Type": "application/pairing+tlv8"});
const responseTLV = tlv.encode(TLVValues.SEQUENCE_NUM, States.M2, TLVValues.SALT, salt, TLVValues.PUBLIC_KEY, srpB);
response.end(flags ? Buffer.concat([responseTLV, tlv.encode(TLVValues.FLAGS, flags)]) : responseTLV);
}), session);
} else if ((split && this._setupCode) || this.accessoryInfo.pincode) {
if (!split) this._setupCode = this.accessoryInfo.pincode;
// create a new SRP server
var srpServer = new srp.Server(srpParams, bufferShim.from(salt), bufferShim.from("Pair-Setup"), bufferShim.from(this._setupCode!), key);
var srpB = srpServer.computeB();
// attach it to the current TCP session
session.srpServer = srpServer;
response.writeHead(200, {"Content-Type": "application/pairing+tlv8"});
const responseTLV = tlv.encode(TLVValues.SEQUENCE_NUM, States.M2, TLVValues.SALT, salt, TLVValues.PUBLIC_KEY, srpB);
response.end(flags ? Buffer.concat([responseTLV, tlv.encode(TLVValues.FLAGS, flags)]) : responseTLV);
} else {
if (transient) debug('No setup code configured');

response.writeHead(200, {"Content-Type": "application/pairing+tlv8"});
response.end(tlv.encode(TLVValues.SEQUENCE_NUM, States.M2, TLVValues.ERROR_CODE, Codes.AUTHENTICATION));
this._pairing = null;
}
});
}

Expand Down
2 changes: 1 addition & 1 deletion src/lib/model/AccessoryInfo.ts
Expand Up @@ -25,7 +25,7 @@ export class AccessoryInfo {
username: string;
displayName: string;
category: Categories;
pincode: string;
pincode: string | null;
signSk: any;
signPk: any;
pairedClients: Record<string, PairingInformation>;
Expand Down

0 comments on commit a6a4810

Please sign in to comment.