UNPKG

nubli

Version:
283 lines (282 loc) 13.9 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const states_1 = require("./states"); const errorHandler_1 = require("./errorHandler"); const events_1 = __importDefault(require("events")); const crypto_1 = __importDefault(require("crypto")); const smartLock_1 = require("./smartLock"); class SmartLockPairer extends events_1.default.EventEmitter { constructor(nukiPairingCharacteristic, nukiConfig, asBridge) { super(); this.state = states_1.PairingState.IDLE; this.partialPayload = null; this.nonceABF = null; // The first packet should not be verified as it does not contain any CRC and is only partial. this.verifyCRC = false; if (nukiPairingCharacteristic === null) { throw new Error("characteristic cannot be null"); } this.nukiPairingCharacteristic = nukiPairingCharacteristic; this.config = nukiConfig; this.asBridge = asBridge; } setupPairListener() { return __awaiter(this, void 0, void 0, function* () { return new Promise((resolve, reject) => { this.nukiPairingCharacteristic.subscribe((error) => { if (error) { reject(error); return; } this.nukiPairingCharacteristic.on('data', (data, isNotification) => this.pairingDataReceived(data, isNotification)); resolve(); }); }); }); } removePairListener() { return __awaiter(this, void 0, void 0, function* () { return new Promise((resolve, reject) => { this.nukiPairingCharacteristic.unsubscribe((error) => { this.nukiPairingCharacteristic.removeListener('data', this.pairingDataReceived); if (error) { reject(error); } resolve(); }); }); }); } writeData(data) { return __awaiter(this, void 0, void 0, function* () { let dataCrc = smartLock_1.SmartLock.appendCRC(data); return new Promise((resolve, reject) => { this.nukiPairingCharacteristic.write(dataCrc, false, (error) => { if (error) { reject(error); } else { resolve(); } }); }); }); } validateCRC(data) { if (this.partialPayload) { data = Buffer.concat([this.partialPayload, data]); } if (!smartLock_1.SmartLock.verifyCRC(data)) { let errorMessage = errorHandler_1.ErrorHandler.errorToMessage(states_1.GeneralError.BAD_CRC); this.emit("pairingFailed", errorMessage); return false; } return true; } getCommandFromPayload(payload) { return payload.readUInt16LE(0); } getDataFromPayload(payload) { return payload.slice(2, payload.length - 2); } printErrorMessage(message, payload) { if (payload != null) { if (this.getCommandFromPayload(payload) == states_1.Command.ERROR_REPORT) { let errorMessage = errorHandler_1.ErrorHandler.errorToMessage(payload.readInt8(2)); this.emit("pairingFailed", errorMessage); } else { this.emit("pairingFailed", message); } } else { this.emit("pairingFailed", message); } } pairingDataReceived(payload, isNotification) { // Only check CRC if we should. if (this.verifyCRC && !this.validateCRC(payload)) return; let data; switch (this.state) { // Smartlock sent first half of it's public key case states_1.PairingState.REQ_PUB_KEY: if (this.getCommandFromPayload(payload) != states_1.Command.PUBLIC_KEY) { this.printErrorMessage("Unexpected data received during REQ_PUB_KEY", payload); } else { this.partialPayload = payload; this.verifyCRC = true; this.state = states_1.PairingState.REQ_PUB_KEY_FIN; } break; // Smartlock has sent it's public key. We send ours now. case states_1.PairingState.REQ_PUB_KEY_FIN: this.config.credentials.slPublicKey = this.getDataFromPayload(Buffer.concat([this.partialPayload, payload])); this.partialPayload = null; data = smartLock_1.SmartLock.prepareCommand(states_1.Command.PUBLIC_KEY, new Buffer(this.config.credentials.publicKey)); this.writeData(data); this.verifyCRC = false; this.state = states_1.PairingState.REQ_CHALLENGE; break; // SmartLock has sent the first part of the challenge. case states_1.PairingState.REQ_CHALLENGE: if (this.getCommandFromPayload(payload) != states_1.Command.CHALLENGE) { this.printErrorMessage("Unexpected data received during REQ_CHALLENGE", payload); } else { this.partialPayload = payload; this.verifyCRC = true; this.state = states_1.PairingState.REQ_CHALLENGE_FIN; } break; // Smartlock has sent the challenge. We calculate the authenticator and send it. case states_1.PairingState.REQ_CHALLENGE_FIN: let nonceK = this.getDataFromPayload(Buffer.concat([this.partialPayload, payload])); this.partialPayload = null; let r = Buffer.concat([this.config.credentials.publicKey, this.config.credentials.slPublicKey, nonceK]); let authenticator = crypto_1.default.createHmac('SHA256', this.config.credentials.sharedSecret).update(r).digest(); data = smartLock_1.SmartLock.prepareCommand(states_1.Command.AUTH_AUTHENTICATOR, authenticator); this.writeData(data); this.verifyCRC = false; this.state = states_1.PairingState.REQ_CHALLENGE_AUTH; break; // Smartlock has sent the first part of the second challenge. case states_1.PairingState.REQ_CHALLENGE_AUTH: if (this.getCommandFromPayload(payload) != states_1.Command.CHALLENGE) { this.printErrorMessage("Unexpected data received DURING REQ_CHALLENGE_AUTH", payload); } else { this.partialPayload = payload; this.verifyCRC = true; this.state = states_1.PairingState.REQ_CHALLENGE_AUTH_FIN; } break; // Smartlock has sent the challenge. We calculate the authorization data and send it. case states_1.PairingState.REQ_CHALLENGE_AUTH_FIN: let nonceK2 = this.getDataFromPayload(Buffer.concat([this.partialPayload, payload])); this.partialPayload = null; let authData = this.generateAuthorizationData(); this.nonceABF = smartLock_1.SmartLock.generateNonce(32); let r2 = Buffer.concat([authData, this.nonceABF, nonceK2]); let authenticator2 = crypto_1.default.createHmac('SHA256', this.config.credentials.sharedSecret).update(r2).digest(); data = Buffer.concat([authenticator2, authData, this.nonceABF]); data = smartLock_1.SmartLock.prepareCommand(states_1.Command.AUTH_DATA, data); this.writeData(data); this.verifyCRC = false; this.state = states_1.PairingState.REQ_AUTH_ID_A; break; //Smartlock has sent the first part of the authorization id case states_1.PairingState.REQ_AUTH_ID_A: if (this.getCommandFromPayload(payload) != states_1.Command.AUTH_ID) { this.printErrorMessage("Unexpected data received during REQ_AUTH_ID_A", payload); } else { this.partialPayload = payload; this.state = states_1.PairingState.REQ_AUTH_ID_B; } break; //Smartlock has sent the second part of the authorization id case states_1.PairingState.REQ_AUTH_ID_B: this.partialPayload = Buffer.concat([this.partialPayload, payload]); this.state = states_1.PairingState.REQ_AUTH_ID_C; break; //Smartlock has sent the third part of the authorization id case states_1.PairingState.REQ_AUTH_ID_C: this.partialPayload = Buffer.concat([this.partialPayload, payload]); this.state = states_1.PairingState.REQ_AUTH_ID_D; break; //Smartlock has sent the fourth part of the authorization id case states_1.PairingState.REQ_AUTH_ID_D: this.partialPayload = Buffer.concat([this.partialPayload, payload]); this.verifyCRC = true; this.state = states_1.PairingState.REQ_AUTH_ID_FIN; break; //Smartlock has sent the fifth part of the authorization id case states_1.PairingState.REQ_AUTH_ID_FIN: let auth = this.getDataFromPayload(Buffer.concat([this.partialPayload, payload])); this.partialPayload = null; let authenticator3 = auth.slice(0, 32); let authIdBuf = auth.slice(32, 36); this.config.authorizationId = authIdBuf.readUInt32LE(0); this.config.slUUID = auth.slice(36, 52); let nonceK3 = auth.slice(52, 84); let r3 = Buffer.concat([authIdBuf, this.config.slUUID, nonceK3, this.nonceABF]); let cr = crypto_1.default.createHmac('SHA256', this.config.credentials.sharedSecret).update(r3).digest(); if (Buffer.compare(authenticator3, cr) !== 0) { this.emit("pairingFailed", "The authenticator could not be verified."); } else { let r4 = Buffer.concat([authIdBuf, nonceK3]); let authenticator4 = crypto_1.default.createHmac('SHA256', this.config.credentials.sharedSecret).update(r4).digest(); data = smartLock_1.SmartLock.prepareCommand(states_1.Command.AUTH_ID_CONFIRM, Buffer.concat([authenticator4, authIdBuf])); this.writeData(data); this.state = states_1.PairingState.REQ_AUTH_ID_CONFIRM; } break; case states_1.PairingState.REQ_AUTH_ID_CONFIRM: if (this.getCommandFromPayload(payload) == states_1.Command.STATUS && this.getDataFromPayload(payload).readUInt8(0) == states_1.Status.COMPLETE) { this.state = states_1.PairingState.PAIRED; this.config.paired = true; this.emit("paired"); } else { this.printErrorMessage("The smart lock indicated that the pairing failed", payload); } break; default: this.emit("pairingFailed", "Unexpected data received"); break; } } generateAuthorizationData() { let id = new Buffer(5); if (this.asBridge) { // We are a bridge id.writeUInt8(1, 0); } else { // We are an app id.writeUInt8(0, 0); } id.writeUInt32LE(this.config.appId, 1); let name = new Buffer(32).fill(0); name.write("Nubli Node.js Library", 0); return Buffer.concat([id, name]); } pair() { return __awaiter(this, void 0, void 0, function* () { this.state = states_1.PairingState.IDLE; return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () { yield this.setupPairListener(); let identifier = new Buffer(2); identifier.writeUInt16LE(states_1.Command.PUBLIC_KEY, 0); let data = smartLock_1.SmartLock.prepareCommand(states_1.Command.REQUEST_DATA, identifier); // First step - Request Public Key from SmartLock this.state = states_1.PairingState.REQ_PUB_KEY; this.writeData(data); this.on('paired', () => { this.removePairListener(); resolve(this.config); }); this.on('pairingFailed', (error) => { this.state = states_1.PairingState.FAILED; this.removePairListener(); reject(error); }); })); }); } } exports.SmartLockPairer = SmartLockPairer;