/* -*- 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;
}
};