<commit_before>var util = require('util');
var nukiConstants = require('./nuki-constants');
var _ = require('underscore');
var crc = require('crc');
var sodium = require('sodium');
var HSalsa20 = require('./hsalsa20');

var bleno = require('bleno');
var BlenoCharacteristic = bleno.Characteristic;
var BlenoDescriptor = bleno.Descriptor;

function ParingGeneralDataInputOutputCharacteristic(keys) {
    this.state = this.PAIRING_IDLE;
    this.keys = keys;
    this.dataStillToSend = new Buffer(0);

    ParingGeneralDataInputOutputCharacteristic.super_.call(this, {
        // uuid: 'a92ee101-5501-11e4-916c-0800200c9a66',
        uuid: 'a92ee101550111e4916c0800200c9a66',
        properties: ['write', 'indicate'],
        descriptors: [
            new BlenoDescriptor({
                uuid: '2902',   // client characterstic configuration
                value: 'Pairing commands'
            })
        ]
    });
}

util.inherits(ParingGeneralDataInputOutputCharacteristic, BlenoCharacteristic);

ParingGeneralDataInputOutputCharacteristic.prototype.PAIRING_IDLE = 0;
ParingGeneralDataInputOutputCharacteristic.prototype.PAIRING_SL_SEND_PUBKEY = 1;
ParingGeneralDataInputOutputCharacteristic.prototype.PAIRING_CL_SEND_PUBKEY = 2;
ParingGeneralDataInputOutputCharacteristic.prototype.PAIRING_SL_SEND_CHALLENGE = 3;

ParingGeneralDataInputOutputCharacteristic.prototype.getNextChunk = function (data) {
    var block0;
    if (data.length > 20) {
        block0 = data.slice(0, 20);
        this.dataStillToSend = data.slice(20);
    } else {
        block0 = data;
        this.dataStillToSend = new Buffer(0);
    }
    return block0;
};

ParingGeneralDataInputOutputCharacteristic.prototype.prepareDataToSend = function (cmd, data) {
    var cmdBuffer = new Buffer(2);
    cmdBuffer.writeUInt16LE(cmd);
    var responseData = Buffer.concat([cmdBuffer, data]);
    var checksum = crc.crc16ccitt(responseData);
    var checksumBuffer = new Buffer(2);
    checksumBuffer.writeUInt16LE(checksum);
    this.dataStillToSend = Buffer.concat([responseData, checksumBuffer]);
    console.log("prepared to send:", this.dataStillToSend);
};

ParingGeneralDataInputOutputCharacteristic.prototype.onWriteRequest = function (data, offset, withoutResponse, callback) {
    var cmdId;
    console.log("ParingGeneralDataInputOutputCharacteristic", data);
    var dataForCrc = data.slice(0, data.length - 2);
    var crcSumCalc = crc.crc16ccitt(dataForCrc);
    var crcSumRetrieved = data.readUInt16LE(data.length - 2);

    if (crcSumCalc === crcSumRetrieved) {
        console.log("checksum is ok");
        if (offset) {
            callback(this.RESULT_ATTR_NOT_LONG);
        } else if (data.length > 200) {
            callback(this.RESULT_INVALID_ATTRIBUTE_LENGTH);
        } else {
            switch (this.state) {
                case ParingGeneralDataInputOutputCharacteristic.prototype.PAIRING_IDLE:
                    var rCmd = data.readUInt16LE(0);
                    cmdId = data.readUInt16LE(2);
                    if (rCmd === nukiConstants.CMD_reqUEST_DATA && cmdId === nukiConstants.CMD_ID_PUBLIC_KEY) {
                        var slPk = new Buffer(0);
                        if (Buffer.isBuffer(this.keys.slPk)) {
                            slPk = this.keys.slPk;
                        } else {
                            if (_.isString(this.keys.slPk)) {
                                slPk = new Buffer(this.keys.slPk, 'hex');
                            } else {
                                if (_.isArray(this.keys.slPk)) {
                                    slPk = new Buffer(this.keys.slPk);
                                }
                            }
                        }

                        if (slPk.length > 0) {
                            this.prepareDataToSend(nukiConstants.CMD_ID_PUBLIC_KEY, slPk);
                            this.state = ParingGeneralDataInputOutputCharacteristic.prototype.PAIRING_SL_SEND_PUBKEY;

                            callback(this.RESULT_SUCCESS);
                        } else {
                            console.log("ERROR missing SL public key");
                            this.state = this.PAIRING_IDLE;
                            callback(this.RESULT_UNLIKELY_ERROR);
                        }
                    }
                    else {
                        console.log("command or command identifier wrong");
                        this.state = this.PAIRING_IDLE;
                        callback(this.RESULT_UNLIKELY_ERROR);
                    }
                    break;
                case ParingGeneralDataInputOutputCharacteristic.prototype.PAIRING_CL_SEND_PUBKEY:
                    cmdId = data.readUInt16LE(0);
                    if (cmdId === nukiConstants.CMD_ID_PUBLIC_KEY) {
                        this.keys.clPk = data.slice(2, data.length - 2);
                        console.log("CL PUBKEY:", this.keys.clPk);

                        var slPk = new Buffer(0);
                        if (Buffer.isBuffer(this.keys.slPk)) {
                            slPk = this.keys.slPk;
                        } else {
                            if (_.isString(this.keys.slPk)) {
                                slPk = new Buffer(this.keys.slPk, 'hex');
                            } else {
                                if (_.isArray(this.keys.slPk)) {
                                    slPk = new Buffer(this.keys.slPk);
                                }
                            }
                        }
                        var slSk = new Buffer(0);
                        if (Buffer.isBuffer(this.keys.slSk)) {
                            slSk = this.keys.slSk;
                        } else {
                            if (_.isString(this.keys.slSk)) {
                                slSk = new Buffer(this.keys.slSk, 'hex');
                            } else {
                                if (_.isArray(this.keys.slSk)) {
                                    slSk = new Buffer(this.keys.slSk);
                                }
                            }
                        }

                        console.log("slPK", slPk);
                        console.log("slSK", slSk);
                        console.log("clPK", this.keys.clPk);

                        // Create Diffie-Hellman key from nuki secret key and clients public key
                        // crypto_scalarmult_curve25519(k,secretKey,pk)
                        var k = sodium.api.crypto_scalarmult(slSk, this.keys.clPk);
                        console.log("SL DH Key from SL SK and CL PK: ", k);

                        // derive a longterm shared secret key s from k using function kdf1
                        // static const unsigned char _0[16];
                        // static const unsigned char sigma[16] = "expand 32-byte k";
                        // crypto_core_hsalsa20(firstKey,_0,sharedKey,sigma)
                        var hsalsa20 = new HSalsa20();
                        var s = new Buffer(32);
                        var inv = new Buffer(16);
                        inv.fill(0);
                        var c = new Buffer("expand 32-byte k");
                        hsalsa20.crypto_core(s, inv, k, c);
                        console.log("derived shared key: ", s);

                        var nonce = new Buffer(nukiConstants.NUKI_NONCEBYTES);
                        sodium.api.randombytes_buf(nonce);
                        // todo remove hardcoded challenge
                        nonce = new Buffer("6CD4163D159050C798553EAA57E278A579AFFCBC56F09FC57FE879E51C42DF17", 'hex');
                        if (nonce.length != nukiConstants.NUKI_NONCEBYTES) {
                            console.log("Nonce length (" + nonce.length + ") is not " + nukiConstants.NUKI_NONCEBYTES);
                            this.state = this.PAIRING_IDLE;
                            callback(this.RESULT_UNLIKELY_ERROR);
                            return;
                        }

                        console.log("Creating one time challenge...");
                        this.prepareDataToSend(nukiConstants.CMD_CHALLENGE, nonce);
                        var value = this.getNextChunk(this.dataStillToSend);
                        if (this._updateValueCallback && value.length > 0) {
                            console.log("sending " + value.length + " bytes");
                            this._updateValueCallback(value);
                        }

                        this.state = ParingGeneralDataInputOutputCharacteristic.prototype.PAIRING_SL_SEND_CHALLENGE;

                        callback(this.RESULT_SUCCESS);
                    }
                    else {
                        console.log("command or command identifier wrong");
                        this.state = this.PAIRING_IDLE;
                        callback(this.RESULT_UNLIKELY_ERROR);
                    }

                    break;
                default:
                    console.log("ERROR unexpected pairing state");
                    this.state = this.PAIRING_IDLE;
                    callback(this.RESULT_UNLIKELY_ERROR);
            }
        }
    } else {
        console.log("checksum is NOT ok");
        this.state = this.PAIRING_IDLE;
        callback(this.RESULT_UNLIKELY_ERROR);
    }
};

ParingGeneralDataInputOutputCharacteristic.prototype.onSubscribe = function (maxValueSize, updateValueCallback) {
    console.log('ParingGeneralDataInputOutputCharacteristic - onSubscribe');

    this._updateValueCallback = updateValueCallback;

    if (this.dataStillToSend.length > 0) {
        switch (this.state) {
            case ParingGeneralDataInputOutputCharacteristic.prototype.PAIRING_SL_SEND_PUBKEY:

                var value = this.getNextChunk(this.dataStillToSend);
                if (value.length > 0) {
                    console.log("sending " + value.length + " bytes from onSubscribe");
                    updateValueCallback(value);
                }

                if (this.dataStillToSend.length === 0) {
                    this.state = ParingGeneralDataInputOutputCharacteristic.prototype.PAIRING_CL_SEND_PUBKEY;
                }

                break;
            default:
                console.log("ERROR unexpected pairing state");
                this.state = this.PAIRING_IDLE;
        }
    } else {
        console.log("don't have more data to notify");
    }
};

ParingGeneralDataInputOutputCharacteristic.prototype.onUnsubscribe = function () {
    console.log('ParingGeneralDataInputOutputCharacteristic - onUnsubscribe');

    this._updateValueCallback = null;
};

