├── .gitignore ├── .npmignore ├── README.md ├── examples └── security-system │ ├── README.md │ └── index.js ├── index.js ├── lib ├── appid-check.js ├── browser-api.js ├── u2f-client.js ├── u2f-device.js └── u2f-hid-device.js ├── package.json └── test ├── main-test.js └── mocha.opts /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | wiki 3 | *.sublime-* 4 | examples/security-system/keys.json 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | wiki 3 | *.sublime-* 4 | 5 | examples 6 | test 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # U2F-Client: Access U2F/USB keys directly, without browser 2 | 3 | For a general description, please see [Universal Second Factor authentication](https://fidoalliance.org/specifications/). 4 | This module provides FIDO client functionality: it provides access to available 5 | USB/HID hardware keys (role that usually filled by a web browser). 6 | 7 | Using this module, you can build a hardware security system with U2F keys authentication, or provide 8 | U2F interface in browser emulation. 9 | 10 | To create and check register/sign requests, please see other module: [u2f](https://github.com/ashtuchkin/u2f). 11 | 12 | ## Features 13 | * Straightforward, node.js style API: callbacks & events. 14 | * Supports all OS-es via excellent node-hid module. 15 | * Supports standard U2F Javascript API for browsers, checking facetId etc. 16 | 17 | ## Usage 18 | ```javascript 19 | var u2fc = require('u2f-client'); 20 | 21 | // High-level API 22 | // registerRequest and signRequest, as well as returned values are specified in U2F documentation 23 | u2fc.register(registerRequest, cb) // Register U2F device, requires user presence 24 | u2fc.sign(signRequest, cb) // Signs with U2F device, requires user presence 25 | u2fc.check(signRequest, cb) // Check if signRequest is acceptable, doesn't require user presence. Returns true or false. 26 | u2fc.devices() // Returns array of connected deviceInfo-s. 27 | 28 | // Events 29 | u2fc.on 'waiting-for-device' // Request user to insert a U2F device, as there's no devices found 30 | u2fc.on 'user-presence-required' // Request user to touch the U2F device (issued after register or sign requests) 31 | 32 | 33 | 34 | // U2F Javascript API for Web Browsers 35 | // Careful, the callback convention is different - both error and result come as first and only argument. 36 | // Origin of the requesting party must be provided and will be checked according to the spec rules. 37 | browserU2F = u2fc.browserApi(origin) 38 | browserU2F.register(registerRequests, signRequests, callback, opt_timeout) 39 | browserU2F.sign(signRequests, callback, opt_timeout) 40 | 41 | 42 | // Options 43 | u2fc.waitForDevicesTimeout = 10000 // How much time should we wait if no device present. 44 | u2fc.userPresenceTimeout = 10000 // How much time to wait for pressing the button on device. 45 | 46 | ``` 47 | 48 | ## Examples 49 | * [REPL security system](https://github.com/ashtuchkin/u2f-client/tree/master/examples/security-system) 50 | 51 | ### TODO 52 | * it seems linux doesn't provide usage & usagePage information -> use hardcoded vendorId/productId table? 53 | * provide more consistency with respect to concurrent usage at the driver level (login+insert key fails), + don't close device every time. 54 | * provide 'disconnected' event on client. 55 | * test U2FClient, browser api. 56 | * comprehensive timeouts: low-level and BrowserAPI 57 | * command-line interface? (u2f sign, u2f register) maybe another module? 58 | 59 | 60 | License: MIT 61 | -------------------------------------------------------------------------------- /examples/security-system/README.md: -------------------------------------------------------------------------------- 1 | # Example security system using U2F hardware keys. 2 | 3 | This system is a REPL that emulates a security system. 4 | It tries to authorize user when the key is inserted to USB port. 5 | Prints == ACCESS GRANTED == when user is authenticated. 6 | User keys are kept in `keys.json` file. 7 | 8 | To run: `node index.js` (don't forget to `npm install`) 9 | 10 | ``` 11 | Commands available: 12 | help Prints this message 13 | register Registers given user with currently connected device 14 | login Try to log in as a given user 15 | remove Clears access for given user 16 | users Prints registered users 17 | devices Prints currently connected devices 18 | ``` 19 | -------------------------------------------------------------------------------- /examples/security-system/index.js: -------------------------------------------------------------------------------- 1 | var u2fc = require('../../'), 2 | u2f = require('u2f'), 3 | fs = require('fs'), 4 | path = require('path'), 5 | repl = require('repl'), 6 | util = require('util'), 7 | async = require('async'), 8 | appId = 'node:u2f-client:examples:security-system', 9 | keyFile = path.join(__dirname, 'keys.json'), 10 | keys = []; 11 | 12 | try { 13 | keys = JSON.parse(fs.readFileSync(keyFile)); 14 | } 15 | catch (e) {} 16 | 17 | function saveKeys() { 18 | fs.writeFileSync(keyFile, JSON.stringify(keys, undefined, 2)); 19 | } 20 | 21 | // Handle user interaction requests. 22 | u2fc.on('user-presence-required', function() { 23 | console.log(" -- Please touch the key"); 24 | }); 25 | 26 | u2fc.on('waiting-for-device', function() { 27 | console.log(" -- Please insert U2F device. Waiting "+u2fc.waitForDevicesTimeout/1000+" sec."); 28 | }); 29 | 30 | // Capture and handle device connect/disconnect events. 31 | function deviceConnected(device) { 32 | console.log("\n -- U2F device/key connected"); 33 | async.filterSeries(keys, function(key, cb) { 34 | u2fc.check(u2f.request(appId, key.keyHandle), function(err, res) { 35 | if (err) { 36 | console.error(err); 37 | return cb(false); 38 | } 39 | cb(res); 40 | }); 41 | 42 | }, function(approvedKeys) { 43 | if (approvedKeys.length == 0) { 44 | console.log(" -- No user keys found. Register with 'register '."); 45 | 46 | } else if (approvedKeys.length == 1) { 47 | var key = approvedKeys[0]; 48 | console.log(" -- Key is registered for user '"+key.user+"'. Trying to log in."); 49 | var req = u2f.request(appId, key.keyHandle); 50 | u2fc.sign(req, function(err, resp) { 51 | if (err) return console.error(err); 52 | var data = u2f.checkSignature(req, resp, key.publicKey); 53 | if (!data.successful) { 54 | console.log(" == ACCESS DENIED == "); 55 | console.log(data.errorMessage); 56 | } else { 57 | console.log(" == ACCESS GRANTED for user "+key.user+" == ") 58 | } 59 | }); 60 | 61 | } else { 62 | console.log(" -- Key is registered for users: "+ approvedKeys.map(function(k) {return k.user}).join(", ")); 63 | console.log(" -- Type 'login ' with one of these usernames to get access."); 64 | } 65 | }); 66 | } 67 | 68 | function deviceDisconnected(deviceId) { 69 | console.log("\n -- U2F device/key disconnected"); 70 | } 71 | 72 | // Poll for changes in devices array. 73 | var devicesSeen = {}; 74 | setInterval(function() { 75 | var devices = u2fc.devices(); 76 | for (var i = 0; i < devices.length; i++) { 77 | var id = devices[i].id; 78 | if (!devicesSeen[id]) 79 | setTimeout(deviceConnected, 0, devices[i]); 80 | else 81 | delete devicesSeen[id]; 82 | } 83 | for (var k in devicesSeen) 84 | deviceDisconnected(k); 85 | 86 | devicesSeen = {}; 87 | for (var i = 0; i < devices.length; i++) 88 | devicesSeen[devices[i].id] = true; 89 | }, 200); 90 | 91 | 92 | // Launch the REPL. 93 | console.log("Welcome to U2F Security System example. Insert U2F key and touch it to get access granted."); 94 | console.log("Type 'register ' to register currently inserted U2F device as belonging to given user."); 95 | console.log("Type 'help' for other commands. Ctrl-D to exit."); 96 | console.log("Registration data is kept in 'keys.json' file."); 97 | 98 | repl.start({ 99 | eval: function(cmd, context, filename, cb) { 100 | cmd = cmd.slice(1, -2).split(' ').filter(Boolean); 101 | if (cmd.length === 0) { 102 | cb(); 103 | 104 | } else if (cmd[0] === 'register' && cmd[1]) { 105 | var user = cmd[1]; 106 | 107 | // Create registration request using U2F client module and send to device. 108 | var registerRequest = u2f.request(appId); 109 | u2fc.register(registerRequest, function(err, resp) { 110 | if (err) return cb(err); 111 | 112 | // Check response is valid. 113 | var keyData = u2f.checkRegistration(registerRequest, resp); 114 | if (!keyData.successful) 115 | return cb(new Error(keyData.errorMessage)); 116 | 117 | keys.push({ 118 | user: user, 119 | keyHandle: keyData.keyHandle, 120 | publicKey: keyData.publicKey, 121 | }); 122 | saveKeys(); 123 | console.log('User '+user+' registered successfully'); 124 | cb(); 125 | }); 126 | 127 | } else if (cmd[0] === 'login' && cmd[1]) { 128 | var user = cmd[1]; 129 | var userKeys = keys.filter(function(key) {return key.user === user;}); 130 | if (userKeys.length == 0) { 131 | console.log("Unknown user."); 132 | return cb(); 133 | } 134 | async.filterSeries(userKeys, function(key, cb) { 135 | u2fc.check(u2f.request(appId, key.keyHandle), function(err, res) { 136 | if (err) { 137 | console.error(err); 138 | return cb(false); 139 | } 140 | cb(res); 141 | }); 142 | }, function(approvedKeys) { 143 | if (approvedKeys.length == 0) { 144 | console.log("No applicable keys found."); 145 | return cb(); 146 | } 147 | var key = approvedKeys[0]; 148 | var req = u2f.request(appId, key.keyHandle); 149 | u2fc.sign(req, function(err, resp) { 150 | if (err) return cb(err); 151 | var data = u2f.checkSignature(req, resp, key.publicKey); 152 | if (!data.successful) { 153 | console.log(" == ACCESS DENIED == "); 154 | console.log(data.errorMessage); 155 | } else { 156 | console.log(" == ACCESS GRANTED for user "+key.user+" == ") 157 | } 158 | }); 159 | }); 160 | 161 | } else if (cmd[0] === 'remove' && cmd[1]) { 162 | var user = cmd[1]; 163 | var newKeys = keys.filter(function(key) {return key.user !== user;}); 164 | if (newKeys.length == keys.length) { 165 | console.log("No keys for user '"+user+"' found."); 166 | } else { 167 | console.log((keys.length-newKeys.length)+" keys removed."); 168 | keys = newKeys; 169 | saveKeys(); 170 | } 171 | cb(); 172 | 173 | } else if (cmd[0] === 'help') { 174 | console.log("Commands available:"); 175 | console.log(" help Prints this message"); 176 | console.log(" register Registers given user with currently connected device"); 177 | console.log(" login Try to log in as a given user"); 178 | console.log(" remove Clears access for given user"); 179 | console.log(" users Prints registered users"); 180 | console.log(" devices Prints currently connected devices"); 181 | cb(); 182 | 183 | } else if (cmd[0] === 'users') { 184 | var users = {}; 185 | for (var i = 0; i < keys.length; i++) 186 | users[keys[i].user] = true; 187 | console.log("Registered users: "+(Object.keys(users).join(", ") || 'none')); 188 | cb(); 189 | 190 | } else if (cmd[0] === 'devices') { 191 | cb(null, u2fc.devices()); 192 | 193 | } else { 194 | cb(null, "Unknown command. Type 'help' to get all available commands."); 195 | } 196 | }, 197 | ignoreUndefined: true, 198 | 199 | }).on('exit', function() { 200 | console.log(); 201 | process.exit(); 202 | }); 203 | 204 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | var U2FClient = require('./lib/u2f-client.js'); 3 | 4 | module.exports = new U2FClient(); 5 | 6 | module.exports.U2FClient = U2FClient; 7 | module.exports.U2FDevice = require('./lib/u2f-device.js'); 8 | module.exports.U2FHIDDevice = require('./lib/u2f-hid-device.js'); 9 | module.exports.BrowserApi = require('./lib/browser-api.js'); 10 | -------------------------------------------------------------------------------- /lib/appid-check.js: -------------------------------------------------------------------------------- 1 | 2 | var psl = require('psl'), 3 | url = require('url'), 4 | async = require('async'), 5 | request = require('request'); 6 | 7 | 8 | // Checks all appIds in given requests are compatible with given facetId (origin). 9 | // See https://fidoalliance.org/specs/fido-appid-and-facets-v1.0-rd-20141008.pdf (Section 3.1.2) 10 | // facetId is either a web origin uri with empty path and default port removed (http://example.com/), 11 | // or a custom scheme uri for mobile applications. 12 | module.exports = function checkAppIds(facetId, version, requests, cb) { 13 | if (!facetId) 14 | return cb(new Error("missing facetId")); 15 | 16 | if (!requests) 17 | return cb(new Error("missing requests")); 18 | if (requests.length == 0) 19 | return cb(null, true); 20 | 21 | async.every(requests, function(request, cb) { 22 | var appId = request.appId; 23 | var app = url.parse(appId); 24 | var facet = url.parse(facetId); 25 | 26 | if (app.protocol !== 'https:' && appId === facetId) 27 | return cb(true); // Trivial match 28 | 29 | if (!appId) { 30 | request.appId = facetId; 31 | return cb(true); // Set appId when it's not set. 32 | } 33 | 34 | if (facet.protocol === 'https:' && facet.hostname === app.hostname) 35 | return cb(true); // Same host names and secure protocol. 36 | 37 | if (app.protocol !== 'https:') 38 | return cb(false); // We don't fetch non-secure resources. 39 | 40 | // Request appId and check it has facetId mentioned. 41 | request({ 42 | url: appId, json: true, strictSSL: true, timeout: 3000, 43 | redirect: function(resp) { 44 | return resp.headers["FIDO-AppID-Redirect-Authorized".toLowerCase()] == 'true'; 45 | }, 46 | }, function(err, resp, body) { 47 | if (err || resp.statusCode !== 200) 48 | return cb(false); 49 | if (resp.headers['content-type'] != 'application/fido.trusted-apps+json') 50 | return cb(false); 51 | if (!body || !body.trustedFacets || !body.trustedFacets.length) 52 | return cb(false); 53 | 54 | var trustedFacets = Array.prototype.filter.call(body.trustedFacets, function(tf) { 55 | return tf.version == version || (tf.version && tf.version.major == version); 56 | })[0]; 57 | if (!trustedFacets || !trustedFacets.ids || !trustedFacets.ids.length) 58 | return cb(false); 59 | 60 | for (var i = 0; i < trustedFacets.ids.length; i++) { 61 | var id = trustedFacets.ids[i]; 62 | var parsed = url.parse(id); 63 | if (parsed.protocol === 'https:') { 64 | // Remove any path, query, etc. 65 | var id = url.format({protocol: parsed.protocol, hostname: parsed.hostname, port: parsed.port, path: '/'}); 66 | if (etldplus1(id) === etldplus1(appId) && id === facetId) 67 | return cb(true); 68 | } 69 | else if (parsed.protocol !== 'http:') { // other protocols. 70 | if (id === facetId) 71 | return cb(true); 72 | } 73 | } 74 | cb(false); 75 | }); 76 | 77 | }, function(res) { 78 | if (res) cb(null, true); 79 | else cb(new Error('bad appId')); 80 | }); 81 | } 82 | 83 | function etldplus1(uri) { 84 | var domain = url.parse(uri, false, true).hostname; 85 | var parsed = psl.parse(domain); 86 | return parsed.sld+'.'+parsed.tld; 87 | } 88 | 89 | -------------------------------------------------------------------------------- /lib/browser-api.js: -------------------------------------------------------------------------------- 1 | var async = require('async'), 2 | appIdCheck = require('./appid-check'), 3 | U2FDevice = require('./u2f-device'); 4 | 5 | var BrowserApi = module.exports = function BrowserApi(client, origin) { 6 | this.client = client; 7 | this.origin = origin; 8 | } 9 | 10 | var ErrorCodes = BrowserApi.ErrorCodes = { 11 | OTHER_ERROR: 1, 12 | BAD_REQUEST: 2, 13 | CONFIGURATION_UNSUPPORTED: 3, 14 | DEVICE_INELIGIBLE: 4, 15 | TIMEOUT: 5, 16 | }; 17 | 18 | 19 | BrowserApi.prototype.register = function register(registerRequests, signRequests, callback, opt_timeoutSeconds) { 20 | if (!registerRequests || !registerRequests.length) 21 | return callback(webError(ErrorCodes.BAD_REQUEST, "No register request objects found")); 22 | 23 | var client = this.client, that = this; 24 | registerRequests = registerRequests.filter(function(req) {return req.version == client.protocolVersion; }); 25 | signRequests =(signRequests||[]).filter(function(req) {return req.version == client.protocolVersion; }); 26 | 27 | // Check & apply sign requests 28 | if (signRequests.length > 0) { 29 | appIdCheck(this.origin, client.protocolVersion, signRequests, function(err) { 30 | if (err) return callback(webError(ErrorCodes.BAD_REQUEST, err.message)); 31 | async.mapSeries(signRequests, client.check.bind(client), function(err, res) { 32 | if (err) return callback(toWebError(err)); 33 | if (res.some(Boolean)) return callback(webError(ErrorCodes.DEVICE_INELIGIBLE, "Register request invalid: signRequests contains valid key.")); 34 | 35 | // No valid sign requests found. Retry without them. 36 | that.register(registerRequests, [], callback, opt_timeout); 37 | }); 38 | }); 39 | return; 40 | } 41 | 42 | // Attempt register operation. 43 | if (!registerRequests.length) 44 | return callback(webError(ErrorCodes.CONFIGURATION_UNSUPPORTED, "No register requests with supported u2f versions found")); 45 | 46 | // Only first register request is tried. 47 | registerRequests.length = 1; 48 | appIdCheck(this.origin, client.protocolVersion, registerRequests, function(err) { 49 | if (err) return callback(toWebError(err)); 50 | client.register(registerRequests[0], function(err, data) { 51 | if (err) return callback(toWebError(err)); 52 | callback(data); 53 | }); 54 | }); 55 | } 56 | 57 | 58 | BrowserApi.prototype.sign = function sign(signRequests, callback, opt_timeoutSeconds) { 59 | if (!signRequests || !signRequests.length) 60 | return callback(webError(ErrorCodes.BAD_REQUEST, "No sign request objects found")); 61 | 62 | var client = this.client, that = this; 63 | signRequests = signRequests.filter(function(req) {return req.version == client.protocolVersion; }); 64 | 65 | if (signRequests.length == 0) 66 | return callback(webError(ErrorCodes.DEVICE_INELIGIBLE, "No applicable sign requests given")); 67 | 68 | // Check & apply sign requests 69 | appIdCheck(this.origin, client.protocolVersion, signRequests, function(err) { 70 | if (err) return callback(webError(ErrorCodes.BAD_REQUEST, err.message)); 71 | 72 | var _err, _res; 73 | async.detectSeries(signRequests, function(req, cb) { 74 | client.sign(req, function(err, res) { 75 | if (err && err.code === U2FDevice.ErrorCodes.SW_WRONG_DATA) 76 | return cb(false); 77 | if (err) 78 | return _err = err, cb(true); 79 | _res = res; 80 | cb(true); 81 | }); 82 | }, function(res) { 83 | if (res) { 84 | if (_err) return callback(toWebError(_err)); 85 | callback(_res); 86 | } 87 | else 88 | return callback(webError(ErrorCodes.DEVICE_INELIGIBLE, "Sign request invalid: no valid keyHandles supplied.")); 89 | }); 90 | }); 91 | } 92 | 93 | function webError(code, message) { 94 | return { 95 | errorCode: code || ErrorCodes.OTHER_ERROR, 96 | errorMessage: message, 97 | }; 98 | } 99 | 100 | 101 | function toWebError(err) { 102 | switch (err.code) { 103 | case U2FDevice.ErrorCodes.SW_WRONG_LENGTH, 104 | U2FDevice.ErrorCodes.SW_WRONG_DATA: 105 | return webError(ErrorCodes.BAD_REQUEST); 106 | 107 | case U2FDevice.ErrorCodes.SW_CONDITIONS_NOT_SATISFIED: 108 | return webError(ErrorCodes.TIMEOUT); 109 | 110 | default: 111 | if (/Timed out/.test(err.message)) 112 | return webError(ErrorCodes.TIMEOUT, err.message); 113 | else 114 | return webError(ErrorCodes.OTHER_ERROR, err.message); 115 | } 116 | } 117 | 118 | 119 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /lib/u2f-client.js: -------------------------------------------------------------------------------- 1 | 2 | var EventEmitter = require('events').EventEmitter, 3 | util = require('util'), 4 | async = require('async'), 5 | U2FHIDDevice = require('./u2f-hid-device'), 6 | U2FDevice = require('./u2f-device'), 7 | browserApi = require('./browser-api'); 8 | 9 | 10 | var U2FClient = module.exports = function U2FClient(options) { 11 | EventEmitter.call(this); 12 | this.drivers = [U2FHIDDevice]; 13 | this.waitForDevicesTimeout = 10000; 14 | this.userPresenceTimeout = 10000; 15 | this.protocolVersion = "U2F_V2"; // Todo: get it from device. 16 | } 17 | 18 | util.inherits(U2FClient, EventEmitter); 19 | 20 | U2FClient.prototype.devices = function(acceptFn) { 21 | if (Date.now() - (this._deviceCacheTime || 0) < 50) 22 | return this._deviceCache; 23 | 24 | var that = this; 25 | this._deviceCache = Array.prototype.concat.apply([], 26 | this.drivers.map(function(driver) { 27 | return driver.enumerate(that.acceptFn.bind(that, driver)) 28 | .map(function(deviceInfo) { 29 | Object.defineProperty(deviceInfo, '_driver', {value: driver}); // Non-enumerable to avoid util.inspect() 30 | return deviceInfo; 31 | }); 32 | }) 33 | ); 34 | this._deviceCacheTime = Date.now(); 35 | return this._deviceCache; 36 | } 37 | 38 | U2FClient.prototype.acceptFn = function(driver, deviceInfo, defAccept) { 39 | return defAccept; 40 | } 41 | 42 | U2FClient.prototype.register = function(req, cb) { 43 | this._doWithDevices(function(device, cb) { 44 | device.register(req, cb); 45 | }, cb); 46 | } 47 | 48 | U2FClient.prototype.sign = function(req, cb) { 49 | this._doWithDevices(function(device, cb) { 50 | device.authenticate(req, cb); 51 | }, cb); 52 | } 53 | 54 | // Returns if current key(s) can sign provided request. 55 | U2FClient.prototype.check = function(req, cb) { 56 | var that = this; 57 | this.waitForDevices(function(err, devices) { 58 | if (err) return cb(err); 59 | async.map(devices, function(deviceInfo, cb) { 60 | deviceInfo._driver.open(deviceInfo, function(err, rawdevice) { 61 | if (err) return cb(err); 62 | var device = new U2FDevice(rawdevice); 63 | device.checkKeyRecognized(req, function(err, resp) { 64 | device.close(); 65 | cb(err, resp); 66 | }); 67 | }); 68 | }, function(err, resp) { 69 | if (err) return cb(err); 70 | cb(null, resp.some(Boolean)); 71 | }); 72 | }); 73 | } 74 | 75 | U2FClient.prototype.browserApi = function(origin) { 76 | return new BrowserApi(this, origin); 77 | } 78 | 79 | 80 | U2FClient.prototype._doWithDevices = function(fn, cb) { 81 | var that = this, 82 | eventEmitted = false; 83 | this.waitForDevices(function(err, devices) { 84 | if (err) return cb(err); 85 | var _err, results = {}; 86 | async.detect(devices, function(deviceInfo, cb) { 87 | deviceInfo._driver.open(deviceInfo, function(err, rawdevice) { 88 | if (err) return _err = err, cb(false); 89 | var device = new U2FDevice(rawdevice); 90 | device.interactionTimeout = that.userPresenceTimeout; 91 | device.on('user-presence-required', function() { 92 | if (!eventEmitted) { 93 | that.emit('user-presence-required'); 94 | eventEmitted = true; 95 | } 96 | }); 97 | fn(device, function(err, resp) { 98 | device.close(); 99 | if (err) return _err = err, cb(false); 100 | results[deviceInfo.id] = resp; 101 | cb(true); 102 | }) 103 | }); 104 | }, function(deviceInfo) { 105 | if (deviceInfo) cb(null, results[deviceInfo.id]); 106 | else cb(_err); 107 | }); 108 | }); 109 | } 110 | 111 | U2FClient.prototype.waitForDevices = function(cb, timeout) { 112 | if (typeof timeout === 'undefined') 113 | timeout = this.waitForDevicesTimeout; 114 | 115 | var startTime = Date.now(), 116 | that = this, 117 | eventEmitted = false; 118 | (function poll() { 119 | var devices = that.devices(); 120 | if (devices.length > 0) 121 | return cb(null, devices); 122 | 123 | if (Date.now() - startTime > that.waitForDevicesTimeout) 124 | return cb(new Error('Timed out waiting for U2F device')); 125 | 126 | if (!eventEmitted) { 127 | that.emit('waiting-for-device'); 128 | eventEmitted = true; 129 | } 130 | 131 | setTimeout(poll, that.waitForDevicesPollInterval || 200); 132 | })(); 133 | } 134 | 135 | -------------------------------------------------------------------------------- /lib/u2f-device.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var crypto = require('crypto'), 4 | events = require('events'), 5 | util = require('util'); 6 | 7 | // U2F Device raw interface. 8 | // https://fidoalliance.org/specs/fido-u2f-raw-message-formats-v1.0-rd-20141008.pdf 9 | var U2FDevice = module.exports = function U2FDevice(driver) { 10 | events.EventEmitter.call(this); 11 | 12 | this.driver = driver; 13 | if (driver.protocolVersion != 2) 14 | throw new Error("Driver has unsupported protocol version: "+driver.protocolVersion+". Only 2 suported.") 15 | 16 | this.driver.on('disconnected', this._onDisconnected.bind(this)); 17 | } 18 | 19 | util.inherits(U2FDevice, events.EventEmitter); 20 | 21 | U2FDevice.prototype._onDisconnected = function() { 22 | this.emit('disconnected'); 23 | } 24 | 25 | U2FDevice.prototype.close = function() { 26 | this.driver.close(); 27 | } 28 | 29 | U2FDevice.prototype.interactionTimeout = 30000; 30 | U2FDevice.prototype.interactionPollInterval = 200; 31 | 32 | var ErrorCodes = U2FDevice.ErrorCodes = { 33 | SW_NO_ERROR: 0x9000, 34 | SW_WRONG_LENGTH: 0x6700, 35 | SW_CONDITIONS_NOT_SATISFIED: 0x6985, 36 | SW_WRONG_DATA: 0x6a80, 37 | SW_INS_NOT_SUPPORTED: 0x6d00, 38 | }; 39 | 40 | 41 | // Send raw U2F Command using APDU message exchange protocol. 42 | // p1, p2 and data args are optional. 43 | U2FDevice.prototype.command = function(cmd, p1, p2, data, cb) { 44 | if (!cb) { cb = data; data = p2; p2 = 0; } 45 | if (!cb) { cb = data; data = p1; p1 = 0; } 46 | if (!cb) { cb = data; data = new Buffer(0); } 47 | 48 | // Create APDU Request frame 49 | var buf = new Buffer(data.length+7); 50 | buf[0] = 0; // CLA 51 | buf[1] = cmd; // INS 52 | buf[2] = p1; // P1 53 | buf[3] = p2; // P2 54 | buf[4] = 0; // LC1 (MSB) 55 | buf.writeUInt16BE(data.length, 5); // LC2, LC3 (LSB) 56 | data.copy(buf, 7); 57 | 58 | var that = this, 59 | startTime = Date.now(), 60 | userPresenceEventSent = false; 61 | 62 | (function sendCommand() { 63 | // Send command to the driver. 64 | that.driver.msg(buf, function(err, res) { 65 | if (err) return cb(err); 66 | if (res.length < 2) return cb(new Error("Cannot decode APDU: returned data too short.")); 67 | 68 | // Decode APDU frame status 69 | var status = res.readUInt16BE(res.length-2); 70 | 71 | if (status == ErrorCodes.SW_NO_ERROR) { // Success; return data 72 | cb(null, res.slice(0, -2)); 73 | 74 | } else if (status == ErrorCodes.SW_CONDITIONS_NOT_SATISFIED 75 | && !(p1 & 0x04) && (Date.now() - startTime < that.interactionTimeout)) { 76 | // We need user presence, but don't have it. 77 | // Wink and retry. 78 | if (!userPresenceEventSent) { 79 | that.emit('user-presence-required'); 80 | userPresenceEventSent = true; 81 | } 82 | that.driver.wink(); 83 | setTimeout(sendCommand, that.interactionPollInterval); 84 | 85 | } else { 86 | var message; 87 | for (var name in ErrorCodes) 88 | if (ErrorCodes[name] === status) 89 | message = name; 90 | if (!message) 91 | message = "SW_UNKNOWN_ERROR: 0x"+status.toString(16) 92 | var err = new Error(message); 93 | err.code = status; 94 | return cb(err); 95 | } 96 | }); 97 | })(); 98 | } 99 | 100 | 101 | // Raw U2F commands 102 | U2FDevice.U2F_REGISTER = 0x01; // Registration command 103 | U2FDevice.U2F_AUTHENTICATE = 0x02; // Authenticate/sign command 104 | U2FDevice.U2F_VERSION = 0x03; // Read version string command 105 | 106 | U2FDevice.U2F_VENDOR_FIRST = 0xC0; 107 | U2FDevice.U2F_VENDOR_LAST = 0xFF; 108 | 109 | U2FDevice.U2F_AUTH_ENFORCE = 0x03; // Enforce user presence and sign 110 | U2FDevice.U2F_AUTH_CHECK_ONLY = 0x07; // Check only 111 | 112 | 113 | U2FDevice.prototype.version = function(cb) { 114 | this.command(U2FDevice.U2F_VERSION, cb); 115 | } 116 | 117 | U2FDevice.prototype.register = function(req, callback) { 118 | var clientData = JSON.stringify({ 119 | typ: "navigator.id.finishEnrollment", 120 | challenge: req.challenge, 121 | origin: req.appId, // We use appId as origin as differentiation doesn't make sense here. 122 | }); 123 | 124 | var buf = Buffer.concat([hash(clientData), hash(req.appId)]); 125 | this.command(U2FDevice.U2F_REGISTER, buf, function(err, data) { 126 | if (err) 127 | return callback(err); 128 | 129 | callback(null, { 130 | registrationData: websafeBase64(data), 131 | clientData: websafeBase64(clientData) 132 | }); 133 | }); 134 | } 135 | 136 | U2FDevice.prototype.authenticate = function(req, callback) { 137 | var clientData = JSON.stringify({ 138 | typ: "navigator.id.getAssertion", 139 | challenge: req.challenge, 140 | origin: req.appId, // We use appId as origin as differentiation doesn't make sense here. 141 | }); 142 | var keyHandle = new Buffer(req.keyHandle, 'base64'); 143 | 144 | var buf = Buffer.concat([hash(clientData), hash(req.appId), new Buffer([keyHandle.length]), keyHandle]); 145 | this.command(U2FDevice.U2F_AUTHENTICATE, U2FDevice.U2F_AUTH_ENFORCE, buf, function(err, data) { 146 | if (err) 147 | return callback(err); 148 | 149 | callback(null, { 150 | keyHandle: req.keyHandle, 151 | signatureData: websafeBase64(data), 152 | clientData: websafeBase64(clientData) 153 | }); 154 | }); 155 | } 156 | 157 | U2FDevice.prototype.checkKeyRecognized = function(req, callback) { 158 | var clientData = ''; // it will not be signed anyway. 159 | var keyHandle = new Buffer(req.keyHandle, 'base64'); 160 | 161 | var buf = Buffer.concat([hash(clientData), hash(req.appId), new Buffer([keyHandle.length]), keyHandle]); 162 | this.command(U2FDevice.U2F_AUTHENTICATE, U2FDevice.U2F_AUTH_CHECK_ONLY, buf, function(err, data) { 163 | if (err.code == ErrorCodes.SW_CONDITIONS_NOT_SATISFIED) // device recognizes given keyHandle 164 | return callback(null, true); 165 | if (err.code == ErrorCodes.SW_WRONG_DATA) // keyHandle not recognized 166 | return callback(null, false); 167 | 168 | callback(err); // Some other error, like timeout. 169 | }); 170 | } 171 | 172 | 173 | 174 | // ============================================================================= 175 | // Utils 176 | 177 | function websafeBase64(buf) { 178 | if (!Buffer.isBuffer(buf)) 179 | buf = new Buffer(buf); 180 | return buf.toString('base64').replace(/\//g,'_').replace(/\+/g,'-').replace(/=/g, ''); 181 | } 182 | 183 | function hash(buf) { 184 | return crypto.createHash('SHA256').update(buf).digest(); 185 | } 186 | 187 | -------------------------------------------------------------------------------- /lib/u2f-hid-device.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var hid = require('node-hid'), 4 | crypto = require('crypto'), 5 | events = require('events'), 6 | util = require('util'); 7 | 8 | // HID driver for U2F Devices. 9 | // Specification: https://fidoalliance.org/specs/fido-u2f-HID-protocol-v1.0-rd-20141008.pdf 10 | // rawDevice is Node v0.8-style stream (with 'data', 'error' events and .write(), .close()) 11 | // that speaks HID protocol to hardware. Luckily, hid.HID() conforms to this spec. 12 | // initCb is called when the device is initialized with either error or this object itself. 13 | var U2FHIDDevice = module.exports = function U2FHIDDevice(rawDevice, initCb) { 14 | events.EventEmitter.call(this); 15 | 16 | this.device = rawDevice; 17 | 18 | this.protocolVersion = 0; 19 | this.deviceVersion = [0, 0, 0]; 20 | this.caps = {}; 21 | this.closed = false; 22 | 23 | this._channelId = U2FHIDDevice.U2FHID_BROADCAST_CID; 24 | this._reportSize = 64; 25 | this._packetBuf = new Buffer(this._reportSize); 26 | 27 | this._queue = []; 28 | this._curTransaction = undefined; 29 | 30 | this._onError = this._onError.bind(this); 31 | this._onData = this._onData.bind(this) 32 | this.device.on('error', this._onError); // 'error' should be first because polling starts when 'data' is bound 33 | this.device.on('data', this._onData); 34 | 35 | this._init(initCb); 36 | } 37 | 38 | util.inherits(U2FHIDDevice, events.EventEmitter); 39 | 40 | // Canonical usage page and usage of U2F HID devices. 41 | U2FHIDDevice.FIDO_USAGE_PAGE = 0xF1D0; 42 | U2FHIDDevice.FIDO_USAGE_U2FHID = 1; 43 | 44 | // Enumerate compatible devices (if acceptFn is null) or all that are accepted by acceptFn. 45 | // acceptFn is called with (deviceInfo, defaultResult) parameters. Should return true/false. 46 | // Callback returns array of deviceInfo-s, with unique id-s. 47 | // Sample deviceInfo: { 48 | // id: 'USB_1050_0120_14200000', 49 | // vendorId: 4176, 50 | // productId: 288, 51 | // path: 'USB_1050_0120_14200000', 52 | // serialNumber: '', 53 | // manufacturer: 'Yubico', 54 | // product: 'Security Key by Yubico', 55 | // release: 819, 56 | // interface: -1, 57 | // usagePage: 61904, 58 | // usage: 1 59 | // } 60 | U2FHIDDevice.enumerate = function enumerate(acceptFn) { 61 | return hid.devices().filter(function(deviceInfo) { 62 | deviceInfo.id = deviceInfo.path; // Add unique identifier to deviceInfo to help distinguishing them. 63 | var isCompatible = (deviceInfo.usagePage == U2FHIDDevice.FIDO_USAGE_PAGE && 64 | deviceInfo.usage == U2FHIDDevice.FIDO_USAGE_U2FHID); 65 | 66 | if (acceptFn) 67 | isCompatible = acceptFn(deviceInfo, isCompatible); 68 | 69 | return isCompatible; 70 | }); 71 | } 72 | 73 | // Open & initialize device using provided deviceInfo. 74 | U2FHIDDevice.open = function open(deviceInfo, cb) { 75 | var rawDevice; 76 | try { 77 | rawDevice = new hid.HID(deviceInfo.path); 78 | rawDevice.info = deviceInfo; // Save information about device for further reference. 79 | } 80 | catch (e) { 81 | cb(e); // ~ "cannot open device with path USB_1050_0120_14200000" 82 | return; 83 | } 84 | 85 | new U2FHIDDevice(rawDevice, cb); // cb will return error or device itself. 86 | } 87 | 88 | // Commands to U2F HID devices. 89 | U2FHIDDevice.U2FHID_PING = 0x80 | 0x01; 90 | U2FHIDDevice.U2FHID_MSG = 0x80 | 0x03; 91 | U2FHIDDevice.U2FHID_LOCK = 0x80 | 0x04; 92 | U2FHIDDevice.U2FHID_INIT = 0x80 | 0x06; 93 | U2FHIDDevice.U2FHID_WINK = 0x80 | 0x08; 94 | U2FHIDDevice.U2FHID_SYNC = 0x80 | 0x3C; 95 | U2FHIDDevice.U2FHID_ERROR = 0x80 | 0x3F; 96 | U2FHIDDevice.U2FHID_VENDOR_FIRST = 0x80 | 0x40; 97 | U2FHIDDevice.U2FHID_VENDOR_LAST = 0x80 | 0x7F; 98 | 99 | U2FHIDDevice.U2FHID_BROADCAST_CID = 0xffffffff; 100 | 101 | var hidErrors = { 102 | 0x00: "No error", 103 | 0x01: "Invalid command", 104 | 0x02: "Invalid parameter", 105 | 0x03: "Invalid message length", 106 | 0x04: "Invalid message sequencing", 107 | 0x05: "Message has timed out", 108 | 0x06: "Channel busy", 109 | 0x0a: "Command requires channel lock", 110 | 0x0b: "SYNC command failed", 111 | 0x7f: "Other unspecified error", 112 | }; 113 | 114 | // Initialize HID device. 115 | U2FHIDDevice.prototype._init = function(cb, forSure) { 116 | var nonce = crypto.pseudoRandomBytes(8); 117 | var that = this; 118 | this.command(U2FHIDDevice.U2FHID_INIT, nonce, function(err, data) { 119 | if (err) 120 | return cb(new Error("Error initializing U2F HID device: " + err.message)); 121 | 122 | // Check nonce. 123 | var nonce2 = data.slice(0, 8); 124 | if (nonce.toString('hex') != nonce2.toString('hex')) 125 | // TODO: Probably we just need to ignore it because other client could have tried to initialize same key with different nonce. 126 | return cb(new Error("Error initializing U2F HID device: incorrect nonce")); 127 | 128 | // Decode other initialization data. 129 | try { 130 | that._channelId = data.readUInt32BE(8); 131 | that.protocolVersion = data.readUInt8(12); 132 | that.deviceVersion = [data.readUInt8(13), data.readUInt8(14), data.readUInt8(15)]; 133 | that.capsRaw = data.readUInt8(16); 134 | that.caps = { 135 | wink: !!(that.capsRaw & 0x01), 136 | }; 137 | } 138 | catch (e) { 139 | cb(new Error("Error initializing U2F HID device: returned initialization data too short.")); 140 | return; 141 | } 142 | 143 | // Check protocol version is compatible. 144 | if (that.protocolVersion != 2) { 145 | cb(new Error("Error initializing U2F HID device: incompatible protocol version: "+that.protocolVersion)); 146 | return; 147 | } 148 | 149 | if ((that._channelId >>> 24) == 0 && !forSure) { 150 | // Some buggy keys give unacceptable channel_ids the first time (which don't work for following commands), so we try again. 151 | that._channelId = U2FHIDDevice.U2FHID_BROADCAST_CID; 152 | that._init(cb, true); 153 | } 154 | else 155 | cb(null, that); // Successful initialization. 156 | }); 157 | } 158 | 159 | // Packetize & send raw command request. 160 | // command - one of U2FHID_* 161 | // data can be empty/null or buffer. 162 | U2FHIDDevice.prototype._sendCommandRequest = function(command, data) { 163 | if (!data) 164 | data = new Buffer(0); 165 | if (!(0x80 <= command && command < 0x100)) 166 | throw new Error("Tried to send incorrect U2F HID command: "+command); 167 | 168 | // Create & send initial packet. 169 | var buf = this._packetBuf; 170 | buf.fill(0); 171 | buf.writeUInt32BE(this._channelId, 0); 172 | buf.writeUInt8(command, 4); 173 | buf.writeUInt16BE(data.length, 5); 174 | data.copy(buf, 7); data = data.slice(buf.length - 7); 175 | this.device.write(buf); 176 | 177 | // Create & send continuation packets. 178 | var seq = 0; 179 | while (data.length > 0 && seq < 0x80) { 180 | buf.fill(0); 181 | buf.writeUInt32BE(this._channelId, 0); 182 | buf.writeUInt8(seq++, 4); 183 | data.copy(buf, 5); data = data.slice(buf.length - 5); 184 | this.device.write(buf); 185 | } 186 | if (data.length > 0) 187 | throw new Error("Tried to send too large data packet to U2F HID device ("+data.length+" bytes didn't fit)."); 188 | } 189 | 190 | // Starts next transaction from the queue. Warning, this overwrites the _curTransaction. 191 | U2FHIDDevice.prototype._sendNextTransaction = function() { 192 | if (this._queue.length == 0) { 193 | this._curTransaction = undefined; 194 | return; 195 | } 196 | 197 | var t = this._curTransaction = this._queue.shift(); 198 | try { 199 | this._sendCommandRequest(t.command, t.data); 200 | } 201 | catch(e) { 202 | // Can be either incorrect command/data, or the device is failed/disconnected ("Cannot write to HID device"). 203 | // In the latter case, an 'error' event will be emitted soon. 204 | // TODO: We're probably in an inconsistent state now. Maybe we need to U2FHID_SYNC. 205 | if (t.cb) t.cb(e); // Transaction errored. 206 | t.cb = null; // Don't call callback anymore. 207 | 208 | this._sendNextTransaction(); // Process next one. 209 | } 210 | } 211 | 212 | // A packet received. Decode & process it. 213 | // TODO: if the buf is smaller then needed, we might end in inconsistent state. 214 | // we need to error out transaction and SYNC. 215 | U2FHIDDevice.prototype._onData = function(buf) { 216 | var t = this._curTransaction; 217 | 218 | if (!t) return; // Ignore packets outside the transaction. 219 | 220 | // Decode packet 221 | var channelId = buf.readUInt32BE(0); 222 | if (channelId !== this._channelId) 223 | return; // Ignore packet addressed to other channels. 224 | 225 | var cmd = buf.readUInt8(4); 226 | if (cmd === U2FHIDDevice.U2FHID_ERROR) { // Errored. 227 | var errCode = buf.readUInt8(7); 228 | var error = new Error(hidErrors[errCode] || hidErrors[0x7f]); 229 | error.code = errCode; 230 | if (t.cb) t.cb(error); 231 | t.cb = null; 232 | 233 | this._sendNextTransaction(); 234 | } 235 | else if (cmd & 0x80) { // Initial packet 236 | if (cmd !== t.command) 237 | return console.error("Transaction decoding failure: response is for different operation: ", cmd, t); 238 | 239 | t.toReceive = buf.readUInt16BE(5); 240 | t.receivedBufs[0] = buf.slice(7); 241 | t.receivedBytes += t.receivedBufs[0].length; 242 | } 243 | else { // Continuation packet. 244 | t.receivedBufs[cmd+1] = buf.slice(5); 245 | t.receivedBytes += t.receivedBufs[cmd+1].length; 246 | } 247 | 248 | // Call callback and finish transaction if read fully. 249 | if (t.receivedBytes >= t.toReceive) { 250 | if (t.cb) 251 | t.cb(null, Buffer.concat(t.receivedBufs).slice(0, t.toReceive)); 252 | t.cb = null; 253 | 254 | this._sendNextTransaction(); 255 | } 256 | } 257 | 258 | // Device is errored. Most likely it's because it was disconnected. Close it. 259 | U2FHIDDevice.prototype._onError = function(err) { 260 | // 'data' events are paused at this point, we could do this.device.resume(), 261 | // but the device is in inconsistent state anyway. 262 | this.close(); 263 | this.emit('disconnected'); 264 | } 265 | 266 | 267 | // Main interface: queue a raw command. 268 | // TODO: Timeout. 269 | U2FHIDDevice.prototype.command = function(command, data, cb) { 270 | if (this.closed) 271 | return cb(new Error("Tried to queue a command for closed device.")); 272 | 273 | // Add transaction to the queue. 274 | this._queue.push({ 275 | command: command, 276 | data: data, 277 | cb: cb, 278 | toReceive: 0xffff, 279 | receivedBytes: 0, 280 | receivedBufs: [], 281 | }); 282 | 283 | if (!this._curTransaction) 284 | this._sendNextTransaction(); 285 | } 286 | 287 | // Close device when it's not needed anymore. 288 | U2FHIDDevice.prototype.close = function() { 289 | if (this.closed) return; 290 | this.closed = true; 291 | // TODO: Cleanup the queue. 292 | 293 | this.device.close(); 294 | this.device.removeListener('error', this._onError); 295 | this.device.removeListener('data', this._onData); 296 | this.emit('closed'); 297 | }; 298 | 299 | 300 | // Higher level interface 301 | U2FHIDDevice.prototype.ping = function(data, cb) { // U2FHID_PING 302 | if (!cb) { cb = data; data = undefined; } 303 | if (typeof data === 'number') data = new Buffer(data); 304 | this.command(U2FHIDDevice.U2FHID_PING, data, cb); 305 | } 306 | 307 | U2FHIDDevice.prototype.wink = function(cb) { // U2FHID_WINK 308 | if (this.caps.wink) 309 | this.command(U2FHIDDevice.U2FHID_WINK, null, cb); 310 | else if (cb) 311 | cb(); 312 | } 313 | 314 | U2FHIDDevice.prototype.msg = function(data, cb) { // U2FHID_MSG 315 | this.command(U2FHIDDevice.U2FHID_MSG, data, cb); 316 | } 317 | 318 | 319 | 320 | 321 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "u2f-client", 3 | "version": "0.1.0", 4 | "description": "Access U2F USB/HID keys directly, without browser", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/ashtuchkin/u2f-client.git" 8 | }, 9 | "keywords": [ 10 | "u2f", 11 | "hid", 12 | "usb", 13 | "token", 14 | "2-factor", 15 | "authentication" 16 | ], 17 | "author": "Alexander Shtuchkin ", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/ashtuchkin/u2f-client/issues" 21 | }, 22 | "homepage": "https://github.com/ashtuchkin/u2f-client", 23 | "scripts": { 24 | "test": "mocha" 25 | }, 26 | "dependencies": { 27 | "node-hid": "^0.5.4", 28 | "async": "", 29 | "psl": "", 30 | "request": "" 31 | }, 32 | "devDependencies": { 33 | "u2f": "", 34 | "mocha": "2" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/main-test.js: -------------------------------------------------------------------------------- 1 | 2 | var assert = require('assert'), 3 | crypto = require('crypto'), 4 | u2f = require('u2f'), 5 | u2fhid = require('../'), 6 | U2FHIDDevice = u2fhid.U2FHIDDevice, 7 | U2FDevice = u2fhid.U2FDevice; 8 | 9 | describe("U2F HID Device", function() { 10 | var device, deviceInfo; 11 | 12 | it("should enumerate available devices", function() { 13 | var devices = U2FHIDDevice.enumerate() 14 | if (devices.length == 0) 15 | throw new Error("No U2F devices found."); 16 | deviceInfo = devices[0]; 17 | assert(deviceInfo); 18 | }); 19 | 20 | it("should open correctly first device", function(done) { 21 | U2FHIDDevice.open(deviceInfo, function(err, dev) { 22 | if (err) return done(err); 23 | device = dev; 24 | assert(device); 25 | done(); 26 | }); 27 | }); 28 | 29 | it("should respond to ping", function(done) { 30 | device.ping(done); 31 | }); 32 | it("should wink if it can", function(done) { 33 | device.wink(done); 34 | }); 35 | it("should respond to msg", function(done) { 36 | device.msg(new Buffer(0), done); 37 | }); 38 | 39 | it("should return error if unknown command", function(done) { 40 | device.command(0x90, null, function(err) { 41 | assert(err); 42 | assert.equal(err.code, 1); 43 | done(); 44 | }); 45 | }); 46 | 47 | after(function() { 48 | device && device.close(); 49 | }); 50 | }); 51 | 52 | describe("U2F Device interface", function() { 53 | var device, userDb; 54 | var appId = "http://demo.com"; 55 | 56 | before(function(done) { 57 | var devices = U2FHIDDevice.enumerate(); 58 | if (devices.length == 0) 59 | return done(new Error("No U2F devices found.")); 60 | U2FHIDDevice.open(devices[0], function(err, dev) { 61 | if (err) return done(err); 62 | 63 | device = new U2FDevice(dev); 64 | done(); 65 | }); 66 | }); 67 | 68 | it("should respond to apdu command", function(done) { 69 | device.command(U2FDevice.U2F_VERSION, done); 70 | }); 71 | 72 | it("should respond to u2f version message", function(done) { 73 | device.version(function(err, data) { 74 | if (err) return done(err); 75 | assert.equal(data.toString(), 'U2F_V2'); 76 | done(); 77 | }); 78 | }); 79 | 80 | it("should respond to u2f register message", function(done) { 81 | this.timeout(35000); 82 | 83 | var req = u2f.request(appId); 84 | 85 | device.register(req, function(err, res) { 86 | assert.ifError(err); 87 | 88 | var checkres = u2f.checkRegistration(req, res); 89 | assert(checkres.successful); 90 | assert(checkres.publicKey); 91 | assert(checkres.keyHandle); 92 | 93 | userDb = {publicKey: checkres.publicKey, keyHandle: checkres.keyHandle}; 94 | 95 | done(); 96 | }); 97 | }); 98 | 99 | it("should respond to u2f sign message", function(done) { 100 | this.timeout(35000); 101 | var req = u2f.request(appId, userDb.keyHandle); 102 | 103 | device.authenticate(req, function(err, res) { 104 | assert.ifError(err); 105 | assert(res); 106 | 107 | var checkres = u2f.checkSignature(req, res, userDb.publicKey); 108 | 109 | assert(checkres.successful); 110 | done(); 111 | }); 112 | }); 113 | 114 | after(function() { 115 | device && device.close(); 116 | }); 117 | }); 118 | 119 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --reporter spec 2 | --check-leaks --------------------------------------------------------------------------------