From 9a2cc918c876c0275e32bd01760bef058cf6505f Mon Sep 17 00:00:00 2001 From: Hadrien Kohl Date: Wed, 7 Feb 2018 17:08:12 +0100 Subject: [PATCH 1/7] Add support for EID fram recognition --- lib/eddystone-beacon-scanner.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/lib/eddystone-beacon-scanner.js b/lib/eddystone-beacon-scanner.js index c0c17d6..9e736d0 100644 --- a/lib/eddystone-beacon-scanner.js +++ b/lib/eddystone-beacon-scanner.js @@ -14,6 +14,7 @@ var SERVICE_UUID = 'feaa'; var UID_FRAME_TYPE = 0x00; var URL_FRAME_TYPE = 0x10; var TLM_FRAME_TYPE = 0x20; +var EID_FRAME_TYPE = 0x30; var EXIT_GRACE_PERIOD = 5000; // milliseconds @@ -132,6 +133,11 @@ EddystoneBeaconScanner.prototype.parseBeacon = function(peripheral) { beacon = this.parseTlmData(data); break; + case EID_FRAME_TYPE: + type = 'eid'; + beacon = this.parseEidData(data); + break; + default: break; } @@ -148,6 +154,13 @@ EddystoneBeaconScanner.prototype.parseBeacon = function(peripheral) { return beacon; }; +EddystoneBeaconScanner.prototype.parseEidData = function(data) { + return { + txPower: data.readInt8(1), + eid: data.slice(2, 9) + }; +}; + EddystoneBeaconScanner.prototype.parseUidData = function(data) { return { txPower: data.readInt8(1), From 9d9166affa7fe5875a204646a132dca75cc0f022 Mon Sep 17 00:00:00 2001 From: Hadrien Kohl Date: Thu, 8 Feb 2018 09:13:03 +0100 Subject: [PATCH 2/7] Add temporary key logic --- lib/eddystone-beacon-scanner.js | 41 +++++++++++++++++++++++++++++++-- package.json | 3 ++- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/lib/eddystone-beacon-scanner.js b/lib/eddystone-beacon-scanner.js index 9e736d0..d1a5278 100644 --- a/lib/eddystone-beacon-scanner.js +++ b/lib/eddystone-beacon-scanner.js @@ -3,6 +3,7 @@ process.env['NOBLE_REPORT_ALL_HCI_EVENTS'] = 1; var events = require('events'); var util = require('util'); +var forge = require('node-forge'); var debug = require('debug')('eddystone-beacon-scanner'); @@ -26,7 +27,8 @@ var EddystoneBeaconScanner = function() { util.inherits(EddystoneBeaconScanner, events.EventEmitter); -EddystoneBeaconScanner.prototype.startScanning = function(allowDuplicates, gracePeriod) { + +EddystoneBeaconScanner.prototype.startScanning = function(allowDuplicates, gracePeriod, encryption) { debug('startScanning'); var startScanningOnPowerOn = function() { @@ -39,6 +41,9 @@ EddystoneBeaconScanner.prototype.startScanning = function(allowDuplicates, grace startScanningOnPowerOn(); + this._secret = forge.util.hexToBytes("00112233445566778899AABBCCDDEEFF"); + this._cipher = forge.cipher.createCipher('AES-ECB', this._secret); + this._gracePeriod = (gracePeriod === undefined) ? EXIT_GRACE_PERIOD : gracePeriod; this._allowDuplicates = allowDuplicates; if (allowDuplicates) { @@ -157,10 +162,42 @@ EddystoneBeaconScanner.prototype.parseBeacon = function(peripheral) { EddystoneBeaconScanner.prototype.parseEidData = function(data) { return { txPower: data.readInt8(1), - eid: data.slice(2, 9) + eid: this.decipher(data.slice(2, 9)) }; }; +EddystoneBeaconScanner.prototype.computeTempKey = function () { + // TODO: Avoid recalculating. + var seconds = new Date().getTime() / 1000; + var buffer = Buffer.alloc(16); + buffer.fill(0x00, 0, 11); // Padding + buffer.fill(0xFF, 11, 12); // Salt + buffer.fill(0x00, 12, 14); // Padding + buffer.fill((seconds & 0xff000000) >> 24, 14, 15); // 16 first bits of the + buffer.fill((seconds & 0x00ff0000) >> 16, 15, 16); // time counter big endian. + try { + this._cipher.start({iv: null}); + this._cipher.update(forge.util.createBuffer(buffer, 'binary')); + return this._cipher.output.toHex(); + } finally { + this._cipher.finish(); + } +}; + +EddystoneBeaconScanner.prototype.decipher = function (data) { + var tempKey = this.computeTempKey(); + + var decipher = forge.cipher.createDecipher('AES-ECB', tempKey); + decipher.start({iv: null}); + decipher.update(forge.util.createBuffer(data)); + if (decipher.finish()) { + return decipher.output.toHex(); + } else { + return null; + } + +}; + EddystoneBeaconScanner.prototype.parseUidData = function(data) { return { txPower: data.readInt8(1), diff --git a/package.json b/package.json index f823e06..70a660c 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,8 @@ }, "dependencies": { "debug": "^2.1.3", + "eddystone-url-encoding": "^1.0.0", "noble": "^1.1.0", - "eddystone-url-encoding": "^1.0.0" + "node-forge": "^0.7.1" } } From d8ec9888a2368df2a7db633e74ba877a49872c7b Mon Sep 17 00:00:00 2001 From: Hadrien Kohl Date: Thu, 8 Feb 2018 10:15:52 +0100 Subject: [PATCH 3/7] Add eid encryption logic --- lib/eddystone-beacon-scanner.js | 40 ++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/lib/eddystone-beacon-scanner.js b/lib/eddystone-beacon-scanner.js index d1a5278..6dedb8f 100644 --- a/lib/eddystone-beacon-scanner.js +++ b/lib/eddystone-beacon-scanner.js @@ -41,6 +41,7 @@ EddystoneBeaconScanner.prototype.startScanning = function(allowDuplicates, grace startScanningOnPowerOn(); + this._scaler = 12; this._secret = forge.util.hexToBytes("00112233445566778899AABBCCDDEEFF"); this._cipher = forge.cipher.createCipher('AES-ECB', this._secret); @@ -165,10 +166,12 @@ EddystoneBeaconScanner.prototype.parseEidData = function(data) { eid: this.decipher(data.slice(2, 9)) }; }; +EddystoneBeaconScanner.prototype.getSeconds = function () { + return Math.floor((new Date).getTime()/1000); +}; -EddystoneBeaconScanner.prototype.computeTempKey = function () { +EddystoneBeaconScanner.prototype.computeTempKey = function (seconds) { // TODO: Avoid recalculating. - var seconds = new Date().getTime() / 1000; var buffer = Buffer.alloc(16); buffer.fill(0x00, 0, 11); // Padding buffer.fill(0xFF, 11, 12); // Salt @@ -176,7 +179,7 @@ EddystoneBeaconScanner.prototype.computeTempKey = function () { buffer.fill((seconds & 0xff000000) >> 24, 14, 15); // 16 first bits of the buffer.fill((seconds & 0x00ff0000) >> 16, 15, 16); // time counter big endian. try { - this._cipher.start({iv: null}); + this._cipher.start(); this._cipher.update(forge.util.createBuffer(buffer, 'binary')); return this._cipher.output.toHex(); } finally { @@ -184,18 +187,29 @@ EddystoneBeaconScanner.prototype.computeTempKey = function () { } }; -EddystoneBeaconScanner.prototype.decipher = function (data) { - var tempKey = this.computeTempKey(); - - var decipher = forge.cipher.createDecipher('AES-ECB', tempKey); - decipher.start({iv: null}); - decipher.update(forge.util.createBuffer(data)); - if (decipher.finish()) { - return decipher.output.toHex(); - } else { - return null; +EddystoneBeaconScanner.prototype.computeEid = function (seconds, scaler, tempKey) { + // TODO: Check scaler. + var buffer = Buffer.alloc(16); + buffer.fill(0x00, 0, 11); // Padding + buffer.writeUInt8(scaler, 11); + //buffer.writeUInt32BE(Math.floor(seconds / (2 ^ scaler)) * (2 ^ scaler), 12, true); + buffer.writeUInt32BE((seconds >> scaler) << scaler, 12, true); + + var cipher = forge.cipher.createCipher('AES-ECB', forge.util.hexToBytes(tempKey)); + try { + cipher.start(); + cipher.update(forge.util.createBuffer(buffer, 'binary')); + return cipher.output.toHex().slice(0,14); + } finally { + cipher.finish(); } +}; +EddystoneBeaconScanner.prototype.decipher = function (data) { + var seconds = this.getSeconds(); + var tempKey = this.computeTempKey(seconds); + var eid = this.computeEid(seconds, this._scaler, tempKey); + return eid === data.toString('hex'); }; EddystoneBeaconScanner.prototype.parseUidData = function(data) { From 4f199d2fbd82f92d2f31d9045655a16b3e76cef7 Mon Sep 17 00:00:00 2001 From: Hadrien Kohl Date: Sat, 10 Feb 2018 18:44:50 +0100 Subject: [PATCH 4/7] Add eax tests and library --- lib/eax.js | 651 ++++++++++++++++++++++++++++++++++++++++++++++++ lib/eax.spec.js | 104 ++++++++ 2 files changed, 755 insertions(+) create mode 100644 lib/eax.js create mode 100644 lib/eax.spec.js diff --git a/lib/eax.js b/lib/eax.js new file mode 100644 index 0000000..a7c6408 --- /dev/null +++ b/lib/eax.js @@ -0,0 +1,651 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 artjomb + */ +var CryptoJS = require("crypto-js"); +(function(C){ + // put on ext property in CryptoJS + var ext; + if (!C.hasOwnProperty("ext")) { + ext = C.ext = {}; + } else { + ext = C.ext; + } + + /** + * Shifts the array by n bits to the left. Zero bits are added as the + * least significant bits. This operation modifies the current array. + * + * @param {WordArray} wordArray WordArray to work on + * @param {int} n Bits to shift by + * + * @returns the WordArray that was passed in + */ + ext.bitshift = function(wordArray, n){ + var carry = 0, + words = wordArray.words, + wres, + skipped = 0, + carryMask; + if (n > 0) { + while(n > 31) { + // delete first element: + words.splice(0, 1); + + // add `0` word to the back + words.push(0); + + n -= 32; + skipped++; + } + if (n == 0) { + // 1. nothing to shift if the shift amount is on a word boundary + // 2. This has to be done, because the following algorithm computes + // wrong values only for n==0 + return carry; + } + for(var i = words.length - skipped - 1; i >= 0; i--) { + wres = words[i]; + words[i] <<= n; + words[i] |= carry; + carry = wres >>> (32 - n); + } + } else if (n < 0) { + while(n < -31) { + // insert `0` word to the front: + words.splice(0, 0, 0); + + // remove last element: + words.length--; + + n += 32; + skipped++; + } + if (n == 0) { + // nothing to shift if the shift amount is on a word boundary + return carry; + } + n = -n; + carryMask = (1 << n) - 1; + for(var i = skipped; i < words.length; i++) { + wres = words[i] & carryMask; + words[i] >>>= n; + words[i] |= carry; + carry = wres << (32 - n); + } + } + return carry; + }; + + /** + * Negates all bits in the WordArray. This manipulates the given array. + * + * @param {WordArray} wordArray WordArray to work on + * + * @returns the WordArray that was passed in + */ + ext.neg = function(wordArray){ + var words = wordArray.words; + for(var i = 0; i < words.length; i++) { + words[i] = ~words[i]; + } + return wordArray; + }; + + /** + * Applies XOR on both given word arrays and returns a third resulting + * WordArray. The initial word arrays must have the same length + * (significant bytes). + * + * @param {WordArray} wordArray1 WordArray + * @param {WordArray} wordArray2 WordArray + * + * @returns first passed WordArray (modified) + */ + ext.xor = function(wordArray1, wordArray2){ + for(var i = 0; i < wordArray1.words.length; i++) { + wordArray1.words[i] ^= wordArray2.words[i]; + } + return wordArray1; + }; + + /** + * Logical AND between the two passed arrays. Both arrays must have the + * same length. + * + * @param {WordArray} arr1 Array 1 + * @param {WordArray} arr2 Array 2 + * + * @returns new WordArray + */ + ext.bitand = function(arr1, arr2){ + var newArr = arr1.clone(), + tw = newArr.words, + ow = arr2.words; + for(var i = 0; i < tw.length; i++) { + tw[i] &= ow[i]; + } + return newArr; + }; +})(CryptoJS); + +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 artjomb + */ +(function(C){ + // put on ext property in CryptoJS + var ext; + if (!C.hasOwnProperty("ext")) { + ext = C.ext = {}; + } else { + ext = C.ext; + } + + // Shortcuts + var Base = C.lib.Base; + var WordArray = C.lib.WordArray; + var AES = C.algo.AES; + + // Constants + ext.const_Zero = WordArray.create([0x00000000, 0x00000000, 0x00000000, 0x00000000]); + ext.const_One = WordArray.create([0x00000000, 0x00000000, 0x00000000, 0x00000001]); + ext.const_Rb = WordArray.create([0x00000000, 0x00000000, 0x00000000, 0x00000087]); // 00..0010000111 + ext.const_Rb_Shifted = WordArray.create([0x80000000, 0x00000000, 0x00000000, 0x00000043]); // 100..001000011 + ext.const_nonMSB = WordArray.create([0xFFFFFFFF, 0xFFFFFFFF, 0x7FFFFFFF, 0x7FFFFFFF]); // 1^64 || 0^1 || 1^31 || 0^1 || 1^31 + + /** + * Looks into the object to see if it is a WordArray. + * + * @param obj Some object + * + * @returns {boolean} + + */ + ext.isWordArray = function(obj) { + return obj && typeof obj.clamp === "function" && typeof obj.concat === "function" && typeof obj.words === "array"; + } + + /** + * This padding is a 1 bit followed by as many 0 bits as needed to fill + * up the block. This implementation doesn't work on bits directly, + * but on bytes. Therefore the granularity is much bigger. + */ + C.pad.OneZeroPadding = { + pad: function (data, blocksize) { + // Shortcut + var blockSizeBytes = blocksize * 4; + + // Count padding bytes + var nPaddingBytes = blockSizeBytes - data.sigBytes % blockSizeBytes; + + // Create padding + var paddingWords = []; + for (var i = 0; i < nPaddingBytes; i += 4) { + var paddingWord = 0x00000000; + if (i === 0) { + paddingWord = 0x80000000; + } + paddingWords.push(paddingWord); + } + var padding = WordArray.create(paddingWords, nPaddingBytes); + + // Add padding + data.concat(padding); + }, + unpad: function () { + // TODO: implement + } + }; + + /** + * No padding is applied. This is necessary for streaming cipher modes + * like CTR. + */ + C.pad.NoPadding = { + pad: function () {}, + unpad: function () {} + }; + + /** + * Returns the n leftmost bytes of the WordArray. + * + * @param {WordArray} wordArray WordArray to work on + * @param {int} n Bytes to retrieve + * + * @returns new WordArray + */ + ext.leftmostBytes = function(wordArray, n){ + var lmArray = wordArray.clone(); + lmArray.sigBytes = n; + lmArray.clamp(); + return lmArray; + }; + + /** + * Returns the n rightmost bytes of the WordArray. + * + * @param {WordArray} wordArray WordArray to work on + * @param {int} n Bytes to retrieve (must be positive) + * + * @returns new WordArray + */ + ext.rightmostBytes = function(wordArray, n){ + wordArray.clamp(); + var wordSize = 32; + var rmArray = wordArray.clone(); + var bitsToShift = (rmArray.sigBytes - n) * 8; + if (bitsToShift >= wordSize) { + var popCount = Math.floor(bitsToShift/wordSize); + bitsToShift -= popCount * wordSize; + rmArray.words.splice(0, popCount); + rmArray.sigBytes -= popCount * wordSize / 8; + } + if (bitsToShift > 0) { + ext.bitshift(rmArray, bitsToShift); + rmArray.sigBytes -= bitsToShift / 8; + } + return rmArray; + }; + + /** + * Returns the n rightmost words of the WordArray. It assumes + * that the current WordArray has at least n words. + * + * @param {WordArray} wordArray WordArray to work on + * @param {int} n Words to retrieve (must be positive) + * + * @returns popped words as new WordArray + */ + ext.popWords = function(wordArray, n){ + var left = ext.leftmostBytes(wordArray, n * 4); + wordArray.words = wordArray.words.slice(n); + wordArray.sigBytes -= n * 4; + return left; + }; + + /** + * Shifts the array to the left and returns the shifted dropped elements + * as WordArray. The initial WordArray must contain at least n bytes and + * they have to be significant. + * + * @param {WordArray} wordArray WordArray to work on (is modified) + * @param {int} n Bytes to shift (must be positive, default 16) + * + * @returns new WordArray + */ + ext.shiftBytes = function(wordArray, n){ + n = n || 16; + var r = n % 4; + n -= r; + + var shiftedArray = WordArray.create(); + for(var i = 0; i < n; i += 4) { + shiftedArray.words.push(wordArray.words.shift()); + wordArray.sigBytes -= 4; + shiftedArray.sigBytes += 4; + } + if (r > 0) { + shiftedArray.words.push(wordArray.words[0]); + shiftedArray.sigBytes += r; + + ext.bitshift(wordArray, r * 8); + wordArray.sigBytes -= r; + } + return shiftedArray; + }; + + /** + * XORs arr2 to the end of arr1 array. This doesn't modify the current + * array aside from clamping. + * + * @param {WordArray} arr1 Bigger array + * @param {WordArray} arr2 Smaller array to be XORed to the end + * + * @returns new WordArray + */ + ext.xorendBytes = function(arr1, arr2){ + // TODO: more efficient + return ext.leftmostBytes(arr1, arr1.sigBytes-arr2.sigBytes) + .concat(ext.xor(ext.rightmostBytes(arr1, arr2.sigBytes), arr2)); + }; + + /** + * Doubling operation on a 128-bit value. This operation modifies the + * passed array. + * + * @param {WordArray} wordArray WordArray to work on + * + * @returns passed WordArray + */ + ext.dbl = function(wordArray){ + var carry = ext.msb(wordArray); + ext.bitshift(wordArray, 1); + ext.xor(wordArray, carry === 1 ? ext.const_Rb : ext.const_Zero); + return wordArray; + }; + + /** + * Inverse operation on a 128-bit value. This operation modifies the + * passed array. + * + * @param {WordArray} wordArray WordArray to work on + * + * @returns passed WordArray + */ + ext.inv = function(wordArray){ + var carry = wordArray.words[4] & 1; + ext.bitshift(wordArray, -1); + ext.xor(wordArray, carry === 1 ? ext.const_Rb_Shifted : ext.const_Zero); + return wordArray; + }; + + /** + * Check whether the word arrays are equal. + * + * @param {WordArray} arr1 Array 1 + * @param {WordArray} arr2 Array 2 + * + * @returns boolean + */ + ext.equals = function(arr1, arr2){ + if (!arr2 || !arr2.words || arr1.sigBytes !== arr2.sigBytes) { + return false; + } + arr1.clamp(); + arr2.clamp(); + var equal = 0; + for(var i = 0; i < arr1.words.length; i++) { + equal |= arr1.words[i] ^ arr2.words[i]; + } + return equal === 0; + }; + + /** + * Retrieves the most significant bit of the WordArray as an Integer. + * + * @param {WordArray} arr + * + * @returns Integer + */ + ext.msb = function(arr) { + return arr.words[0] >>> 31; + } +})(CryptoJS); + +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 artjomb + */ +(function(C){ + // Shortcuts + var Base = C.lib.Base; + var WordArray = C.lib.WordArray; + var AES = C.algo.AES; + var ext = C.ext; + var OneZeroPadding = C.pad.OneZeroPadding; + + function aesBlock(key, data){ + var aes128 = AES.createEncryptor(key, { iv: WordArray.create(), padding: C.pad.NoPadding }); + var arr = aes128.finalize(data); + return arr; + } + + var CMAC = C.algo.CMAC = Base.extend({ + /** + * Initializes a newly created CMAC + * + * @param {WordArray} key The secret key + * + * @example + * + * var cmacer = CryptoJS.algo.CMAC.create(key); + */ + init: function(key){ + // generate sub keys... + + // Step 1 + var L = aesBlock(key, ext.const_Zero); + + // Step 2 + var K1 = L.clone(); + ext.dbl(K1); + + // Step 3 + if (!this._isTwo) { + var K2 = K1.clone(); + ext.dbl(K2); + } else { + var K2 = L.clone(); + ext.inv(K2); + } + + this._K1 = K1; + this._K2 = K2; + this._K = key; + + this._const_Bsize = 16; + + this.reset(); + }, + + reset: function () { + this._x = ext.const_Zero.clone(); + this._counter = 0; + this._buffer = WordArray.create(); + }, + + update: function (messageUpdate) { + if (!messageUpdate) { + return this; + } + + // Shortcuts + var buffer = this._buffer; + var bsize = this._const_Bsize; + + if (typeof messageUpdate === "string") { + messageUpdate = C.enc.Utf8.parse(messageUpdate); + } + + buffer.concat(messageUpdate); + + while(buffer.sigBytes > bsize){ + var M_i = ext.shiftBytes(buffer, bsize); + ext.xor(this._x, M_i); + this._x.clamp(); + this._x = aesBlock(this._K, this._x); + this._counter++; + } + + // Chainable + return this; + }, + + finalize: function (messageUpdate) { + this.update(messageUpdate); + + // Shortcuts + var buffer = this._buffer; + var bsize = this._const_Bsize; + + var M_last = buffer.clone(); + if (buffer.sigBytes === bsize) { + ext.xor(M_last, this._K1); + } else { + OneZeroPadding.pad(M_last, bsize/4); + ext.xor(M_last, this._K2); + } + + ext.xor(M_last, this._x); + + this.reset(); // Can be used immediately afterwards + + return aesBlock(this._K, M_last); + }, + + _isTwo: false + }); + + /** + * Directly invokes the CMAC and returns the calculated MAC. + * + * @param {WordArray} key The key to be used for CMAC + * @param {WordArray|string} message The data to be MAC'ed (either WordArray or UTF-8 encoded string) + * + * @returns {WordArray} MAC + */ + C.CMAC = function(key, message){ + return CMAC.create(key).finalize(message); + }; + + C.algo.OMAC1 = CMAC; + C.algo.OMAC2 = CMAC.extend({ + _isTwo: true + }); +})(CryptoJS); + +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 artjomb + */ +(function(C){ + // Shortcuts + var Base = C.lib.Base; + var WordArray = C.lib.WordArray; + var AES = C.algo.AES; + var ext = C.ext; + var CMAC = C.algo.CMAC; + var zero = WordArray.create([0x0, 0x0, 0x0, 0x0]); + var one = WordArray.create([0x0, 0x0, 0x0, 0x1]); + var two = WordArray.create([0x0, 0x0, 0x0, 0x2]); + var blockLength = 16; + + var EAX = C.EAX = Base.extend({ + /** + * Initializes the key of the cipher. + * + * @param {WordArray} key Key to be used for CMAC and CTR + * @param {object} options Additonal options to tweak the encryption: + * splitKey - If true then the first half of the passed key will be + * the CMAC key and the second half the CTR key + * tagLength - Length of the tag in bytes (for created tag and expected tag) + */ + init: function(key, options){ + var macKey; + if (options && options.splitKey) { + var len = Math.floor(key.sigBytes / 2); + macKey = ext.shiftBytes(key, len); + } else { + macKey = key.clone(); + } + this._ctrKey = key; + this._mac = CMAC.create(macKey); + + this._tagLen = (options && options.tagLength) || blockLength; + this.reset(); + }, + reset: function(){ + this._mac.update(one); + if (this._ctr) { + this._ctr.reset(); + } + }, + updateAAD: function(header){ + this._mac.update(header); + return this; + }, + initCrypt: function(isEncrypt, nonce){ + var self = this; + self._tag = self._mac.finalize(); + self._isEnc = isEncrypt; + + self._mac.update(zero); + nonce = self._mac.finalize(nonce); + + ext.xor(self._tag, nonce); + + self._ctr = AES.createEncryptor(self._ctrKey, { + iv: nonce, + mode: C.mode.CTR, + padding: C.pad.NoPadding + }); + self._buf = WordArray.create(); + + self._mac.update(two); + + return self; + }, + update: function(msg) { + if (typeof msg === "string") { + msg = C.enc.Utf8.parse(msg); + } + var self = this; + var buffer = self._buf; + var isEncrypt = self._isEnc; + buffer.concat(msg); + + var useBytes = isEncrypt ? buffer.sigBytes : Math.max(buffer.sigBytes - self._tagLen, 0); + + var data = useBytes > 0 ? ext.shiftBytes(buffer, useBytes) : WordArray.create(); // guaranteed to be pure plaintext or ciphertext (without a tag during decryption) + var xoredData = self._ctr.process(data); + + self._mac.update(isEncrypt ? xoredData : data); + + return xoredData; + }, + finalize: function(msg){ + var self = this; + var xoredData = msg ? self.update(msg) : WordArray.create(); + var mac = self._mac; + var ctFin = self._ctr.finalize(); + + if (self._isEnc) { + var ctTag = mac.finalize(ctFin); + + ext.xor(self._tag, ctTag); + self.reset(); + return xoredData.concat(ctFin).concat(self._tag); + } else { + // buffer must contain only the tag at this point + var ctTag = mac.finalize(); + + ext.xor(self._tag, ctTag); + self.reset(); + if (ext.equals(self._tag, self._buf)) { + return xoredData.concat(ctFin); + } else { + return false; // tag doesn't match + } + } + }, + encrypt: function(plaintext, nonce, adArray){ + var self = this; + if (adArray) { + Array.prototype.forEach.call(adArray, function(ad){ + self.updateAAD(ad); + }); + } + self.initCrypt(true, nonce); + + return self.finalize(plaintext); + }, + decrypt: function(ciphertext, nonce, adArray){ + var self = this; + if (adArray) { + Array.prototype.forEach.call(adArray, function(ad){ + self.updateAAD(ad); + }); + } + self.initCrypt(false, nonce); + + return self.finalize(ciphertext); + } + }); +})(CryptoJS); + +module.exports = CryptoJS; diff --git a/lib/eax.spec.js b/lib/eax.spec.js new file mode 100644 index 0000000..db3bf70 --- /dev/null +++ b/lib/eax.spec.js @@ -0,0 +1,104 @@ +'use strict'; + +const expect = require('chai').expect; +const CryptoJS = require('./eax'); + +// EAX-AES128 Tests taken from http://web.cs.ucdavis.edu/~rogaway/papers/eax.pdf +const eax128Tests = [ + { + msg: '', + key: '233952DEE4D5ED5F9B9C6D6FF80FF478', + nonce: '62EC67F9C3A4A407FCB2A8C49031A8B3', + header:'6BFB914FD07EAE6B', + cipher: 'E037830E8389F27B025A2D6527E79D01' + },{ + msg: 'F7FB', + key: '91945D3F4DCBEE0BF45EF52255F095A4', + nonce: 'BECAF043B0A23D843194BA972C66DEBD', + header:'FA3BFD4806EB53FA', + cipher: '19DD5C4C9331049D0BDAB0277408F67967E5' + },{ + msg: '1A47CB4933', + key: '01F74AD64077F2E704C0F60ADA3DD523', + nonce: '70C3DB4F0D26368400A10ED05D2BFF5E', + header:'234A3463C1264AC6', + cipher: 'D851D5BAE03A59F238A23E39199DC9266626C40F80' + },{ + msg: '481C9E39B1', + key: 'D07CF6CBB7F313BDDE66B727AFD3C5E8', + nonce: '8408DFFF3C1A2B1292DC199E46B7D617', + header:'33CCE2EABFF5A79D', + cipher: '632A9D131AD4C168A4225D8E1FF755939974A7BEDE' + },{ + msg: '40D0C07DA5E4', + key: '35B6D0580005BBC12B0587124557D2C2', + nonce: 'FDB6B06676EEDC5C61D74276E1F8E816', + header:'AEB96EAEBE2970E9', + cipher: '071DFE16C675CB0677E536F73AFE6A14B74EE49844DD' + },{ + msg: '4DE3B35C3FC039245BD1FB7D', + key: 'BD8E6E11475E60B268784C38C62FEB22', + nonce: '6EAC5C93072D8E8513F750935E46DA1B', + header:'D4482D1CA78DCE0F', + cipher: '835BB4F15D743E350E728414ABB8644FD6CCB86947C5E10590210A4F' + },{ + msg: '8B0A79306C9CE7ED99DAE4F87F8DD61636', + key: '7C77D6E813BED5AC98BAA417477A2E7D', + nonce: '1A8C98DCD73D38393B2BF1569DEEFC19', + header:'65D2017990D62528', + cipher: '02083E3979DA014812F59F11D52630DA30137327D10649B0AA6E1C181DB617D7F2' + },{ + msg: '1BDA122BCE8A8DBAF1877D962B8592DD2D56', + key: '5FFF20CAFAB119CA2FC73549E20F5B0D', + nonce: 'DDE59B97D722156D4D9AFF2BC7559826', + header:'54B9F04E6A09189A', + cipher: '2EC47B2C4954A489AFC7BA4897EDCDAE8CC33B60450599BD02C96382902AEF7F832A' + },{ + msg: '6CF36720872B8513F6EAB1A8A44438D5EF11', + key: 'A4A4782BCFFD3EC5E7EF6D8C34A56123', + nonce: 'B781FCF2F75FA5A8DE97A9CA48E522EC', + header:'899A175897561D7E', + cipher: '0DE18FD0FDD91E7AF19F1D8EE8733938B1E8E7F6D2231618102FDB7FE55FF1991700' + },{ + msg: 'CA40D7446E545FFAED3BD12A740A659FFBBB3CEAB7', + key: '8395FCF1E95BEBD697BD010BC766AAC3', + nonce: '22E7ADD93CFC6393C57EC0B3C17D6B44', + header:'126735FCC320D25A', + cipher: 'CB8920F87A6C75CFF39627B56E3ED197C552D295A7CFC46AFC253B4652B1AF3795B124AB6E' + } +]; + +describe("AES-EAX mode", () => { + eax128Tests.forEach(test => { + + const key = CryptoJS.enc.Hex.parse(test.key.toLowerCase()); + let cipher; + + it('should accept the key', () => { + cipher = CryptoJS.EAX.create(key); + }); + + const nonce = CryptoJS.enc.Hex.parse(test.nonce.toLowerCase()); + const additionalData = CryptoJS.enc.Hex.parse(test.header.toLowerCase()); + const message = CryptoJS.enc.Hex.parse(test.msg.toLowerCase()); + const expectedCipher = CryptoJS.enc.Hex.parse(test.cipher.toLowerCase()); + + let result; + it('should encrypt the message', () => { + result = cipher.encrypt(message, nonce, [ additionalData ]); + expect(result.toString()).to.equal(expectedCipher.toString()); + }); + + it('should decrypt the cipher', () => { + let deciphered = cipher.decrypt(expectedCipher, nonce, [ additionalData ]); + expect(deciphered.toString()).to.equal(message.toString()); + }); + + it('should decrypt the ciphe without additional data', () => { + let ciphered = cipher.encrypt(message, nonce); + let deciphered = cipher.decrypt(ciphered, nonce); + expect(deciphered.toString()).to.equal(message.toString()); + }); + }); +}); + From e281ce3f3f63efcb5f2d8bd75d8cb6375f944327 Mon Sep 17 00:00:00 2001 From: Hadrien Kohl Date: Mon, 12 Feb 2018 09:45:19 +0100 Subject: [PATCH 5/7] Fix tag length bug --- lib/eax.js | 4 ++-- lib/eax.spec.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/eax.js b/lib/eax.js index a7c6408..058c858 100644 --- a/lib/eax.js +++ b/lib/eax.js @@ -609,14 +609,14 @@ var CryptoJS = require("crypto-js"); ext.xor(self._tag, ctTag); self.reset(); - return xoredData.concat(ctFin).concat(self._tag); + return xoredData.concat(ctFin).concat(ext.leftmostBytes(self._tag, self._tagLen)); } else { // buffer must contain only the tag at this point var ctTag = mac.finalize(); ext.xor(self._tag, ctTag); self.reset(); - if (ext.equals(self._tag, self._buf)) { + if (ext.equals(ext.leftmostBytes(self._tag, self._tagLen), self._buf)) { return xoredData.concat(ctFin); } else { return false; // tag doesn't match diff --git a/lib/eax.spec.js b/lib/eax.spec.js index db3bf70..305253f 100644 --- a/lib/eax.spec.js +++ b/lib/eax.spec.js @@ -94,7 +94,7 @@ describe("AES-EAX mode", () => { expect(deciphered.toString()).to.equal(message.toString()); }); - it('should decrypt the ciphe without additional data', () => { + it('should decrypt the cipher without additional data', () => { let ciphered = cipher.encrypt(message, nonce); let deciphered = cipher.decrypt(ciphered, nonce); expect(deciphered.toString()).to.equal(message.toString()); From 9bb6a8779caa54c109e1c56338ba824d0dc41895 Mon Sep 17 00:00:00 2001 From: Hadrien Kohl Date: Mon, 12 Feb 2018 09:56:27 +0100 Subject: [PATCH 6/7] Add tests and rewrite the aes using standard crypto module --- lib/eddystone-beacon-scanner.js | 67 ++++++++++++++++++++------------- lib/eid.spec.js | 20 ++++++++++ lib/etlm.spec.js | 51 +++++++++++++++++++++++++ 3 files changed, 112 insertions(+), 26 deletions(-) create mode 100644 lib/eid.spec.js create mode 100644 lib/etlm.spec.js diff --git a/lib/eddystone-beacon-scanner.js b/lib/eddystone-beacon-scanner.js index 6dedb8f..08b0e68 100644 --- a/lib/eddystone-beacon-scanner.js +++ b/lib/eddystone-beacon-scanner.js @@ -3,7 +3,10 @@ process.env['NOBLE_REPORT_ALL_HCI_EVENTS'] = 1; var events = require('events'); var util = require('util'); -var forge = require('node-forge'); + +var crypto = require('crypto'); + +const CryptoJS = require('./eax'); var debug = require('debug')('eddystone-beacon-scanner'); @@ -22,6 +25,13 @@ var EXIT_GRACE_PERIOD = 5000; // milliseconds var EddystoneBeaconScanner = function() { this._discovered = {}; + this._scaler = 5; + this._secret = Buffer.from('00112233445566778899AABBCCDDEEFF', 'hex'); + + this._cipher = crypto.createCipheriv("aes-128-ecb", this._secret, ''); + this._cipher.setAutoPadding(false); + //this._cipher = forge.cipher.createCipher('AES-ECB', this._secret); + noble.on('discover', this.onDiscover.bind(this)); }; @@ -41,12 +51,7 @@ EddystoneBeaconScanner.prototype.startScanning = function(allowDuplicates, grace startScanningOnPowerOn(); - this._scaler = 12; - this._secret = forge.util.hexToBytes("00112233445566778899AABBCCDDEEFF"); - this._cipher = forge.cipher.createCipher('AES-ECB', this._secret); - this._gracePeriod = (gracePeriod === undefined) ? EXIT_GRACE_PERIOD : gracePeriod; - this._allowDuplicates = allowDuplicates; if (allowDuplicates) { this._lostCheckInterval = setInterval(this.checkLost.bind(this), this._gracePeriod / 2); } @@ -172,37 +177,47 @@ EddystoneBeaconScanner.prototype.getSeconds = function () { EddystoneBeaconScanner.prototype.computeTempKey = function (seconds) { // TODO: Avoid recalculating. - var buffer = Buffer.alloc(16); + const buffer = Buffer.alloc(16); + buffer.fill(0x00, 0, 11); // Padding buffer.fill(0xFF, 11, 12); // Salt buffer.fill(0x00, 12, 14); // Padding - buffer.fill((seconds & 0xff000000) >> 24, 14, 15); // 16 first bits of the - buffer.fill((seconds & 0x00ff0000) >> 16, 15, 16); // time counter big endian. - try { - this._cipher.start(); - this._cipher.update(forge.util.createBuffer(buffer, 'binary')); - return this._cipher.output.toHex(); - } finally { - this._cipher.finish(); - } + buffer.writeInt16BE(seconds >> 16, 14); // 16 first bits of the time counter. + + const cipher = crypto.createCipheriv("aes-128-ecb", this._secret, ''); + cipher.setAutoPadding(false); + return Buffer.concat([cipher.update(buffer), cipher.final()]).slice(0,16); }; EddystoneBeaconScanner.prototype.computeEid = function (seconds, scaler, tempKey) { - // TODO: Check scaler. + // TODO: Check scaler value. var buffer = Buffer.alloc(16); buffer.fill(0x00, 0, 11); // Padding buffer.writeUInt8(scaler, 11); - //buffer.writeUInt32BE(Math.floor(seconds / (2 ^ scaler)) * (2 ^ scaler), 12, true); buffer.writeUInt32BE((seconds >> scaler) << scaler, 12, true); - var cipher = forge.cipher.createCipher('AES-ECB', forge.util.hexToBytes(tempKey)); - try { - cipher.start(); - cipher.update(forge.util.createBuffer(buffer, 'binary')); - return cipher.output.toHex().slice(0,14); - } finally { - cipher.finish(); - } + const cipher = crypto.createCipheriv("aes-128-ecb", tempKey, ''); + cipher.setAutoPadding(false); + return Buffer.concat([cipher.update(buffer), cipher.final()]).slice(0,8); +}; + +EddystoneBeaconScanner.prototype.encryptTlm = function () { +}; + +EddystoneBeaconScanner.prototype.decryptTlm = function (frame, seconds, scaler) { + + const nonce = Buffer.alloc(6); + nonce.writeUInt32BE((seconds >> scaler) << scaler); // Time base. + const salt = frame.readUInt16BE(14); // BE? + nonce.writeUInt16BE(salt, 4); // Salt. + + const data = Buffer.alloc(14); + frame.copy(data, 0, 2, 14); + frame.copy(data, 12, 16, 18); + + let cipher = CryptoJS.EAX.create(CryptoJS.enc.Hex.parse(this._secret.toString('hex')), {tagLength: 2}); + return cipher.decrypt(CryptoJS.enc.Hex.parse(data.toString('hex')), CryptoJS.enc.Hex.parse(nonce.toString('hex'))); + }; EddystoneBeaconScanner.prototype.decipher = function (data) { diff --git a/lib/eid.spec.js b/lib/eid.spec.js new file mode 100644 index 0000000..1dea5c2 --- /dev/null +++ b/lib/eid.spec.js @@ -0,0 +1,20 @@ +'use strict'; + +const expect = require('chai').expect; +const EddystoneBeaconScanner = require('./eddystone-beacon-scanner'); +const scanner = new EddystoneBeaconScanner(); + +describe('EID Frame', () => { + it('should compute the temporary key', () => { + const temporaryKey = scanner.computeTempKey(1518080946); + const expected = Buffer.from('b7c2d909d27d22df8343271162480210', 'hex'); + expect(temporaryKey.equals(expected)).to.be.true; + }); + + it('should compute the ephemeral key', () => { + const temporaryKey = scanner.computeEid(1518080946, 12, Buffer.from('b7c2d909d27d22df8343271162480210', 'hex')); + const expected = Buffer.from('7dd8169dae94258c', 'hex'); + expect(temporaryKey.equals(expected)).to.be.true; + }) +}); + diff --git a/lib/etlm.spec.js b/lib/etlm.spec.js new file mode 100644 index 0000000..18cce91 --- /dev/null +++ b/lib/etlm.spec.js @@ -0,0 +1,51 @@ +//'use strict'; + +const expect = require('chai').expect; + +const EddystoneBeaconScanner = require('./eddystone-beacon-scanner'); +const scanner = new EddystoneBeaconScanner(); + +const CryptoJS = require('./eax'); + +function encryptFrame(frame, seconds, scaler, salt) { + + + + const nonce = Buffer.alloc(6); + nonce.writeUInt32BE((seconds >> scaler) << scaler); // Time base. + nonce.writeUInt16BE(salt, 4); // Salt. + + const key = CryptoJS.enc.Hex.parse("00112233445566778899AABBCCDDEEFF"); + let cipher = CryptoJS.EAX.create(key, {tagLength: 2}); + // 12 bytes frame. + const rawFrame = CryptoJS.enc.Hex.parse(frame); + + const result = cipher.encrypt(rawFrame, CryptoJS.enc.Hex.parse(nonce.toString('hex'))); + + const encrypted = Buffer.from(result.toString(), 'hex'); + + const encryptedFrame = Buffer.alloc(18); + + encrypted.copy(encryptedFrame, 2, 0, 12); + encryptedFrame.writeUInt16BE(salt, 14); + encrypted.copy(encryptedFrame, 16, 12, 14); + + return encryptedFrame; + +} +describe('eTML Frame', () => { + it('should be decrypted', done => { + + const seconds = 1518080946; + const scaler = 4; + const salt = 65535; + + const frame = encryptFrame("FFEEDDCCBBAA998877665544", seconds, scaler, salt); + + const decrypted = scanner.decryptTlm(frame, seconds, scaler); + + expect(decrypted.toString()).to.equal("ffeeddccbbaa998877665544"); + + done(); + }); +}); From 81c42f35515292bc929b255a77b6bb55ba2dc29e Mon Sep 17 00:00:00 2001 From: Hadrien Kohl Date: Mon, 12 Feb 2018 10:27:43 +0100 Subject: [PATCH 7/7] Extract encryption to own methods --- lib/eddystone-beacon-scanner.js | 67 ++++++++++++++++++++++----------- lib/etlm.spec.js | 2 +- 2 files changed, 45 insertions(+), 24 deletions(-) diff --git a/lib/eddystone-beacon-scanner.js b/lib/eddystone-beacon-scanner.js index 08b0e68..bd6b688 100644 --- a/lib/eddystone-beacon-scanner.js +++ b/lib/eddystone-beacon-scanner.js @@ -179,45 +179,66 @@ EddystoneBeaconScanner.prototype.computeTempKey = function (seconds) { // TODO: Avoid recalculating. const buffer = Buffer.alloc(16); - buffer.fill(0x00, 0, 11); // Padding - buffer.fill(0xFF, 11, 12); // Salt - buffer.fill(0x00, 12, 14); // Padding + buffer.fill(0x00, 0, 11); // Padding + buffer.fill(0xFF, 11, 12); // Salt + buffer.fill(0x00, 12, 14); // Padding buffer.writeInt16BE(seconds >> 16, 14); // 16 first bits of the time counter. - const cipher = crypto.createCipheriv("aes-128-ecb", this._secret, ''); - cipher.setAutoPadding(false); - return Buffer.concat([cipher.update(buffer), cipher.final()]).slice(0,16); + return this.aes_ecb_encrypt(buffer, this._secret).slice(0,16); }; EddystoneBeaconScanner.prototype.computeEid = function (seconds, scaler, tempKey) { // TODO: Check scaler value. - var buffer = Buffer.alloc(16); - buffer.fill(0x00, 0, 11); // Padding - buffer.writeUInt8(scaler, 11); - buffer.writeUInt32BE((seconds >> scaler) << scaler, 12, true); - - const cipher = crypto.createCipheriv("aes-128-ecb", tempKey, ''); - cipher.setAutoPadding(false); - return Buffer.concat([cipher.update(buffer), cipher.final()]).slice(0,8); -}; + const buffer = Buffer.alloc(16); + const scaledSeconds = (seconds >> scaler) << scaler; + buffer.fill(0x00, 0, 11); // Padding + buffer.writeUInt8(scaler, 11); // Salt + buffer.writeUInt32BE(scaledSeconds, 12, true); // Top 16 bits of the time counter in 16-bit big-endian format -EddystoneBeaconScanner.prototype.encryptTlm = function () { + return this.aes_ecb_encrypt(buffer, tempKey).slice(0,8); }; EddystoneBeaconScanner.prototype.decryptTlm = function (frame, seconds, scaler) { + const salt = frame.readUInt16BE(14); // Read salt + // Recreate the nonce. const nonce = Buffer.alloc(6); - nonce.writeUInt32BE((seconds >> scaler) << scaler); // Time base. - const salt = frame.readUInt16BE(14); // BE? - nonce.writeUInt16BE(salt, 4); // Salt. + nonce.writeUInt32BE((seconds >> scaler) << scaler); // Time base + nonce.writeUInt16BE(salt, 4); // Salt + // Remove the salt. const data = Buffer.alloc(14); - frame.copy(data, 0, 2, 14); - frame.copy(data, 12, 16, 18); + frame.copy(data, 0, 2, 14); // TLM data + frame.copy(data, 12, 16, 18); // MIC (message integrity check) + + const deciphered = this.aes_eax_decrypt(data, this._secret, nonce); + if (deciphered) { + return deciphered; + } else { + return null; + } +}; + +EddystoneBeaconScanner.prototype.aes_ecb_encrypt = function(dataBuffer, keyBuffer) { + const cipher = crypto.createCipheriv("aes-128-ecb", keyBuffer, ''); + cipher.setAutoPadding(false); + return Buffer.concat([cipher.update(dataBuffer), cipher.final()]); +}; + +EddystoneBeaconScanner.prototype.aes_eax_decrypt = function(dataBuffer, keyBuffer, nonceBuffer) { + // Convert the buffers. + const key = CryptoJS.enc.Hex.parse(keyBuffer.toString('hex')); + let cipher = CryptoJS.EAX.create(key, {tagLength: 2}); + const data = CryptoJS.enc.Hex.parse(dataBuffer.toString('hex')); + const nonce = CryptoJS.enc.Hex.parse(nonceBuffer.toString('hex')); - let cipher = CryptoJS.EAX.create(CryptoJS.enc.Hex.parse(this._secret.toString('hex')), {tagLength: 2}); - return cipher.decrypt(CryptoJS.enc.Hex.parse(data.toString('hex')), CryptoJS.enc.Hex.parse(nonce.toString('hex'))); + const deciphered = cipher.decrypt(data, nonce); + if (!deciphered) { + return null; + } else { + return Buffer.from(deciphered.toString(), 'hex'); + } }; EddystoneBeaconScanner.prototype.decipher = function (data) { diff --git a/lib/etlm.spec.js b/lib/etlm.spec.js index 18cce91..ce2c471 100644 --- a/lib/etlm.spec.js +++ b/lib/etlm.spec.js @@ -44,7 +44,7 @@ describe('eTML Frame', () => { const decrypted = scanner.decryptTlm(frame, seconds, scaler); - expect(decrypted.toString()).to.equal("ffeeddccbbaa998877665544"); + expect(decrypted.toString('hex')).to.equal("ffeeddccbbaa998877665544"); done(); });