ParingGeneralDataInputOutputCharacteristic.prototype.onIndicate = function () {
    console.log("ParingGeneralDataInputOutputCharacteristic indicate");
    if (this.dataStillToSend.length > 0) {
        if (this._updateValueCallback) {
            switch (this.state) {
                case ParingGeneralDataInputOutputCharacteristic.prototype.PAIRING_SL_SEND_PUBKEY:
                    var value = this.getNextChunk(this.dataStillToSend);
                    if (value.length > 0) {
                        console.log("sending " + value.length + " bytes as indication");
                        this._updateValueCallback(value);
                    }
                    if (this.dataStillToSend.length === 0) {
                        this.state = ParingGeneralDataInputOutputCharacteristic.prototype.PAIRING_CL_SEND_PUBKEY;
                    }

                    break;
                default:
                    console.log("ERROR unexpected pairing state");
                    this.state = this.PAIRING_IDLE;
            }
        } else {
            console.log("don't have updateValueCallback on indicate");
            this.state = this.PAIRING_IDLE;
        }
    } else {
        console.log("don't have more data to indicate");
    }
};

module.exports = ParingGeneralDataInputOutputCharacteristic;
<commit_msg>fix typo new state for cl sends authenticator<commit_after>var util = require('util');
var nukiConstants = require('./nuki-constants');
var _ = require('underscore');
var crc = require('crc');
var sodium = require('sodium');
var HSalsa20 = require('./hsalsa20');

var bleno = require('bleno');
var BlenoCharacteristic = bleno.Characteristic;
var BlenoDescriptor = bleno.Descriptor;

function PairingGeneralDataInputOutputCharacteristic(keys) {
    this.state = this.PAIRING_IDLE;
    this.keys = keys;
    this.dataStillToSend = new Buffer(0);

    PairingGeneralDataInputOutputCharacteristic.super_.call(this, {
        // uuid: 'a92ee101-5501-11e4-916c-0800200c9a66',
        uuid: 'a92ee101550111e4916c0800200c9a66',
        properties: ['write', 'indicate'],
        descriptors: [
            new BlenoDescriptor({
                uuid: '2902',   // client characterstic configuration
                value: 'Pairing commands'
            })
        ]
    });
}

util.inherits(PairingGeneralDataInputOutputCharacteristic, BlenoCharacteristic);

PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_IDLE = 0;
PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_SL_SEND_PUBKEY = 1;
PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_CL_SEND_PUBKEY = 2;
PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_SL_SEND_CHALLENGE = 3;
PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_CL_SEND_AUTHENTICATOR = 4;

PairingGeneralDataInputOutputCharacteristic.prototype.getNextChunk = function (data) {
    var block0;
    if (data.length > 20) {
        block0 = data.slice(0, 20);
        this.dataStillToSend = data.slice(20);
    } else {
        block0 = data;
        this.dataStillToSend = new Buffer(0);
    }
    return block0;
};

PairingGeneralDataInputOutputCharacteristic.prototype.prepareDataToSend = function (cmd, data) {
    var cmdBuffer = new Buffer(2);
    cmdBuffer.writeUInt16LE(cmd);
    var responseData = Buffer.concat([cmdBuffer, data]);
    var checksum = crc.crc16ccitt(responseData);
    var checksumBuffer = new Buffer(2);
    checksumBuffer.writeUInt16LE(checksum);
    this.dataStillToSend = Buffer.concat([responseData, checksumBuffer]);
    console.log("prepared to send:", this.dataStillToSend);
};

PairingGeneralDataInputOutputCharacteristic.prototype.onWriteRequest = function (data, offset, withoutResponse, callback) {
    var cmdId;
    console.log("PairingGeneralDataInputOutputCharacteristic", data);
    var dataForCrc = data.slice(0, data.length - 2);
    var crcSumCalc = crc.crc16ccitt(dataForCrc);
    var crcSumRetrieved = data.readUInt16LE(data.length - 2);

    if (crcSumCalc === crcSumRetrieved) {
        console.log("checksum is ok");
        if (offset) {
            callback(this.RESULT_ATTR_NOT_LONG);
        } else if (data.length > 200) {
            callback(this.RESULT_INVALID_ATTRIBUTE_LENGTH);
        } else {
            switch (this.state) {
                case PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_IDLE:
                    var rCmd = data.readUInt16LE(0);
                    cmdId = data.readUInt16LE(2);
                    if (rCmd === nukiConstants.CMD_reqUEST_DATA && cmdId === nukiConstants.CMD_ID_PUBLIC_KEY) {
                        var slPk = new Buffer(0);
                        if (Buffer.isBuffer(this.keys.slPk)) {
                            slPk = this.keys.slPk;
                        } else {
                            if (_.isString(this.keys.slPk)) {
                                slPk = new Buffer(this.keys.slPk, 'hex');
                            } else {
                                if (_.isArray(this.keys.slPk)) {
                                    slPk = new Buffer(this.keys.slPk);
                                }
                            }
                        }

                        if (slPk.length > 0) {
                            this.prepareDataToSend(nukiConstants.CMD_ID_PUBLIC_KEY, slPk);
                            this.state = PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_SL_SEND_PUBKEY;

                            callback(this.RESULT_SUCCESS);
                        } else {
                            console.log("ERROR missing SL public key");
                            this.state = this.PAIRING_IDLE;
                            callback(this.RESULT_UNLIKELY_ERROR);
                        }
                    }
                    else {
                        console.log("command or command identifier wrong");
                        this.state = this.PAIRING_IDLE;
                        callback(this.RESULT_UNLIKELY_ERROR);
                    }
                    break;
                case PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_CL_SEND_PUBKEY:
                    cmdId = data.readUInt16LE(0);
                    if (cmdId === nukiConstants.CMD_ID_PUBLIC_KEY) {
                        this.keys.clPk = data.slice(2, data.length - 2);
                        console.log("CL PUBKEY:", this.keys.clPk);

                        var slPk = new Buffer(0);
                        if (Buffer.isBuffer(this.keys.slPk)) {
                            slPk = this.keys.slPk;
                        } else {
                            if (_.isString(this.keys.slPk)) {
                                slPk = new Buffer(this.keys.slPk, 'hex');
                            } else {
                                if (_.isArray(this.keys.slPk)) {
                                    slPk = new Buffer(this.keys.slPk);
                                }
                            }
                        }
                        var slSk = new Buffer(0);
                        if (Buffer.isBuffer(this.keys.slSk)) {
                            slSk = this.keys.slSk;
                        } else {
                            if (_.isString(this.keys.slSk)) {
                                slSk = new Buffer(this.keys.slSk, 'hex');
                            } else {
                                if (_.isArray(this.keys.slSk)) {
                                    slSk = new Buffer(this.keys.slSk);
                                }
                            }
                        }

                        console.log("slPK", slPk);
                        console.log("slSK", slSk);
                        console.log("clPK", this.keys.clPk);

                        // Create Diffie-Hellman key from nuki secret key and clients public key
                        // crypto_scalarmult_curve25519(k,secretKey,pk)
                        var k = sodium.api.crypto_scalarmult(slSk, this.keys.clPk);
                        console.log("SL DH Key from SL SK and CL PK: ", k);

                        // derive a longterm shared secret key s from k using function kdf1
                        // static const unsigned char _0[16];
                        // static const unsigned char sigma[16] = "expand 32-byte k";
                        // crypto_core_hsalsa20(firstKey,_0,sharedKey,sigma)
                        var hsalsa20 = new HSalsa20();
                        var s = new Buffer(32);
                        var inv = new Buffer(16);
                        inv.fill(0);
                        var c = new Buffer("expand 32-byte k");
                        hsalsa20.crypto_core(s, inv, k, c);
                        console.log("derived shared key: ", s);

                        var nonce = new Buffer(nukiConstants.NUKI_NONCEBYTES);
                        sodium.api.randombytes_buf(nonce);
                        // todo remove hardcoded challenge
                        nonce = new Buffer("6CD4163D159050C798553EAA57E278A579AFFCBC56F09FC57FE879E51C42DF17", 'hex');
                        if (nonce.length != nukiConstants.NUKI_NONCEBYTES) {
                            console.log("Nonce length (" + nonce.length + ") is not " + nukiConstants.NUKI_NONCEBYTES);
                            this.state = this.PAIRING_IDLE;
                            callback(this.RESULT_UNLIKELY_ERROR);
                            return;
                        }

                        console.log("Creating one time challenge...");
                        this.prepareDataToSend(nukiConstants.CMD_CHALLENGE, nonce);
                        var value = this.getNextChunk(this.dataStillToSend);
                        if (this._updateValueCallback && value.length > 0) {
                            console.log("sending " + value.length + " bytes");
                            this._updateValueCallback(value);
                        }

                        this.state = PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_SL_SEND_CHALLENGE;

                        callback(this.RESULT_SUCCESS);
                    }
                    else {
                        console.log("command or command identifier wrong");
                        this.state = this.PAIRING_IDLE;
                        callback(this.RESULT_UNLIKELY_ERROR);
                    }

                    break;
                case PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_CL_SEND_AUTHENTICATOR:
                    console.log("waiting for authenticator from CL");
                    break;
                default:
                    console.log("ERROR unexpected pairing state");
                    this.state = this.PAIRING_IDLE;
                    callback(this.RESULT_UNLIKELY_ERROR);
            }
        }
    } else {
        console.log("checksum is NOT ok");
        this.state = this.PAIRING_IDLE;
        callback(this.RESULT_UNLIKELY_ERROR);
    }
};

PairingGeneralDataInputOutputCharacteristic.prototype.onSubscribe = function (maxValueSize, updateValueCallback) {
    console.log('PairingGeneralDataInputOutputCharacteristic - onSubscribe');

    this._updateValueCallback = updateValueCallback;

    if (this.dataStillToSend.length > 0) {
        switch (this.state) {
            case PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_SL_SEND_PUBKEY:

                var value = this.getNextChunk(this.dataStillToSend);
                if (value.length > 0) {
                    console.log("sending " + value.length + " bytes from onSubscribe");
                    updateValueCallback(value);
                }

                if (this.dataStillToSend.length === 0) {
                    this.state = PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_CL_SEND_PUBKEY;
                }

                break;
            default:
                console.log("ERROR unexpected pairing state");
                this.state = this.PAIRING_IDLE;
        }
    } else {
        console.log("don't have more data to notify");
    }
};

PairingGeneralDataInputOutputCharacteristic.prototype.onUnsubscribe = function () {
    console.log('PairingGeneralDataInputOutputCharacteristic - onUnsubscribe');

    this._updateValueCallback = null;
};

