/* -*- Mode: js; js-indent-level: 2; indent-tabs-mode: nil -*- */ /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ /* Copyright © 2013, Deutsche Telekom, Inc. */ /* globals MozNDEFRecord, NDEF, NfcUtils, NfcBuffer */ /* exported NDEFUtils */ 'use strict'; /******************************************************************************* * NDEFUtils offers a set of utility functions to handle NDEF messages * according to NFCForum-TS-NDEF_1.0. It exports the following functions: * * - parseHandoverNDEF: parse a NDEF message that represents a handover request * or a handover select message * - searchForBluetoothAC: search for a Bluetooth Alternate Carrier in a * handover NDEF message * - parseBluetoothSSP: Parses a Carrier Data Record that contains a * Bluetooth Secure Simple Pairing record * - encodeHandoverRequest: returns a NDEF message that contains a handover * request message * - encodeHandoverSelect: returns a NDEF message that contains a handover * select message */ var NDEFUtils = { DEBUG: false, /***************************************************************************** ***************************************************************************** * Utility functions/classes ***************************************************************************** ****************************************************************************/ /** * Debug method */ _debug: function _debug(msg, optObject) { if (this.DEBUG) { this._logVisibly(msg, optObject); } }, _logVisibly: function _logVisibly(msg, optObject) { var output = '[NDEFUtils]: ' + msg; if (optObject) { output += JSON.stringify(optObject); } console.log(output); }, /** * Parse Handover Request NDEF message. Only 'Hr' and 'Hs' records * are supported (NFCForum-TS-ConnectionHandover_1_2.doc). * * Returns handover object similar to: * { * majorVersion: 1, * minorVersion: 2, * type: 'Hr' // or 'Hs' * cr: 123 // 'Hr' only * ac: [ // Array of BT OOB records * { * cps: 1, * cdr: ... // MozNDEFRecord with details, i.e. * }, // Bluetooth MAC address. * ... * ] * } * * @param {Array} ndefMsg NDEF message (array of MozNDEFRecords) * @return {Object} Parsed handover request or null if ndefMsg * was not valid or well formatted. */ parseHandoverNDEF: function parseHandoverNDEF(ndefMsg) { try { return this._doParseHandoverNDEF(ndefMsg); } catch (err) { this._logVisibly(err); return null; } }, _doParseHandoverNDEF: function doParseHandoverNDEF(msg) { var nfcUtils = new NfcUtils(); var hRecordBuffer = new NfcBuffer(msg[0].payload); var version = hRecordBuffer.getOctet(); var h = { majorVersion: version >>> 4, minorVersion: version & 0x0f, type: nfcUtils.toUTF8(msg[0].type), ac: [] }; if (msg[0].tnf !== NDEF.TNF_WELL_KNOWN) { throw Error('Expected Well Known TNF in Hs/Hr record'); } if (['Hs', 'Hr'].indexOf(h.type) < 0) { throw Error('Record "' + h.type + '" not supported.'); } var hRecord = nfcUtils.parseNDEF(hRecordBuffer); if (!hRecord) { throw Error('Could not parse embedded NDEF in Hr/Hs record'); } if (hRecordBuffer.offset < msg[0].payload.length) { throw Error('Embedded NDEF payload contains extraneous bytes'); } for (var i = 0; i < hRecord.length; i += 1) { var type = nfcUtils.toUTF8(hRecord[i].type); if ('ac' === type) { h.ac.push(this._parseAlternativeCarrier(hRecord[i].payload, msg)); } else if ('cr' === type) { h.cr = this._parseCollisionResolution(hRecord[i].payload); } } // Make sure collision resolution record is present // in Hr message. if ('Hr' === h.type && !h.cr) { throw Error('Collision resolution record missing'); } return h; }, _parseAlternativeCarrier: function _parseAlternativeCarrier(bytes, msg) { var nfcUtils = new NfcUtils(); var b = new NfcBuffer(bytes); var ac = { cps: b.getOctet() & 0x03 }; var recordId = b.getOctetArray(b.getOctet()); ac.cdr = msg.filter(function(record) { return nfcUtils.equalArrays(record.id, recordId); }.bind(this))[0]; if (!ac.cdr) { throw Error('Could not find record with given id'); } return ac; }, _parseCollisionResolution: function _parseCollisionResolution(bytes) { if (bytes.length !== 2) { throw Error('Expected random number in Collision Resolution Record'); } return (bytes[0] << 8) | bytes[1]; }, /** * Returns first record from handover message which * contains Bluetooth OOB (Out of Band) data. * * @param {Array} h Handover message (array of records). * @return {Object} MozNDEFRecord with Bluetooth OOB. */ searchForBluetoothAC: function searchForBluetoothAC(h) { var nfcUtils = new NfcUtils(); for (var i = 0; i < h.ac.length; i++) { var cdr = h.ac[i].cdr; if (cdr.tnf === NDEF.TNF_MIME_MEDIA) { if (nfcUtils.equalArrays(cdr.type, NDEF.MIME_BLUETOOTH_OOB)) { return cdr; } } } return null; }, /** * Parses a Carrier Data Record according to NFCForum-AD-BTSSP_1.0. * This method will parse MAC address. In addition to that, it will * parse, if found in EIR data, Bluetooth Local Name. Other EIR fields * are currently unsupported. * * @param {Object} cdr Carrier Data Record. It's payload field should * contain Bluetooth OOB data. * @returns {Object} Parsed record. Will always contain 'mac' property. * If BT local name was present in cdr, will also have * 'localName' property. Null if cdr was invalid. */ parseBluetoothSSP: function parseBluetoothSSP(cdr) { var nfcUtils = new NfcUtils(); if (!cdr || !cdr.payload || cdr.payload.length < 8) { return null; } var btssp = {}; var buf = new NfcBuffer(cdr.payload); var btsspLen = buf.getOctet() | (buf.getOctet() << 8); if (cdr.payload.length !== btsspLen) { this._debug('Invalid BT SSP record. Length indicated:' + btsspLen + ', actual length: ' + cdr.payload.length); return null; } btssp.mac = this.formatMAC(buf.getOctetArray(6)); while (buf.offset != cdr.payload.length) { // Read OOB value var len = buf.getOctet() - 1 /* 'len' */; var type = buf.getOctet(); if (buf.offset + len > buf.uint8array.length) { this._debug('EIR field ' + type + ' indicated length=' + len + ', but only ' + (buf.uint8array.length - buf.offset) + ' characters left in buffer.'); return null; } switch (type) { case 0x08: case 0x09: // Local name var n = buf.getOctetArray(len); btssp.localName = nfcUtils.toUTF8(n); break; default: // Ignore OOB value buf.skip(len); break; } } return btssp; }, /** * Formats MAC address as a MAC-48 string (colon-separated). * * @param {Array} Array of six numbers representing MAC. * @returns {String} MAC address. */ formatMAC: function formatMAC(mac) { if (!mac || mac.length !== 6) { return null; } var res = []; for (var i = 0; i < 6; i += 1) { var m = mac[i].toString(16); res.unshift(m.length === 1 ? '0' + m : m); } return res.join(':').toUpperCase(); }, /** * Parses Bluetooth MAC-48 address. * * @param {String} MAC address. * @returns {Array} Array of six numbers representing MAC or null * if it was not valid. */ parseMAC: function parseMAC(mac) { if (!mac || !/^([0-9A-F]{2}:){6}$/i.test(mac + ':')) { return null; } var macVals = mac.split(':'); var m = []; for (var i = 5; i >= 0; i -= 1) { m.push(parseInt(macVals[i], 16)); } return m; }, /** * According to [CH] (Connection Handover Technical Specification), * CPS can be one of the following: * - 0 - Inactive * - 1 - Active * - 2 - Activating * - 3 - Unknown * * @params {Integer} cps Carrier Power State * @returns {Boolean} True when cps valid. False otherwise. */ validateCPS: function validateCPS(cps) { var allowedValues = [NDEF.CPS_INACTIVE, NDEF.CPS_ACTIVE, NDEF.CPS_ACTIVATING, NDEF.CPS_UNKNOWN]; return (allowedValues.indexOf(cps) >= 0); }, /** * Returns a Bluetooth Handover Request message. This method DOES * NOT implement a full set of arguments as defined in * NFCForum-AD-BTSSP_1.0.1. * * First record is a Handover Request Record (type="Hr"). It * contains CPS and 2-byte random value used for collision detection * (autogenerated). * * Second record contains Bluetooth OOB data. For explanation, * @see encodeHandoverSelect() and specification. * * @param {String} mac MAC address, ie.: "01:23:45:67:89:AB". * @param {Integer} cps Carrier Power State. * @returns {Array} NDEF records for handover select message. * */ encodeHandoverRequest: function encodeHandoverRequest(mac, cps) { var nfcUtils = new NfcUtils(); var m = this.parseMAC(mac); if (!m) { this._debug('Invalid BT MAC address: ' + mac); return null; } if (!this.validateCPS(cps)) { this._debug('Invalid CPS: ' + cps); return null; } var rndMSB = Math.floor(Math.random() * 0xff) & 0xff; var rndLSB = Math.floor(Math.random() * 0xff) & 0xff; var OOBLength = 2 + m.length; var OOB = [OOBLength, 0].concat(m); // Payload ID var pid = nfcUtils.fromUTF8('0'); var hr = [ new MozNDEFRecord({tnf: NDEF.TNF_WELL_KNOWN, type: NDEF.RTD_HANDOVER_REQUEST, payload: new Uint8Array([0x12, 0x91, 0x02, 0x02, 0x63, 0x72, rndMSB, rndLSB, 0x51, 0x02, 0x04, 0x61, 0x63, cps, 0x01, pid[0], 0x00])}), new MozNDEFRecord({tnf: NDEF.TNF_MIME_MEDIA, type: NDEF.MIME_BLUETOOTH_OOB, id: pid, payload: new Uint8Array(OOB)})]; return hr; }, /** * Returns a Bluetooth Handover Select message. This method * DOES NOT implement the full spec as defined in * NFCForum-AD-BTSSP_1.0.1. In particular, this method supports * only Bluetooth device MAC address and Local Name in the Bluetooth * Carrier Configuration Record. * * First record is Handover Select Record (type="Hs"). It contains, * among other things, the CPS (Carrier Power State). * * Second record of this message contains Bluetooth Out of Band (OOB) data. * It contains following fields: * - [2 octets] OOB Data Length - Mandatory. Total length of OOB data, * including this field itself. * - [6 octets] BT Device Address - Mandatory. BT device address (MAC). * - [N octets] OOB Optional Data - The remaining data in EIR format. * This method supports only device name * here. * * EIR data, as of this implementation, is optional. It will be ommited * if you do not pass btDeviceName parameter. If you do, OOB Optional * Data will be in a form: * - [1 octet] EIR Data Length - Does not include this field. * - [1 octet] EIR Data Type - only 0x09 (BT Local Name) supported. * - [N octets] Contents - BT Local Name * * @param {String} mac MAC address, ie.: "01:23:45:67:89:AB". * @param {Integer} cps Carrier Power State. * @param {UInt8Array} btDeviceName Optional, user-friendly name * of Bluetooth device. * @returns {Array} NDEF records for handover select message. * */ encodeHandoverSelect: function encodeHandoverSelect(mac, cps, btDeviceName) { var nfcUtils = new NfcUtils(); var m = this.parseMAC(mac); if (!m) { this._debug('Invalid BT MAC address: ' + mac); return null; } if (!this.validateCPS(cps)) { this._debug('Invalid CPS: ' + cps); return null; } // OOB Data Length var OOBLength = 2 + m.length; // OOB = [Data Length | BT Device Address] var OOB = [OOBLength, 0].concat(m); // If btDeviceName supplied, attach EIR with it as OOB Optional Data // and update OOB Data Length accordingly. if (btDeviceName) { var EIRLength = 1 + btDeviceName.length; OOB[0] += EIRLength + 1; OOB = OOB.concat(EIRLength, 0x09, Array.apply([], btDeviceName)); } // Payload ID var pid = nfcUtils.fromUTF8('0'); var hs = [new MozNDEFRecord({tnf: NDEF.TNF_WELL_KNOWN, type: NDEF.RTD_HANDOVER_SELECT, payload: new Uint8Array( [0x12, 0xD1, 0x02, 0x04, 0x61, 0x63, cps, 0x01, pid[0], 0x00])}), new MozNDEFRecord({tnf: NDEF.TNF_MIME_MEDIA, type: NDEF.MIME_BLUETOOTH_OOB, id: pid, payload: new Uint8Array(OOB)})]; return hs; }, /** * Returns an empty Handover Select NDEF message (i.e., a Hs message with no * AC). * @returns {Array} NDEF records for an empty handover select message. */ encodeEmptyHandoverSelect: function encodeEmptyHandoverSelect() { var hs = [new MozNDEFRecord({tnf: NDEF.TNF_WELL_KNOWN, type: NDEF.RTD_HANDOVER_SELECT, payload: new Uint8Array([0x12])})]; return hs; } };