├── sample ├── images │ ├── nfcjs.ico │ ├── nfcjs.jpeg │ ├── acr122u.png │ └── scl3711.png ├── screenshots │ └── 1040x811.png ├── background.js ├── manifest.json ├── app.css ├── app.html ├── app.js ├── lib │ └── bootstrap.min.js └── chrome-nfc.js ├── LICENSE ├── src ├── tag.js ├── b64.js ├── util.js ├── usb.js ├── sha256.js ├── devmanager.js ├── nfc.js ├── tt2.js ├── ndef.js ├── mifare-classic.js └── scl3711.js ├── compile.py ├── CONTRIBUTING.md └── README.md /sample/images/nfcjs.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justinribeiro/chrome-nfc/HEAD/sample/images/nfcjs.ico -------------------------------------------------------------------------------- /sample/images/nfcjs.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justinribeiro/chrome-nfc/HEAD/sample/images/nfcjs.jpeg -------------------------------------------------------------------------------- /sample/images/acr122u.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justinribeiro/chrome-nfc/HEAD/sample/images/acr122u.png -------------------------------------------------------------------------------- /sample/images/scl3711.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justinribeiro/chrome-nfc/HEAD/sample/images/scl3711.png -------------------------------------------------------------------------------- /sample/screenshots/1040x811.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justinribeiro/chrome-nfc/HEAD/sample/screenshots/1040x811.png -------------------------------------------------------------------------------- /sample/background.js: -------------------------------------------------------------------------------- 1 | chrome.app.runtime.onLaunched.addListener(function() { 2 | chrome.app.window.create('app.html', { 3 | 'id': 'appWindow', 4 | 'bounds': { 'width': 1024, 'height': 768 }}); 5 | }); 6 | -------------------------------------------------------------------------------- /sample/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "NFC Sample", 3 | "version": "0.1", 4 | "description": "chrome.nfc API sample", 5 | "manifest_version": 2, 6 | 7 | "permissions": [ 8 | "usb", 9 | { 10 | "usbDevices": [ 11 | { "vendorId": 1254, "productId": 21905 }, // SCL3711 12 | { "vendorId": 1839, "productId": 8704 } // ACR122U 13 | ] 14 | } 15 | ], 16 | 17 | "app": { 18 | "background": { 19 | "scripts": [ "background.js" ] 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2014 Google Inc. All rights reserved. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /src/tag.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | /** 17 | * @fileoverview Tag() class 18 | */ 19 | 20 | 21 | /* 22 | * Unfortunately, some tags, such as Ultralight-*, require async read 23 | * to distiguish the tag type. The 'cb' will be called if the order matters. 24 | */ 25 | function Tag(tag_name, tag_id) { 26 | switch (tag_name) { 27 | case "tt2": 28 | return new TT2(tag_id); 29 | 30 | case "mifare_classic": 31 | return new MifareClassic(tag_id); 32 | } 33 | 34 | return null; 35 | } 36 | 37 | -------------------------------------------------------------------------------- /sample/app.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | } 4 | 5 | .wrapper { 6 | margin: 0px 20px 0 20px; 7 | } 8 | 9 | textarea { 10 | display: block; 11 | width: 660px; 12 | height: 220px; 13 | font-family: "Courier New", monospace; 14 | } 15 | 16 | .hidden { 17 | display: none 18 | } 19 | 20 | dd { 21 | margin-bottom: 10px; 22 | } 23 | 24 | #device-thumbnail { 25 | position: fixed; 26 | } 27 | 28 | #device-info { 29 | margin-left: 150px; 30 | } 31 | 32 | .tab-content { 33 | margin-bottom: 10px; 34 | } 35 | 36 | pre { 37 | -webkit-user-select: text; 38 | cursor: text; 39 | } 40 | 41 | .tab-content { 42 | overflow: initial; 43 | } 44 | 45 | #logContainer { 46 | height: 200px; 47 | width: 100%; 48 | position: fixed; 49 | bottom: 0; 50 | background: #F5F5F5; 51 | transition: height .2s ease-in-out; 52 | } 53 | 54 | .logs { 55 | overflow-y: auto; 56 | height: 190px; 57 | } 58 | 59 | .logs pre { 60 | margin: 0 0 1px; 61 | border: none; 62 | border-bottom: 1px dotted #DDD; 63 | border-radius: 0px; 64 | padding: 5px; 65 | } 66 | 67 | .drawer { 68 | height: 10px; 69 | background: #DDD; 70 | cursor: pointer; 71 | } 72 | 73 | .small { 74 | height: 10px !important; 75 | } 76 | -------------------------------------------------------------------------------- /compile.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import json 4 | import os 5 | import sys 6 | import urllib.parse 7 | import urllib.request 8 | 9 | BASE_DIR = os.path.dirname(sys.argv[0]) 10 | CLOSURE_URL = 'http://closure-compiler.appspot.com/compile' 11 | TARGET_JS = os.path.join(BASE_DIR, 'sample', 'chrome-nfc.js') 12 | 13 | def print_errors(errors, js_files): 14 | for error in errors: 15 | if error['file'].lower().find('externs') >= 0: 16 | filename = error['file'] 17 | else: 18 | fileno = int(error['file'][6:]) 19 | filename = js_files[fileno] 20 | if 'error' in error: 21 | text = error['error'] 22 | else: 23 | text = error['warning'] 24 | print(filename + ':' + str(error['lineno']) + ' ' + text) 25 | print(error['line']) 26 | print() 27 | 28 | 29 | JS_FILES = [ 30 | 'src/b64.js', 31 | 'src/mifare-classic.js', 32 | 'src/ndef.js', 33 | 'src/nfc.js', 34 | 'src/devmanager.js', 35 | 'src/scl3711.js', 36 | 'src/sha256.js', 37 | 'src/tag.js', 38 | 'src/tt2.js', 39 | 'src/usb.js', 40 | 'src/util.js' 41 | ] 42 | 43 | def main(): 44 | print('Compiling JavaScript code.') 45 | 46 | params = [ 47 | ('compilation_level', 'WHITESPACE_ONLY'), 48 | ('formatting', 'pretty_print'), 49 | ('language', 'ECMASCRIPT5'), 50 | ('output_format', 'json'), 51 | ('output_info', 'statistics'), 52 | ('output_info', 'warnings'), 53 | ('output_info', 'errors'), 54 | ('output_info', 'compiled_code') 55 | ] 56 | 57 | for js_file in JS_FILES: 58 | params.append(('js_code', open(os.path.join(BASE_DIR, js_file)).read())) 59 | 60 | params = bytes(urllib.parse.urlencode(params, encoding='utf8'), 'utf8') 61 | headers = {'Content-Type': 'application/x-www-form-urlencoded'} 62 | 63 | print('Connecting', CLOSURE_URL) 64 | out = urllib.request.urlopen(CLOSURE_URL, data=params) 65 | result = json.loads(out.read().decode('utf8')) 66 | 67 | if 'errors' in result and len(result['errors']): 68 | print('Errors:') 69 | print_errors(result['errors'], JS_FILES) 70 | 71 | if 'warnings' in result and len(result['warnings']): 72 | print('Warnings:') 73 | print_errors(result['warnings'], JS_FILES) 74 | 75 | print('Writing', TARGET_JS) 76 | open(TARGET_JS, 'w').write(result['compiledCode']) 77 | 78 | if __name__ == '__main__': 79 | main() 80 | -------------------------------------------------------------------------------- /src/b64.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // WebSafeBase64Escape and Unescape. 18 | 19 | function B64_encode(bytes, opt_length) { 20 | if (!opt_length) opt_length = bytes.length; 21 | var b64out = 22 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; 23 | var result = ""; 24 | var shift = 0; 25 | var accu = 0; 26 | var input_index = 0; 27 | while (opt_length--) { 28 | accu <<= 8; 29 | accu |= bytes[input_index++]; 30 | shift += 8; 31 | while (shift >= 6) { 32 | var i = (accu >> (shift - 6)) & 63; 33 | result += b64out.charAt(i); 34 | shift -= 6; 35 | } 36 | } 37 | if (shift) { 38 | accu <<= 8; 39 | shift += 8; 40 | var i = (accu >> (shift - 6)) & 63; 41 | result += b64out.charAt(i); 42 | } 43 | return result; 44 | }; 45 | 46 | // Normal base64 encode; not websafe, including padding. 47 | function base64_encode(bytes, opt_length) { 48 | if (!opt_length) opt_length = bytes.length; 49 | var b64out = 50 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; 51 | var result = ""; 52 | var shift = 0; 53 | var accu = 0; 54 | var input_index = 0; 55 | while (opt_length--) { 56 | accu <<= 8; 57 | accu |= bytes[input_index++]; 58 | shift += 8; 59 | while (shift >= 6) { 60 | var i = (accu >> (shift - 6)) & 63; 61 | result += b64out.charAt(i); 62 | shift -= 6; 63 | } 64 | } 65 | if (shift) { 66 | accu <<= 8; 67 | shift += 8; 68 | var i = (accu >> (shift - 6)) & 63; 69 | result += b64out.charAt(i); 70 | } 71 | while (result.length % 4) result += '='; 72 | return result; 73 | }; 74 | 75 | var B64_inmap = 76 | [ 77 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 63, 0, 0, 78 | 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 0, 0, 0, 0, 0, 0, 79 | 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 80 | 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 0, 0, 0, 0, 64, 81 | 0, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 82 | 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 0, 0, 0, 0, 0 83 | ]; 84 | 85 | function B64_decode(string) { 86 | var bytes = []; 87 | var accu = 0; 88 | var shift = 0; 89 | for (var i = 0; i < string.length; ++i) { 90 | var c = string.charCodeAt(i); 91 | if (c < 32 || c > 127 || !B64_inmap[c - 32]) return []; 92 | accu <<= 6; 93 | accu |= (B64_inmap[c - 32] - 1); 94 | shift += 6; 95 | if (shift >= 8) { 96 | bytes.push((accu >> (shift - 8)) & 255); 97 | shift -= 8; 98 | } 99 | } 100 | return bytes; 101 | }; 102 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute # 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | a just a few small guidelines you need to follow. 5 | 6 | 7 | ## Contributor License Agreement ## 8 | 9 | Contributions to any Google project must be accompanied by a Contributor 10 | License Agreement. This is not a copyright **assignment**, it simply gives 11 | Google permission to use and redistribute your contributions as part of the 12 | project. 13 | 14 | * If you are an individual writing original source code and you're sure you 15 | own the intellectual property, then you'll need to sign an [individual 16 | CLA][]. 17 | 18 | * If you work for a company that wants to allow you to contribute your work, 19 | then you'll need to sign a [corporate CLA][]. 20 | 21 | You generally only need to submit a CLA once, so if you've already submitted 22 | one (even if it was for a different project), you probably don't need to do it 23 | again. 24 | 25 | [individual CLA]: https://developers.google.com/open-source/cla/individual 26 | [corporate CLA]: https://developers.google.com/open-source/cla/corporate 27 | 28 | 29 | ## Submitting a patch ## 30 | 31 | 1. It's generally best to start by opening a new issue describing the bug or 32 | feature you're intending to fix. Even if you think it's relatively minor, 33 | it's helpful to know what people are working on. Mention in the initial 34 | issue that you are planning to work on that bug or feature so that it can 35 | be assigned to you. 36 | 37 | 1. Follow the normal process of [forking][] the project, and setup a new 38 | branch to work in. It's important that each group of changes be done in 39 | separate branches in order to ensure that a pull request only includes the 40 | commits related to that bug or feature. 41 | 42 | 1. Any significant changes should almost always be accompanied by tests. The 43 | project already has good test coverage, so look at some of the existing 44 | tests if you're unsure how to go about it. 45 | 46 | 1. Do your best to have [well-formed commit messages][] for each change. 47 | This provides consistency throughout the project, and ensures that commit 48 | messages are able to be formatted properly by various git tools. 49 | 50 | 1. Finally, push the commits to your fork and submit a [pull request][]. 51 | 52 | [forking]: https://help.github.com/articles/fork-a-repo 53 | [well-formed commit messages]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html 54 | [squash]: http://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits 55 | [pull request]: https://help.github.com/articles/creating-a-pull-request 56 | 57 | 58 | ## Other notes on code organization ## 59 | 60 | Currently, everything is defined in the main `github` package, with API methods 61 | broken into separate service objects. These services map directly to how 62 | the [GitHub API documentation][] is organized, so use that as your guide for 63 | where to put new methods. 64 | 65 | Sub-service (e.g. [Repo Hooks][]) implementations are split into separate files 66 | based on the APIs they provide. These files are named service_api.go (e.g. 67 | repos_hooks.go) to describe the API to service mappings. 68 | 69 | [GitHub API documentation]: http://developer.github.com/v3/ 70 | [Repo Hooks]: http://developer.github.com/v3/repos/hooks/ 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chrome App NFC Library 2 | 3 | With this simple library, you can build a [Chrome App](https://developer.chrome.com/apps) that communicates over USB with NFC Readers. 4 | 5 | ## Supported NFC Readers 6 | 7 | [ACR122U](http://www.acs.com.hk/en/products/3/acr122u-usb-nfc-reader) | [SCL3711](http://www.identive-group.com/products-and-solutions/identification-products/mobility-solutions/mobile-readers/scl3711-contactless-usb-smart-card-reader) 8 | --- | --- 9 | | 10 | 11 | ## Play with the Chrome App sample 12 | 13 | * Check `Developer Mode` in `chrome://extensions` 14 | * Click "Load unpacked extension..." in `chrome://extensions` and select the [sample](/sample) folder. 15 | * Launch it. 16 | 17 | 18 | 19 | ## Caveats 20 | 21 | Learn more about USB Devices Caveats at https://developer.chrome.com/apps/app_usb#caveats 22 | 23 | ## Usage 24 | 25 | Once you've imported the [chrome-nfc.js](https:///raw.github.com/GoogleChrome/chrome-nfc/master/sample/chrome-nfc.js) javascript library into your Chrome App, you need to add the permissions below to your manifest file: 26 | 27 | ```javascript 28 | "permissions": [ 29 | "usb", 30 | { 31 | "usbDevices": [ 32 | { "vendorId": 1254, "productId": 21905 }, // SCL3711 33 | { "vendorId": 1839, "productId": 8704 } // ACR122U 34 | ] 35 | } 36 | ] 37 | ``` 38 | 39 | ### Enumerate NFC readers 40 | 41 | ``` javascript 42 | chrome.nfc.findDevices(function(devices) { 43 | console.log("Found " + devices.length + " NFC device(s)"); 44 | for (var i = 0; i < devices.length; i++) { 45 | var device = devices[i]; 46 | console.log(device.vendorId, device.productId); 47 | } 48 | }); 49 | ``` 50 | 51 | ### Read NFC tag 52 | 53 | ``` javascript 54 | chrome.nfc.findDevices(function(devices) { 55 | var device = devices[0]; 56 | chrome.nfc.read(device, {}, function(type, ndef) { 57 | console.lof(ndef); 58 | var uri = ndef.ndef[0]["uri"]; 59 | console.log(uri); 60 | var text = ndef.ndef[1]["text"]; 61 | console.log(text); 62 | }); 63 | }); 64 | ``` 65 | 66 | ### Write NFC tag 67 | 68 | ``` javascript 69 | chrome.nfc.findDevices(function(devices) { 70 | var device = devices[0]; 71 | var ndef = [ 72 | {"text": "Chromium.org website" }, 73 | {"uri": "http://chromium.org" }, 74 | {"aar": "com.google.samples.apps.iosched" }, 75 | ]; 76 | chrome.nfc.write(device, {"ndef": ndef}, function(rc) { 77 | if (!rc) { 78 | console.log("WRITE() success!"); 79 | } else { 80 | console.log("WRITE() FAILED, rc = " + rc); 81 | } 82 | }); 83 | }); 84 | ``` 85 | 86 | ### Emulate NFC tag 87 | 88 | ``` javascript 89 | chrome.nfc.findDevices(function(devices) { 90 | var device = devices[0]; 91 | var ndef = [ 92 | {"type": "URI", "uri": "http://chromium.org"} 93 | ]; 94 | chrome.nfc.emulate_tag(device, {"ndef": ndef}, function(rc) { 95 | if (!rc) { 96 | console.log("EMULATE() success!"); 97 | } else { 98 | console.log("EMULATE() FAILED, rc = " + rc); 99 | } 100 | }); 101 | }); 102 | ``` 103 | 104 | ### Read Mifare Classic tag (Logic Mode) 105 | 106 | ``` javascript 107 | chrome.nfc.findDevices(function(devices) { 108 | var device = devices[0]; 109 | chrome.nfc.read_logic(device, 0, 2, function(rc, data) { 110 | console.log(UTIL_BytesToHex(data)); 111 | }); 112 | }); 113 | ``` 114 | 115 | ### Write Mifare Classic tag (Logic Mode) 116 | 117 | ``` javascript 118 | chrome.nfc.findDevices(function(devices) { 119 | var device = devices[0]; 120 | var data = new Uint8Array([ // NDEF(http://google.com) 121 | 0xdb, 0x00, 0x03, 0xe1, 0x00, 0x00, 0x00, 0x00, // block 0 (MAD1) 122 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 123 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // block 1 (MAD1) 124 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 125 | 0x03, 0x0f, 0xd1, 0x01, 0x0b, 0x55, 0x03, 0x67, // block 2 (NDEF) 126 | 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 127 | 0x6d, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // block 3 (NDEF) 128 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 129 | ]); 130 | chrome.nfc.write_logic(device, 0, data, function(rc) { 131 | console.log("WRITE_LOGIC() SUCCESS"); 132 | }); 133 | }); 134 | ``` 135 | 136 | ## Compiling the library 137 | 138 | Compiling script requires [Python 3.0](http://www.python.org/download/releases/3.0/) and will use online [Closure Compiler](https://developers.google.com/closure/). Just run 139 | 140 | python3 compile.py 141 | 142 | and the library will be written to `chrome-nfc.js`. 143 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | function UTIL_StringToBytes(s, bytes) { 20 | bytes = bytes || new Array(s.length); 21 | for (var i = 0; i < s.length; ++i) 22 | bytes[i] = s.charCodeAt(i); 23 | return bytes; 24 | } 25 | 26 | function UTIL_BytesToString(b) { 27 | var tmp = new String(); 28 | for (var i = 0; i < b.length; ++i) 29 | tmp += String.fromCharCode(b[i]); 30 | return tmp; 31 | } 32 | 33 | function UTIL_BytesToHex(b) { 34 | if (!b) return '(null)'; 35 | var hexchars = '0123456789ABCDEF'; 36 | var hexrep = new Array(b.length * 2); 37 | 38 | for (var i = 0; i < b.length; ++i) { 39 | hexrep[i * 2 + 0] = hexchars.charAt((b[i] >> 4) & 15); 40 | hexrep[i * 2 + 1] = hexchars.charAt(b[i] & 15); 41 | } 42 | return hexrep.join(''); 43 | } 44 | 45 | function UTIL_BytesToHexWithSeparator(b, sep) { 46 | var hexchars = '0123456789ABCDEF'; 47 | var stride = 2 + (sep?1:0); 48 | var hexrep = new Array(b.length * stride); 49 | 50 | for (var i = 0; i < b.length; ++i) { 51 | if (sep) hexrep[i * stride + 0] = sep; 52 | hexrep[i * stride + stride - 2] = hexchars.charAt((b[i] >> 4) & 15); 53 | hexrep[i * stride + stride - 1] = hexchars.charAt(b[i] & 15); 54 | } 55 | return (sep?hexrep.slice(1):hexrep).join(''); 56 | } 57 | 58 | function UTIL_HexToBytes(h) { 59 | var hexchars = '0123456789ABCDEFabcdef'; 60 | var res = new Uint8Array(h.length / 2); 61 | for (var i = 0; i < h.length; i += 2) { 62 | if (hexchars.indexOf(h.substring(i, i + 1)) == -1) break; 63 | res[i / 2] = parseInt(h.substring(i, i + 2), 16); 64 | } 65 | return res; 66 | } 67 | 68 | function UTIL_equalArrays(a, b) { 69 | if (!a || !b) return false; 70 | if (a.length != b.length) return false; 71 | var accu = 0; 72 | for (var i = 0; i < a.length; ++i) 73 | accu |= a[i] ^ b[i]; 74 | return accu === 0; 75 | } 76 | 77 | function UTIL_ltArrays(a, b) { 78 | if (a.length < b.length) return true; 79 | if (a.length > b.length) return false; 80 | for (var i = 0; i < a.length; ++i) { 81 | if (a[i] < b[i]) return true; 82 | if (a[i] > b[i]) return false; 83 | } 84 | return false; 85 | } 86 | 87 | function UTIL_geArrays(a, b) { 88 | return !UTIL_ltArrays(a, b); 89 | } 90 | 91 | function UTIL_getRandom(a) { 92 | var tmp = new Array(a); 93 | var rnd = new Uint8Array(a); 94 | window.crypto.getRandomValues(rnd); // Yay! 95 | for (var i = 0; i < a; ++i) tmp[i] = rnd[i] & 255; 96 | return tmp; 97 | } 98 | 99 | function UTIL_equalArrays(a, b) { 100 | if (!a || !b) return false; 101 | if (a.length != b.length) return false; 102 | var accu = 0; 103 | for (var i = 0; i < a.length; ++i) 104 | accu |= a[i] ^ b[i]; 105 | return accu === 0; 106 | } 107 | 108 | function UTIL_setFavicon(icon) { 109 | // Construct a new favion link tag 110 | var faviconLink = document.createElement("link"); 111 | faviconLink.rel = "Shortcut Icon"; 112 | faviconLink.type = 'image/x-icon'; 113 | faviconLink.href = icon; 114 | 115 | // Remove the old favion, if it exists 116 | var head = document.getElementsByTagName("head")[0]; 117 | var links = head.getElementsByTagName("link"); 118 | for (var i=0; i < links.length; i++) { 119 | var link = links[i]; 120 | if (link.type == faviconLink.type && link.rel == faviconLink.rel) { 121 | head.removeChild(link); 122 | } 123 | } 124 | 125 | // Add in the new one 126 | head.appendChild(faviconLink); 127 | } 128 | 129 | // Erase all entries in array 130 | function UTIL_clear(a) { 131 | if (a instanceof Array) { 132 | for (var i = 0; i < a.length; ++i) 133 | a[i] = 0; 134 | } 135 | } 136 | 137 | // hr:min:sec.milli string 138 | function UTIL_time() { 139 | var d = new Date(); 140 | var m = '000' + d.getMilliseconds(); 141 | var s = d.toTimeString().substring(0, 8) + '.' + m.substring(m.length - 3); 142 | return s; 143 | } 144 | 145 | function UTIL_fmt(s) { 146 | return UTIL_time() + ' ' + s; 147 | } 148 | 149 | // a and b are Uint8Array. Returns Uint8Array. 150 | function UTIL_concat(a, b) { 151 | var c = new Uint8Array(a.length + b.length); 152 | var i, n = 0; 153 | for (i = 0; i < a.length; i++, n++) c[n] = a[i]; 154 | for (i = 0; i < b.length; i++, n++) c[n] = b[i]; 155 | return c; 156 | } 157 | 158 | -------------------------------------------------------------------------------- /src/usb.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | /** 17 | * @fileoverview Low level usb cruft to SCL3711 NFC token. 18 | */ 19 | 20 | 'use strict'; 21 | 22 | // Low level 'driver'. One per physical USB device. 23 | function llSCL3711(dev, acr122) { 24 | this.dev = dev; 25 | this.txqueue = []; 26 | this.clients = []; 27 | this.acr122 = acr122; 28 | if (acr122) { 29 | this.endpoint = 2; 30 | } else { 31 | // scl3711 32 | this.endpoint = 4; 33 | } 34 | 35 | this.readLoop(); 36 | } 37 | 38 | llSCL3711.prototype.notifyClientOfClosure = function(client) { 39 | var cb = client.onclose; 40 | if (cb) window.setTimeout(cb, 0); 41 | }; 42 | 43 | llSCL3711.prototype.close = function() { 44 | // Tell clients. 45 | while (this.clients.length != 0) { 46 | this.notifyClientOfClosure(this.clients.shift()); 47 | } 48 | 49 | // Tell global list to drop this device. 50 | dev_manager.dropDevice(this); 51 | }; 52 | 53 | llSCL3711.prototype.publishFrame = function(f) { 54 | // Push frame to all clients. 55 | var old = this.clients; 56 | 57 | var remaining = []; 58 | var changes = false; 59 | for (var i = 0; i < old.length; ++i) { 60 | var client = old[i]; 61 | if (client.receivedFrame(f)) { 62 | // Client still alive; keep on list. 63 | remaining.push(client); 64 | } else { 65 | changes = true; 66 | console.log(UTIL_fmt( 67 | '[' + client.cid.toString(16) + '] left?')); 68 | } 69 | } 70 | if (changes) this.clients = remaining; 71 | }; 72 | 73 | llSCL3711.prototype.readLoop = function() { 74 | if (!this.dev) return; 75 | 76 | // console.log(UTIL_fmt('entering readLoop ' + this.dev.handle)); 77 | 78 | var self = this; 79 | chrome.usb.bulkTransfer( 80 | this.dev, 81 | { direction:'in', endpoint:this.endpoint, length:2048 }, 82 | function(x) { 83 | if (x.data) { 84 | if (x.data.byteLength >= 5) { 85 | 86 | var u8 = new Uint8Array(x.data); 87 | console.log(UTIL_fmt('<' + UTIL_BytesToHex(u8))); 88 | 89 | self.publishFrame(x.data); 90 | 91 | // Read more. 92 | window.setTimeout(function() { self.readLoop(); } , 0); 93 | } else { 94 | console.error(UTIL_fmt('tiny reply!')); 95 | console.error(x); 96 | // TODO(yjlou): I don't think a tiny reply requires close. 97 | // Maybe call dev_manager.close(null, clients[0])? 98 | // window.setTimeout(function() { self.close(); }, 0); 99 | } 100 | 101 | } else { 102 | console.log('no x.data!'); 103 | console.log(x); 104 | throw 'no x.data!'; 105 | } 106 | } 107 | ); 108 | }; 109 | 110 | // Register an opener. 111 | llSCL3711.prototype.registerClient = function(who) { 112 | this.clients.push(who); 113 | }; 114 | 115 | // De-register an opener. 116 | // Returns number of remaining listeners for this device. 117 | llSCL3711.prototype.deregisterClient = function(who) { 118 | var current = this.clients; 119 | this.clients = []; 120 | for (var i = 0; i < current.length; ++i) { 121 | var client = current[i]; 122 | if (client != who) this.clients.push(client); 123 | } 124 | return this.clients.length; 125 | }; 126 | 127 | // Stuffs all queued frames from txqueue[] to device. 128 | llSCL3711.prototype.writePump = function() { 129 | if (!this.dev) return; // Ignore. 130 | 131 | if (this.txqueue.length == 0) return; // Done with current queue. 132 | 133 | var frame = this.txqueue[0]; 134 | 135 | var self = this; 136 | function transferComplete(x) { 137 | self.txqueue.shift(); // drop sent frame from queue. 138 | if (self.txqueue.length != 0) { 139 | window.setTimeout(function() { self.writePump(); }, 0); 140 | } 141 | }; 142 | 143 | var u8 = new Uint8Array(frame); 144 | console.log(UTIL_fmt('>' + UTIL_BytesToHex(u8))); 145 | 146 | chrome.usb.bulkTransfer( 147 | this.dev, 148 | {direction:'out', endpoint:this.endpoint, data:frame}, 149 | transferComplete 150 | ); 151 | }; 152 | 153 | // Queue frame to be sent. 154 | // If queue was empty, start the write pump. 155 | // Returns false if device is MIA. 156 | llSCL3711.prototype.writeFrame = function(frame) { 157 | if (!this.dev) return false; 158 | 159 | var wasEmpty = (this.txqueue.length == 0); 160 | this.txqueue.push(frame); 161 | if (wasEmpty) this.writePump(); 162 | 163 | return true; 164 | }; 165 | -------------------------------------------------------------------------------- /sample/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | chrome.nfc API sample 5 | 6 | 7 | 8 | 9 |
10 | 13 | 14 | 32 |
33 |
34 | 35 |
36 |
Name
37 |
38 |
Vendor ID
39 |
40 |
Product ID
41 |
42 |
43 |
44 |
45 |

 46 |         
 47 |       
48 |
49 |

 50 |         
 51 |       
52 |
53 |

 54 |         
55 |
56 | 57 | 62 |
63 |
64 | 65 | 66 |
67 |
68 | 69 |
70 |
71 |
72 |
73 |

 74 |         
75 |
76 | 77 | 88 |
89 |
90 | 91 |
92 |
93 |
94 |
95 |

 96 |         
97 |
98 | 99 | 104 |
105 |
106 | 107 | 108 |
109 |
110 | 111 |
112 |
113 |
114 |
115 |
116 | 117 |
118 |
119 |
120 |
121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /src/sha256.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | // SHA256 { 17 | // SHA256(); 18 | // void reset(); 19 | // void update(byte[] data, opt_length); 20 | // byte[32] digest(); 21 | // } 22 | 23 | function SHA256() { 24 | this._buf = new Array(64); 25 | this._W = new Array(64); 26 | this._pad = new Array(64); 27 | this._k = [ 28 | 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 29 | 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 30 | 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 31 | 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 32 | 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 33 | 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 34 | 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, 35 | 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 ]; 36 | 37 | this._pad[0] = 0x80; 38 | for (var i = 1; i < 64; ++i) this._pad[i] = 0; 39 | 40 | this.reset(); 41 | }; 42 | 43 | SHA256.prototype.reset = function() { 44 | this._chain = [ 45 | 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f,0x9b05688c, 0x1f83d9ab, 0x5be0cd19 ]; 46 | 47 | this._inbuf = 0; 48 | this._total = 0; 49 | }; 50 | 51 | SHA256.prototype._compress = function(buf) { 52 | var W = this._W; 53 | var k = this._k; 54 | 55 | function _rotr(w, r) { return ((w << (32 -r)) | (w >>> r)); }; 56 | 57 | // get 16 big endian words 58 | for (var i = 0; i < 64; i += 4) { 59 | var w = (buf[i] << 24) | (buf[i+1] << 16) | (buf[i+2] << 8) | (buf[i+3]); 60 | W[i / 4] = w; 61 | } 62 | 63 | // expand to 64 words 64 | for ( var i = 16; i < 64; ++i ) { 65 | var s0 = _rotr(W[i - 15], 7) ^ _rotr(W[i - 15], 18) ^ (W[i - 15] >>> 3); 66 | var s1 = _rotr(W[i - 2], 17) ^ _rotr(W[i - 2], 19) ^ (W[i - 2] >>> 10); 67 | W[i] = (W[i - 16] + s0 + W[i - 7] + s1) & 0xffffffff; 68 | } 69 | 70 | var A = this._chain[0]; 71 | var B = this._chain[1]; 72 | var C = this._chain[2]; 73 | var D = this._chain[3]; 74 | var E = this._chain[4]; 75 | var F = this._chain[5]; 76 | var G = this._chain[6]; 77 | var H = this._chain[7]; 78 | 79 | for (var i = 0; i < 64; ++i) { 80 | var S0 = _rotr(A, 2) ^ _rotr(A, 13) ^ _rotr(A, 22); 81 | var maj = (A & B) ^ (A & C) ^ (B & C); 82 | var t2 = (S0 + maj) & 0xffffffff; 83 | var S1 = _rotr(E, 6) ^ _rotr(E, 11) ^ _rotr(E, 25); 84 | var ch = (E & F) ^ ((~E) & G); 85 | var t1 = (H + S1 + ch + k[i] + W[i]) & 0xffffffff; 86 | 87 | H = G; 88 | G = F; 89 | F = E; 90 | E = (D + t1) & 0xffffffff; 91 | D = C; 92 | C = B; 93 | B = A; 94 | A = (t1 + t2) & 0xffffffff; 95 | } 96 | 97 | this._chain[0] += A; 98 | this._chain[1] += B; 99 | this._chain[2] += C; 100 | this._chain[3] += D; 101 | this._chain[4] += E; 102 | this._chain[5] += F; 103 | this._chain[6] += G; 104 | this._chain[7] += H; 105 | }; 106 | 107 | SHA256.prototype.update = function(bytes, opt_length) { 108 | if ( !opt_length ) opt_length = bytes.length; 109 | 110 | this._total += opt_length; 111 | for ( var n = 0; n < opt_length; ++n ) { 112 | this._buf[this._inbuf++] = bytes[n]; 113 | if ( this._inbuf == 64 ) { 114 | this._compress(this._buf); 115 | this._inbuf = 0; 116 | } 117 | } 118 | }; 119 | 120 | SHA256.prototype.updateRange = function(bytes, start, end) { 121 | this._total += (end - start); 122 | for ( var n = start; n < end; ++n ) { 123 | this._buf[this._inbuf++] = bytes[n]; 124 | if ( this._inbuf == 64 ) { 125 | this._compress(this._buf); 126 | this._inbuf = 0; 127 | } 128 | } 129 | }; 130 | 131 | SHA256.prototype.digest = function() { 132 | for (var i = 0; i < arguments.length; ++i) 133 | this.update(arguments[i]); 134 | 135 | var digest = new Array(32); 136 | var totalBits = this._total * 8; 137 | 138 | // add pad 0x80 0x00* 139 | if (this._inbuf < 56) 140 | this.update(this._pad, 56 - this._inbuf ); 141 | else 142 | this.update(this._pad, 64 - ( this._inbuf - 56 ) ); 143 | 144 | // add # bits, big endian 145 | for (var i = 63; i >= 56; --i) { 146 | this._buf[i] = totalBits & 255; 147 | totalBits >>>= 8; 148 | } 149 | 150 | this._compress(this._buf); 151 | 152 | var n = 0; 153 | for (var i = 0; i < 8; ++i) 154 | for (var j = 24; j >= 0; j -= 8) 155 | digest[n++] = (this._chain[i] >> j) & 255; 156 | 157 | return digest; 158 | }; 159 | -------------------------------------------------------------------------------- /sample/app.js: -------------------------------------------------------------------------------- 1 | var compatibleDevices = [ 2 | { 3 | deviceName: 'ACR122U USB NFC Reader', 4 | productId: 0x2200, 5 | vendorId: 0x072f, 6 | thumbnailURL: chrome.runtime.getURL('images/acr122u.png') 7 | }, 8 | { 9 | deviceName: 'SCL3711 Contactless USB Smart Card Reader', 10 | productId: 0x5591, 11 | vendorId: 0x04e6, 12 | thumbnailURL: chrome.runtime.getURL('images/scl3711.png') 13 | } 14 | ] 15 | 16 | var device = null; 17 | 18 | function log(message, object) { 19 | var logArea = document.querySelector('.logs'); 20 | var pre = document.createElement('pre'); 21 | pre.textContent = message; 22 | if (object) 23 | pre.textContent += ': ' + JSON.stringify(object, null, 2) + '\n'; 24 | logArea.appendChild(pre); 25 | logArea.scrollTop = logArea.scrollHeight; 26 | document.querySelector('#logContainer').classList.remove('small'); 27 | } 28 | 29 | function handleDeviceTimeout(func, args) { 30 | var timeoutInMs = 1000; 31 | var hasTags = false; 32 | setTimeout(function() { 33 | if (!hasTags) { 34 | log('Timeout! No tag detected'); 35 | } 36 | }, timeoutInMs); 37 | var args = args || []; 38 | args = args.concat([function() { hasTags = true; }]); 39 | func.apply(this, args); 40 | } 41 | 42 | function onReadNdefTagButtonClicked() { 43 | handleDeviceTimeout(readNdefTag); 44 | } 45 | 46 | function readNdefTag(callback) { 47 | chrome.nfc.read(device, {}, function(type, ndef) { 48 | log('Found ' + ndef.ndef.length + ' NFC Tag(s)'); 49 | for (var i = 0; i < ndef.ndef.length; i++) 50 | log('NFC Tag', ndef.ndef[i]); 51 | callback(); 52 | }); 53 | } 54 | 55 | function onReadMifareTagButtonClicked() { 56 | handleDeviceTimeout(readMifareTag); 57 | } 58 | 59 | function readMifareTag(callback) { 60 | var blockNumber = 0; // starting logic block number. 61 | var blocksCount = 2; // logic block counts. 62 | chrome.nfc.read_logic(device, blockNumber, blocksCount, function(rc, data) { 63 | log('Mifare Classic Tag', UTIL_BytesToHex(data)); 64 | callback(); 65 | }); 66 | } 67 | 68 | function onWriteNdefTagButtonClicked() { 69 | var ndefType = document.querySelector('#write-ndef-type').value; 70 | var ndefValue = document.querySelector('#write-ndef-value').value; 71 | handleDeviceTimeout(writeNdefTag, [ndefType, ndefValue]); 72 | } 73 | 74 | function writeNdefTag(ndefType, ndefValue, callback) { 75 | var ndef = {}; 76 | ndef[ndefType] = ndefValue; 77 | chrome.nfc.write(device, {"ndef": [ndef]}, function(rc) { 78 | if (!rc) { 79 | log('NFC Tag written!'); 80 | } else { 81 | log('NFC Tag write operation failed', rc); 82 | } 83 | callback(); 84 | }); 85 | } 86 | 87 | function onWriteMifareTagButtonClicked() { 88 | try { 89 | var mifareData = JSON.parse(document.querySelector('#mifare-data').value); 90 | handleDeviceTimeout(writeMifareTag, [mifareData]); 91 | } 92 | catch(e) { 93 | log('Error', 'Mifare Data is not an Array.'); 94 | } 95 | } 96 | 97 | function writeMifareTag(mifareData, callback) { 98 | var data = new Uint8Array(mifareData); 99 | var blockNumber = 0; // starting logic block number. 100 | chrome.nfc.write_logic(device, 0, data, function(rc) { 101 | if (!rc) { 102 | log('Mifare Tag written!'); 103 | } else { 104 | log('Mifare Tag write operation failed', rc); 105 | } 106 | callback(); 107 | }); 108 | } 109 | 110 | function onEmulateTagButtonClicked() { 111 | var ndefType = document.querySelector('#emulate-ndef-type').value; 112 | var ndefValue = document.querySelector('#emulate-ndef-value').value; 113 | handleDeviceTimeout(emulateTag, [ndefType, ndefValue]); 114 | } 115 | 116 | function emulateTag(ndefType, ndefValue, callback) { 117 | var ndef = {}; 118 | ndef[ndefType] = ndefValue; 119 | chrome.nfc.emulate_tag(device, {"ndef": [ndef]}, function(rc) { 120 | if (!rc) { 121 | log('NFC Tag emulated!'); 122 | } else { 123 | log('NFC Tag emulate operation failed', rc); 124 | } 125 | callback(); 126 | }); 127 | } 128 | 129 | function showDeviceInfo() { 130 | var deviceInfo = null; 131 | for (var i = 0; i < compatibleDevices.length; i++) 132 | if (device.productId === compatibleDevices[i].productId && 133 | device.vendorId === compatibleDevices[i].vendorId) 134 | deviceInfo = compatibleDevices[i]; 135 | 136 | if (!deviceInfo) 137 | return; 138 | 139 | var thumbnail = document.querySelector('#device-thumbnail'); 140 | thumbnail.src = deviceInfo.thumbnailURL; 141 | thumbnail.classList.remove('hidden'); 142 | 143 | var deviceName = document.querySelector('#device-name'); 144 | deviceName.textContent = deviceInfo.deviceName; 145 | 146 | var productId = document.querySelector('#device-product-id'); 147 | productId.textContent = deviceInfo.productId; 148 | 149 | var vendorId = document.querySelector('#device-vendor-id'); 150 | vendorId.textContent = deviceInfo.vendorId; 151 | 152 | $('a[href="#device-info"]').tab('show'); 153 | } 154 | 155 | function enumerateDevices() { 156 | chrome.nfc.findDevices(function(devices) { 157 | device = devices[0]; 158 | showDeviceInfo(); 159 | }); 160 | } 161 | 162 | enumerateDevices(); 163 | 164 | document.querySelector('#read-ndef pre').textContent = readNdefTag.toString(); 165 | document.querySelector('#read-ndef button').addEventListener('click', onReadNdefTagButtonClicked); 166 | 167 | document.querySelector('#read-mifare pre').textContent = readMifareTag.toString(); 168 | document.querySelector('#read-mifare button').addEventListener('click', onReadMifareTagButtonClicked); 169 | 170 | document.querySelector('#write-ndef pre').textContent = writeNdefTag.toString(); 171 | document.querySelector('#write-ndef button').addEventListener('click', onWriteNdefTagButtonClicked); 172 | 173 | document.querySelector('#write-mifare pre').textContent = writeMifareTag.toString(); 174 | document.querySelector('#write-mifare button').addEventListener('click', onWriteMifareTagButtonClicked); 175 | 176 | document.querySelector('#emulate pre').textContent = emulateTag.toString(); 177 | document.querySelector('#emulate button').addEventListener('click', onEmulateTagButtonClicked); 178 | 179 | $('a[data-toggle="tab"]').on('shown.bs.tab', function (e) { 180 | document.querySelector('#logContainer').classList.add('small'); 181 | }); 182 | 183 | document.querySelector('.drawer').addEventListener('click', function(e) { 184 | document.querySelector('#logContainer').classList.toggle('small'); 185 | }); 186 | -------------------------------------------------------------------------------- /src/devmanager.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | /** 17 | * @fileoverview USB device manager. 18 | * 19 | * +-----------------+ 20 | * | Reader-specific | 21 | * | driver, like | The "who" in the open() function. 22 | * | scl3711.js | 23 | * +-----------------+ For low level driver, this is "client". 24 | * | 25 | * v 26 | * +-----------------+ 27 | * | dev_manager: | 28 | * | open and enum | 29 | * | low level devs | 30 | * +-----------------+ 31 | * | | 32 | * v v 33 | * +-------+ +-------+ The "which" in the open() function. 34 | * |llSCL37| |llSCL37| 35 | * | | | | Low level USB driver. 36 | * | | | | each maps to a physical device instance. 37 | * | | | | handling Tx/Rx queues. 38 | * +-------+ +-------+ 39 | * 40 | */ 41 | 42 | 'use strict'; 43 | 44 | 45 | // List of enumerated usb devices. 46 | function devManager() { 47 | this.devs = []; // array storing the low level device. 48 | this.enumerators = []; // array storing the pending callers of enumerate(). 49 | } 50 | 51 | // Remove a device from devs[] list. 52 | devManager.prototype.dropDevice = function(dev) { 53 | var tmp = this.devs; 54 | this.devs = []; 55 | 56 | var present = false; 57 | for (var i = 0; i < tmp.length; ++i) { 58 | if (tmp[i] !== dev) { 59 | this.devs.push(tmp[i]); 60 | } else { 61 | present = true; 62 | } 63 | } 64 | if (!present) return; // Done. 65 | 66 | if (dev.dev) { 67 | chrome.usb.releaseInterface(dev.dev, 0, 68 | function() { console.log(UTIL_fmt('released')); }); 69 | chrome.usb.closeDevice(dev.dev, 70 | function() { console.log(UTIL_fmt('closed')); }); 71 | dev.dev = null; 72 | } 73 | 74 | console.log(this.devs.length + ' devices remaining'); 75 | }; 76 | 77 | // Close all enumerated devices. 78 | devManager.prototype.closeAll = function(cb) { 79 | 80 | console.debug("devManager.closeAll() is called"); 81 | 82 | // First close and stop talking to any device we already 83 | // have enumerated. 84 | var d = this.devs.slice(0); 85 | for (var i = 0; i < d.length; ++i) { 86 | d[i].close(); 87 | } 88 | 89 | if (cb) { 90 | cb(); 91 | } 92 | }; 93 | 94 | // When an app needs a device, it must claim before use (so that kernel 95 | // can handle the lock). 96 | devManager.prototype.enumerate = function(cb) { 97 | var self = this; 98 | 99 | function enumerated(d, acr122) { 100 | var nDevice = 0; 101 | 102 | if (d && d.length != 0) { 103 | console.log(UTIL_fmt('Enumerated ' + d.length + ' devices')); 104 | console.log(d); 105 | nDevice = d.length; 106 | } else { 107 | if (d) { 108 | console.log('No devices found'); 109 | } else { 110 | /* TODO(yjlou): Review this case later (d==undefined). 111 | * Is this real lacking permission. 112 | */ 113 | console.log('Lacking permission?'); 114 | do { 115 | (function(cb) { 116 | if (cb) window.setTimeout(function() { cb(-666); }, 0); 117 | })(self.enumerators.shift()); 118 | } while (self.enumerators.length); 119 | return; 120 | } 121 | } 122 | 123 | // Found multiple devices. Create a low level SCL3711 per device. 124 | for (var i = 0; i < nDevice; ++i) { 125 | (function(dev, i) { 126 | window.setTimeout(function() { 127 | chrome.usb.claimInterface(dev, 0, function(result) { 128 | console.log(UTIL_fmt('claimed')); 129 | console.log(dev); 130 | 131 | // Push the new low level device to the devs[]. 132 | self.devs.push(new llSCL3711(dev, acr122)); 133 | 134 | // Only callback after the last device is claimed. 135 | if (i == (nDevice - 1)) { 136 | var u8 = new Uint8Array(4); 137 | u8[0] = nDevice >> 24; 138 | u8[1] = nDevice >> 16; 139 | u8[2] = nDevice >> 8; 140 | u8[3] = nDevice; 141 | 142 | // Notify all enumerators. 143 | while (self.enumerators.length) { 144 | (function(cb) { 145 | window.setTimeout(function() { if (cb) cb(0, u8); }, 20); 146 | })(self.enumerators.shift()); 147 | } 148 | } 149 | }); 150 | }, 0); 151 | })(d[i], i); 152 | } 153 | }; 154 | /* end of enumerated() */ 155 | 156 | if (this.devs.length != 0) { 157 | // Already have devices. Report number right away. 158 | // TODO(yjlou): The new plugged-in NFC reader may not be detected after 159 | // the first time enumerate() is called. 160 | var u8 = new Uint8Array(4); 161 | u8[0] = this.devs.length >> 24; 162 | u8[1] = this.devs.length >> 16; 163 | u8[2] = this.devs.length >> 8; 164 | u8[3] = this.devs.length; 165 | if (cb) cb(0, u8); 166 | } else { 167 | var first = this.enumerators.length == 0; 168 | 169 | // Queue callback. 170 | this.enumerators.push(cb); 171 | 172 | if (first) { 173 | // Only first requester calls actual low level. 174 | window.setTimeout(function() { 175 | chrome.usb.findDevices({'vendorId': 0x04e6, 'productId': 0x5591}, 176 | function (d) { 177 | if (d && d.length != 0) { 178 | enumerated(d, false); 179 | } else { 180 | chrome.usb.findDevices( 181 | {'vendorId': 0x072f, 'productId': 0x2200}, 182 | function (d) { 183 | if (d && d.length != 0) { 184 | enumerated(d, true); 185 | } 186 | }); 187 | } 188 | }); 189 | }, 0); 190 | } 191 | } 192 | }; 193 | 194 | devManager.prototype.open = function(which, who, cb) { 195 | var self = this; 196 | // Make sure we have enumerated devices. 197 | this.enumerate(function() { 198 | var dev = self.devs[which]; 199 | if (dev) dev.registerClient(who); 200 | if (cb) { cb(dev || null); } 201 | }); 202 | }; 203 | 204 | devManager.prototype.close = function(singledev, who) { 205 | // De-register client from all known devices, 206 | // since the client might have opened them implicitly w/ enumerate(). 207 | // This will thus release any device without active clients. 208 | var alldevs = this.devs; 209 | for (var i = 0; i < alldevs.length; ++i) { 210 | var dev = alldevs[i]; 211 | var nremaining = dev.deregisterClient(who); 212 | // TODO: uncomment when Chrome stabilizes. 213 | /* 214 | if (nremaining == 0) { 215 | // This device has no active clients remaining. 216 | // Close it so libusb releases its claim and other processes 217 | // can try attach to the device. 218 | this.dropDevice(dev); 219 | } 220 | */ 221 | } 222 | }; 223 | 224 | // For console interaction. 225 | // rc - a number. 226 | // data - an ArrayBuffer. 227 | var defaultCallback = function(rc, data) { 228 | var msg = 'defaultCallback('+rc; 229 | if (data) msg += ', ' + UTIL_BytesToHex(new Uint8Array(data)); 230 | msg += ')'; 231 | console.log(UTIL_fmt(msg)); 232 | }; 233 | 234 | 235 | // Singleton tracking available devices. 236 | var dev_manager = new devManager(); 237 | 238 | -------------------------------------------------------------------------------- /src/nfc.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | /** 17 | * @fileoverview chrome.nfc 18 | */ 19 | 20 | 'use strict'; 21 | 22 | function NFC() { 23 | var self = this; 24 | 25 | // private functions 26 | function construct_ndef_obj(ndef_array) { 27 | var ndef_obj = new NDEF(); 28 | 29 | for (var i = 0; i < ndef_array.length; i++) { 30 | ndef_obj.add(ndef_array[i]); 31 | } 32 | 33 | return ndef_obj; 34 | } 35 | 36 | function wait_for_passive_target(device, cb, timeout) { 37 | if (timeout == undefined) timeout = 9999999999; 38 | 39 | device.wait_for_passive_target(timeout, function(rc, tag_type, tag_id) { 40 | if (rc) { 41 | console.log("NFC.wait_for_passive_target() = " + rc); 42 | cb(rc); 43 | return rc; 44 | } 45 | console.log("[DEBUG] nfc.wait_for_passive_target: " + tag_type + " with ID: " + UTIL_BytesToHex(new Uint8Array(tag_id))); 46 | cb(rc, tag_type, tag_id); 47 | }); 48 | } 49 | 50 | var pub = { 51 | /* 52 | * This function is to get use-able NFC device(s). 53 | * 54 | * TODO: Currently, this function returns at most 1 device. 55 | * 56 | * cb(devices) is called after enumeration. 'devices' is an array of all 57 | * found devices. It is an empty array if no NFC device is found. 58 | */ 59 | "findDevices": function(cb) { 60 | var device = new usbSCL3711(); 61 | window.setTimeout(function() { 62 | device.open(0, function(rc) { 63 | if (rc) { 64 | console.log("NFC.device.open() = " + rc); 65 | cb([]); 66 | return rc; 67 | } 68 | // cache device info 69 | device.vendorId = device.dev.dev.vendorId; 70 | device.productId = device.dev.dev.productId; 71 | 72 | cb([device]); 73 | }, function() { 74 | console.debug("device.onclose() is called."); 75 | }); 76 | }, 1000); 77 | }, 78 | 79 | /* 80 | * Read a tag. 81 | * 82 | * 'options' is a dictionary with optional parameters. If a parameter is 83 | * missed, a default value is applied. Options include: 84 | * 85 | * 'timeout': timeout for this operation. Default: infinite 86 | * TODO: 'type': type of tag to listen. Default: "any" for any type. 87 | * However, currently only tag 2 and NDEF is supported. 88 | * 89 | * 'cb' lists callback functions for particular tag contents. 90 | * When called, 2 parameters are given: 'type' and 'content'. 91 | * 'type' indicates the tag type detected in the hierarchical form, ex: 92 | * "tt2.ndef". Then 'content' is the NDEF object. 93 | */ 94 | "read": function(device, options, cb) { 95 | var timeout = options["timeout"]; 96 | var callback = cb; 97 | 98 | wait_for_passive_target(device, function(rc, tag_type, tag_id) { 99 | var tag = new Tag(tag_type, tag_id); 100 | if (!tag) { 101 | console.log("nfc.read: unknown tag_type: " + tag_type); 102 | return; 103 | } 104 | 105 | tag.read(device, function(rc, ndef){ 106 | if (rc) { 107 | console.log("NFC.read.read() = " + rc); 108 | callback(null, null); /* no type reported */ 109 | return rc; 110 | } 111 | var ndef_obj = new NDEF(ndef); 112 | callback(tag_type + ".ndef", ndef_obj); 113 | }); 114 | }, timeout); 115 | }, 116 | 117 | /* 118 | * Read logic blocks. 119 | */ 120 | "read_logic": function(device, logic_block, cnt, cb) { 121 | var callback = cb; 122 | 123 | wait_for_passive_target(device, function(rc, tag_type, tag_id) { 124 | var tag = new Tag(tag_type, tag_id); 125 | if (!tag) { 126 | console.log("nfc.read_logic: unknown tag_type: " + tag_type); 127 | return; 128 | } 129 | if (!tag.read_logic) { 130 | console.log("nfc.read: " + tag_type + 131 | " doesn't support reading logic block"); 132 | return; 133 | } 134 | 135 | tag.read_logic(device, logic_block, cnt, function(data) { 136 | callback(0, data); 137 | }); 138 | }); 139 | }, 140 | 141 | /* 142 | * Return tag_id as soon as a tag is detected. 143 | */ 144 | "wait_for_tag": function(device, timeout, cb) { 145 | var callback = cb; 146 | 147 | var loop = function(timeout) { 148 | 149 | wait_for_passive_target(device, function(rc, tag_type, tag_id) { 150 | if(rc >= 0) { 151 | callback(tag_type, tag_id); 152 | } 153 | else { 154 | if(timeout > 0) { 155 | window.setTimeout(function() { 156 | loop(timeout-250) 157 | }, 250); 158 | } else 159 | callback(null, null); 160 | } 161 | }); 162 | } 163 | loop(timeout); 164 | }, 165 | 166 | /* 167 | * Write content to tag. 168 | * 169 | * 'content' is a dictionary containing structures to write. Supports: 170 | * ['ndef']: an array of NDEF dictionary. Will be written as a tag 171 | * type 2. 172 | * 173 | * cb(0) is called if success. 174 | * timeout is optional. 175 | */ 176 | "write": function(device, content, cb, timeout) { 177 | wait_for_passive_target(device, function(rc, tag_type, tag_id) { 178 | var tag = new Tag(tag_type, tag_id); 179 | if (!tag) { 180 | console.log("nfc.write: unknown tag_type: " + tag_type); 181 | return; 182 | } 183 | 184 | var ndef_obj = construct_ndef_obj(content["ndef"]); 185 | tag.write(device, ndef_obj.compose(), function(rc) { 186 | cb(rc); 187 | }); 188 | }, timeout); 189 | }, 190 | 191 | /* 192 | * Write to logic blocks. 193 | * 194 | * 'logic_block': the starting logic block number. 195 | * 'data': Uint8Array. Can large than 16-byte. 196 | */ 197 | "write_logic": function(device, logic_block, data, cb) { 198 | var callback = cb; 199 | 200 | wait_for_passive_target(device, function(rc, tag_type, tag_id) { 201 | var tag = new Tag(tag_type, tag_id); 202 | if (!tag) { 203 | console.log("nfc.write_logic: unknown tag_type: " + tag_type); 204 | return; 205 | } 206 | 207 | if (!tag.write_logic) { 208 | console.log("nfc.read: " + tag_type + 209 | " doesn't support reading logic block"); 210 | return; 211 | } 212 | 213 | tag.write_logic(device, logic_block, data, function(rc) { 214 | callback(rc); 215 | }); 216 | }); 217 | }, 218 | 219 | 220 | /* 221 | * Write to physical blocks. 222 | * 223 | * 'physical_block': the starting physical block number. 224 | * 'data': Uint8Array. Can large than 16-byte. 225 | */ 226 | "write_physical": function(device, physical_block, key, data, cb) { 227 | var callback = cb; 228 | 229 | wait_for_passive_target(device, function(rc, tag_type, tag_id) { 230 | var tag = new Tag(tag_type, tag_id); 231 | if (!tag) { 232 | console.log("nfc.write_physical: unknown tag_type: " + tag_type); 233 | return; 234 | } 235 | 236 | if (!tag.write_physical) { 237 | console.log("nfc.read: " + tag_type + 238 | " doesn't support reading physical block"); 239 | return; 240 | } 241 | 242 | tag.write_physical(device, physical_block, key, data, function(rc) { 243 | callback(rc); 244 | }); 245 | }); 246 | }, 247 | 248 | /* 249 | * Emulate as a tag. 250 | * 251 | * 'content' is a dictionary containing structures to write. Supports: 252 | * ['ndef']: an array of NDEF dictionary. Will be written as a tag 253 | * type 2. 254 | * 255 | * cb(0) is called if success. 256 | * timeout is optional. 257 | */ 258 | "emulate_tag": function(device, content, cb, timeout) { 259 | if (timeout == undefined) timeout = 9999999999; 260 | wait_for_passive_target(device, function(rc, tag_type, tag_id) { 261 | var tt2 = new TT2(); 262 | var ndef_obj = construct_ndef_obj(content["ndef"]); 263 | tt2.emulate(device, ndef_obj, timeout, function(rc) { 264 | cb(rc); 265 | }); 266 | }, timeout); 267 | } 268 | }; 269 | 270 | return pub; 271 | } 272 | 273 | chrome.nfc = NFC(); 274 | -------------------------------------------------------------------------------- /src/tt2.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | /** 17 | * @fileoverview 18 | */ 19 | 20 | 'use strict'; 21 | 22 | 23 | function TT2(tag_id) { 24 | this.tag_id = new Uint8Array(tag_id); 25 | this.type_name = null; // vendor and its card name 26 | 27 | /* 28 | * TODO: detect at beginning -- if we have a reliable way to detect. 29 | * this.detect_type_name(cb); 30 | */ 31 | 32 | this.lock_contorl = []; 33 | } 34 | 35 | TT2.prototype.detect_type_name = function(cb) { 36 | var self = this; 37 | var callback = cb; 38 | 39 | if (this.tag_id[0] == 0x04) { 40 | // NxP, Try to read page 0x10. If success, it is Ultralight C. 41 | this.device.read_block(0x10, function(rc, bn) { 42 | if (rc) { 43 | self.type_name = "Mifare Ultralight"; 44 | } else { 45 | self.type_name = "Mifare Ultralight C"; 46 | } 47 | 48 | console.debug("[DEBUG] TT2.type_name = " + self.type_name); 49 | if (callback) callback(); 50 | }); 51 | } 52 | } 53 | 54 | 55 | // read NFC Type 2 tag spec 1.0 for memory structure. 56 | // The callback is called with cb(NDEF Uint8Array). 57 | TT2.prototype.read = function(device, cb) { 58 | var self = this; 59 | if (!cb) cb = defaultCallback; 60 | var callback = cb; 61 | 62 | function poll_block0(rc, b0_b3) { 63 | if (rc) return callback(rc); 64 | 65 | var card = new Uint8Array(b0_b3); 66 | var data = new Uint8Array(b0_b3); 67 | var data_size = data[14] * 8; // CC2: unit is 8 bytes. 68 | var CC0 = data[12]; // CC0: 0xE1 = NDEF 69 | var CC1 = data[13]; // CC1: version of this Type 2 tag spec. 70 | var CC3 = data[15]; // CC3: b7-b4: read permission. 71 | 72 | function check_ver(cc1) { 73 | var major = (cc1 & 0xf0 ) >> 4; 74 | var minor = cc1 & 0x0f; 75 | if (major == 0x1) return true; 76 | return false; 77 | } 78 | function readable(cc3) { 79 | return (cc3 & 0xf0) == 0x00 ? true : false; 80 | } 81 | 82 | /* TODO: support protocol other than NDEF */ 83 | if (CC0 != 0xE1 || !check_ver(CC1) || !readable(CC3)) { 84 | console.log("UNsupported type 2 tag: CC0=" + CC0 + 85 | ", CC1=" + CC1 + 86 | ", CC3=" + CC3); 87 | return callback(0x0777, data.buffer); 88 | } 89 | 90 | // poll data out 91 | var poll_n = Math.floor((data_size + 15) / 16); 92 | var block = 4; // data starts from block 4 93 | 94 | function poll_block(card, block, poll_n) { 95 | console.log("[DEBUG] poll_n: " + poll_n); 96 | if (--poll_n < 0) { 97 | defaultCallback("[DEBUG] got a type 2 tag:", card.buffer); 98 | 99 | /* TODO: call tlv.js instead */ 100 | /* TODO: now pass NDEF only. Support non-NDEF in the future. */ 101 | for (var i = 0x10; i < card.length;) { 102 | switch (card[i]) { 103 | case 0x00: /* NULL */ 104 | console.debug("NULL TLV"); 105 | i++; 106 | break; 107 | 108 | case 0x01: /* Lock Control TLV */ 109 | console.debug("Found Lock Control TLV"); 110 | 111 | /* TODO: refactor and share code with Memory Control TLV */ 112 | var PageAddr = card[i + 2] >> 4; 113 | var ByteOffset = card[i + 2] & 0xf; 114 | var Size = card[i + 3]; 115 | if (Size == 0) Size = 256; /* 256 bits */ 116 | var BytesPerPage = Math.pow(2, card[i + 4] & 0xf); 117 | var BytesLockedPerLockBit = card[i + 4] >> 4; 118 | 119 | console.debug("Lock control: " + 120 | "BytesLockedPerLockBit=" + BytesLockedPerLockBit + 121 | ", Size=" + Size); 122 | 123 | var ByteAddr = PageAddr * BytesPerPage + ByteOffset; 124 | 125 | console.info("Lock control: ByteAddr=" + ByteAddr); 126 | console.info(" Locked bytes:"); 127 | var lock_offset = 64; 128 | for (var j = 0; j < (Size + 7) / 8; j++) { 129 | var k = ByteAddr + j; 130 | 131 | if (k >= card.length) { 132 | console.warn(" card[" + k + "] haven't read out yet."); 133 | /* TODO: read out and continue the following parse */ 134 | break; 135 | } 136 | 137 | var mask = card[k]; 138 | console.debug(" [" + k + "]: " + mask.toString(16)); 139 | 140 | if (mask & 1) console.debug("* block-locking"); 141 | for (var l = 1; l < 8; l++) { 142 | if (j * 8 + l >= Size) continue; 143 | 144 | for (var s = "", m = 0; 145 | m < BytesLockedPerLockBit; 146 | lock_offset++) { 147 | s += "0x" + lock_offset.toString(16) + ", "; 148 | } 149 | if (mask & (1 << l)) console.info(" " + s); 150 | } 151 | } 152 | 153 | i += (1/*T*/ + 1/*L*/ + card[i + 1]/*len: 3*/); 154 | break; 155 | 156 | /* TODO: 0x02 -- Memory Control TLV */ 157 | 158 | case 0xFE: /* Terminator */ 159 | console.debug("Terminator TLV."); 160 | return; 161 | 162 | case 0x03: /* NDEF */ 163 | var len = card[i + 1]; 164 | if ((i + 2 + len) > card.length) { 165 | console.warn("TLV len " + len + " > card len " + card.length); 166 | } 167 | return callback(0, 168 | new Uint8Array(card.subarray(i + 2, i + 2 + len)).buffer); 169 | 170 | default: 171 | console.error("Unknown Type [" + card[i] + "]"); 172 | return; 173 | } 174 | } /* end of for */ 175 | } 176 | 177 | device.read_block(block, function(rc, bn) { 178 | if (rc) return callback(rc); 179 | card = UTIL_concat(card, new Uint8Array(bn)); 180 | return poll_block(card, block + 4, poll_n); 181 | }); 182 | } 183 | poll_block(card, block, poll_n); 184 | } 185 | 186 | device.read_block(0, poll_block0); 187 | } 188 | 189 | 190 | /* Input: 191 | * ndef - Uint8Array 192 | */ 193 | TT2.prototype.compose = function(ndef) { 194 | 195 | var blen; // CC2 196 | var need_lock_control_tlv = 0; 197 | 198 | if ((ndef.length + 16 /* tt2_header */ 199 | + 2 /* ndef_tlv */ 200 | + 1 /* terminator_tlv */) > 64) { 201 | /* 202 | * CC bytes of MF0ICU2 (MIFARE Ultralight-C) is OTP (One Time Program). 203 | * Set to maximum available size (144 bytes). 204 | */ 205 | blen = 144 / 8; 206 | need_lock_control_tlv = 1; 207 | 208 | /* TODO: check if the ndef.length + overhead are larger than card */ 209 | } else { 210 | /* 211 | * CC bytes of MF0ICU1 (MIFARE Ultralight) is OTP (One Time Program). 212 | * Set to maximum available size (48 bytes). 213 | */ 214 | blen = 48 / 8; 215 | } 216 | 217 | var tt2_header = new Uint8Array([ 218 | 0x00, 0x00, 0x00, 0x00, /* UID0, UID1, UID2, Internal0 */ 219 | 0x00, 0x00, 0x00, 0x00, /* UID3, UID4, UID5, UID6 */ 220 | 0x00, 0x00, 0x00, 0x00, /* Internal1, Internal2, Lock0, Lock1 */ 221 | 0xe1, 0x10, blen, 0x00 /* CC0, CC1, CC2(len), CC3 */ 222 | ]); 223 | 224 | var lock_control_tlv = (need_lock_control_tlv) ? 225 | new Uint8Array([ 226 | /*T*/ 0x01, 227 | /*L*/ 0x03, 228 | /*V*/ 0xA0, 0x10, 0x44 /* BytesLockedPerLockBit=4, Size=16 229 | * ByteAddr=160 230 | */ 231 | ]) : 232 | new Uint8Array([]); 233 | 234 | var ndef_tlv = new Uint8Array([ 235 | 0x03, ndef.length /* NDEF Message TLV */ 236 | ]); 237 | var terminator_tlv = new Uint8Array([ 238 | 0xfe 239 | ]); 240 | var ret = UTIL_concat(tt2_header, 241 | UTIL_concat(lock_control_tlv, 242 | UTIL_concat(ndef_tlv, 243 | UTIL_concat(new Uint8Array(ndef), 244 | terminator_tlv)))); 245 | return ret; 246 | } 247 | 248 | 249 | // Input: 250 | // ndef: ArrayBuffer. Just ndef is needed. TT2 header is handled. 251 | TT2.prototype.write = function(device, ndef, cb) { 252 | if (!cb) cb = defaultCallback; 253 | 254 | var self = this; 255 | var callback = cb; 256 | var card = self.compose(new Uint8Array(ndef)); 257 | var card_blknum = Math.floor((card.length + 3) / 4); 258 | 259 | /* TODO: check memory size according to CC value */ 260 | if (card_blknum > (64 / 4)) { 261 | console.warn("write_tt2() card length: " + card.length + 262 | " is larger than 64 bytes. Try to write as Ultralight-C."); 263 | if (card_blknum > (192 / 4)) { 264 | console.error("write_tt2() card length: " + card.length + 265 | " is larger than 192 bytes (more than Ultralight-C" + 266 | " can provide)."); 267 | return callback(0xbbb); 268 | } 269 | } 270 | 271 | function write_block(card, block_no) { 272 | if (block_no >= card_blknum) { return callback(0); } 273 | 274 | var data = card.subarray(block_no * 4, block_no * 4 + 4); 275 | if (data.length < 4) data = UTIL_concat(data, 276 | new Uint8Array(4 - data.length)); 277 | 278 | device.write_block(block_no, data, function(rc) { 279 | if (rc) return callback(rc); 280 | write_block(card, block_no + 1); 281 | }); 282 | } 283 | 284 | /* Start from CC* fields */ 285 | write_block(card, 3); 286 | } 287 | 288 | 289 | TT2.prototype.emulate = function(device, ndef_obj, timeout, cb) { 290 | var data = this.compose(new Uint8Array(ndef_obj.compose())); 291 | return device.emulate_tag(data, timeout, cb); 292 | } 293 | -------------------------------------------------------------------------------- /src/ndef.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | /** 17 | * @fileoverview NDEF messgae parser. 18 | */ 19 | 20 | 'use strict'; 21 | 22 | 23 | /* Input: 24 | * raw is either ArrayBuffer. 25 | */ 26 | function NDEF(raw, cb) { 27 | this.ndef = []; 28 | this.prepending = [ /* for RTD_URI */ 29 | "", 30 | "http://www.", 31 | "https://www.", 32 | "http://", 33 | "https://", 34 | "tel:", 35 | "mailto:", 36 | "ftp://anonymous:anonymous@", 37 | "ftp://ftp.", 38 | "ftps://", 39 | "sftp://", 40 | "smb://", 41 | "nfs://", 42 | "ftp://", 43 | "dav://", 44 | "news:", 45 | "telnet://", 46 | "imap:", 47 | "rtsp://", 48 | "urn:", 49 | "pop:", 50 | "sip:", 51 | "sips:", 52 | "tftp:", 53 | "btspp://", 54 | "btl2cpa://", 55 | "btgoep://", 56 | "tcpobex://", 57 | "irdaobex://", 58 | "file://", 59 | "urn:epc:id:", 60 | "urn:epc:tag:", 61 | "urn:epc:pat:", 62 | "urn:epc:raw:", 63 | "urn:epc:", 64 | "urn:nfc:" 65 | ]; 66 | 67 | if (raw) { 68 | this.ndef = this.parse(raw, cb); 69 | } 70 | 71 | } 72 | 73 | /* Input: 74 | * raw is either ArrayBuffer. 75 | * 76 | * Output: 77 | * The callback function will get a JS structure for NDEF content. 78 | * 79 | * For the message format, please refer to Chapter 3 of NDEF spec. 80 | */ 81 | NDEF.prototype.parse = function(raw, cb) { 82 | var i; /* index to access raw[] */ 83 | var ret = []; 84 | raw = new Uint8Array(raw); 85 | 86 | for (i = 0; i < raw.length; i++) { 87 | var MB = (raw[i] & 0x80) >> 7; /* Message Begin */ 88 | var ME = (raw[i] & 0x40) >> 6; /* Message End */ 89 | var CF = (raw[i] & 0x20) >> 5; /* Chunk Flag */ 90 | var SR = (raw[i] & 0x10) >> 4; /* Short Record */ 91 | var IL = (raw[i] & 0x08) >> 3; /* ID_LENGTH field is present */ 92 | var TNF = (raw[i] & 0x07) >> 0; /* Type Name Format */ 93 | var type_off; 94 | var type_len = raw[i + 1]; 95 | var id; 96 | var type; 97 | var payload_off = 4 + type_len; 98 | var payload_len; 99 | var payload; 100 | 101 | if (SR) { 102 | type_off = 3; 103 | payload_off = 3 + type_len; 104 | payload_len = raw[i + 2]; 105 | } else { 106 | type_off = 6; 107 | payload_off = 6 + type_len; 108 | payload_len = ((raw[i + 2] * 256 + raw[i + 3]) * 256 + 109 | raw[i + 4]) * 256 + raw[i + 5]; 110 | } 111 | if (IL) { 112 | type_off += 1; 113 | var id_len = raw[i + type_off - 1]; 114 | payload_off += 1 + id_len; 115 | var id_off = type_off + type_len; 116 | id = raw.subarray(i + id_off, i + id_off + id_len); 117 | } else { 118 | id = null; 119 | } 120 | 121 | type = new Uint8Array(raw.subarray(i + type_off, i + type_off + type_len)); 122 | payload = new Uint8Array( 123 | raw.subarray(i + payload_off, i + payload_off + payload_len)); 124 | 125 | if (1) { /* for DEBUG */ 126 | console.log("raw[i]: " + raw[i]); 127 | console.log("MB: " + MB); 128 | console.log("ME: " + ME); 129 | console.log("SR: " + SR); 130 | console.log("IL: " + IL); 131 | console.log("TNF: " + TNF); 132 | console.log("type_off: " + type_off); 133 | console.log("type_len: " + type_len); 134 | console.log("payload_off: " + payload_off); 135 | console.log("payload_len: " + payload_len); 136 | console.log("type: " + UTIL_BytesToHex(type)); 137 | console.log("payload: " + UTIL_BytesToHex(payload)); 138 | } 139 | 140 | switch (TNF) { 141 | case 0x01: /* NFC RTD - so called Well-known type */ 142 | ret.push(this.parse_RTD(type[0], payload)); 143 | break; 144 | case 0x02: /* MIME - RFC 2046 */ 145 | ret.push(this.parse_MIME(type, payload)); 146 | break; 147 | case 0x04: /* NFC RTD - so called External type */ 148 | ret.push(this.parse_ExternalType(type, payload)); 149 | break; 150 | default: 151 | console.error("Unsupported TNF: " + TNF); 152 | break; 153 | } 154 | 155 | i = payload_off + payload_len - 1; 156 | if (ME) break; 157 | } 158 | 159 | if (cb) 160 | cb(ret); 161 | 162 | return ret; 163 | } 164 | 165 | 166 | /* Input: 167 | * None. 168 | * 169 | * Output: 170 | * ArrayBuffer. 171 | * 172 | */ 173 | NDEF.prototype.compose = function() { 174 | var out = new Uint8Array(); 175 | var arr = []; 176 | 177 | for (var i = 0; i < this.ndef.length; i++) { 178 | var entry = this.ndef[i]; 179 | 180 | switch (entry["type"]) { 181 | case "TEXT": 182 | case "Text": 183 | arr.push({"TNF": 1, 184 | "TYPE": new Uint8Array([0x54 /* T */]), 185 | "PAYLOAD": this.compose_RTD_TEXT(entry["lang"], 186 | entry["text"])}); 187 | break; 188 | case "URI": 189 | arr.push({"TNF": 1, 190 | "TYPE": new Uint8Array([0x55 /* U */]), 191 | "PAYLOAD": this.compose_RTD_URI(entry["uri"])}); 192 | break; 193 | case "MIME": 194 | arr.push({"TNF": 2, 195 | "TYPE": new Uint8Array(UTIL_StringToBytes(entry["mime_type"])), 196 | "PAYLOAD": this.compose_MIME(entry["payload"])}); 197 | break; 198 | case "AAR": 199 | arr.push({"TNF": 4, 200 | "TYPE": new Uint8Array(UTIL_StringToBytes('android.com:pkg')), 201 | "PAYLOAD": this.compose_AAR(entry["aar"])}); 202 | break; 203 | default: 204 | console.error("Unsupported RTD type:" + entry["type"]); 205 | break; 206 | } 207 | } 208 | 209 | for (var i = 0; i < arr.length; i++) { 210 | var flags = 0x10 | arr[i]["TNF"]; /* SR and TNF */ 211 | flags |= (i == 0) ? 0x80 : 0x00; /* MB */ 212 | flags |= (i == (arr.length - 1)) ? 0x40 : 0x00; /* ME */ 213 | 214 | var type = arr[i]["TYPE"]; 215 | var payload = arr[i]["PAYLOAD"]; 216 | out = UTIL_concat(out, [flags, type.length, payload.length]); 217 | out = UTIL_concat(out, type); 218 | out = UTIL_concat(out, payload); 219 | } 220 | 221 | return out.buffer; 222 | } 223 | 224 | 225 | /* Input: 226 | * A dictionary, with "type": 227 | * "Text": RTD Text. Require: "encoding", "lang" and "text". 228 | * "URI": RTD URI. Require: "uri". 229 | * "MIME": RFC 2046 media types. Require: "mime_type" and "payload". 230 | * "AAR": Android Application Record. Require: "aar". 231 | * 232 | * Output: 233 | * true for success. 234 | * 235 | */ 236 | NDEF.prototype.add = function(d) { 237 | // short-cut 238 | if ("uri" in d) { 239 | d["type"] = "URI"; 240 | } else if ("text" in d) { 241 | d["type"] = "TEXT"; 242 | } else if ("aar" in d) { 243 | d["type"] = "AAR"; 244 | } else if ("payload" in d) { 245 | d["type"] = "MIME"; 246 | } 247 | 248 | switch (d["type"]) { 249 | case "TEXT": 250 | case "Text": 251 | /* set default values */ 252 | if (!("encoding" in d)) { 253 | d["encoding"] = "utf8"; 254 | } 255 | if (!("lang" in d)) { 256 | d["lang"] = "en"; 257 | } 258 | 259 | if ("text" in d) { 260 | this.ndef.push(d); 261 | return true; 262 | } 263 | break; 264 | 265 | case "URI": 266 | if ("uri" in d) { 267 | this.ndef.push(d); 268 | return true; 269 | } 270 | break; 271 | 272 | case "MIME": 273 | if (("mime_type" in d) && ("payload" in d)) { 274 | this.ndef.push(d); 275 | return true; 276 | } 277 | 278 | case "AAR": 279 | if ("aar" in d) { 280 | this.ndef.push(d); 281 | return true; 282 | } 283 | break; 284 | 285 | default: 286 | console.log("Unsupported RTD type:" + d["type"]); 287 | break; 288 | } 289 | return false; 290 | } 291 | 292 | 293 | /* 294 | * Input: 295 | * type -- a byte, see RTD Type Names 296 | * rtd -- Uint8Array. 297 | * 298 | * Output: 299 | * JS structure 300 | */ 301 | NDEF.prototype.parse_RTD = function(type, rtd) { 302 | switch (type) { 303 | case 0x54: /* 'T' */ 304 | return this.parse_RTD_TEXT(rtd); 305 | case 0x55: /* 'U' */ 306 | return this.parse_RTD_URI(rtd); 307 | default: 308 | console.log("Unsupported RTD type: " + type); 309 | } 310 | } 311 | 312 | 313 | /* 314 | * Input: 315 | * mime_type -- Uint8Array. See RFC 2046. 316 | * payload -- Uint8Array. 317 | * 318 | * Output: 319 | * JS structure 320 | */ 321 | NDEF.prototype.parse_MIME = function(mime_type, payload) { 322 | return {"type": "MIME", 323 | "mime_type": UTIL_BytesToString(mime_type), 324 | "payload": UTIL_BytesToString(payload)}; 325 | } 326 | 327 | 328 | /* 329 | * Input: 330 | * mime_type and payload: string. 331 | * 332 | * Output: 333 | * rtd_text -- Uint8Array. 334 | */ 335 | NDEF.prototype.compose_MIME = function(payload) { 336 | return new Uint8Array(UTIL_StringToBytes(payload)); 337 | } 338 | 339 | 340 | /* 341 | * Input: 342 | * payload -- Uint8Array. 343 | * 344 | * Output: 345 | * JS structure 346 | */ 347 | NDEF.prototype.parse_AAR = function(payload) { 348 | return {"type": "AAR", 349 | "payload": UTIL_BytesToString(payload)}; 350 | } 351 | 352 | /* 353 | * Input: 354 | * type -- Uint8Array. 355 | * payload -- Uint8Array. 356 | * 357 | * Output: 358 | * JS structure 359 | */ 360 | NDEF.prototype.parse_ExternalType = function(type, payload) { 361 | if (UTIL_BytesToString(type) == "android.com:pkg") 362 | return this.parse_AAR(payload); 363 | else 364 | return {"type": type, 365 | "payload": UTIL_BytesToString(payload)}; 366 | } 367 | 368 | 369 | /* 370 | * Input: 371 | * payload: string. 372 | * 373 | * Output: 374 | * Uint8Array. 375 | */ 376 | NDEF.prototype.compose_AAR = function(payload) { 377 | return new Uint8Array(UTIL_StringToBytes(payload)); 378 | } 379 | 380 | 381 | /* 382 | * Input: 383 | * rtd_text -- Uint8Array. 384 | * 385 | * Output: 386 | * JS structure 387 | */ 388 | NDEF.prototype.parse_RTD_TEXT = function(rtd_text) { 389 | var utf16 = (rtd_text[0] & 0x80) >> 7; 390 | var lang_len = (rtd_text[0] & 0x3f); 391 | var lang = rtd_text.subarray(1, 1 + lang_len); 392 | var text = rtd_text.subarray(1 + lang_len, rtd_text.length); 393 | 394 | return {"type": "Text", 395 | "encoding": utf16 ? "utf16" : "utf8", 396 | "lang": UTIL_BytesToString(lang), 397 | "text": UTIL_BytesToString(text)}; 398 | } 399 | 400 | 401 | /* 402 | * Input: 403 | * Language and text (assume UTF-8 encoded). 404 | * 405 | * Output: 406 | * rtd_text -- Uint8Array. 407 | */ 408 | NDEF.prototype.compose_RTD_TEXT = function(lang, text) { 409 | var l = lang.length; 410 | l = (l > 0x3f) ? 0x3f : l; 411 | return new Uint8Array([l].concat( 412 | UTIL_StringToBytes(lang.substring(0, l))).concat( 413 | UTIL_StringToBytes(text))); 414 | } 415 | 416 | 417 | /* 418 | * Input: 419 | * rtd_uri -- Uint8Array. 420 | * 421 | * Output: 422 | * JS structure 423 | */ 424 | NDEF.prototype.parse_RTD_URI = function(rtd_uri) { 425 | return {"type": "URI", 426 | "uri": this.prepending[rtd_uri[0]] + 427 | UTIL_BytesToString(rtd_uri.subarray(1, rtd_uri.length))}; 428 | } 429 | 430 | /* 431 | * Input: 432 | * Thr URI to compose (assume UTF-8). 433 | * 434 | * Output: 435 | * Uint8Array. 436 | */ 437 | NDEF.prototype.compose_RTD_URI = function(uri) { 438 | var longest = -1; 439 | var longest_i; 440 | for (var i = 0; i < this.prepending.length; i++) { 441 | if (uri.substring(0, this.prepending[i].length) == this.prepending[i]) { 442 | if (this.prepending[i].length > longest) { 443 | longest_i = i; 444 | longest = this.prepending[i].length; 445 | } 446 | } 447 | } 448 | // assume at least longest_i matches prepending[0], which is "". 449 | 450 | return new Uint8Array([longest_i].concat( 451 | UTIL_StringToBytes(uri.substring(longest)))); 452 | } 453 | 454 | -------------------------------------------------------------------------------- /src/mifare-classic.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | /** 17 | * @fileoverview Mifare Classic driver 18 | */ 19 | 20 | 'use strict'; 21 | 22 | // TODO: support Classic 4K 23 | 24 | /* 25 | * AN1305 - MIFARE Classic as NFC Type MIFARE Classic Tag 26 | * 27 | * +--------------------+ 28 | * | Manufacturer Block | Physical Block 0 29 | * |--------------------| 30 | * | Logic block 0 | Physical Block 1 (MAD1) 31 | * Sector 0 |--------------------| 32 | * | Logic block 1 | Physical Block 2 (MAD1) 33 | * |--------------------| 34 | * | Sector Trailer | Physical Block 3 35 | * --+--------------------+ 36 | * | Logic block 2 | Physical Block 4 37 | * |--------------------| 38 | * | Logic block 3 | Physical Block 5 39 | * Sector 1 |--------------------| 40 | * | Logic block 4 | Physical Block 6 41 | * |--------------------| 42 | * | Sector Trailer | Physical Block 7 43 | * --+--------------------+ 44 | * | Logic block 5 | Physical Block 8 45 | * |--------------------| 46 | * | Logic block 6 | Physical Block 9 47 | * Sector 2 |--------------------| 48 | * | Logic block 7 | Physical Block 10 49 | * |--------------------| 50 | * | Sector Trailer | Physical Block 11 51 | * +--------------------+ 52 | * | ... | ... 53 | * 54 | * 55 | * 56 | * 57 | * 58 | */ 59 | 60 | function MifareClassic(tag_id) { 61 | this.tag_id = new Uint8Array(tag_id); 62 | this.type_name = "MIFARE Classic 1K"; 63 | 64 | this.WRITE_COMMAND = 0xA0; // differ to type 2's 0xA2. 65 | } 66 | 67 | // private functions 68 | 69 | // Logic block number to sector number 70 | MifareClassic.prototype.log2sec = function(logic_blknum) { 71 | if (logic_blknum < 2) return 0; 72 | return Math.floor((logic_blknum - 2) / 3) + 1; 73 | } 74 | 75 | // Logic block number to physical block number 76 | MifareClassic.prototype.log2phy = function(logic_blknum) { 77 | if (logic_blknum < 2) return logic_blknum + 1; 78 | 79 | var sector = this.log2sec(logic_blknum); 80 | return sector * 4 + ((logic_blknum - 2) % 3); 81 | } 82 | 83 | // input: Uint8Array 84 | MifareClassic.prototype.mif_calc_crc8 = function(input) { 85 | var crc = 0xc7; // bit-swapped 0xe3 86 | 87 | for (var i = 0; i < input.length; i++) { 88 | crc = crc ^ input[i]; 89 | 90 | for (var j = 0; j < 8; j++) { 91 | if (crc & 0x80) 92 | crc = (crc << 1) ^ 0x1d; 93 | else 94 | crc = crc << 1; 95 | } 96 | } 97 | return crc; 98 | } 99 | 100 | // input: Uint8Array 101 | MifareClassic.prototype.mif_calc_crc16 = function(input) { 102 | var crc = 0xc78c; // bit-swapped 0x31e3 103 | for (var i = 0; i < input.length; i++) { 104 | crc = crc ^ (input[i] << 8); 105 | for (var j = 0; j < 8; j++) { 106 | if (crc & 0x8000) 107 | crc = (crc << 1) ^ 0x1021; 108 | else 109 | crc = crc << 1; 110 | } 111 | } 112 | return crc; 113 | } 114 | 115 | 116 | /* Since the Key A is not readable so that we need to copy that from the 117 | * successfully authenticated key storage. 118 | * We keep key B all-0xff until one day we decide to use it. 119 | */ 120 | MifareClassic.prototype.copy_auth_keys = function(data, dev) { 121 | for (var i = 0; i < 6; i++) { 122 | data[i] = dev.auth_key[i]; 123 | } 124 | // Leave KEY B as default. TODO: don't overwrite if key B is readable. 125 | for (var i = 0; i < 6; i++) { 126 | data[i + 10] = 0xff; 127 | } 128 | 129 | return data; 130 | } 131 | 132 | 133 | MifareClassic.prototype.read_physical = function(device, phy_block, cnt, cb) { 134 | var self = this; 135 | var callback = cb; 136 | var dev = device; 137 | var readed = new Uint8Array(); // for closure 138 | var max_block = 1024 / 16; // TODO: assume Classic 1K 139 | 140 | if (cnt != null) max_block = phy_block + cnt; 141 | 142 | // Reading whole card is too long (~4secs). This function would return 143 | // a smaller max_block value if MAD is read and NDEF sectors are recognized. 144 | function fast_read(phy_block, data, max_block) { 145 | if (phy_block == 3 && data[0x39] != 0x69 ) { // personalized GBP 146 | // TODO: check CRC in MAD. 147 | var nfc_cnt; 148 | for (nfc_cnt = 0; // assume the NDEF is in the 1st sector. 149 | data[0x12 + nfc_cnt * 2 + 0] == 0x03 && 150 | data[0x12 + nfc_cnt * 2 + 1] == 0xE1; 151 | nfc_cnt++) {}; 152 | var new_num = (nfc_cnt + 1) * 4; 153 | if (new_num < max_block) 154 | return new_num; 155 | else 156 | return max_block; 157 | } else { 158 | return max_block; 159 | } 160 | } 161 | 162 | function read_next(phy_block) { 163 | var blk_no = phy_block; 164 | dev.publicAuthentication(blk_no, function(rc, data) { 165 | if (rc) return callback(rc); 166 | dev.read_block(blk_no, function(rc, bn) { 167 | if (rc) return callback(rc); 168 | var bn = new Uint8Array(bn); 169 | 170 | // copy KEY A with auth_key from device. 171 | if ((blk_no % 4) == 3) { 172 | bn = self.copy_auth_keys(bn, dev); 173 | } 174 | 175 | readed = UTIL_concat(readed, bn); 176 | 177 | max_block = fast_read(blk_no, readed, max_block); 178 | if ((blk_no + 1)>= max_block) 179 | return callback(readed); 180 | else 181 | return read_next(blk_no + 1, cb); 182 | }); 183 | }); 184 | } 185 | read_next(phy_block); 186 | } 187 | 188 | 189 | // The callback is called with cb(NDEF Uint8Array). 190 | MifareClassic.prototype.read = function(device, cb) { 191 | var self = this; 192 | if (!cb) cb = defaultCallback; 193 | var callback = cb; 194 | var card = new Uint8Array(); 195 | 196 | self.read_physical(device, 0, null, function(data) { 197 | for(var i = 0; i < Math.ceil(data.length / 16); i++) { 198 | console.log(UTIL_fmt("[DEBUG] Sector[" + UTIL_BytesToHex([i]) + "] " + 199 | UTIL_BytesToHex(data.subarray(i * 16, 200 | i * 16 + 16)))); 201 | } 202 | 203 | var GPB = data[0x39]; /* the first GPB */ 204 | if (GPB == 0x69) { 205 | console.log("[DEBUG] Sector 0 is non-personalized (0x69)."); 206 | } else { 207 | var DA = (GPB & 0x80) >> 7; // MAD available: 1 for yes. 208 | var MA = (GPB & 0x40) >> 6; // Multiapplication card: 1 for yes. 209 | var ADV = (GPB & 0x03) >> 0; // (MAD version code: 1 for v1, 2 for v2) 210 | 211 | // TODO: check CRC in MAD. 212 | var nfc_cnt; 213 | for (nfc_cnt = 0; // assume the NDEF is in the 1st sector. 214 | data[0x12 + nfc_cnt * 2 + 0] == 0x03 && 215 | data[0x12 + nfc_cnt * 2 + 1] == 0xE1; 216 | nfc_cnt++) {}; 217 | var tlv = new Uint8Array(); 218 | for(var i = 1; i <= nfc_cnt; i++) { 219 | tlv = UTIL_concat(tlv, data.subarray(i * 0x40, i * 0x40 + 0x30)); 220 | } 221 | 222 | // TODO: move to tlv.js 223 | for (var i = 0; i < tlv.length; i++) { 224 | switch (tlv[i]) { 225 | case 0x00: /* NULL */ 226 | console.log("[DEBUG] NULL TLV."); 227 | break; 228 | case 0xFE: /* Terminator */ 229 | console.log("[DEBUG] Terminator TLV."); 230 | return; 231 | case 0x03: /* NDEF */ 232 | var len = tlv[i + 1]; 233 | if ((len + 2) > tlv.length) { 234 | console.log("[WARN] Vlen:" + len + " > totla len:" + tlv.length); 235 | } 236 | return callback(0, 237 | new Uint8Array(tlv.subarray(i + 2, i + 2 + len)).buffer); 238 | /* TODO: now pass NDEF only. Support non-NDEF in the future. */ 239 | // i += len + 1; 240 | default: 241 | console.log("[ERROR] Unsupported TLV: " + UTIL_BytesToHex(tlv[0])); 242 | return; 243 | } 244 | } 245 | } 246 | }); 247 | } 248 | 249 | 250 | MifareClassic.prototype.read_logic = function(device, logic_block, cnt, cb) { 251 | var self = this; 252 | var callback = cb; 253 | var card = new Uint8Array(); 254 | 255 | function next_logic(logic_block, cnt) { 256 | var blk_no = logic_block; 257 | var count = cnt; 258 | if (count <= 0) return callback(card); 259 | self.read_physical(device, self.log2phy(logic_block), 1, function(data) { 260 | card = UTIL_concat(card, data); 261 | next_logic(blk_no + 1, count - 1); 262 | }); 263 | } 264 | next_logic(logic_block, cnt); 265 | } 266 | 267 | 268 | // TODO: support multiple data set 269 | /* Input: 270 | * ndef - Uint8Array 271 | * 272 | * Output: 273 | * Whole tag image. 274 | */ 275 | MifareClassic.prototype.compose = function(ndef) { 276 | var self = this; 277 | 278 | /* ====== Build up TLV blocks first ====== */ 279 | var ndef_tlv = new Uint8Array([ 280 | 0x03, ndef.length /* NDEF Message TLV */ 281 | ]); 282 | var terminator_tlv = new Uint8Array([ 283 | 0xfe 284 | ]); 285 | var TLV = UTIL_concat(ndef_tlv, 286 | UTIL_concat(new Uint8Array(ndef), 287 | terminator_tlv)); 288 | 289 | /* frag into sectors */ 290 | var TLV_sector_num = Math.ceil(TLV.length / 0x30); 291 | var TLV_blocks = new Uint8Array(); 292 | for (var i = 0; i < TLV_sector_num; i++) { 293 | TLV_blocks = UTIL_concat(TLV_blocks, 294 | TLV.subarray(i * 0x30, (i + 1) * 0x30)); 295 | 296 | var padding; 297 | if ((i + 1) == TLV_sector_num) { // last sector 298 | padding = new Uint8Array(0x30 - (TLV.length % 0x30)); 299 | } else { 300 | padding = new Uint8Array(0); 301 | } 302 | TLV_blocks = UTIL_concat(TLV_blocks, padding); 303 | TLV_blocks = UTIL_concat(TLV_blocks, new Uint8Array([ // Sector Trailer 304 | 0xd3, 0xf7, 0xd3, 0xf7, 0xd3, 0xf7, // NFC pub key 305 | 0x7f, 0x07, 0x88, 0x40, // access bits, GPB 306 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // KEY B 307 | ])); 308 | } 309 | 310 | /* ====== Build up MAD ====== */ 311 | var classic_header = new Uint8Array([ 312 | /* Manufacturer Block */ 313 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 314 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 315 | 316 | /* MAD1 */ 317 | 0x00, 0x00, 0x03, 0xe1, // CRC, info, AID 1 318 | 0x00, 0x00, 0x00, 0x00, // AID 2, AID 3 319 | 0x00, 0x00, 0x00, 0x00, // AID 4, AID 5 320 | 0x00, 0x00, 0x00, 0x00, // AID 6, AID 7 321 | 0x00, 0x00, 0x00, 0x00, // AID 8, AID 9 322 | 0x00, 0x00, 0x00, 0x00, // AID a, AID b 323 | 0x00, 0x00, 0x00, 0x00, // AID c, AID d 324 | 0x00, 0x00, 0x00, 0x00, // AID e, AID f 325 | 326 | /* Sector Trailer */ 327 | 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, // MAD access key 328 | 0x78, 0x77, 0x88, 0xc1, // access bits, GPB 329 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // KEY B 330 | ]); 331 | 332 | for (var i = 0; i < TLV_sector_num; i++) { 333 | classic_header[0x10 + (i + 1) * 2 + 0] = 0x03; 334 | classic_header[0x10 + (i + 1) * 2 + 1] = 0xe1; 335 | } 336 | classic_header[0x10] = 337 | self.mif_calc_crc8(classic_header.subarray(0x11, 0x30)); 338 | 339 | var ret = UTIL_concat(classic_header, TLV_blocks); 340 | return ret; 341 | } 342 | 343 | 344 | // Input: 345 | // block_no: starting physical block number 346 | // data: Uint8Array of data to write. Reminding data will be write to 347 | // next block continously. 348 | MifareClassic.prototype.write_physical = function(device, block_no, key, 349 | all_data, cb) { 350 | var dev = device; 351 | var blk_no = block_no; // for closure 352 | var data = all_data; 353 | var callback = cb; 354 | var self = this; 355 | 356 | if (data.length == 0) { return callback(0); } 357 | if (data.length < 16) { 358 | // Pad to 16 bytes 359 | data = UTIL_concat(data, new Uint8Array(16 - data.length)); 360 | } 361 | 362 | function authenticationCallback (rc, dummy) { 363 | if (rc) return callback(rc); 364 | 365 | var block_data = data.subarray(0, 16); 366 | dev.write_block(blk_no, block_data, function(rc) { 367 | if (rc) return callback(rc); 368 | self.write_physical(dev, blk_no + 1, key, data.subarray(16), callback); 369 | }, self.WRITE_COMMAND); 370 | } 371 | if (key == null) 372 | dev.publicAuthentication(blk_no, authenticationCallback); 373 | else 374 | dev.privateAuthentication(blk_no, key, authenticationCallback); 375 | } 376 | 377 | 378 | // Input: 379 | // ndef: ArrayBuffer. Just ndef is needed. Classic header is handled. 380 | MifareClassic.prototype.write = function(device, ndef, cb) { 381 | var self = this; 382 | if (!cb) cb = defaultCallback; 383 | var callback = cb; 384 | var card = self.compose(new Uint8Array(ndef)); 385 | var dev = device; 386 | 387 | var max_block = Math.ceil(card.length / 16); 388 | 389 | if (max_block > (1024 / 16)) { 390 | console.log("write Classic() card is too big (max: 1024 bytes): " + 391 | card.length); 392 | return callback(0xbbb); 393 | } 394 | 395 | /* Start from MAD */ 396 | self.write_physical(dev, 1, null, card.subarray(16), callback); 397 | } 398 | 399 | 400 | // Input: 401 | // logic_block: logic block number 402 | // data: Uint8Array of data to write. Reminding data will be write to 403 | // next block continously. 404 | // 405 | // Note that the GPB will be written to no-MAD (MA=0) to fully access 406 | // all data blocks. 407 | MifareClassic.prototype.write_logic = function(device, logic_block, 408 | all_data, cb) { 409 | var self = this; 410 | var callback = cb; 411 | 412 | 413 | function write_next(device, logic_block, all_data) { 414 | var dev = device; 415 | var blk_no = logic_block; 416 | var data = all_data; 417 | 418 | if (data.length == 0) return callback(0); 419 | 420 | self.write_physical(dev, self.log2phy(blk_no), null, 421 | data.subarray(0, 16), 422 | function(rc) { 423 | if (rc) return callback(rc); 424 | 425 | // update the corresponding GPB to 0x00. 426 | var gpb_phy = self.log2sec(blk_no) * 4 + 3; 427 | dev.read_block(gpb_phy, function(rc, gpb_data) { 428 | if (rc) return callback(rc); 429 | var gpb_data = new Uint8Array(gpb_data); 430 | gpb_data = self.copy_auth_keys(gpb_data, dev); 431 | 432 | if (gpb_phy == 3) 433 | gpb_data[0x9] = 0xc1; // the first GPB: DA=MA=1, ADV=1 434 | else 435 | gpb_data[0x9] = 0x40; // non-first GPB: MA=1. 436 | 437 | dev.write_block(gpb_phy, gpb_data, function(rc) { 438 | // move to next block 439 | blk_no = blk_no + 1; 440 | data = data.subarray(16); 441 | return write_next(dev, blk_no, data); 442 | }, self.WRITE_COMMAND); 443 | }); 444 | }); 445 | } 446 | write_next(device, logic_block, all_data); 447 | } 448 | 449 | 450 | MifareClassic.prototype.emulate = function(device, ndef_obj, timeout, cb) { 451 | /* TODO: still presents as TT2 */ 452 | var data = this.compose(new Uint8Array(ndef_obj.compose())); 453 | return device.emulate_tag(data, timeout, cb); 454 | } 455 | -------------------------------------------------------------------------------- /sample/lib/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap.js by @fat & @mdo 3 | * Copyright 2013 Twitter, Inc. 4 | * http://www.apache.org/licenses/LICENSE-2.0.txt 5 | */ 6 | !function(e){"use strict";e(function(){e.support.transition=function(){var e=function(){var e=document.createElement("bootstrap"),t={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"},n;for(n in t)if(e.style[n]!==undefined)return t[n]}();return e&&{end:e}}()})}(window.jQuery),!function(e){"use strict";var t='[data-dismiss="alert"]',n=function(n){e(n).on("click",t,this.close)};n.prototype.close=function(t){function s(){i.trigger("closed").remove()}var n=e(this),r=n.attr("data-target"),i;r||(r=n.attr("href"),r=r&&r.replace(/.*(?=#[^\s]*$)/,"")),i=e(r),t&&t.preventDefault(),i.length||(i=n.hasClass("alert")?n:n.parent()),i.trigger(t=e.Event("close"));if(t.isDefaultPrevented())return;i.removeClass("in"),e.support.transition&&i.hasClass("fade")?i.on(e.support.transition.end,s):s()};var r=e.fn.alert;e.fn.alert=function(t){return this.each(function(){var r=e(this),i=r.data("alert");i||r.data("alert",i=new n(this)),typeof t=="string"&&i[t].call(r)})},e.fn.alert.Constructor=n,e.fn.alert.noConflict=function(){return e.fn.alert=r,this},e(document).on("click.alert.data-api",t,n.prototype.close)}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=e.extend({},e.fn.button.defaults,n)};t.prototype.setState=function(e){var t="disabled",n=this.$element,r=n.data(),i=n.is("input")?"val":"html";e+="Text",r.resetText||n.data("resetText",n[i]()),n[i](r[e]||this.options[e]),setTimeout(function(){e=="loadingText"?n.addClass(t).attr(t,t):n.removeClass(t).removeAttr(t)},0)},t.prototype.toggle=function(){var e=this.$element.closest('[data-toggle="buttons-radio"]');e&&e.find(".active").removeClass("active"),this.$element.toggleClass("active")};var n=e.fn.button;e.fn.button=function(n){return this.each(function(){var r=e(this),i=r.data("button"),s=typeof n=="object"&&n;i||r.data("button",i=new t(this,s)),n=="toggle"?i.toggle():n&&i.setState(n)})},e.fn.button.defaults={loadingText:"loading..."},e.fn.button.Constructor=t,e.fn.button.noConflict=function(){return e.fn.button=n,this},e(document).on("click.button.data-api","[data-toggle^=button]",function(t){var n=e(t.target);n.hasClass("btn")||(n=n.closest(".btn")),n.button("toggle")})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.$indicators=this.$element.find(".carousel-indicators"),this.options=n,this.options.pause=="hover"&&this.$element.on("mouseenter",e.proxy(this.pause,this)).on("mouseleave",e.proxy(this.cycle,this))};t.prototype={cycle:function(t){return t||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(e.proxy(this.next,this),this.options.interval)),this},getActiveIndex:function(){return this.$active=this.$element.find(".item.active"),this.$items=this.$active.parent().children(),this.$items.index(this.$active)},to:function(t){var n=this.getActiveIndex(),r=this;if(t>this.$items.length-1||t<0)return;return this.sliding?this.$element.one("slid",function(){r.to(t)}):n==t?this.pause().cycle():this.slide(t>n?"next":"prev",e(this.$items[t]))},pause:function(t){return t||(this.paused=!0),this.$element.find(".next, .prev").length&&e.support.transition.end&&(this.$element.trigger(e.support.transition.end),this.cycle(!0)),clearInterval(this.interval),this.interval=null,this},next:function(){if(this.sliding)return;return this.slide("next")},prev:function(){if(this.sliding)return;return this.slide("prev")},slide:function(t,n){var r=this.$element.find(".item.active"),i=n||r[t](),s=this.interval,o=t=="next"?"left":"right",u=t=="next"?"first":"last",a=this,f;this.sliding=!0,s&&this.pause(),i=i.length?i:this.$element.find(".item")[u](),f=e.Event("slide",{relatedTarget:i[0],direction:o});if(i.hasClass("active"))return;this.$indicators.length&&(this.$indicators.find(".active").removeClass("active"),this.$element.one("slid",function(){var t=e(a.$indicators.children()[a.getActiveIndex()]);t&&t.addClass("active")}));if(e.support.transition&&this.$element.hasClass("slide")){this.$element.trigger(f);if(f.isDefaultPrevented())return;i.addClass(t),i[0].offsetWidth,r.addClass(o),i.addClass(o),this.$element.one(e.support.transition.end,function(){i.removeClass([t,o].join(" ")).addClass("active"),r.removeClass(["active",o].join(" ")),a.sliding=!1,setTimeout(function(){a.$element.trigger("slid")},0)})}else{this.$element.trigger(f);if(f.isDefaultPrevented())return;r.removeClass("active"),i.addClass("active"),this.sliding=!1,this.$element.trigger("slid")}return s&&this.cycle(),this}};var n=e.fn.carousel;e.fn.carousel=function(n){return this.each(function(){var r=e(this),i=r.data("carousel"),s=e.extend({},e.fn.carousel.defaults,typeof n=="object"&&n),o=typeof n=="string"?n:s.slide;i||r.data("carousel",i=new t(this,s)),typeof n=="number"?i.to(n):o?i[o]():s.interval&&i.pause().cycle()})},e.fn.carousel.defaults={interval:5e3,pause:"hover"},e.fn.carousel.Constructor=t,e.fn.carousel.noConflict=function(){return e.fn.carousel=n,this},e(document).on("click.carousel.data-api","[data-slide], [data-slide-to]",function(t){var n=e(this),r,i=e(n.attr("data-target")||(r=n.attr("href"))&&r.replace(/.*(?=#[^\s]+$)/,"")),s=e.extend({},i.data(),n.data()),o;i.carousel(s),(o=n.attr("data-slide-to"))&&i.data("carousel").pause().to(o).cycle(),t.preventDefault()})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=e.extend({},e.fn.collapse.defaults,n),this.options.parent&&(this.$parent=e(this.options.parent)),this.options.toggle&&this.toggle()};t.prototype={constructor:t,dimension:function(){var e=this.$element.hasClass("width");return e?"width":"height"},show:function(){var t,n,r,i;if(this.transitioning||this.$element.hasClass("in"))return;t=this.dimension(),n=e.camelCase(["scroll",t].join("-")),r=this.$parent&&this.$parent.find("> .accordion-group > .in");if(r&&r.length){i=r.data("collapse");if(i&&i.transitioning)return;r.collapse("hide"),i||r.data("collapse",null)}this.$element[t](0),this.transition("addClass",e.Event("show"),"shown"),e.support.transition&&this.$element[t](this.$element[0][n])},hide:function(){var t;if(this.transitioning||!this.$element.hasClass("in"))return;t=this.dimension(),this.reset(this.$element[t]()),this.transition("removeClass",e.Event("hide"),"hidden"),this.$element[t](0)},reset:function(e){var t=this.dimension();return this.$element.removeClass("collapse")[t](e||"auto")[0].offsetWidth,this.$element[e!==null?"addClass":"removeClass"]("collapse"),this},transition:function(t,n,r){var i=this,s=function(){n.type=="show"&&i.reset(),i.transitioning=0,i.$element.trigger(r)};this.$element.trigger(n);if(n.isDefaultPrevented())return;this.transitioning=1,this.$element[t]("in"),e.support.transition&&this.$element.hasClass("collapse")?this.$element.one(e.support.transition.end,s):s()},toggle:function(){this[this.$element.hasClass("in")?"hide":"show"]()}};var n=e.fn.collapse;e.fn.collapse=function(n){return this.each(function(){var r=e(this),i=r.data("collapse"),s=e.extend({},e.fn.collapse.defaults,r.data(),typeof n=="object"&&n);i||r.data("collapse",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.collapse.defaults={toggle:!0},e.fn.collapse.Constructor=t,e.fn.collapse.noConflict=function(){return e.fn.collapse=n,this},e(document).on("click.collapse.data-api","[data-toggle=collapse]",function(t){var n=e(this),r,i=n.attr("data-target")||t.preventDefault()||(r=n.attr("href"))&&r.replace(/.*(?=#[^\s]+$)/,""),s=e(i).data("collapse")?"toggle":n.data();n[e(i).hasClass("in")?"addClass":"removeClass"]("collapsed"),e(i).collapse(s)})}(window.jQuery),!function(e){"use strict";function r(){e(".dropdown-backdrop").remove(),e(t).each(function(){i(e(this)).removeClass("open")})}function i(t){var n=t.attr("data-target"),r;n||(n=t.attr("href"),n=n&&/#/.test(n)&&n.replace(/.*(?=#[^\s]*$)/,"")),r=n&&e(n);if(!r||!r.length)r=t.parent();return r}var t="[data-toggle=dropdown]",n=function(t){var n=e(t).on("click.dropdown.data-api",this.toggle);e("html").on("click.dropdown.data-api",function(){n.parent().removeClass("open")})};n.prototype={constructor:n,toggle:function(t){var n=e(this),s,o;if(n.is(".disabled, :disabled"))return;return s=i(n),o=s.hasClass("open"),r(),o||("ontouchstart"in document.documentElement&&e('