PairingGeneralDataInputOutputCharacteristic.prototype.onIndicate = function () {
    console.log("PairingGeneralDataInputOutputCharacteristic indicate");
    if (this.dataStillToSend.length > 0) {
        if (this._updateValueCallback) {
            var value;
            switch (this.state) {
                case PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_SL_SEND_PUBKEY:
                    value = this.getNextChunk(this.dataStillToSend);
                    if (value.length > 0) {
                        console.log("sending " + value.length + " bytes as indication");
                        this._updateValueCallback(value);
                    }
                    if (this.dataStillToSend.length === 0) {
                        this.state = PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_CL_SEND_PUBKEY;
                    }

                    break;
                case PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_SL_SEND_CHALLENGE:
                    value = this.getNextChunk(this.dataStillToSend);
                    if (value.length > 0) {
                        console.log("sending " + value.length + " bytes as indication");
                        this._updateValueCallback(value);
                    }
                    if (this.dataStillToSend.length === 0) {
                        this.state = PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_CL_SEND_AUTHENTICATOR;
                    }
                    break;
                default:
                    console.log("ERROR unexpected pairing state");
                    this.state = this.PAIRING_IDLE;
            }
        } else {
            console.log("don't have updateValueCallback on indicate");
            this.state = this.PAIRING_IDLE;
        }
    } else {
        console.log("don't have more data to indicate");
    }
};

module.exports = PairingGeneralDataInputOutputCharacteristic;
<|endoftext|>


<commit_before>/* eslint-disable no-param-reassign */

let regex; // make global as it's used by both single and multiHandler

function getOptions() {
  return new Promise((resolve) => {
    chrome.storage.sync.get(null, options => resolve(options));
  });
}

// Regex test cases here: https://regexr.com/3gv3c
function genBracketsRegex(options) {
  const genRegexStr = `(?:\\()([A-Z .,!?:"'\`\\\\/\\&+-]\\d?){${options.lowLimit},${options.upLimit}}(?:\\))`;
  regex = new RegExp(genRegexStr, 'gi');
  return regex;
}

