nubli
Version:
Nuki Bluetooth Library
283 lines (282 loc) • 13.9 kB
JavaScript
"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;