Source: js/ndef_utils.js

  1. /* -*- Mode: js; js-indent-level: 2; indent-tabs-mode: nil -*- */
  2. /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
  3. /* Copyright © 2013, Deutsche Telekom, Inc. */
  4. /* globals MozNDEFRecord, NDEF, NfcUtils, NfcBuffer */
  5. /* exported NDEFUtils */
  6. 'use strict';
  7. /*******************************************************************************
  8. * NDEFUtils offers a set of utility functions to handle NDEF messages
  9. * according to NFCForum-TS-NDEF_1.0. It exports the following functions:
  10. *
  11. * - parseHandoverNDEF: parse a NDEF message that represents a handover request
  12. * or a handover select message
  13. * - searchForBluetoothAC: search for a Bluetooth Alternate Carrier in a
  14. * handover NDEF message
  15. * - parseBluetoothSSP: Parses a Carrier Data Record that contains a
  16. * Bluetooth Secure Simple Pairing record
  17. * - encodeHandoverRequest: returns a NDEF message that contains a handover
  18. * request message
  19. * - encodeHandoverSelect: returns a NDEF message that contains a handover
  20. * select message
  21. */
  22. var NDEFUtils = {
  23. DEBUG: false,
  24. /*****************************************************************************
  25. *****************************************************************************
  26. * Utility functions/classes
  27. *****************************************************************************
  28. ****************************************************************************/
  29. /**
  30. * Debug method
  31. */
  32. _debug: function _debug(msg, optObject) {
  33. if (this.DEBUG) {
  34. this._logVisibly(msg, optObject);
  35. }
  36. },
  37. _logVisibly: function _logVisibly(msg, optObject) {
  38. var output = '[NDEFUtils]: ' + msg;
  39. if (optObject) {
  40. output += JSON.stringify(optObject);
  41. }
  42. console.log(output);
  43. },
  44. /**
  45. * Parse Handover Request NDEF message. Only 'Hr' and 'Hs' records
  46. * are supported (NFCForum-TS-ConnectionHandover_1_2.doc).
  47. *
  48. * Returns handover object similar to:
  49. * {
  50. * majorVersion: 1,
  51. * minorVersion: 2,
  52. * type: 'Hr' // or 'Hs'
  53. * cr: 123 // 'Hr' only
  54. * ac: [ // Array of BT OOB records
  55. * {
  56. * cps: 1,
  57. * cdr: ... // MozNDEFRecord with details, i.e.
  58. * }, // Bluetooth MAC address.
  59. * ...
  60. * ]
  61. * }
  62. *
  63. * @param {Array} ndefMsg NDEF message (array of MozNDEFRecords)
  64. * @return {Object} Parsed handover request or null if ndefMsg
  65. * was not valid or well formatted.
  66. */
  67. parseHandoverNDEF: function parseHandoverNDEF(ndefMsg) {
  68. try {
  69. return this._doParseHandoverNDEF(ndefMsg);
  70. } catch (err) {
  71. this._logVisibly(err);
  72. return null;
  73. }
  74. },
  75. _doParseHandoverNDEF: function doParseHandoverNDEF(msg) {
  76. var nfcUtils = new NfcUtils();
  77. var hRecordBuffer = new NfcBuffer(msg[0].payload);
  78. var version = hRecordBuffer.getOctet();
  79. var h = {
  80. majorVersion: version >>> 4,
  81. minorVersion: version & 0x0f,
  82. type: nfcUtils.toUTF8(msg[0].type),
  83. ac: []
  84. };
  85. if (msg[0].tnf !== NDEF.TNF_WELL_KNOWN) {
  86. throw Error('Expected Well Known TNF in Hs/Hr record');
  87. }
  88. if (['Hs', 'Hr'].indexOf(h.type) < 0) {
  89. throw Error('Record "' + h.type + '" not supported.');
  90. }
  91. var hRecord = nfcUtils.parseNDEF(hRecordBuffer);
  92. if (!hRecord) {
  93. throw Error('Could not parse embedded NDEF in Hr/Hs record');
  94. }
  95. if (hRecordBuffer.offset < msg[0].payload.length) {
  96. throw Error('Embedded NDEF payload contains extraneous bytes');
  97. }
  98. for (var i = 0; i < hRecord.length; i += 1) {
  99. var type = nfcUtils.toUTF8(hRecord[i].type);
  100. if ('ac' === type) {
  101. h.ac.push(this._parseAlternativeCarrier(hRecord[i].payload, msg));
  102. } else if ('cr' === type) {
  103. h.cr = this._parseCollisionResolution(hRecord[i].payload);
  104. }
  105. }
  106. // Make sure collision resolution record is present
  107. // in Hr message.
  108. if ('Hr' === h.type && !h.cr) {
  109. throw Error('Collision resolution record missing');
  110. }
  111. return h;
  112. },
  113. _parseAlternativeCarrier: function _parseAlternativeCarrier(bytes, msg) {
  114. var nfcUtils = new NfcUtils();
  115. var b = new NfcBuffer(bytes);
  116. var ac = {
  117. cps: b.getOctet() & 0x03
  118. };
  119. var recordId = b.getOctetArray(b.getOctet());
  120. ac.cdr = msg.filter(function(record) {
  121. return nfcUtils.equalArrays(record.id, recordId);
  122. }.bind(this))[0];
  123. if (!ac.cdr) {
  124. throw Error('Could not find record with given id');
  125. }
  126. return ac;
  127. },
  128. _parseCollisionResolution: function _parseCollisionResolution(bytes) {
  129. if (bytes.length !== 2) {
  130. throw Error('Expected random number in Collision Resolution Record');
  131. }
  132. return (bytes[0] << 8) | bytes[1];
  133. },
  134. /**
  135. * Returns first record from handover message which
  136. * contains Bluetooth OOB (Out of Band) data.
  137. *
  138. * @param {Array} h Handover message (array of records).
  139. * @return {Object} MozNDEFRecord with Bluetooth OOB.
  140. */
  141. searchForBluetoothAC: function searchForBluetoothAC(h) {
  142. var nfcUtils = new NfcUtils();
  143. for (var i = 0; i < h.ac.length; i++) {
  144. var cdr = h.ac[i].cdr;
  145. if (cdr.tnf === NDEF.TNF_MIME_MEDIA) {
  146. if (nfcUtils.equalArrays(cdr.type, NDEF.MIME_BLUETOOTH_OOB)) {
  147. return cdr;
  148. }
  149. }
  150. }
  151. return null;
  152. },
  153. /**
  154. * Parses a Carrier Data Record according to NFCForum-AD-BTSSP_1.0.
  155. * This method will parse MAC address. In addition to that, it will
  156. * parse, if found in EIR data, Bluetooth Local Name. Other EIR fields
  157. * are currently unsupported.
  158. *
  159. * @param {Object} cdr Carrier Data Record. It's payload field should
  160. * contain Bluetooth OOB data.
  161. * @returns {Object} Parsed record. Will always contain 'mac' property.
  162. * If BT local name was present in cdr, will also have
  163. * 'localName' property. Null if cdr was invalid.
  164. */
  165. parseBluetoothSSP: function parseBluetoothSSP(cdr) {
  166. var nfcUtils = new NfcUtils();
  167. if (!cdr || !cdr.payload || cdr.payload.length < 8) {
  168. return null;
  169. }
  170. var btssp = {};
  171. var buf = new NfcBuffer(cdr.payload);
  172. var btsspLen = buf.getOctet() | (buf.getOctet() << 8);
  173. if (cdr.payload.length !== btsspLen) {
  174. this._debug('Invalid BT SSP record. Length indicated:' +
  175. btsspLen + ', actual length: ' + cdr.payload.length);
  176. return null;
  177. }
  178. btssp.mac = this.formatMAC(buf.getOctetArray(6));
  179. while (buf.offset != cdr.payload.length) {
  180. // Read OOB value
  181. var len = buf.getOctet() - 1 /* 'len' */;
  182. var type = buf.getOctet();
  183. if (buf.offset + len > buf.uint8array.length) {
  184. this._debug('EIR field ' + type + ' indicated length=' +
  185. len + ', but only ' + (buf.uint8array.length - buf.offset) +
  186. ' characters left in buffer.');
  187. return null;
  188. }
  189. switch (type) {
  190. case 0x08:
  191. case 0x09:
  192. // Local name
  193. var n = buf.getOctetArray(len);
  194. btssp.localName = nfcUtils.toUTF8(n);
  195. break;
  196. default:
  197. // Ignore OOB value
  198. buf.skip(len);
  199. break;
  200. }
  201. }
  202. return btssp;
  203. },
  204. /**
  205. * Formats MAC address as a MAC-48 string (colon-separated).
  206. *
  207. * @param {Array} Array of six numbers representing MAC.
  208. * @returns {String} MAC address.
  209. */
  210. formatMAC: function formatMAC(mac) {
  211. if (!mac || mac.length !== 6) {
  212. return null;
  213. }
  214. var res = [];
  215. for (var i = 0; i < 6; i += 1) {
  216. var m = mac[i].toString(16);
  217. res.unshift(m.length === 1 ? '0' + m : m);
  218. }
  219. return res.join(':').toUpperCase();
  220. },
  221. /**
  222. * Parses Bluetooth MAC-48 address.
  223. *
  224. * @param {String} MAC address.
  225. * @returns {Array} Array of six numbers representing MAC or null
  226. * if it was not valid.
  227. */
  228. parseMAC: function parseMAC(mac) {
  229. if (!mac || !/^([0-9A-F]{2}:){6}$/i.test(mac + ':')) {
  230. return null;
  231. }
  232. var macVals = mac.split(':');
  233. var m = [];
  234. for (var i = 5; i >= 0; i -= 1) {
  235. m.push(parseInt(macVals[i], 16));
  236. }
  237. return m;
  238. },
  239. /**
  240. * According to [CH] (Connection Handover Technical Specification),
  241. * CPS can be one of the following:
  242. * - 0 - Inactive
  243. * - 1 - Active
  244. * - 2 - Activating
  245. * - 3 - Unknown
  246. *
  247. * @params {Integer} cps Carrier Power State
  248. * @returns {Boolean} True when cps valid. False otherwise.
  249. */
  250. validateCPS: function validateCPS(cps) {
  251. var allowedValues = [NDEF.CPS_INACTIVE, NDEF.CPS_ACTIVE,
  252. NDEF.CPS_ACTIVATING, NDEF.CPS_UNKNOWN];
  253. return (allowedValues.indexOf(cps) >= 0);
  254. },
  255. /**
  256. * Returns a Bluetooth Handover Request message. This method DOES
  257. * NOT implement a full set of arguments as defined in
  258. * NFCForum-AD-BTSSP_1.0.1.
  259. *
  260. * First record is a Handover Request Record (type="Hr"). It
  261. * contains CPS and 2-byte random value used for collision detection
  262. * (autogenerated).
  263. *
  264. * Second record contains Bluetooth OOB data. For explanation,
  265. * @see encodeHandoverSelect() and specification.
  266. *
  267. * @param {String} mac MAC address, ie.: "01:23:45:67:89:AB".
  268. * @param {Integer} cps Carrier Power State.
  269. * @returns {Array} NDEF records for handover select message.
  270. *
  271. */
  272. encodeHandoverRequest: function encodeHandoverRequest(mac, cps) {
  273. var nfcUtils = new NfcUtils();
  274. var m = this.parseMAC(mac);
  275. if (!m) {
  276. this._debug('Invalid BT MAC address: ' + mac);
  277. return null;
  278. }
  279. if (!this.validateCPS(cps)) {
  280. this._debug('Invalid CPS: ' + cps);
  281. return null;
  282. }
  283. var rndMSB = Math.floor(Math.random() * 0xff) & 0xff;
  284. var rndLSB = Math.floor(Math.random() * 0xff) & 0xff;
  285. var OOBLength = 2 + m.length;
  286. var OOB = [OOBLength, 0].concat(m);
  287. // Payload ID
  288. var pid = nfcUtils.fromUTF8('0');
  289. var hr = [
  290. new MozNDEFRecord({tnf: NDEF.TNF_WELL_KNOWN,
  291. type: NDEF.RTD_HANDOVER_REQUEST,
  292. payload: new Uint8Array([0x12, 0x91, 0x02, 0x02, 0x63,
  293. 0x72, rndMSB, rndLSB, 0x51, 0x02, 0x04, 0x61, 0x63,
  294. cps, 0x01, pid[0], 0x00])}),
  295. new MozNDEFRecord({tnf: NDEF.TNF_MIME_MEDIA,
  296. type: NDEF.MIME_BLUETOOTH_OOB,
  297. id: pid,
  298. payload: new Uint8Array(OOB)})];
  299. return hr;
  300. },
  301. /**
  302. * Returns a Bluetooth Handover Select message. This method
  303. * DOES NOT implement the full spec as defined in
  304. * NFCForum-AD-BTSSP_1.0.1. In particular, this method supports
  305. * only Bluetooth device MAC address and Local Name in the Bluetooth
  306. * Carrier Configuration Record.
  307. *
  308. * First record is Handover Select Record (type="Hs"). It contains,
  309. * among other things, the CPS (Carrier Power State).
  310. *
  311. * Second record of this message contains Bluetooth Out of Band (OOB) data.
  312. * It contains following fields:
  313. * - [2 octets] OOB Data Length - Mandatory. Total length of OOB data,
  314. * including this field itself.
  315. * - [6 octets] BT Device Address - Mandatory. BT device address (MAC).
  316. * - [N octets] OOB Optional Data - The remaining data in EIR format.
  317. * This method supports only device name
  318. * here.
  319. *
  320. * EIR data, as of this implementation, is optional. It will be ommited
  321. * if you do not pass btDeviceName parameter. If you do, OOB Optional
  322. * Data will be in a form:
  323. * - [1 octet] EIR Data Length - Does not include this field.
  324. * - [1 octet] EIR Data Type - only 0x09 (BT Local Name) supported.
  325. * - [N octets] Contents - BT Local Name
  326. *
  327. * @param {String} mac MAC address, ie.: "01:23:45:67:89:AB".
  328. * @param {Integer} cps Carrier Power State.
  329. * @param {UInt8Array} btDeviceName Optional, user-friendly name
  330. * of Bluetooth device.
  331. * @returns {Array} NDEF records for handover select message.
  332. *
  333. */
  334. encodeHandoverSelect: function encodeHandoverSelect(mac, cps, btDeviceName) {
  335. var nfcUtils = new NfcUtils();
  336. var m = this.parseMAC(mac);
  337. if (!m) {
  338. this._debug('Invalid BT MAC address: ' + mac);
  339. return null;
  340. }
  341. if (!this.validateCPS(cps)) {
  342. this._debug('Invalid CPS: ' + cps);
  343. return null;
  344. }
  345. // OOB Data Length
  346. var OOBLength = 2 + m.length;
  347. // OOB = [Data Length | BT Device Address]
  348. var OOB = [OOBLength, 0].concat(m);
  349. // If btDeviceName supplied, attach EIR with it as OOB Optional Data
  350. // and update OOB Data Length accordingly.
  351. if (btDeviceName) {
  352. var EIRLength = 1 + btDeviceName.length;
  353. OOB[0] += EIRLength + 1;
  354. OOB = OOB.concat(EIRLength, 0x09, Array.apply([], btDeviceName));
  355. }
  356. // Payload ID
  357. var pid = nfcUtils.fromUTF8('0');
  358. var hs = [new MozNDEFRecord({tnf: NDEF.TNF_WELL_KNOWN,
  359. type: NDEF.RTD_HANDOVER_SELECT,
  360. payload: new Uint8Array(
  361. [0x12, 0xD1, 0x02, 0x04, 0x61, 0x63, cps,
  362. 0x01, pid[0], 0x00])}),
  363. new MozNDEFRecord({tnf: NDEF.TNF_MIME_MEDIA,
  364. type: NDEF.MIME_BLUETOOTH_OOB,
  365. id: pid,
  366. payload: new Uint8Array(OOB)})];
  367. return hs;
  368. },
  369. /**
  370. * Returns an empty Handover Select NDEF message (i.e., a Hs message with no
  371. * AC).
  372. * @returns {Array} NDEF records for an empty handover select message.
  373. */
  374. encodeEmptyHandoverSelect: function encodeEmptyHandoverSelect() {
  375. var hs = [new MozNDEFRecord({tnf: NDEF.TNF_WELL_KNOWN,
  376. type: NDEF.RTD_HANDOVER_SELECT,
  377. payload: new Uint8Array([0x12])})];
  378. return hs;
  379. }
  380. };