function genTag(text) {
  const dataSafeText = text.replace(/&/g, '&').replace(/"/g, '"'); // so it doesn't break html layout
  return `<bracket-less data-bracketless="${dataSafeText}">${text}</bracket-less>`;
}

function injectTag(text, tag, target) {
  const replaceRegex = new RegExp(text.replace(/[.,!?:"'`\\/&-(+)]/g, '\\$&')); // to match exactly that text
  target.innerHTML = target.innerHTML.replace(replaceRegex, tag);
}

function singleHandler(textNode, multiClone) {
  const textArr = textNode.textContent.match(regex);
  const parent = multiClone || textNode.parentNode;

  if (textArr.length > 1) { // multiple brackets
    const replica = multiClone ? parent : parent.cloneNode(true);
    textArr.forEach((text) => {
      text = text.slice(1, -1);
      injectTag(text, genTag(text, replica), replica);
    });
    parent.innerHTML = replica.innerHTML;
  } else { // single bracket
    textArr[0] = textArr[0].slice(1, -1); // slice brackets
    injectTag(textArr[0], genTag(textArr[0]), parent);
  }
}

function multiHandler(textNodes) {
  const parentClone = textNodes[0].parentNode.cloneNode(true);
  textNodes.forEach((siblingText) => {
    singleHandler(siblingText, parentClone);
  });
  textNodes[0].parentNode.innerHTML = parentClone.innerHTML;
}

function iterateNodes(nodesArr) {
  nodesArr.forEach((textNode) => {
    if (Array.isArray(textNode)) {
      multiHandler(textNode);
    } else {
      singleHandler(textNode);
    }
  });
}

function nativeTreeWalker(bracketsRegex) {
  const ignoredTags = ['A', 'PRE', 'CODE', 'SCRIPT', 'STYLE'];
  // only accept nodes that have allowed text and are not in ignored tag
  const filter = {
    acceptNode(node) {
      if (bracketsRegex.test(node.data) && !ignoredTags.includes(node.parentNode.tagName)) {
        return NodeFilter.FILTER_ACCEPT;
      }
      return NodeFilter.FILTER_SKIP;
    },
  };
  const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, filter);
  walker.nextNode(); // first element is the root element!
  const nodes = [walker.currentNode]; // store one value immediately so length is not 0
  const sameParent = [];

  // no brackets found on the page
  if (Object.is(walker.nextNode(), null)) return [];

  while (walker.nextNode()) {
    const prev = nodes[nodes.length - 1];
    const cur = walker.currentNode;
    if (cur.parentNode.isSameNode(prev.parentNode)) {
      // empty? add prev element
      if (!sameParent.length) sameParent.push(prev);
      sameParent.push(cur);
    } else {
      // if not empty replace last single elem and empty
      if (sameParent.length) nodes[nodes.length - 1] = sameParent.splice(0);
      nodes.push(cur);
    }
  }
  return nodes;
}

const regexPr = getOptions()
  .then(genBracketsRegex);

regexPr
  .then(nativeTreeWalker)
  .then(iterateNodes);
<commit_msg>DISABLE BREAKING! Remove edge case hanlding<commit_after>/* eslint-disable no-param-reassign */

let regex; // make global as it's used by both single and multiHandler

function getOptions() {
  return new Promise((resolve) => {
    chrome.storage.sync.get(null, options => resolve(options));
  });
}

// Regex test cases here: https://regexr.com/3gv3c
function genBracketsRegex(options) {
  const genRegexStr = `(?:\\()([A-Z .,!?:"'\`\\\\/\\&+-]\\d?){${options.lowLimit},${options.upLimit}}(?:\\))`;
  regex = new RegExp(genRegexStr, 'gi');
  return regex;
}

function genTag(text) {
  const dataSafeText = text.replace(/&/g, '&').replace(/"/g, '"'); // so it doesn't break html layout
  return `<bracket-less data-bracketless="${dataSafeText}">${text}</bracket-less>`;
}

function injectTag(text, tag, target) {
  const replaceRegex = new RegExp(text.replace(/[.,!?:"'`\\/&-(+)]/g, '\\$&')); // to match exactly that text
  target.innerHTML = target.innerHTML.replace(replaceRegex, tag);
}

function singleHandler(textNode, multiClone) {
  const textArr = textNode.textContent.match(regex);
  const parent = multiClone || textNode.parentNode;

  if (textArr.length > 1) { // multiple brackets
    const replica = multiClone ? parent : parent.cloneNode(true);
    textArr.forEach((text) => {
      text = text.slice(1, -1);
      injectTag(text, genTag(text, replica), replica);
    });
    parent.innerHTML = replica.innerHTML;
  } else { // single bracket
    textArr[0] = textArr[0].slice(1, -1); // slice brackets
    injectTag(textArr[0], genTag(textArr[0]), parent);
  }
}

function multiHandler(textNodes) {
  const parentClone = textNodes[0].parentNode.cloneNode(true);
  textNodes.forEach((siblingText) => {
    singleHandler(siblingText, parentClone);
  });
  textNodes[0].parentNode.innerHTML = parentClone.innerHTML;
}

function iterateNodes(nodesArr) {
  nodesArr.forEach((textNode) => {
    if (Array.isArray(textNode)) {
      multiHandler(textNode);
    } else {
      singleHandler(textNode);
    }
  });
}

function nativeTreeWalker(bracketsRegex) {
  const ignoredTags = ['A', 'PRE', 'CODE', 'SCRIPT', 'STYLE'];
  // only accept nodes that have allowed text and are not in ignored tag
  const filter = {
    acceptNode(node) {
      if (bracketsRegex.test(node.data) && !ignoredTags.includes(node.parentNode.tagName)) {
        return NodeFilter.FILTER_ACCEPT;
      }
      return NodeFilter.FILTER_SKIP;
    },
  };
  const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, filter);
  walker.nextNode(); // first element is the root element!
  const nodes = [walker.currentNode]; // store one value immediately so length is not 0
  const sameParent = [];

  // TODO: handle no brackets found on the page

  while (walker.nextNode()) {
    const prev = nodes[nodes.length - 1];
    const cur = walker.currentNode;
    if (cur.parentNode.isSameNode(prev.parentNode)) {
      // empty? add prev element
      if (!sameParent.length) sameParent.push(prev);
      sameParent.push(cur);
    } else {
      // if not empty replace last single elem and empty
      if (sameParent.length) nodes[nodes.length - 1] = sameParent.splice(0);
      nodes.push(cur);
    }
  }
  return nodes;
}

const regexPr = getOptions()
  .then(genBracketsRegex);

regexPr
  .then(nativeTreeWalker)
  .then(iterateNodes);
<|endoftext|>


<commit_before>if(!process.argv[2]) {
	console.error('usage: node ' + process.argv[1] + ' <port>');
	process.exit(1);
}

var express = require('express'),
    bodyParser = require('body-parser'),
    app = express(),
    actions = [],
    tasks = [],
    port = parseInt(process.argv[2]);

function pushAction(id, action) {
  if(!actions[id]) actions[id] = [];
  actions[id].push(action);
}

function getAction(id) {
  if(!actions[id] || actions[id].length <= 0) return {action: "none"};
  else return actions[id].pop();
}

function getClientTasks(id) {
  if(!actions[id]) return [];
  else return actions[id];
}

app.use(bodyParser.json());

app.get('/get', function (req, res) {
  var id = req.query.id;
  if (typeof id !== 'undefined' && id) {
    res.type('application/json');
    var tsk = JSON.stringify(getAction(id));
    console.log("send " + id + " task: " + tsk);
    res.send(tsk);
  } else res.status(500).end();
});

app.get('/get_report', function (req, res) {
  var id = req.query.id;
  if (typeof id !== 'undefined' && id) {
    res.type('application/json');
    var rpt = JSON.stringify(getClientTasks(id));
    console.log("send " + id + " report: " + rpt);
    res.send(rpt);
  } else res.status(500).end();
});

app.post('/control', function (req, res) {
  var id = req.query.id;
  if (typeof id !== 'undefined' && id) {
    try {
      console.log(id + " added task: " + JSON.stringify(req.body));
      pushAction(id, req.body);
      res.status(200).end();
    } catch(e) {
      console.log(e);
      res.status(500).end();
    }
  } else res.status(500).end();
});

app.post('/report', function (req, res) {
  var id = req.query.id;
  if (typeof id !== 'undefined' && id) {
    try {
      console.log(id + " reported: " + JSON.stringify(req.body));
      tasks[id] = req.body;
      res.status(200).end();
    } catch(e) {
      console.log(e);
      res.status(500).end();
    }
  } else res.status(500).end();
});

app.listen(port, function () {
  console.log("nato_play server running on port " + port + ".");
});
<commit_msg>make max queue size 20.<commit_after>if(!process.argv[2]) {
	console.error('usage: node ' + process.argv[1] + ' <port>');
	process.exit(1);
}

var express = require('express'),
    bodyParser = require('body-parser'),
    app = express(),
    actions = [],
    tasks = [],
    port = parseInt(process.argv[2]);

function pushAction(id, action) {
  if(!actions[id]) actions[id] = [];
  if(actions[id].length <= 20) actions[id].push(action);
}

function getAction(id) {
  if(!actions[id] || actions[id].length <= 0) return {action: "none"};
  else return actions[id].pop();
}

function getClientTasks(id) {
  if(!actions[id]) return [];
  else return actions[id];
}

app.use(bodyParser.json());

app.get('/get', function (req, res) {
  var id = req.query.id;
  if (typeof id !== 'undefined' && id) {
    res.type('application/json');
    var tsk = JSON.stringify(getAction(id));
    console.log("send " + id + " task: " + tsk);
    res.send(tsk);
  } else res.status(500).end();
});

app.get('/get_report', function (req, res) {
  var id = req.query.id;
  if (typeof id !== 'undefined' && id) {
    res.type('application/json');
    var rpt = JSON.stringify(getClientTasks(id));
    console.log("send " + id + " report: " + rpt);
    res.send(rpt);
  } else res.status(500).end();
});

app.post('/control', function (req, res) {
  var id = req.query.id;
  if (typeof id !== 'undefined' && id) {
    try {
      console.log(id + " added task: " + JSON.stringify(req.body));
      pushAction(id, req.body);
      res.status(200).end();
    } catch(e) {
      console.log(e);
      res.status(500).end();
    }
  } else res.status(500).end();
});

app.post('/report', function (req, res) {
  var id = req.query.id;
  if (typeof id !== 'undefined' && id) {
    try {
      console.log(id + " reported: " + JSON.stringify(req.body));
      tasks[id] = req.body;
      res.status(200).end();
    } catch(e) {
      console.log(e);
      res.status(500).end();
    }
  } else res.status(500).end();
});

app.listen(port, function () {
  console.log("nato_play server running on port " + port + ".");
});
<|endoftext|>


<commit_before>const aqp = require('api-query-params').default;    // uses ES6
// --------------------------------------------------------------------------------------
exp = {};
// --------------------------------------------------------------------------------------
// parse query string
// args:
// 1) request url
// 2) array of collection keys, that should be whitelisted
// 3) function to validate time interval
// 4) optional parameter to validation function [bool]
//    only used in validate_interval
//    defines if both dates (starting and ending are requied)
// --------------------------------------------------------------------------------------
exp.parse_query_string = function(url, whitelist, validate, date_req) {

  var qs = url.match(/\?.*$/)[0];     // extract query string part from request url
  qs = qs.substr(1);   // remove '?'

  var query = aqp(qs, {         // parse query string
    whitelist : whitelist       // whitelist collection keys
  });

  if(query.filter.timestamp == undefined) {    // do not search if timestamp is not defined
    throw { error : "timestamp must be defined!"};
  }

  // validation
  var keys = query.filter.timestamp;

  for(var key in keys)
    if(isNaN(Date.parse(keys[key])))    // invalid date
      throw { error : "invalid date: " + keys[key]};

  if(!validate(query, date_req)) {  // check correctly specified time interval
    throw { error : "incorrect interval specification!"};
  }

  return query;
}
// --------------------------------------------------------------------------------------
// validate query for correct time interval
// --------------------------------------------------------------------------------------
exp.validate_days = function(query) {
  if(typeof(query.filter.timestamp) == "object" && Object.keys(query.filter.timestamp).length > 1) {  // range
    var keys = Object.keys(query.filter.timestamp);
    for(var key in keys) {
      if(!(query.filter.timestamp[keys[key]].getHours() == 0 && query.filter.timestamp[keys[key]].getMinutes() == 0
        && query.filter.timestamp[keys[key]].getSeconds() == 0 && query.filter.timestamp[keys[key]].getMilliseconds() == 0))
        return false;       // some part is set incorrectly
    }
  }
  else {    // one day only
    return (query.filter.timestamp.getHours() == 0 && query.filter.timestamp.getMinutes() == 0
    && query.filter.timestamp.getSeconds() == 0 && query.filter.timestamp.getMilliseconds() == 0);
  }

  return true;
}
// --------------------------------------------------------------------------------------
// validate query for correct time interval
// --------------------------------------------------------------------------------------
exp.validate_interval = function(query, date_req) {
  if(typeof(query.filter.timestamp) == "object" && Object.keys(query.filter.timestamp).length > 1) {  // range
    // is any checking needed here ?
    // TODO
  }
  else {
    if(date_req)        // both dates are required
      return false;
    else                // one date is sufficient
      return true;
  }

  return true;
}
// --------------------------------------------------------------------------------------
module.exports = exp;
<commit_msg>fix query string matching<commit_after>const aqp = require('api-query-params').default;    // uses ES6
// --------------------------------------------------------------------------------------
exp = {};
// --------------------------------------------------------------------------------------
// parse query string
// args:
// 1) request url
// 2) array of collection keys, that should be whitelisted
// 3) function to validate time interval
// 4) optional parameter to validation function [bool]
//    only used in validate_interval
//    defines if both dates (starting and ending are requied)
// --------------------------------------------------------------------------------------
exp.parse_query_string = function(url, whitelist, validate, date_req) {
  var qs = url.match(/\?.*$/);  // try to match query string if present

  if(!qs)
    throw { error : "no query string present!"};

  qs = qs[0];           // extract query string part from request url
  qs = qs.substr(1);   // remove '?'

  var query = aqp(qs, {         // parse query string
    whitelist : whitelist       // whitelist collection keys
  });

  if(query.filter.timestamp == undefined) {    // do not search if timestamp is not defined
    throw { error : "timestamp must be defined!"};
  }

  // validation
  var keys = query.filter.timestamp;

  for(var key in keys)
    if(isNaN(Date.parse(keys[key])))    // invalid date
      throw { error : "invalid date: " + keys[key]};

  if(!validate(query, date_req)) {  // check correctly specified time interval
    throw { error : "incorrect interval specification!"};
  }

  return query;
}
// --------------------------------------------------------------------------------------
// validate query for correct time interval
// --------------------------------------------------------------------------------------
exp.validate_days = function(query) {
  if(typeof(query.filter.timestamp) == "object" && Object.keys(query.filter.timestamp).length > 1) {  // range
    var keys = Object.keys(query.filter.timestamp);
    for(var key in keys) {
      if(!(query.filter.timestamp[keys[key]].getHours() == 0 && query.filter.timestamp[keys[key]].getMinutes() == 0
        && query.filter.timestamp[keys[key]].getSeconds() == 0 && query.filter.timestamp[keys[key]].getMilliseconds() == 0))
        return false;       // some part is set incorrectly
    }
  }
  else {    // one day only
    return (query.filter.timestamp.getHours() == 0 && query.filter.timestamp.getMinutes() == 0
    && query.filter.timestamp.getSeconds() == 0 && query.filter.timestamp.getMilliseconds() == 0);
  }

  return true;
}
// --------------------------------------------------------------------------------------
// validate query for correct time interval
// --------------------------------------------------------------------------------------
exp.validate_interval = function(query, date_req) {
  if(typeof(query.filter.timestamp) == "object" && Object.keys(query.filter.timestamp).length > 1) {  // range
    // is any checking needed here ?
    // TODO
  }
  else {
    if(date_req)        // both dates are required
      return false;
    else                // one date is sufficient
      return true;
  }

  return true;
}
// --------------------------------------------------------------------------------------
module.exports = exp;
<|endoftext|>


<commit_before>whenWeAreReady(user,function() {

  var xhttp = new XMLHttpRequest();
  xhttp.onreadystatechange = function() {
    if (xhttp.readyState == 4 && xhttp.status == 200) {
      results = JSON.parse(xhttp.responseText);

      user.hq = results;

      //menu bar code
      var ul = document.getElementsByClassName("nav navbar-nav");

      var li = document.createElement("li");
      li.classList.add("dropdown");

      var tonight = new Date();

      tonight = new Date(tonight.getTime() + (tonight.getTimezoneOffset() * 60000)); //DST offset because beacon has stupid times

      tonight.setHours(23, 59, 59, 0);

      var thismorning = new Date();
      thismorning.setDate(thismorning.getDate()); //then

      thismorning = new Date(thismorning.getTime() + (thismorning.getTimezoneOffset() * 60000)); //DST offset because beacon has stupid times

      thismorning.setHours(0, 0, 0, 0);

      var vars = "?host=" + location.hostname + "&hq=" + user.currentHqId + "&start=" + encodeURIComponent(thismorning.toISOString()) + "&end=" + encodeURIComponent(tonight.toISOString());

      var jobsummaryUrl = lighthouseUrl + "pages/summary.html" + vars;
      var jobstatsUrl = lighthouseUrl + "pages/stats.html" + vars;
      var jobexportUrl = lighthouseUrl + "pages/advexport.html" + vars;

      var teamsummaryUrl = lighthouseUrl + "pages/teamsummary.html" + vars;

      var aboutURL = "https://github.com/OSPFNeighbour/Lighthouse/blob/master/README.md" //chrome.extension.getURL("lighthouse/about.html");

      li.innerHTML = "Lighthouse

Jobs

Job Summary (" + results.Code + " Today)

Job Statistics (" + results.Code + " Today)

Job Export (" + results.Code + " Today)

Teams

Team Summary (" + results.Code + " Today)

About

About Lighthouse
";

      $('.nav .navbar-nav:not(".navbar-right")').append(li);

      //lighthouse menu

      if (location.pathname == "/Jobs") {

        var filtermenu = '
  Lighthouse Quick Filters
  Job Status Open Jobs Closed Jobs All Jobs   Job Type Rescue Jobs Storm Jobs All Job Types   Locations $UNIT $REGION NSW

';

        filtermenu = filtermenu.replace(/\$LHURL/g, lighthouseUrl);
        filtermenu = filtermenu.replace(/\$UNIT/g, user.hq.Code);
        filtermenu = filtermenu.replace(/\$REGION/g, user.hq.ParentEntity.Code);

        $('.main-menu > li:nth-child(1)').after(filtermenu);

        $("#filerrescue").click(function() {
          filterViewModel.selectedParentJobTypes.removeAll();
          filterViewModel.parentJobTypeClicked({
            Id: 5,
            Name: "Rescue",
            Description: "Rescue",
            ParentId: null
          });
          filterViewModel.updateFilters();
        });

        $("#filerstorm").click(function() {
          filterViewModel.selectedParentJobTypes.removeAll();
          filterViewModel.selectedRescueTypes.removeAll();
          filterViewModel.parentJobTypeClicked({
            Id: 1,
            Name: "Storm",
            Description: "Storm",
            ParentId: null
          });
          filterViewModel.updateFilters();
        });

        $("#fileralltype").click(function() {
          filterViewModel.selectedParentJobTypes.removeAll();
          filterViewModel.selectedRescueTypes.removeAll();
          filterViewModel.updateFilters();
        });

        $("#fileropen").click(function() {
          filterViewModel.selectedStatusTypes.removeAll();
          filterViewModel.selectedStatusTypes.push({
            Id: 2,
            Name: "Acknowledged",
            Description: "Acknowledged",
            ParentId: null
          });
          filterViewModel.selectedStatusTypes.push({
            Id: 1,
            Name: "New",
            Description: "New",
            ParentId: null
          });
          filterViewModel.selectedStatusTypes.push({
            Id: 4,
            Name: "Tasked",
            Description: "Tasked",
            ParentId: null
          });
          filterViewModel.selectedStatusTypes.push({
            Id: 5,
            Name: "Referred",
            Description: "Referred",
            ParentId: null
          });
          filterViewModel.updateFilters();
        });

        $("#filerclosed").click(function() {
          filterViewModel.selectedStatusTypes.removeAll();
          filterViewModel.selectedStatusTypes.push({
            Id: 6,
            Name: "Complete",
            Description: "Complete",
            ParentId: null
          });
          filterViewModel.selectedStatusTypes.push({
            Id: 7,
            Name: "Cancelled",
            Description: "Cancelled",
            ParentId: null
          });
          filterViewModel.selectedStatusTypes.push({
            Id: 3,
            Name: "Rejected",
            Description: "Rejected",
            ParentId: null
          });
          filterViewModel.selectedStatusTypes.push({
            Id: 8,
            Name: "Finalised",
            Description: "Finalised",
            ParentId: null
          });
          filterViewModel.updateFilters();
        });

        $("#filerallstatus").click(function() {
          filterViewModel.selectedStatusTypes.removeAll();
          filterViewModel.updateFilters();
        });

        $("#filerallmyregion").click(function() {
          filterViewModel.selectedEntities.removeAll();
          filtershowallmyregion();
        });

        $("#filermyhq").click(function() {
          filterViewModel.selectedEntities.removeAll();
          filterViewModel.selectedEntities.push(user.hq);
          filterViewModel.updateFilters();
        });

        $("#clearlocator").click(function() {
          filterViewModel.selectedEntities.removeAll();
          filterViewModel.updateFilters();
        });

      } // location.pathname == "/Jobs"
    } // xhttp.readyState == 4 && xhttp.status == 200
  } // xhttp.onreadystatechange

  xhttp.open("GET", "https://" + location.hostname + "/Api/v1/Entities/" + user.currentHqId, true);
  xhttp.send();

});

function filtershowallmyregion() {
  filterViewModel.selectedEntities.destroyAll() //flush first :-)
  var xhttp = new XMLHttpRequest();
  xhttp.onreadystatechange = function() {
    if (xhttp.readyState == 4 && xhttp.status == 200) {
      results = JSON.parse(xhttp.responseText);
      console.log(results);
      results.forEach(function(d) {
        filterViewModel.selectedEntities.push(d);
      });
      filterViewModel.updateFilters();
    }
  }
  xhttp.open("GET", "https://" + location.hostname + "/Api/v1/Entities/" + user.currentRegionId + "/Children", true);
  xhttp.send();
}

function whenWeAreReady(varToCheck,cb) { //when external vars have loaded
  var waiting = setInterval(function() { //run every 1sec until we have loaded the page (dont hate me Sam)
    if (typeof varToCheck != "undefined") {
      console.log("We are ready");
      clearInterval(waiting); //stop timer
      cb(); //call back
    }
  }, 200);
}

$(function(){

  // Shortcuts search box - if Job Number entered, jump straight to that job
  $('form#layoutJobSearchForm')
    .on('submit',function(e){
      var $t = $(this) ,
          $q = $('#layoutJobSearchFormJobQuery',$t) ,
          val = $q.val();
      if ( /^\d{0,4}\-?\d{4}$/.test( val ) ) { // if the field contains 4 or more digits and nothing else
        document.location = '/Jobs/' + parseInt( val.replace(/\D/g,'') , 10 );
        e.preventDefault();
        e.stopImmediatePropagation();
      }
    });

});
<commit_msg>fixed bad "today" times in lighthouse menu<commit_after>whenWeAreReady(user,function() {

  var xhttp = new XMLHttpRequest();
  xhttp.onreadystatechange = function() {
    if (xhttp.readyState == 4 && xhttp.status == 200) {
      results = JSON.parse(xhttp.responseText);

      user.hq = results;

      //menu bar code
      var ul = document.getElementsByClassName("nav navbar-nav");

      var li = document.createElement("li");
      li.classList.add("dropdown");

      var tonight = new Date();

      tonight.setHours(23, 59, 59, 0);

      tonight = new Date(tonight.getTime());

      var thismorning = new Date();

      thismorning.setHours(0, 0, 0, 0);

      thismorning = new Date(thismorning.getTime());

      var vars = "?host=" + location.hostname + "&hq=" + user.currentHqId + "&start=" + encodeURIComponent(thismorning.toISOString()) + "&end=" + encodeURIComponent(tonight.toISOString());

      var jobsummaryUrl = lighthouseUrl + "pages/summary.html" + vars;
      var jobstatsUrl = lighthouseUrl + "pages/stats.html" + vars;
      var jobexportUrl = lighthouseUrl + "pages/advexport.html" + vars;

      var teamsummaryUrl = lighthouseUrl + "pages/teamsummary.html" + vars;

      var aboutURL = "https://github.com/OSPFNeighbour/Lighthouse/blob/master/README.md" //chrome.extension.getURL("lighthouse/about.html");

      li.innerHTML = "Lighthouse

Jobs

Job Summary (" + results.Code + " Today)

Job Statistics (" + results.Code + " Today)

Job Export (" + results.Code + " Today)

Teams

Team Summary (" + results.Code + " Today)

About

About Lighthouse
";

      $('.nav .navbar-nav:not(".navbar-right")').append(li);

      //lighthouse menu

      if (location.pathname == "/Jobs") {

        var filtermenu = '
  Lighthouse Quick Filters
  Job Status Open Jobs Closed Jobs All Jobs   Job Type Rescue Jobs Storm Jobs All Job Types   Locations $UNIT $REGION NSW

';

        filtermenu = filtermenu.replace(/\$LHURL/g, lighthouseUrl);
        filtermenu = filtermenu.replace(/\$UNIT/g, user.hq.Code);
        filtermenu = filtermenu.replace(/\$REGION/g, user.hq.ParentEntity.Code);

        $('.main-menu > li:nth-child(1)').after(filtermenu);

        $("#filerrescue").click(function() {
          filterViewModel.selectedParentJobTypes.removeAll();
          filterViewModel.parentJobTypeClicked({
            Id: 5,
            Name: "Rescue",
            Description: "Rescue",
            ParentId: null
          });
          filterViewModel.updateFilters();
        });

        $("#filerstorm").click(function() {
          filterViewModel.selectedParentJobTypes.removeAll();
          filterViewModel.selectedRescueTypes.removeAll();
          filterViewModel.parentJobTypeClicked({
            Id: 1,
            Name: "Storm",
            Description: "Storm",
            ParentId: null
          });
          filterViewModel.updateFilters();
        });

        $("#fileralltype").click(function() {
          filterViewModel.selectedParentJobTypes.removeAll();
          filterViewModel.selectedRescueTypes.removeAll();
          filterViewModel.updateFilters();
        });

        $("#fileropen").click(function() {
          filterViewModel.selectedStatusTypes.removeAll();
          filterViewModel.selectedStatusTypes.push({
            Id: 2,
            Name: "Acknowledged",
            Description: "Acknowledged",
            ParentId: null
          });
          filterViewModel.selectedStatusTypes.push({
            Id: 1,
            Name: "New",
            Description: "New",
            ParentId: null
          });
          filterViewModel.selectedStatusTypes.push({
            Id: 4,
            Name: "Tasked",
            Description: "Tasked",
            ParentId: null
          });
          filterViewModel.selectedStatusTypes.push({
            Id: 5,
            Name: "Referred",
            Description: "Referred",
            ParentId: null
          });
          filterViewModel.updateFilters();
        });

        $("#filerclosed").click(function() {
          filterViewModel.selectedStatusTypes.removeAll();
          filterViewModel.selectedStatusTypes.push({
            Id: 6,
            Name: "Complete",
            Description: "Complete",
            ParentId: null
          });
          filterViewModel.selectedStatusTypes.push({
            Id: 7,
            Name: "Cancelled",
            Description: "Cancelled",
            ParentId: null
          });
          filterViewModel.selectedStatusTypes.push({
            Id: 3,
            Name: "Rejected",
            Description: "Rejected",
            ParentId: null
          });
          filterViewModel.selectedStatusTypes.push({
            Id: 8,
            Name: "Finalised",
            Description: "Finalised",
            ParentId: null
          });
          filterViewModel.updateFilters();
        });

        $("#filerallstatus").click(function() {
          filterViewModel.selectedStatusTypes.removeAll();
          filterViewModel.updateFilters();
        });

        $("#filerallmyregion").click(function() {
          filterViewModel.selectedEntities.removeAll();
          filtershowallmyregion();
        });

        $("#filermyhq").click(function() {
          filterViewModel.selectedEntities.removeAll();
          filterViewModel.selectedEntities.push(user.hq);
          filterViewModel.updateFilters();
        });

        $("#clearlocator").click(function() {
          filterViewModel.selectedEntities.removeAll();
          filterViewModel.updateFilters();
        });

      } // location.pathname == "/Jobs"
    } // xhttp.readyState == 4 && xhttp.status == 200
  } // xhttp.onreadystatechange

  xhttp.open("GET", "https://" + location.hostname + "/Api/v1/Entities/" + user.currentHqId, true);
  xhttp.send();

});

function filtershowallmyregion() {
  filterViewModel.selectedEntities.destroyAll() //flush first :-)
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
  if (xhttp.readyState == 4 && xhttp.status == 200) {
    results = JSON.parse(xhttp.responseText);
    console.log(results);
    results.forEach(function(d) {
      filterViewModel.selectedEntities.push(d);
    });
    filterViewModel.updateFilters();
  }
}
xhttp.open("GET", "https://" + location.hostname + "/Api/v1/Entities/" + user.currentRegionId + "/Children", true);
xhttp.send();
}

function whenWeAreReady(varToCheck,cb) { //when external vars have loaded
  var waiting = setInterval(function() { //run every 1sec until we have loaded the page (dont hate me Sam)
    if (typeof varToCheck != "undefined") {
      console.log("We are ready");
      clearInterval(waiting); //stop timer
      cb(); //call back
    }
  }, 200);
}

$(function(){

  // Shortcuts search box - if Job Number entered, jump straight to that job
  $('form#layoutJobSearchForm')
  .on('submit',function(e){
    var $t = $(this) ,
    $q = $('#layoutJobSearchFormJobQuery',$t) ,
    val = $q.val();
      if ( /^\d{0,4}\-?\d{4}$/.test( val ) ) { // if the field contains 4 or more digits and nothing else
        document.location = '/Jobs/' + parseInt( val.replace(/\D/g,'') , 10 );
        e.preventDefault();
        e.stopImmediatePropagation();
      }
    });

});
<|endoftext|>


<commit_before>var util = require('util');
var nukiConstants = require('./nuki-constants');
var _ = require('underscore');
var crc = require('crc');
var sodium = require('sodium');
var HSalsa20 = require('./hsalsa20');
var crypto = require('crypto');

var bleno = require('bleno');
var BlenoCharacteristic = bleno.Characteristic;
var BlenoDescriptor = bleno.Descriptor;

function PairingGeneralDataInputOutputCharacteristic(keys) {
    this.state = this.PAIRING_IDLE;
    this.keys = keys;
    this.dataStillToSend = new Buffer(0);

    PairingGeneralDataInputOutputCharacteristic.super_.call(this, {
        // uuid: 'a92ee101-5501-11e4-916c-0800200c9a66',
        uuid: 'a92ee101550111e4916c0800200c9a66',
        properties: ['write', 'indicate'],
        descriptors: [
            new BlenoDescriptor({
                uuid: '2902',   // client characterstic configuration
                value: 'Pairing commands'
            })
        ]
    });
}

util.inherits(PairingGeneralDataInputOutputCharacteristic, BlenoCharacteristic);

PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_IDLE = 0;
PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_SL_SEND_PUBKEY = 1;
PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_CL_SEND_PUBKEY = 2;
PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_SL_SEND_CHALLENGE = 3;
PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_CL_SEND_AUTHENTICATOR = 4;
PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_SL_SEND_CHALLENGE_2 = 5;
PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_CL_SEND_AUTHORIZATION_DATA = 6;

PairingGeneralDataInputOutputCharacteristic.prototype.getNextChunk = function (data) {
    var block0;
    if (data.length > 20) {
        block0 = data.slice(0, 20);
        this.dataStillToSend = data.slice(20);
    } else {
        block0 = data;
        this.dataStillToSend = new Buffer(0);
    }
    return block0;
};

PairingGeneralDataInputOutputCharacteristic.prototype.prepareDataToSend = function (cmd, data) {
    var cmdBuffer = new Buffer(2);
    cmdBuffer.writeUInt16LE(cmd);
    var responseData = Buffer.concat([cmdBuffer, data]);
    var checksum = crc.crc16ccitt(responseData);
    var checksumBuffer = new Buffer(2);
    checksumBuffer.writeUInt16LE(checksum);
    this.dataStillToSend = Buffer.concat([responseData, checksumBuffer]);
    console.log("prepared to send:", this.dataStillToSend);
};

PairingGeneralDataInputOutputCharacteristic.prototype.onWriteRequest = function (data, offset, withoutResponse, callback) {
    var cmdId, slPk, value;
    console.log("PairingGeneralDataInputOutputCharacteristic", data);
    var dataForCrc = data.slice(0, data.length - 2);
    var crcSumCalc = crc.crc16ccitt(dataForCrc);
    var crcSumRetrieved = data.readUInt16LE(data.length - 2);

    if (crcSumCalc === crcSumRetrieved) {
        console.log("checksum is ok");
        if (offset) {
            callback(this.RESULT_ATTR_NOT_LONG);
        } else if (data.length > 200) {
            callback(this.RESULT_INVALID_ATTRIBUTE_LENGTH);
        } else {
            switch (this.state) {
                case PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_IDLE:
                    var rCmd = data.readUInt16LE(0);
                    cmdId = data.readUInt16LE(2);
                    if (rCmd === nukiConstants.CMD_reqUEST_DATA && cmdId === nukiConstants.CMD_ID_PUBLIC_KEY) {
                        slPk = new Buffer(0);
                        if (Buffer.isBuffer(this.keys.slPk)) {
                            slPk = this.keys.slPk;
                        } else {
                            if (_.isString(this.keys.slPk)) {
                                slPk = new Buffer(this.keys.slPk, 'hex');
                            } else {
                                if (_.isArray(this.keys.slPk)) {
                                    slPk = new Buffer(this.keys.slPk);
                                }
                            }
                        }

                        if (slPk.length > 0) {
                            this.prepareDataToSend(nukiConstants.CMD_ID_PUBLIC_KEY, slPk);
                            this.state = PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_SL_SEND_PUBKEY;

                            callback(this.RESULT_SUCCESS);
                        } else {
                            console.log("ERROR missing SL public key");
                            this.state = this.PAIRING_IDLE;
                            callback(this.RESULT_UNLIKELY_ERROR);
                        }
                    }
                    else {
                        console.log("command or command identifier wrong");
                        this.state = this.PAIRING_IDLE;
                        callback(this.RESULT_UNLIKELY_ERROR);
                    }
                    break;
                case PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_CL_SEND_PUBKEY:
                    cmdId = data.readUInt16LE(0);
                    if (cmdId === nukiConstants.CMD_ID_PUBLIC_KEY) {
                        this.keys.clPk = data.slice(2, data.length - 2);
                        console.log("CL PUBKEY:", this.keys.clPk);

                        slPk = new Buffer(0);
                        if (Buffer.isBuffer(this.keys.slPk)) {
                            slPk = this.keys.slPk;
                        } else {
                            if (_.isString(this.keys.slPk)) {
                                slPk = new Buffer(this.keys.slPk, 'hex');
                            } else {
                                if (_.isArray(this.keys.slPk)) {
                                    slPk = new Buffer(this.keys.slPk);
                                }
                            }
                        }
                        var slSk = new Buffer(0);
                        if (Buffer.isBuffer(this.keys.slSk)) {
                            slSk = this.keys.slSk;
                        } else {
                            if (_.isString(this.keys.slSk)) {
                                slSk = new Buffer(this.keys.slSk, 'hex');
                            } else {
                                if (_.isArray(this.keys.slSk)) {
                                    slSk = new Buffer(this.keys.slSk);
                                }
                            }
                        }

                        this.keys.slPk = slPk;
                        this.keys.slSk = slSk;

                        console.log("slPK", slPk);
                        console.log("slSK", slSk);
                        console.log("clPK", this.keys.clPk);

                        // Create Diffie-Hellman key from nuki secret key and clients public key
                        // crypto_scalarmult_curve25519(k,secretKey,pk)
                        var k = sodium.api.crypto_scalarmult(slSk, this.keys.clPk);
                        console.log("SL DH Key from SL SK and CL PK: ", k);

                        // derive a longterm shared secret key s from k using function kdf1
                        // static const unsigned char _0[16];
                        // static const unsigned char sigma[16] = "expand 32-byte k";
                        // crypto_core_hsalsa20(firstKey,_0,sharedKey,sigma)
                        var hsalsa20 = new HSalsa20();
                        var sharedSecret = new Buffer(32);
                        var inv = new Buffer(16);
                        inv.fill(0);
                        var c = new Buffer("expand 32-byte k");
                        hsalsa20.crypto_core(sharedSecret, inv, k, c);
                        console.log("derived shared key: ", sharedSecret);

                        console.log("Creating one time challenge...");
                        this.keys.sc = new Buffer(nukiConstants.NUKI_NONCEBYTES);
                        sodium.api.randombytes_buf(this.keys.sc);
                        // todo remove hardcoded challenge
                        this.keys.sc = new Buffer("6CD4163D159050C798553EAA57E278A579AFFCBC56F09FC57FE879E51C42DF17", 'hex');
                        if (this.keys.sc.length != nukiConstants.NUKI_NONCEBYTES) {
                            console.log("Nonce length (" + this.keys.sc.length + ") is not " + nukiConstants.NUKI_NONCEBYTES);
                            this.state = this.PAIRING_IDLE;
                            callback(this.RESULT_UNLIKELY_ERROR);
                            return;
                        }

                        this.state = PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_SL_SEND_CHALLENGE;
                        this.prepareDataToSend(nukiConstants.CMD_CHALLENGE, this.keys.sc);
                        value = this.getNextChunk(this.dataStillToSend);
                        if (this._updateValueCallback && value.length > 0) {
                            console.log("sending challenge 1: " + value.length + " bytes");
                            this._updateValueCallback(value);
                        }

                        var r = Buffer.concat([this.keys.clPk, slPk, this.keys.sc]);
                        // use HMAC-SHA256 to create the authenticator
                        var a = crypto.createHmac('SHA256', sharedSecret).update(r).digest();
                        console.log("SL Authorization authenticator", a);

                        callback(this.RESULT_SUCCESS);
                    }
                    else {
                        console.log("command or command identifier wrong");
                        this.state = this.PAIRING_IDLE;
                        callback(this.RESULT_UNLIKELY_ERROR);
                    }

                    break;
                case PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_CL_SEND_AUTHENTICATOR:
                    cmdId = data.readUInt16LE(0);
                    if (cmdId === nukiConstants.CMD_AUTHORIZATION_AUTHENTICATOR) {
                        var clCr = data.slice(2, data.length - 2);
                        console.log("CL authorization authenticator", clCr);

                        // create authenticator with data from server side
                        console.log("keys:", this.keys.slPk, this.keys.clPk, this.keys.sc);

                        r = Buffer.concat([this.keys.slPk, this.keys.clPk, this.keys.sc]);
                        // use HMAC-SHA256 to create the authenticator
                        var cr = crypto.createHmac('SHA256', sharedSecret).update(r).digest();
                        console.log("SL Authorization authenticator", cr);

                        // Step 14: verify authenticator
                        if (Buffer.compare(clCr, cr) === 0) {
                            console.log("Authenticators verified ok");
                            // Step 15: send second challenge
                            console.log("Creating second one time challenge...");
                            this.keys.sc = new Buffer(nukiConstants.NUKI_NONCEBYTES);
                            sodium.api.randombytes_buf(this.keys.sc);
                            // todo remove hardcoded challenge
                            this.keys.sc = new Buffer("E0742CFEA39CB46109385BF91286A3C02F40EE86B0B62FC34033094DE41E2C0D", 'hex');
                            if (this.keys.sc.length != nukiConstants.NUKI_NONCEBYTES) {
                                console.log("Nonce length (" + this.keys.sc.length + ") is not " + nukiConstants.NUKI_NONCEBYTES);
                                this.state = this.PAIRING_IDLE;
                                callback(this.RESULT_UNLIKELY_ERROR);
                                return;
                            }
                            this.state = PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_SL_SEND_CHALLENGE_2;
                            this.prepareDataToSend(nukiConstants.CMD_CHALLENGE, this.keys.sc);
                            value = this.getNextChunk(this.dataStillToSend);
                            if (this._updateValueCallback && value.length > 0) {
                                console.log("sending challenge 2: " + value.length + " bytes");
                                this._updateValueCallback(value);
                            }

                            callback(this.RESULT_SUCCESS);
                        } else {
                            console.log("CL and SL authenticators are not equal. Possible man in the middle attack. Exiting.");
                            this.state = this.PAIRING_IDLE;
                            callback(this.RESULT_UNLIKELY_ERROR);
                        }
                    } else {
                        console.log("command or command identifier wrong");
                        this.state = this.PAIRING_IDLE;
                        callback(this.RESULT_UNLIKELY_ERROR);
                    }
                    break;
                case PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_CL_SEND_AUTHORIZATION_DATA:
                    cmdId = data.readUInt16LE(0);
                    if (cmdId === nukiConstants.CMD_AUTHORIZATION_DATA) {
                        var clAuthData = data.slice(2, data.length - 2);
                        console.log("CL sent authorization data", clAuthData);

                        // todo verify authenticator
                        // todo store new user and generate new authorization-id
                        // todo send new authorization-id

                        // todo continue with state machine

                    } else {
                        console.log("command or command identifier wrong");
                        this.state = this.PAIRING_IDLE;
                        callback(this.RESULT_UNLIKELY_ERROR);
                    }
                    break;
                default:
                    console.log("ERROR unexpected pairing state");
                    this.state = this.PAIRING_IDLE;
                    callback(this.RESULT_UNLIKELY_ERROR);
            }
        }
    } else {
        console.log("checksum is NOT ok");
        this.state = this.PAIRING_IDLE;
        callback(this.RESULT_UNLIKELY_ERROR);
    }
};

PairingGeneralDataInputOutputCharacteristic.prototype.onSubscribe = function (maxValueSize, updateValueCallback) {
    console.log('PairingGeneralDataInputOutputCharacteristic - onSubscribe');

    this._updateValueCallback = updateValueCallback;

    if (this.dataStillToSend.length > 0) {
        switch (this.state) {
            case PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_SL_SEND_PUBKEY:

                var value = this.getNextChunk(this.dataStillToSend);
                if (value.length > 0) {
                    console.log("sending " + value.length + " bytes from onSubscribe");
                    updateValueCallback(value);
                }

                if (this.dataStillToSend.length === 0) {
                    this.state = PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_CL_SEND_PUBKEY;
                }

                break;
            default:
                console.log("ERROR unexpected pairing state");
                this.state = this.PAIRING_IDLE;
        }
    } else {
        console.log("don't have more data to notify");
    }
};

PairingGeneralDataInputOutputCharacteristic.prototype.onUnsubscribe = function () {
    console.log('PairingGeneralDataInputOutputCharacteristic - onUnsubscribe');

    this._updateValueCallback = null;
};

PairingGeneralDataInputOutputCharacteristic.prototype.onIndicate = function () {
    console.log("PairingGeneralDataInputOutputCharacteristic indicate");
    if (this.dataStillToSend.length > 0) {
        if (this._updateValueCallback) {
            var value;
            switch (this.state) {
                case PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_SL_SEND_PUBKEY:
                    value = this.getNextChunk(this.dataStillToSend);
                    if (value.length > 0) {
                        console.log("sending PK: " + value.length + " bytes as indication");
                        this._updateValueCallback(value);
                    }
                    if (this.dataStillToSend.length === 0) {
                        this.state = PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_CL_SEND_PUBKEY;
                    }

                    break;
                case PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_SL_SEND_CHALLENGE:
                    value = this.getNextChunk(this.dataStillToSend);
                    if (value.length > 0) {
                        console.log("sending challenge 1: " + value.length + " bytes as indication");
                        this._updateValueCallback(value);
                    }
                    if (this.dataStillToSend.length === 0) {
                        this.state = PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_CL_SEND_AUTHENTICATOR;
                    }
                    break;
                case PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_SL_SEND_CHALLENGE_2:
                    value = this.getNextChunk(this.dataStillToSend);
                    if (value.length > 0) {
                        console.log("sending challenge 2: " + value.length + " bytes as indication");
                        this._updateValueCallback(value);
                    }
                    if (this.dataStillToSend.length === 0) {
                        this.state = PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_CL_SEND_AUTHORIZATION_DATA;
                    }
                    break;
                default:
                    console.log("ERROR unexpected pairing state");
                    this.state = this.PAIRING_IDLE;
            }
        } else {
            console.log("don't have updateValueCallback on indicate");
            this.state = this.PAIRING_IDLE;
        }
    } else {
        console.log("don't have more data to indicate");
    }
};

module.exports = PairingGeneralDataInputOutputCharacteristic;
<commit_msg>sharedKey passing to second call fixed<commit_after>var util = require('util');
var nukiConstants = require('./nuki-constants');
var _ = require('underscore');
var crc = require('crc');
var sodium = require('sodium');
var HSalsa20 = require('./hsalsa20');
var crypto = require('crypto');

var bleno = require('bleno');
var BlenoCharacteristic = bleno.Characteristic;
var BlenoDescriptor = bleno.Descriptor;

function PairingGeneralDataInputOutputCharacteristic(keys) {
    this.state = this.PAIRING_IDLE;
    this.keys = keys;
    this.dataStillToSend = new Buffer(0);

    PairingGeneralDataInputOutputCharacteristic.super_.call(this, {
        // uuid: 'a92ee101-5501-11e4-916c-0800200c9a66',
        uuid: 'a92ee101550111e4916c0800200c9a66',
        properties: ['write', 'indicate'],
        descriptors: [
            new BlenoDescriptor({
                uuid: '2902',   // client characterstic configuration
                value: 'Pairing commands'
            })
        ]
    });
}

util.inherits(PairingGeneralDataInputOutputCharacteristic, BlenoCharacteristic);

PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_IDLE = 0;
PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_SL_SEND_PUBKEY = 1;
PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_CL_SEND_PUBKEY = 2;
PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_SL_SEND_CHALLENGE = 3;
PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_CL_SEND_AUTHENTICATOR = 4;
PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_SL_SEND_CHALLENGE_2 = 5;
PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_CL_SEND_AUTHORIZATION_DATA = 6;

PairingGeneralDataInputOutputCharacteristic.prototype.getNextChunk = function (data) {
    var block0;
    if (data.length > 20) {
        block0 = data.slice(0, 20);
        this.dataStillToSend = data.slice(20);
    } else {
        block0 = data;
        this.dataStillToSend = new Buffer(0);
    }
    return block0;
};

PairingGeneralDataInputOutputCharacteristic.prototype.prepareDataToSend = function (cmd, data) {
    var cmdBuffer = new Buffer(2);
    cmdBuffer.writeUInt16LE(cmd);
    var responseData = Buffer.concat([cmdBuffer, data]);
    var checksum = crc.crc16ccitt(responseData);
    var checksumBuffer = new Buffer(2);
    checksumBuffer.writeUInt16LE(checksum);
    this.dataStillToSend = Buffer.concat([responseData, checksumBuffer]);
    console.log("prepared to send:", this.dataStillToSend);
};

PairingGeneralDataInputOutputCharacteristic.prototype.onWriteRequest = function (data, offset, withoutResponse, callback) {
    var cmdId, slPk, value;
    console.log("PairingGeneralDataInputOutputCharacteristic", data);
    var dataForCrc = data.slice(0, data.length - 2);
    var crcSumCalc = crc.crc16ccitt(dataForCrc);
    var crcSumRetrieved = data.readUInt16LE(data.length - 2);

    if (crcSumCalc === crcSumRetrieved) {
        console.log("checksum is ok");
        if (offset) {
            callback(this.RESULT_ATTR_NOT_LONG);
        } else if (data.length > 200) {
            callback(this.RESULT_INVALID_ATTRIBUTE_LENGTH);
        } else {
            switch (this.state) {
                case PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_IDLE:
                    var rCmd = data.readUInt16LE(0);
                    cmdId = data.readUInt16LE(2);
                    if (rCmd === nukiConstants.CMD_reqUEST_DATA && cmdId === nukiConstants.CMD_ID_PUBLIC_KEY) {
                        slPk = new Buffer(0);
                        if (Buffer.isBuffer(this.keys.slPk)) {
                            slPk = this.keys.slPk;
                        } else {
                            if (_.isString(this.keys.slPk)) {
                                slPk = new Buffer(this.keys.slPk, 'hex');
                            } else {
                                if (_.isArray(this.keys.slPk)) {
                                    slPk = new Buffer(this.keys.slPk);
                                }
                            }
                        }

                        if (slPk.length > 0) {
                            this.prepareDataToSend(nukiConstants.CMD_ID_PUBLIC_KEY, slPk);
                            this.state = PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_SL_SEND_PUBKEY;

                            callback(this.RESULT_SUCCESS);
                        } else {
                            console.log("ERROR missing SL public key");
                            this.state = this.PAIRING_IDLE;
                            callback(this.RESULT_UNLIKELY_ERROR);
                        }
                    }
                    else {
                        console.log("command or command identifier wrong");
                        this.state = this.PAIRING_IDLE;
                        callback(this.RESULT_UNLIKELY_ERROR);
                    }
                    break;
                case PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_CL_SEND_PUBKEY:
                    cmdId = data.readUInt16LE(0);
                    if (cmdId === nukiConstants.CMD_ID_PUBLIC_KEY) {
                        this.keys.clPk = data.slice(2, data.length - 2);
                        console.log("CL PUBKEY:", this.keys.clPk);

                        slPk = new Buffer(0);
                        if (Buffer.isBuffer(this.keys.slPk)) {
                            slPk = this.keys.slPk;
                        } else {
                            if (_.isString(this.keys.slPk)) {
                                slPk = new Buffer(this.keys.slPk, 'hex');
                            } else {
                                if (_.isArray(this.keys.slPk)) {
                                    slPk = new Buffer(this.keys.slPk);
                                }
                            }
                        }
                        var slSk = new Buffer(0);
                        if (Buffer.isBuffer(this.keys.slSk)) {
                            slSk = this.keys.slSk;
                        } else {
                            if (_.isString(this.keys.slSk)) {
                                slSk = new Buffer(this.keys.slSk, 'hex');
                            } else {
                                if (_.isArray(this.keys.slSk)) {
                                    slSk = new Buffer(this.keys.slSk);
                                }
                            }
                        }

                        this.keys.slPk = slPk;
                        this.keys.slSk = slSk;

                        console.log("slPK", slPk);
                        console.log("slSK", slSk);
                        console.log("clPK", this.keys.clPk);

                        // Create Diffie-Hellman key from nuki secret key and clients public key
                        // crypto_scalarmult_curve25519(k,secretKey,pk)
                        var k = sodium.api.crypto_scalarmult(slSk, this.keys.clPk);
                        console.log("SL DH Key from SL SK and CL PK: ", k);

                        // derive a longterm shared secret key s from k using function kdf1
                        // static const unsigned char _0[16];
                        // static const unsigned char sigma[16] = "expand 32-byte k";
                        // crypto_core_hsalsa20(firstKey,_0,sharedKey,sigma)
                        var hsalsa20 = new HSalsa20();
                        this.keys.sharedSecret = new Buffer(32);
                        var inv = new Buffer(16);
                        inv.fill(0);
                        var c = new Buffer("expand 32-byte k");
                        hsalsa20.crypto_core(this.keys.sharedSecret, inv, k, c);
                        console.log("derived shared key: ", this.keys.sharedSecret);

                        console.log("Creating one time challenge...");
                        this.keys.sc = new Buffer(nukiConstants.NUKI_NONCEBYTES);
                        sodium.api.randombytes_buf(this.keys.sc);
                        // todo remove hardcoded challenge
                        this.keys.sc = new Buffer("6CD4163D159050C798553EAA57E278A579AFFCBC56F09FC57FE879E51C42DF17", 'hex');
                        if (this.keys.sc.length != nukiConstants.NUKI_NONCEBYTES) {
                            console.log("Nonce length (" + this.keys.sc.length + ") is not " + nukiConstants.NUKI_NONCEBYTES);
                            this.state = this.PAIRING_IDLE;
                            callback(this.RESULT_UNLIKELY_ERROR);
                            return;
                        }

                        this.state = PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_SL_SEND_CHALLENGE;
                        this.prepareDataToSend(nukiConstants.CMD_CHALLENGE, this.keys.sc);
                        value = this.getNextChunk(this.dataStillToSend);
                        if (this._updateValueCallback && value.length > 0) {
                            console.log("sending challenge 1: " + value.length + " bytes");
                            this._updateValueCallback(value);
                        }

                        var r = Buffer.concat([this.keys.clPk, slPk, this.keys.sc]);
                        // use HMAC-SHA256 to create the authenticator
                        var a = crypto.createHmac('SHA256', this.keys.sharedSecret).update(r).digest();
                        console.log("SL Authorization authenticator", a);

                        callback(this.RESULT_SUCCESS);
                    }
                    else {
                        console.log("command or command identifier wrong");
                        this.state = this.PAIRING_IDLE;
                        callback(this.RESULT_UNLIKELY_ERROR);
                    }

                    break;
                case PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_CL_SEND_AUTHENTICATOR:
                    cmdId = data.readUInt16LE(0);
                    if (cmdId === nukiConstants.CMD_AUTHORIZATION_AUTHENTICATOR) {
                        var clCr = data.slice(2, data.length - 2);
                        console.log("CL authorization authenticator", clCr);

                        // create authenticator with data from server side
                        console.log("keys:", this.keys.slPk, this.keys.clPk, this.keys.sc);

                        r = Buffer.concat([this.keys.slPk, this.keys.clPk, this.keys.sc]);
                        // use HMAC-SHA256 to create the authenticator
                        var cr = crypto.createHmac('SHA256', this.keys.sharedSecret).update(r).digest();
                        console.log("SL Authorization authenticator", cr);

                        // Step 14: verify authenticator
                        if (Buffer.compare(clCr, cr) === 0) {
                            console.log("Authenticators verified ok");
                            // Step 15: send second challenge
                            console.log("Creating second one time challenge...");
                            this.keys.sc = new Buffer(nukiConstants.NUKI_NONCEBYTES);
                            sodium.api.randombytes_buf(this.keys.sc);
                            // todo remove hardcoded challenge
                            this.keys.sc = new Buffer("E0742CFEA39CB46109385BF91286A3C02F40EE86B0B62FC34033094DE41E2C0D", 'hex');
                            if (this.keys.sc.length != nukiConstants.NUKI_NONCEBYTES) {
                                console.log("Nonce length (" + this.keys.sc.length + ") is not " + nukiConstants.NUKI_NONCEBYTES);
                                this.state = this.PAIRING_IDLE;
                                callback(this.RESULT_UNLIKELY_ERROR);
                                return;
                            }
                            this.state = PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_SL_SEND_CHALLENGE_2;
                            this.prepareDataToSend(nukiConstants.CMD_CHALLENGE, this.keys.sc);
                            value = this.getNextChunk(this.dataStillToSend);
                            if (this._updateValueCallback && value.length > 0) {
                                console.log("sending challenge 2: " + value.length + " bytes");
                                this._updateValueCallback(value);
                            }

                            callback(this.RESULT_SUCCESS);
                        } else {
                            console.log("CL and SL authenticators are not equal. Possible man in the middle attack. Exiting.");
                            this.state = this.PAIRING_IDLE;
                            callback(this.RESULT_UNLIKELY_ERROR);
                        }
                    } else {
                        console.log("command or command identifier wrong");
                        this.state = this.PAIRING_IDLE;
                        callback(this.RESULT_UNLIKELY_ERROR);
                    }
                    break;
                case PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_CL_SEND_AUTHORIZATION_DATA:
                    cmdId = data.readUInt16LE(0);
                    if (cmdId === nukiConstants.CMD_AUTHORIZATION_DATA) {
                        var clAuthData = data.slice(2, data.length - 2);
                        console.log("CL sent authorization data", clAuthData);

                        // todo verify authenticator
                        // todo store new user and generate new authorization-id
                        // todo send new authorization-id

                        // todo continue with state machine

                    } else {
                        console.log("command or command identifier wrong");
                        this.state = this.PAIRING_IDLE;
                        callback(this.RESULT_UNLIKELY_ERROR);
                    }
                    break;
                default:
                    console.log("ERROR unexpected pairing state");
                    this.state = this.PAIRING_IDLE;
                    callback(this.RESULT_UNLIKELY_ERROR);
            }
        }
    } else {
        console.log("checksum is NOT ok");
        this.state = this.PAIRING_IDLE;
        callback(this.RESULT_UNLIKELY_ERROR);
    }
};

PairingGeneralDataInputOutputCharacteristic.prototype.onSubscribe = function (maxValueSize, updateValueCallback) {
    console.log('PairingGeneralDataInputOutputCharacteristic - onSubscribe');

    this._updateValueCallback = updateValueCallback;

    if (this.dataStillToSend.length > 0) {
        switch (this.state) {
            case PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_SL_SEND_PUBKEY:

                var value = this.getNextChunk(this.dataStillToSend);
                if (value.length > 0) {
                    console.log("sending " + value.length + " bytes from onSubscribe");
                    updateValueCallback(value);
                }

                if (this.dataStillToSend.length === 0) {
                    this.state = PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_CL_SEND_PUBKEY;
                }

                break;
            default:
                console.log("ERROR unexpected pairing state");
                this.state = this.PAIRING_IDLE;
        }
    } else {
        console.log("don't have more data to notify");
    }
};

PairingGeneralDataInputOutputCharacteristic.prototype.onUnsubscribe = function () {
    console.log('PairingGeneralDataInputOutputCharacteristic - onUnsubscribe');

    this._updateValueCallback = null;
};

PairingGeneralDataInputOutputCharacteristic.prototype.onIndicate = function () {
    console.log("PairingGeneralDataInputOutputCharacteristic indicate");
    if (this.dataStillToSend.length > 0) {
        if (this._updateValueCallback) {
            var value;
            switch (this.state) {
                case PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_SL_SEND_PUBKEY:
                    value = this.getNextChunk(this.dataStillToSend);
                    if (value.length > 0) {
                        console.log("sending PK: " + value.length + " bytes as indication");
                        this._updateValueCallback(value);
                    }
                    if (this.dataStillToSend.length === 0) {
                        this.state = PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_CL_SEND_PUBKEY;
                    }

                    break;
                case PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_SL_SEND_CHALLENGE:
                    value = this.getNextChunk(this.dataStillToSend);
                    if (value.length > 0) {
                        console.log("sending challenge 1: " + value.length + " bytes as indication");
                        this._updateValueCallback(value);
                    }
                    if (this.dataStillToSend.length === 0) {
                        this.state = PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_CL_SEND_AUTHENTICATOR;
                    }
                    break;
                case PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_SL_SEND_CHALLENGE_2:
                    value = this.getNextChunk(this.dataStillToSend);
                    if (value.length > 0) {
                        console.log("sending challenge 2: " + value.length + " bytes as indication");
                        this._updateValueCallback(value);
                    }
                    if (this.dataStillToSend.length === 0) {
                        this.state = PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_CL_SEND_AUTHORIZATION_DATA;
                    }
                    break;
                default:
                    console.log("ERROR unexpected pairing state");
                    this.state = this.PAIRING_IDLE;
            }
        } else {
            console.log("don't have updateValueCallback on indicate");
            this.state = this.PAIRING_IDLE;
        }
    } else {
        console.log("don't have more data to indicate");
    }
};

module.exports = PairingGeneralDataInputOutputCharacteristic;
<|endoftext|>