├── .babelrc ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── package.json ├── src ├── LedgerWallet.js ├── index.js └── u2f-api.js ├── test-e2e ├── .eslintrc.json ├── .gitignore ├── config.js.example ├── ganache-cli.sh ├── setup.js ├── test.js └── web │ └── index.html ├── test ├── .eslintrc.json └── LedgerWallet.spec.js ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "include": ["transform-regenerator"], 5 | "targets": { 6 | "browsers": ["last 2 versions"], 7 | "node": "6.12" 8 | } 9 | }] 10 | ], 11 | "plugins": ["transform-runtime"] 12 | } -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /lib/ 2 | /src/u2f-api.js 3 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["airbnb-base", "prettier"], 3 | "env": { 4 | "browser": true, 5 | "node": true 6 | }, 7 | "rules": { 8 | "prettier/prettier": "error", 9 | "import/prefer-default-export": "off" 10 | }, 11 | "plugins": [ 12 | "prettier" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /lib/ 2 | /node_modules/ 3 | .idea 4 | yarn-error.log 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /src/ 2 | /test 3 | /test-e2e 4 | .babelrc 5 | .eslintignore 6 | .eslintrc.json 7 | .gitignore 8 | webpack.config.js 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Neufund 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NOT MAINTAINED 2 | Please use the offical subprovider from the ledger team (https://github.com/LedgerHQ/ledgerjs) 3 | 4 | # LedgerWalletProvider 5 | 6 | The LedgerWalletProvider lets your dapp communicate directly with a user's [Ledger Nano S](https://www.ledgerwallet.com/products/ledger-nano-s) using the [zero client provider engine](https://github.com/MetaMask/provider-engine) developed by Metamask. 7 | 8 | Instead of setting your web3's provider to an HttpProvider or IpcProvider, you can create a custom provider using the [provider engine](https://github.com/MetaMask/provider-engine) and tell it to use LedgerWalletProvider for all id management requests (e.g getAccounts, approveTransaction and signTransaction). This way, your users can confirm your dapp's transactions directly from their Ledger Nano S! 9 | 10 | # Requirements 11 | 12 | In order for your dapp to play nicely with the LedgerWallet over U2F, it will need to be served over https. In addition to this, your browser must support U2F. Firefox users can use this [U2F extension](https://addons.mozilla.org/en-US/firefox/addon/u2f-support-add-on/). If on chrome or opera, LedgerWalletProvider will automatically polyfill U2F support for you. 13 | 14 | # Installation 15 | 16 | ``` 17 | npm install ledger-wallet-provider --save 18 | ``` 19 | 20 | # Usage 21 | 22 | In order to have a working provider you can pass to your web3, you will need these additional dependencies installed: 23 | 24 | ``` 25 | npm install web3-provider-engine --save 26 | ``` 27 | ``` 28 | npm install web3 --save 29 | ``` 30 | 31 | In your project, add the following: 32 | 33 | ``` 34 | var Web3 = require('web3'); 35 | var ProviderEngine = require('web3-provider-engine'); 36 | var RpcSubprovider = require('web3-provider-engine/subproviders/rpc'); 37 | var LedgerWalletSubproviderFactory = require('ledger-wallet-provider').default; 38 | 39 | var engine = new ProviderEngine(); 40 | var web3 = new Web3(engine); 41 | 42 | var ledgerWalletSubProvider = async LedgerWalletSubproviderFactory(); 43 | engine.addProvider(ledgerWalletSubProvider); 44 | engine.addProvider(new RpcSubprovider({rpcUrl: '/api'})); // you need RPC endpoint 45 | engine.start(); 46 | 47 | web3.eth.getAccounts(console.log); 48 | ``` 49 | 50 | To change derivation path that will be used to derive private/public keys on your nano, modify snippet above as follows 51 | 52 | ``` 53 | var derivation_path = "44'/60'/103'/0'"; 54 | var ledgerWalletSubProvider = async LedgerWalletSubproviderFactory(derivation_path); 55 | ``` 56 | 57 | All paths must start with `44'/60'` or `44'/61'`. 58 | 59 | **Note:** In order to send requests to the Ledger wallet, the user must have done the following: 60 | - Plugged-in their Ledger Wallet Nano S 61 | - Input their 4 digit pin 62 | - Navigated to the Ethereum app on their device 63 | - Enabled 'browser' support from the Ethereum app settings 64 | 65 | It is your responsibility to show the user a friendly message, instructing them to do so. In order to detect when they have completed these steps, you can poll `web3.eth.getAccounts` which will return `undefined` until the Ledger Wallet is accessible. 66 | 67 | If you would like to detect whether or not a user's browser supports U2F, you can call the `isSupported` convenience method on the `ledgerWalletSubProvider`: 68 | 69 | ``` 70 | var LedgerWalletSubproviderFactory = require('ledger-wallet-provider').default; 71 | 72 | var ledgerWalletSubProvider = LedgerWalletSubproviderFactory(); 73 | ledgerWalletSubProvider.isSupported() 74 | .then(function(isSupported) { 75 | console.log(isSupported ? 'Yes' : 'No'); 76 | }); 77 | ``` 78 | 79 | This might be helpful if you want to conditionally show Ledger Nano S support to users who could actually take advantage of it. 80 | 81 | # Development 82 | ## Running tests 83 | Currently we provide only kind of end to end tests. As we are testing integration with physical device it has to be manual process. 84 | There are following steps: 85 | ### Obtain dependencies 86 | Run `yarn` command. 87 | ### Prepare `config.js` 88 | Copy `config.js.example` to `config.js` and edit it setting up your Nano's public keys. You can obtain them using [myetherwallet](https://www.myetherwallet.com/). 89 | ### Run ganche-cli (former testrpc) 90 | Run `yarn ganache` command. 91 | ### Transfer test ether to Nano accounts that will be used in tests 92 | Run `yarn test-e2e-setup` command. 93 | ### Run tests using node 94 | Change Nano's settings - disable browser support. 95 | Run `yarn test-e2e-node` command. 96 | ### Run tests in browser 97 | Change Nano's settings - enable browser support. 98 | Run `yarn test-e2e-web` command 99 | Browser should open on url `https://localhost:8080`. Open dev console (`F12`) and check console for errors. 100 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ledger-wallet-provider", 3 | "version": "2.0.1-beta.4", 4 | "description": "Ledger Nano S wallet provider for the Web3 ProviderEngine", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "ganache": "./test-e2e/ganache-cli.sh", 8 | "test-e2e-setup": "./node_modules/.bin/babel-node ./test-e2e/setup.js", 9 | "test-e2e-node": "./node_modules/.bin/babel-node ./test-e2e/test.js", 10 | "test-e2e-web": "webpack-dev-server --https --open", 11 | "lint": "eslint .", 12 | "lint-fix": "eslint --fix .", 13 | "test": "mocha --require babel-core/register ./test/**/*.spec.js", 14 | "build": "babel src --out-dir lib" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/Neufund/ledger-wallet-provider.git" 19 | }, 20 | "author": "Neufund", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/Neufund/ledger-wallet-provider/issues" 24 | }, 25 | "homepage": "https://github.com/Neufund/ledger-wallet-provider#readme", 26 | "devDependencies": { 27 | "babel-cli": "^6.26.0", 28 | "babel-loader": "^7.1.2", 29 | "babel-plugin-transform-runtime": "^6.23.0", 30 | "babel-preset-env": "^1.6.1", 31 | "bluebird": "^3.5.1", 32 | "chai": "^4.1.2", 33 | "eslint": "^4.12.1", 34 | "eslint-config-airbnb-base": "^12.1.0", 35 | "eslint-config-prettier": "^2.9.0", 36 | "eslint-plugin-import": "^2.8.0", 37 | "eslint-plugin-prettier": "^2.3.1", 38 | "ganache-cli": "^6.0.3", 39 | "mocha": "^4.0.1", 40 | "prettier": "1.9.2", 41 | "web3": "^0.20.0", 42 | "webpack-dev-server": "^2.9.5" 43 | }, 44 | "dependencies": { 45 | "babel-runtime": "^6.23.0", 46 | "ethereumjs-tx": "^1.3.3", 47 | "ledgerco": "Neufund/ledgerjs#tomek/update_hid", 48 | "promise-timeout": "^1.1.1", 49 | "strip-hex-prefix": "^1.0.0", 50 | "web3-provider-engine": "^13.3.3" 51 | }, 52 | "files": [ 53 | "/lib", 54 | "yarn.lock" 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /src/LedgerWallet.js: -------------------------------------------------------------------------------- 1 | import ledger from "ledgerco"; 2 | import stripHexPrefix from "strip-hex-prefix"; 3 | import EthereumTx from "ethereumjs-tx"; 4 | import { timeout } from "promise-timeout"; 5 | 6 | import u2f from "./u2f-api"; 7 | 8 | const isNode = typeof window === "undefined"; 9 | 10 | if (!isNode && window.u2f === undefined) { 11 | window.u2f = u2f; 12 | } 13 | 14 | const NOT_SUPPORTED_ERROR_MSG = 15 | "LedgerWallet uses U2F which is not supported by your browser. " + 16 | "Use Chrome, Opera or Firefox with a U2F extension." + 17 | "Also make sure you're on an HTTPS connection"; 18 | /** 19 | * @class LedgerWallet 20 | * 21 | * 22 | * Paths: 23 | * Minimum Nano Ledger S accepts are: 24 | * 25 | * * 44'/60' 26 | * * 44'/61' 27 | * 28 | * MyEtherWallet.com by default uses the range which is not compatible with 29 | * BIP44/EIP84 30 | * 31 | * * 44'/60'/0'/n 32 | * 33 | * Note: no hardened derivation on the `n` 34 | * 35 | * @see https://github.com/MetaMask/provider-engine 36 | * @see https://github.com/ethereum/wiki/wiki/JavaScript-API 37 | */ 38 | const allowedHdPaths = ["44'/60'", "44'/61'"]; 39 | 40 | class LedgerWallet { 41 | constructor(getNetworkId, path, askForOnDeviceConfirmation = false) { 42 | this.askForOnDeviceConfirmation = askForOnDeviceConfirmation; 43 | this.getNetworkId = getNetworkId; 44 | this.isU2FSupported = null; 45 | this.connectionOpened = false; 46 | this.getAppConfig = this.getAppConfig.bind(this); 47 | this.getAccounts = this.getAccounts.bind(this); 48 | this.getMultipleAccounts = this.getMultipleAccounts.bind(this); 49 | this.signTransaction = this.signTransaction.bind(this); 50 | this.signMessage = this.signMessage.bind(this); 51 | this.getLedgerConnection = this.getLedgerConnection.bind(this); 52 | this.setDerivationPath = this.setDerivationPath.bind(this); 53 | this.setDerivationPath(path); 54 | } 55 | 56 | async init() { 57 | this.isU2FSupported = await LedgerWallet.isSupported(); 58 | } 59 | 60 | /** 61 | * Checks if the browser supports u2f. 62 | * Currently there is no good way to do feature-detection, 63 | * so we call getApiVersion and wait for 100ms 64 | */ 65 | static isSupported() { 66 | return new Promise(resolve => { 67 | if (isNode) { 68 | resolve(true); 69 | } 70 | if (window.u2f && !window.u2f.getApiVersion) { 71 | // u2f object is found (Firefox with extension) 72 | resolve(true); 73 | } else { 74 | // u2f object was not found. Using Google polyfill 75 | const intervalId = setTimeout(() => { 76 | resolve(false); 77 | }, 3000); 78 | u2f.getApiVersion(() => { 79 | clearTimeout(intervalId); 80 | resolve(true); 81 | }); 82 | } 83 | }); 84 | } 85 | 86 | async getLedgerConnection() { 87 | if (this.connectionOpened) { 88 | throw new Error( 89 | "You can only have one ledger connection active at a time" 90 | ); 91 | } else { 92 | this.connectionOpened = true; 93 | // eslint-disable-next-line new-cap 94 | return new ledger.eth( 95 | isNode 96 | ? await ledger.comm_node.create_async() 97 | : await ledger.comm_u2f.create_async() 98 | ); 99 | } 100 | } 101 | 102 | async closeLedgerConnection(eth) { 103 | this.connectionOpened = false; 104 | await eth.comm.close_async(); 105 | } 106 | 107 | setDerivationPath(path) { 108 | const newPath = path || "44'/60'/0'/0"; 109 | if (!allowedHdPaths.some(hdPref => newPath.startsWith(hdPref))) { 110 | throw new Error( 111 | `hd derivation path for Nano Ledger S may only start [${allowedHdPaths}], ${newPath} was provided` 112 | ); 113 | } 114 | this.path = newPath; 115 | } 116 | 117 | /** 118 | * @typedef {function} failableCallback 119 | * @param error 120 | * @param result 121 | * */ 122 | 123 | /** 124 | * Gets the version of installed Ethereum app 125 | * Check the isSupported() before calling that function 126 | * otherwise it never returns 127 | * @param {failableCallback} callback 128 | * @param ttl - timeout 129 | */ 130 | // TODO: order of parameters should be reversed so it follows pattern parameter callback and can be promisfied 131 | async getAppConfig(callback, ttl) { 132 | if (!this.isU2FSupported) { 133 | callback(new Error(NOT_SUPPORTED_ERROR_MSG), null); 134 | return; 135 | } 136 | const eth = await this.getLedgerConnection(); 137 | const cleanupCallback = (error, data) => { 138 | this.closeLedgerConnection(eth); 139 | callback(error, data); 140 | }; 141 | timeout(eth.getAppConfiguration_async(), ttl) 142 | .then(config => cleanupCallback(null, config)) 143 | .catch(error => cleanupCallback(error)); 144 | } 145 | 146 | async getMultipleAccounts(derivationPath, indexOffset, accountsNo) { 147 | let eth = null; 148 | if (!this.isU2FSupported) { 149 | throw new Error(NOT_SUPPORTED_ERROR_MSG); 150 | } 151 | try { 152 | const pathComponents = LedgerWallet.obtainPathComponentsFromDerivationPath( 153 | derivationPath 154 | ); 155 | 156 | const chainCode = false; // Include the chain code 157 | eth = await this.getLedgerConnection(); 158 | const addresses = {}; 159 | for (let i = indexOffset; i < indexOffset + accountsNo; i += 1) { 160 | const path = 161 | pathComponents.basePath + (pathComponents.index + i).toString(); 162 | // eslint-disable-next-line no-await-in-loop 163 | const address = await eth.getAddress_async( 164 | path, 165 | this.askForOnDeviceConfirmation, 166 | chainCode 167 | ); 168 | addresses[path] = address.address; 169 | } 170 | return addresses; 171 | } finally { 172 | if (eth !== null) { 173 | // This is fishy but currently ledger library always returns empty 174 | // resolved promise when closing connection so there is no point in 175 | // doing anything with returned Promise. 176 | await this.closeLedgerConnection(eth); 177 | } 178 | } 179 | } 180 | 181 | /** 182 | * PathComponent contains derivation path divided into base path and index. 183 | * @typedef {Object} PathComponent 184 | * @property {string} basePath - Base path of derivation path. 185 | * @property {number} index - index of addresses. 186 | */ 187 | 188 | /** 189 | * Returns derivation path components: base path and index 190 | * used by getMultipleAccounts. 191 | * @param derivationPath 192 | * @returns {PathComponent} PathComponent 193 | */ 194 | static obtainPathComponentsFromDerivationPath(derivationPath) { 195 | // check if derivation path follows 44'/60'/x'/n (ledger native) 196 | // or 44'/60'/x'/[0|1]/0 (BIP44) pattern 197 | const regExp = /^(44'\/6[0|1]'\/\d+'?\/(?:[0|1]\/)?)(\d+)$/; 198 | const matchResult = regExp.exec(derivationPath); 199 | if (matchResult === null) { 200 | throw new Error( 201 | "Derivation path must follow pattern 44'/60|61'/x'/n or BIP 44" 202 | ); 203 | } 204 | 205 | return { basePath: matchResult[1], index: parseInt(matchResult[2], 10) }; 206 | } 207 | 208 | async signTransactionAsync(txData) { 209 | let eth = null; 210 | if (!this.isU2FSupported) { 211 | throw new Error(NOT_SUPPORTED_ERROR_MSG); 212 | } 213 | try { 214 | // Encode using ethereumjs-tx 215 | const tx = new EthereumTx(txData); 216 | const chainId = parseInt(await this.getNetworkId(), 10); 217 | 218 | // Set the EIP155 bits 219 | tx.raw[6] = Buffer.from([chainId]); // v 220 | tx.raw[7] = Buffer.from([]); // r 221 | tx.raw[8] = Buffer.from([]); // s 222 | 223 | // Encode as hex-rlp for Ledger 224 | const hex = tx.serialize().toString("hex"); 225 | 226 | eth = await this.getLedgerConnection(); 227 | 228 | // Pass to _ledger for signing 229 | const result = await eth.signTransaction_async(this.path, hex); 230 | 231 | // Store signature in transaction 232 | tx.v = Buffer.from(result.v, "hex"); 233 | tx.r = Buffer.from(result.r, "hex"); 234 | tx.s = Buffer.from(result.s, "hex"); 235 | 236 | // EIP155: v should be chain_id * 2 + {35, 36} 237 | const signedChainId = Math.floor((tx.v[0] - 35) / 2); 238 | if (signedChainId !== chainId) { 239 | throw new Error( 240 | "Invalid signature received. Please update your Ledger Nano S." 241 | ); 242 | } 243 | 244 | // Return the signed raw transaction 245 | return `0x${tx.serialize().toString("hex")}`; 246 | } finally { 247 | if (eth !== null) { 248 | // This is fishy but currently ledger library always returns empty 249 | // resolved promise when closing connection so there is no point in 250 | // doing anything with returned Promise. 251 | await this.closeLedgerConnection(eth); 252 | } 253 | } 254 | } 255 | 256 | async signMessageAsync(msgData) { 257 | if (!this.isU2FSupported) { 258 | throw new Error(NOT_SUPPORTED_ERROR_MSG); 259 | } 260 | let eth = null; 261 | 262 | try { 263 | eth = await this.getLedgerConnection(); 264 | 265 | const result = await eth.signPersonalMessage_async( 266 | this.path, 267 | stripHexPrefix(msgData.data) 268 | ); 269 | // v should be tranmitted with chainCode (27) still added to be compatible with most signers like metamask, parity and geth 270 | const v = parseInt(result.v, 10); 271 | let vHex = v.toString(16); 272 | if (vHex.length < 2) { 273 | vHex = `0${v}`; 274 | } 275 | return `0x${result.r}${result.s}${vHex}`; 276 | } finally { 277 | if (eth !== null) { 278 | // This is fishy but currently ledger library always returns empty 279 | // resolved promise when closing connection so there is no point in 280 | // doing anything with returned Promise. 281 | await this.closeLedgerConnection(eth); 282 | } 283 | } 284 | } 285 | 286 | /** 287 | * Gets a list of accounts from a device - currently it's returning just 288 | * first one according to derivation path 289 | * @param {failableCallback} callback 290 | */ 291 | getAccounts(callback) { 292 | this.getMultipleAccounts(this.path, 0, 1) 293 | .then(res => callback(null, Object.values(res))) 294 | .catch(err => callback(err, null)); 295 | } 296 | 297 | /** 298 | * Signs txData in a format that ethereumjs-tx accepts 299 | * @param {object} txData - transaction to sign 300 | * @param {failableCallback} callback - callback 301 | */ 302 | signTransaction(txData, callback) { 303 | this.signTransactionAsync(txData) 304 | .then(res => callback(null, res)) 305 | .catch(err => callback(err, null)); 306 | } 307 | 308 | signMessage(txData, callback) { 309 | this.signMessageAsync(txData) 310 | .then(res => callback(null, res)) 311 | .catch(err => callback(err, null)); 312 | } 313 | } 314 | 315 | module.exports = LedgerWallet; 316 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import HookedWalletSubprovider from "web3-provider-engine/subproviders/hooked-wallet"; 2 | import LedgerWallet from "./LedgerWallet"; 3 | 4 | export default async function( 5 | getNetworkId, 6 | pathOverride, 7 | askForOnDeviceConfirmation 8 | ) { 9 | const ledger = new LedgerWallet( 10 | getNetworkId, 11 | pathOverride, 12 | askForOnDeviceConfirmation 13 | ); 14 | await ledger.init(); 15 | const LedgerWalletSubprovider = new HookedWalletSubprovider(ledger); 16 | 17 | // This convenience method lets you handle the case where your users browser doesn't support U2F 18 | // before adding the LedgerWalletSubprovider to a providerEngine instance. 19 | LedgerWalletSubprovider.isSupported = ledger.isU2FSupported; 20 | LedgerWalletSubprovider.ledger = ledger; 21 | 22 | return LedgerWalletSubprovider; 23 | } 24 | -------------------------------------------------------------------------------- /src/u2f-api.js: -------------------------------------------------------------------------------- 1 | //Copyright 2014-2015 Google Inc. All rights reserved. 2 | 3 | //Use of this source code is governed by a BSD-style 4 | //license that can be found in the LICENSE file or at 5 | //https://developers.google.com/open-source/licenses/bsd 6 | 7 | /** 8 | * @fileoverview The U2F api. 9 | */ 10 | 'use strict'; 11 | 12 | 13 | /** 14 | * Namespace for the U2F api. 15 | * @type {Object} 16 | */ 17 | var u2f = u2f || {}; 18 | 19 | /** 20 | * Require integration 21 | */ 22 | if (typeof module != "undefined") { 23 | module.exports = u2f; 24 | } 25 | 26 | /** 27 | * FIDO U2F Javascript API Version 28 | * @number 29 | */ 30 | var js_api_version; 31 | 32 | /** 33 | * The U2F extension id 34 | * @const {string} 35 | */ 36 | // The Chrome packaged app extension ID. 37 | // Uncomment this if you want to deploy a server instance that uses 38 | // the package Chrome app and does not require installing the U2F Chrome extension. 39 | u2f.EXTENSION_ID = 'kmendfapggjehodndflmmgagdbamhnfd'; 40 | // The U2F Chrome extension ID. 41 | // Uncomment this if you want to deploy a server instance that uses 42 | // the U2F Chrome extension to authenticate. 43 | // u2f.EXTENSION_ID = 'pfboblefjcgdjicmnffhdgionmgcdmne'; 44 | 45 | 46 | /** 47 | * Message types for messsages to/from the extension 48 | * @const 49 | * @enum {string} 50 | */ 51 | u2f.MessageTypes = { 52 | 'U2F_REGISTER_REQUEST': 'u2f_register_request', 53 | 'U2F_REGISTER_RESPONSE': 'u2f_register_response', 54 | 'U2F_SIGN_REQUEST': 'u2f_sign_request', 55 | 'U2F_SIGN_RESPONSE': 'u2f_sign_response', 56 | 'U2F_GET_API_VERSION_REQUEST': 'u2f_get_api_version_request', 57 | 'U2F_GET_API_VERSION_RESPONSE': 'u2f_get_api_version_response' 58 | }; 59 | 60 | 61 | /** 62 | * Response status codes 63 | * @const 64 | * @enum {number} 65 | */ 66 | u2f.ErrorCodes = { 67 | 'OK': 0, 68 | 'OTHER_ERROR': 1, 69 | 'BAD_REQUEST': 2, 70 | 'CONFIGURATION_UNSUPPORTED': 3, 71 | 'DEVICE_INELIGIBLE': 4, 72 | 'TIMEOUT': 5 73 | }; 74 | 75 | 76 | /** 77 | * A message for registration requests 78 | * @typedef {{ 79 | * type: u2f.MessageTypes, 80 | * appId: ?string, 81 | * timeoutSeconds: ?number, 82 | * requestId: ?number 83 | * }} 84 | */ 85 | u2f.U2fRequest; 86 | 87 | 88 | /** 89 | * A message for registration responses 90 | * @typedef {{ 91 | * type: u2f.MessageTypes, 92 | * responseData: (u2f.Error | u2f.RegisterResponse | u2f.SignResponse), 93 | * requestId: ?number 94 | * }} 95 | */ 96 | u2f.U2fResponse; 97 | 98 | 99 | /** 100 | * An error object for responses 101 | * @typedef {{ 102 | * errorCode: u2f.ErrorCodes, 103 | * errorMessage: ?string 104 | * }} 105 | */ 106 | u2f.Error; 107 | 108 | /** 109 | * Data object for a single sign request. 110 | * @typedef {enum {BLUETOOTH_RADIO, BLUETOOTH_LOW_ENERGY, USB, NFC}} 111 | */ 112 | u2f.Transport; 113 | 114 | 115 | /** 116 | * Data object for a single sign request. 117 | * @typedef {Array} 118 | */ 119 | u2f.Transports; 120 | 121 | /** 122 | * Data object for a single sign request. 123 | * @typedef {{ 124 | * version: string, 125 | * challenge: string, 126 | * keyHandle: string, 127 | * appId: string 128 | * }} 129 | */ 130 | u2f.SignRequest; 131 | 132 | 133 | /** 134 | * Data object for a sign response. 135 | * @typedef {{ 136 | * keyHandle: string, 137 | * signatureData: string, 138 | * clientData: string 139 | * }} 140 | */ 141 | u2f.SignResponse; 142 | 143 | 144 | /** 145 | * Data object for a registration request. 146 | * @typedef {{ 147 | * version: string, 148 | * challenge: string 149 | * }} 150 | */ 151 | u2f.RegisterRequest; 152 | 153 | 154 | /** 155 | * Data object for a registration response. 156 | * @typedef {{ 157 | * version: string, 158 | * keyHandle: string, 159 | * transports: Transports, 160 | * appId: string 161 | * }} 162 | */ 163 | u2f.RegisterResponse; 164 | 165 | 166 | /** 167 | * Data object for a registered key. 168 | * @typedef {{ 169 | * version: string, 170 | * keyHandle: string, 171 | * transports: ?Transports, 172 | * appId: ?string 173 | * }} 174 | */ 175 | u2f.RegisteredKey; 176 | 177 | 178 | /** 179 | * Data object for a get API register response. 180 | * @typedef {{ 181 | * js_api_version: number 182 | * }} 183 | */ 184 | u2f.GetJsApiVersionResponse; 185 | 186 | 187 | //Low level MessagePort API support 188 | 189 | /** 190 | * Sets up a MessagePort to the U2F extension using the 191 | * available mechanisms. 192 | * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback 193 | */ 194 | u2f.getMessagePort = function(callback) { 195 | if (typeof chrome != 'undefined' && chrome.runtime) { 196 | // The actual message here does not matter, but we need to get a reply 197 | // for the callback to run. Thus, send an empty signature request 198 | // in order to get a failure response. 199 | var msg = { 200 | type: u2f.MessageTypes.U2F_SIGN_REQUEST, 201 | signRequests: [] 202 | }; 203 | chrome.runtime.sendMessage(u2f.EXTENSION_ID, msg, function() { 204 | if (!chrome.runtime.lastError) { 205 | // We are on a whitelisted origin and can talk directly 206 | // with the extension. 207 | u2f.getChromeRuntimePort_(callback); 208 | } else { 209 | // chrome.runtime was available, but we couldn't message 210 | // the extension directly, use iframe 211 | u2f.getIframePort_(callback); 212 | } 213 | }); 214 | } else if (u2f.isAndroidChrome_()) { 215 | u2f.getAuthenticatorPort_(callback); 216 | } else if (u2f.isIosChrome_()) { 217 | u2f.getIosPort_(callback); 218 | } else { 219 | // chrome.runtime was not available at all, which is normal 220 | // when this origin doesn't have access to any extensions. 221 | u2f.getIframePort_(callback); 222 | } 223 | }; 224 | 225 | /** 226 | * Detect chrome running on android based on the browser's useragent. 227 | * @private 228 | */ 229 | u2f.isAndroidChrome_ = function() { 230 | var userAgent = navigator.userAgent; 231 | return userAgent.indexOf('Chrome') != -1 && 232 | userAgent.indexOf('Android') != -1; 233 | }; 234 | 235 | /** 236 | * Detect chrome running on iOS based on the browser's platform. 237 | * @private 238 | */ 239 | u2f.isIosChrome_ = function() { 240 | return ["iPhone", "iPad", "iPod"].indexOf(navigator.platform) > -1; 241 | }; 242 | 243 | /** 244 | * Connects directly to the extension via chrome.runtime.connect. 245 | * @param {function(u2f.WrappedChromeRuntimePort_)} callback 246 | * @private 247 | */ 248 | u2f.getChromeRuntimePort_ = function(callback) { 249 | var port = chrome.runtime.connect(u2f.EXTENSION_ID, 250 | {'includeTlsChannelId': true}); 251 | setTimeout(function() { 252 | callback(new u2f.WrappedChromeRuntimePort_(port)); 253 | }, 0); 254 | }; 255 | 256 | /** 257 | * Return a 'port' abstraction to the Authenticator app. 258 | * @param {function(u2f.WrappedAuthenticatorPort_)} callback 259 | * @private 260 | */ 261 | u2f.getAuthenticatorPort_ = function(callback) { 262 | setTimeout(function() { 263 | callback(new u2f.WrappedAuthenticatorPort_()); 264 | }, 0); 265 | }; 266 | 267 | /** 268 | * Return a 'port' abstraction to the iOS client app. 269 | * @param {function(u2f.WrappedIosPort_)} callback 270 | * @private 271 | */ 272 | u2f.getIosPort_ = function(callback) { 273 | setTimeout(function() { 274 | callback(new u2f.WrappedIosPort_()); 275 | }, 0); 276 | }; 277 | 278 | /** 279 | * A wrapper for chrome.runtime.Port that is compatible with MessagePort. 280 | * @param {Port} port 281 | * @constructor 282 | * @private 283 | */ 284 | u2f.WrappedChromeRuntimePort_ = function(port) { 285 | this.port_ = port; 286 | }; 287 | 288 | /** 289 | * Format and return a sign request compliant with the JS API version supported by the extension. 290 | * @param {Array} signRequests 291 | * @param {number} timeoutSeconds 292 | * @param {number} reqId 293 | * @return {Object} 294 | */ 295 | u2f.formatSignRequest_ = 296 | function(appId, challenge, registeredKeys, timeoutSeconds, reqId) { 297 | if (js_api_version === undefined || js_api_version < 1.1) { 298 | // Adapt request to the 1.0 JS API 299 | var signRequests = []; 300 | for (var i = 0; i < registeredKeys.length; i++) { 301 | signRequests[i] = { 302 | version: registeredKeys[i].version, 303 | challenge: challenge, 304 | keyHandle: registeredKeys[i].keyHandle, 305 | appId: appId 306 | }; 307 | } 308 | return { 309 | type: u2f.MessageTypes.U2F_SIGN_REQUEST, 310 | signRequests: signRequests, 311 | timeoutSeconds: timeoutSeconds, 312 | requestId: reqId 313 | }; 314 | } 315 | // JS 1.1 API 316 | return { 317 | type: u2f.MessageTypes.U2F_SIGN_REQUEST, 318 | appId: appId, 319 | challenge: challenge, 320 | registeredKeys: registeredKeys, 321 | timeoutSeconds: timeoutSeconds, 322 | requestId: reqId 323 | }; 324 | }; 325 | 326 | /** 327 | * Format and return a register request compliant with the JS API version supported by the extension.. 328 | * @param {Array} signRequests 329 | * @param {Array} signRequests 330 | * @param {number} timeoutSeconds 331 | * @param {number} reqId 332 | * @return {Object} 333 | */ 334 | u2f.formatRegisterRequest_ = 335 | function(appId, registeredKeys, registerRequests, timeoutSeconds, reqId) { 336 | if (js_api_version === undefined || js_api_version < 1.1) { 337 | // Adapt request to the 1.0 JS API 338 | for (var i = 0; i < registerRequests.length; i++) { 339 | registerRequests[i].appId = appId; 340 | } 341 | var signRequests = []; 342 | for (var i = 0; i < registeredKeys.length; i++) { 343 | signRequests[i] = { 344 | version: registeredKeys[i].version, 345 | challenge: registerRequests[0], 346 | keyHandle: registeredKeys[i].keyHandle, 347 | appId: appId 348 | }; 349 | } 350 | return { 351 | type: u2f.MessageTypes.U2F_REGISTER_REQUEST, 352 | signRequests: signRequests, 353 | registerRequests: registerRequests, 354 | timeoutSeconds: timeoutSeconds, 355 | requestId: reqId 356 | }; 357 | } 358 | // JS 1.1 API 359 | return { 360 | type: u2f.MessageTypes.U2F_REGISTER_REQUEST, 361 | appId: appId, 362 | registerRequests: registerRequests, 363 | registeredKeys: registeredKeys, 364 | timeoutSeconds: timeoutSeconds, 365 | requestId: reqId 366 | }; 367 | }; 368 | 369 | 370 | /** 371 | * Posts a message on the underlying channel. 372 | * @param {Object} message 373 | */ 374 | u2f.WrappedChromeRuntimePort_.prototype.postMessage = function(message) { 375 | this.port_.postMessage(message); 376 | }; 377 | 378 | 379 | /** 380 | * Emulates the HTML 5 addEventListener interface. Works only for the 381 | * onmessage event, which is hooked up to the chrome.runtime.Port.onMessage. 382 | * @param {string} eventName 383 | * @param {function({data: Object})} handler 384 | */ 385 | u2f.WrappedChromeRuntimePort_.prototype.addEventListener = 386 | function(eventName, handler) { 387 | var name = eventName.toLowerCase(); 388 | if (name == 'message' || name == 'onmessage') { 389 | this.port_.onMessage.addListener(function(message) { 390 | // Emulate a minimal MessageEvent object 391 | handler({'data': message}); 392 | }); 393 | } else { 394 | console.error('WrappedChromeRuntimePort only supports onMessage'); 395 | } 396 | }; 397 | 398 | /** 399 | * Wrap the Authenticator app with a MessagePort interface. 400 | * @constructor 401 | * @private 402 | */ 403 | u2f.WrappedAuthenticatorPort_ = function() { 404 | this.requestId_ = -1; 405 | this.requestObject_ = null; 406 | } 407 | 408 | /** 409 | * Launch the Authenticator intent. 410 | * @param {Object} message 411 | */ 412 | u2f.WrappedAuthenticatorPort_.prototype.postMessage = function(message) { 413 | var intentUrl = 414 | u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ + 415 | ';S.request=' + encodeURIComponent(JSON.stringify(message)) + 416 | ';end'; 417 | document.location = intentUrl; 418 | }; 419 | 420 | /** 421 | * Tells what type of port this is. 422 | * @return {String} port type 423 | */ 424 | u2f.WrappedAuthenticatorPort_.prototype.getPortType = function() { 425 | return "WrappedAuthenticatorPort_"; 426 | }; 427 | 428 | 429 | /** 430 | * Emulates the HTML 5 addEventListener interface. 431 | * @param {string} eventName 432 | * @param {function({data: Object})} handler 433 | */ 434 | u2f.WrappedAuthenticatorPort_.prototype.addEventListener = function(eventName, handler) { 435 | var name = eventName.toLowerCase(); 436 | if (name == 'message') { 437 | var self = this; 438 | /* Register a callback to that executes when 439 | * chrome injects the response. */ 440 | window.addEventListener( 441 | 'message', self.onRequestUpdate_.bind(self, handler), false); 442 | } else { 443 | console.error('WrappedAuthenticatorPort only supports message'); 444 | } 445 | }; 446 | 447 | /** 448 | * Callback invoked when a response is received from the Authenticator. 449 | * @param function({data: Object}) callback 450 | * @param {Object} message message Object 451 | */ 452 | u2f.WrappedAuthenticatorPort_.prototype.onRequestUpdate_ = 453 | function(callback, message) { 454 | var messageObject = JSON.parse(message.data); 455 | var intentUrl = messageObject['intentURL']; 456 | 457 | var errorCode = messageObject['errorCode']; 458 | var responseObject = null; 459 | if (messageObject.hasOwnProperty('data')) { 460 | responseObject = /** @type {Object} */ ( 461 | JSON.parse(messageObject['data'])); 462 | } 463 | 464 | callback({'data': responseObject}); 465 | }; 466 | 467 | /** 468 | * Base URL for intents to Authenticator. 469 | * @const 470 | * @private 471 | */ 472 | /* 473 | u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ = 474 | 'intent:#Intent;action=com.google.android.apps.authenticator.AUTHENTICATE'; 475 | */ 476 | u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ = 477 | 'intent:#Intent;action=com.ledger.android.u2f.bridge.AUTHENTICATE'; 478 | 479 | 480 | /** 481 | * Wrap the iOS client app with a MessagePort interface. 482 | * @constructor 483 | * @private 484 | */ 485 | u2f.WrappedIosPort_ = function() {}; 486 | 487 | /** 488 | * Launch the iOS client app request 489 | * @param {Object} message 490 | */ 491 | u2f.WrappedIosPort_.prototype.postMessage = function(message) { 492 | var str = JSON.stringify(message); 493 | var url = "u2f://auth?" + encodeURI(str); 494 | location.replace(url); 495 | }; 496 | 497 | /** 498 | * Tells what type of port this is. 499 | * @return {String} port type 500 | */ 501 | u2f.WrappedIosPort_.prototype.getPortType = function() { 502 | return "WrappedIosPort_"; 503 | }; 504 | 505 | /** 506 | * Emulates the HTML 5 addEventListener interface. 507 | * @param {string} eventName 508 | * @param {function({data: Object})} handler 509 | */ 510 | u2f.WrappedIosPort_.prototype.addEventListener = function(eventName, handler) { 511 | var name = eventName.toLowerCase(); 512 | if (name !== 'message') { 513 | console.error('WrappedIosPort only supports message'); 514 | } 515 | }; 516 | 517 | /** 518 | * Sets up an embedded trampoline iframe, sourced from the extension. 519 | * @param {function(MessagePort)} callback 520 | * @private 521 | */ 522 | u2f.getIframePort_ = function(callback) { 523 | // Create the iframe 524 | var iframeOrigin = 'chrome-extension://' + u2f.EXTENSION_ID; 525 | var iframe = document.createElement('iframe'); 526 | iframe.src = iframeOrigin + '/u2f-comms.html'; 527 | iframe.setAttribute('style', 'display:none'); 528 | document.body.appendChild(iframe); 529 | 530 | var channel = new MessageChannel(); 531 | var ready = function(message) { 532 | if (message.data == 'ready') { 533 | channel.port1.removeEventListener('message', ready); 534 | callback(channel.port1); 535 | } else { 536 | console.error('First event on iframe port was not "ready"'); 537 | } 538 | }; 539 | channel.port1.addEventListener('message', ready); 540 | channel.port1.start(); 541 | 542 | iframe.addEventListener('load', function() { 543 | // Deliver the port to the iframe and initialize 544 | iframe.contentWindow.postMessage('init', iframeOrigin, [channel.port2]); 545 | }); 546 | }; 547 | 548 | 549 | //High-level JS API 550 | 551 | /** 552 | * Default extension response timeout in seconds. 553 | * @const 554 | */ 555 | u2f.EXTENSION_TIMEOUT_SEC = 30; 556 | 557 | /** 558 | * A singleton instance for a MessagePort to the extension. 559 | * @type {MessagePort|u2f.WrappedChromeRuntimePort_} 560 | * @private 561 | */ 562 | u2f.port_ = null; 563 | 564 | /** 565 | * Callbacks waiting for a port 566 | * @type {Array} 567 | * @private 568 | */ 569 | u2f.waitingForPort_ = []; 570 | 571 | /** 572 | * A counter for requestIds. 573 | * @type {number} 574 | * @private 575 | */ 576 | u2f.reqCounter_ = 0; 577 | 578 | /** 579 | * A map from requestIds to client callbacks 580 | * @type {Object.} 582 | * @private 583 | */ 584 | u2f.callbackMap_ = {}; 585 | 586 | /** 587 | * Creates or retrieves the MessagePort singleton to use. 588 | * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback 589 | * @private 590 | */ 591 | u2f.getPortSingleton_ = function(callback) { 592 | if (u2f.port_) { 593 | callback(u2f.port_); 594 | } else { 595 | if (u2f.waitingForPort_.length == 0) { 596 | u2f.getMessagePort(function(port) { 597 | u2f.port_ = port; 598 | u2f.port_.addEventListener('message', 599 | /** @type {function(Event)} */ (u2f.responseHandler_)); 600 | 601 | // Careful, here be async callbacks. Maybe. 602 | while (u2f.waitingForPort_.length) 603 | u2f.waitingForPort_.shift()(u2f.port_); 604 | }); 605 | } 606 | u2f.waitingForPort_.push(callback); 607 | } 608 | }; 609 | 610 | /** 611 | * Handles response messages from the extension. 612 | * @param {MessageEvent.} message 613 | * @private 614 | */ 615 | u2f.responseHandler_ = function(message) { 616 | var response = message.data; 617 | var reqId = response['requestId']; 618 | if (!reqId || !u2f.callbackMap_[reqId]) { 619 | console.error('Unknown or missing requestId in response.'); 620 | return; 621 | } 622 | var cb = u2f.callbackMap_[reqId]; 623 | delete u2f.callbackMap_[reqId]; 624 | cb(response['responseData']); 625 | }; 626 | 627 | /** 628 | * Dispatches an array of sign requests to available U2F tokens. 629 | * If the JS API version supported by the extension is unknown, it first sends a 630 | * message to the extension to find out the supported API version and then it sends 631 | * the sign request. 632 | * @param {string=} appId 633 | * @param {string=} challenge 634 | * @param {Array} registeredKeys 635 | * @param {function((u2f.Error|u2f.SignResponse))} callback 636 | * @param {number=} opt_timeoutSeconds 637 | */ 638 | u2f.sign = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) { 639 | if (js_api_version === undefined) { 640 | // Send a message to get the extension to JS API version, then send the actual sign request. 641 | u2f.getApiVersion( 642 | function (response) { 643 | js_api_version = response['js_api_version'] === undefined ? 0 : response['js_api_version']; 644 | //console.log("Extension JS API Version: ", js_api_version); 645 | u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds); 646 | }); 647 | } else { 648 | // We know the JS API version. Send the actual sign request in the supported API version. 649 | u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds); 650 | } 651 | }; 652 | 653 | /** 654 | * Dispatches an array of sign requests to available U2F tokens. 655 | * @param {string=} appId 656 | * @param {string=} challenge 657 | * @param {Array} registeredKeys 658 | * @param {function((u2f.Error|u2f.SignResponse))} callback 659 | * @param {number=} opt_timeoutSeconds 660 | */ 661 | u2f.sendSignRequest = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) { 662 | u2f.getPortSingleton_(function(port) { 663 | var reqId = ++u2f.reqCounter_; 664 | u2f.callbackMap_[reqId] = callback; 665 | var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ? 666 | opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC); 667 | var req = u2f.formatSignRequest_(appId, challenge, registeredKeys, timeoutSeconds, reqId); 668 | port.postMessage(req); 669 | }); 670 | }; 671 | 672 | /** 673 | * Dispatches register requests to available U2F tokens. An array of sign 674 | * requests identifies already registered tokens. 675 | * If the JS API version supported by the extension is unknown, it first sends a 676 | * message to the extension to find out the supported API version and then it sends 677 | * the register request. 678 | * @param {string=} appId 679 | * @param {Array} registerRequests 680 | * @param {Array} registeredKeys 681 | * @param {function((u2f.Error|u2f.RegisterResponse))} callback 682 | * @param {number=} opt_timeoutSeconds 683 | */ 684 | u2f.register = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) { 685 | if (js_api_version === undefined) { 686 | // Send a message to get the extension to JS API version, then send the actual register request. 687 | u2f.getApiVersion( 688 | function (response) { 689 | js_api_version = response['js_api_version'] === undefined ? 0: response['js_api_version']; 690 | //console.log("Extension JS API Version: ", js_api_version); 691 | u2f.sendRegisterRequest(appId, registerRequests, registeredKeys, 692 | callback, opt_timeoutSeconds); 693 | }); 694 | } else { 695 | // We know the JS API version. Send the actual register request in the supported API version. 696 | u2f.sendRegisterRequest(appId, registerRequests, registeredKeys, 697 | callback, opt_timeoutSeconds); 698 | } 699 | }; 700 | 701 | /** 702 | * Dispatches register requests to available U2F tokens. An array of sign 703 | * requests identifies already registered tokens. 704 | * @param {string=} appId 705 | * @param {Array} registerRequests 706 | * @param {Array} registeredKeys 707 | * @param {function((u2f.Error|u2f.RegisterResponse))} callback 708 | * @param {number=} opt_timeoutSeconds 709 | */ 710 | u2f.sendRegisterRequest = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) { 711 | u2f.getPortSingleton_(function(port) { 712 | var reqId = ++u2f.reqCounter_; 713 | u2f.callbackMap_[reqId] = callback; 714 | var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ? 715 | opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC); 716 | var req = u2f.formatRegisterRequest_( 717 | appId, registeredKeys, registerRequests, timeoutSeconds, reqId); 718 | port.postMessage(req); 719 | }); 720 | }; 721 | 722 | 723 | /** 724 | * Dispatches a message to the extension to find out the supported 725 | * JS API version. 726 | * If the user is on a mobile phone and is thus using Google Authenticator instead 727 | * of the Chrome extension, don't send the request and simply return 0. 728 | * @param {function((u2f.Error|u2f.GetJsApiVersionResponse))} callback 729 | * @param {number=} opt_timeoutSeconds 730 | */ 731 | u2f.getApiVersion = function(callback, opt_timeoutSeconds) { 732 | u2f.getPortSingleton_(function(port) { 733 | // If we are using Android Google Authenticator or iOS client app, 734 | // do not fire an intent to ask which JS API version to use. 735 | if (port.getPortType) { 736 | var apiVersion; 737 | switch (port.getPortType()) { 738 | case 'WrappedIosPort_': 739 | case 'WrappedAuthenticatorPort_': 740 | apiVersion = 1.1; 741 | break; 742 | 743 | default: 744 | apiVersion = 0; 745 | break; 746 | } 747 | callback({ 'js_api_version': apiVersion }); 748 | return; 749 | } 750 | var reqId = ++u2f.reqCounter_; 751 | u2f.callbackMap_[reqId] = callback; 752 | var req = { 753 | type: u2f.MessageTypes.U2F_GET_API_VERSION_REQUEST, 754 | timeoutSeconds: (typeof opt_timeoutSeconds !== 'undefined' ? 755 | opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC), 756 | requestId: reqId 757 | }; 758 | port.postMessage(req); 759 | }); 760 | }; -------------------------------------------------------------------------------- /test-e2e/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "import/no-extraneous-dependencies": "off" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test-e2e/.gitignore: -------------------------------------------------------------------------------- 1 | config.js 2 | -------------------------------------------------------------------------------- /test-e2e/config.js.example: -------------------------------------------------------------------------------- 1 | export const config = { 2 | dp0: "44'/60'/0'/0", 3 | dp0Acc0: "0x0000000000000000000000000000000000000000", 4 | dp0Acc1: "0x0000000000000000000000000000000000000000", 5 | dp1: "44'/60'/1'/0", 6 | dp1Acc0: "0x0000000000000000000000000000000000000000", 7 | dp1Acc1: "0x0000000000000000000000000000000000000000", 8 | }; 9 | -------------------------------------------------------------------------------- /test-e2e/ganache-cli.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | exec ./node_modules/.bin/ganache-cli -i 44 \ 3 | --deterministic \ 4 | --account="0x47be2b1589bb515b76b47c514be96b23cd60ee37e81d63c2ae9c92f7d7667e1a, 1000000000000000000000" \ 5 | --account="0x72a4d3589099f14b31725dee59b186419bac41c42d2d02b2c70c1a8af2a2b6bb, 1000000000000000000000" \ 6 | --account="0x1ff8271bf14ac9bef0b641cced40dc2a7ebd2e37d8e16d25b4aa1911364219af, 1000000000000000000000" \ 7 | --account="0x1444ab10c1d1e8aabb89534218854df60d90bb45f39b55634777461d5a465e2e, 1000000000000000000000" \ 8 | --account="0xbff5647520d5e327178330ec0085ab27a58fb26ecb942f770397a940fa5c5d29, 1000000000000000000000" \ 9 | --account="0x8db53d08e85593ffb623e89e352bfed4eea350e6cc9812f11eac4de576f3cfda, 0" \ 10 | --account="0x24e467ab36f3cf70767135775ec1f7cc2a8b17363055e548113d85072136f945, 0" \ 11 | --account="0xc3bc1a16a82622f9bddf48f8e754c98092755e2e3782aafdca4ce21a1082747f, 0" \ 12 | --account="0xe54c55b3c5d80d445841afa3141e52592bec8523d8993d8df1811bfc5bf64d59, 0" 13 | -------------------------------------------------------------------------------- /test-e2e/setup.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-alert, no-console */ 2 | 3 | const Web3 = require("web3"); 4 | const { promisify } = require("bluebird"); 5 | 6 | const { config } = require("./config"); 7 | 8 | async function main() { 9 | const web3 = new Web3( 10 | new Web3.providers.HttpProvider("http://localhost:8545") 11 | ); 12 | web3.eth.getAccountsAsync = promisify(web3.eth.getAccounts); 13 | web3.eth.getBalanceAsync = promisify(web3.eth.getBalance); 14 | web3.eth.sendTransactionAsync = promisify(web3.eth.sendTransaction); 15 | 16 | const account0 = (await web3.eth.getAccountsAsync())[0]; 17 | 18 | await web3.eth.sendTransactionAsync({ 19 | from: account0, 20 | to: config.dp0Acc0, 21 | value: web3.toWei(10, "ether") 22 | }); 23 | 24 | await web3.eth.sendTransactionAsync({ 25 | from: account0, 26 | to: config.dp0Acc1, 27 | value: web3.toWei(10, "ether") 28 | }); 29 | 30 | await web3.eth.sendTransactionAsync({ 31 | from: account0, 32 | to: config.dp1Acc0, 33 | value: web3.toWei(10, "ether") 34 | }); 35 | 36 | await web3.eth.sendTransactionAsync({ 37 | from: account0, 38 | to: config.dp1Acc1, 39 | value: web3.toWei(10, "ether") 40 | }); 41 | 42 | console.log( 43 | web3 44 | .fromWei(await web3.eth.getBalanceAsync(config.dp0Acc0), "ether") 45 | .toString() 46 | ); 47 | console.log( 48 | web3 49 | .fromWei(await web3.eth.getBalanceAsync(config.dp0Acc1), "ether") 50 | .toString() 51 | ); 52 | console.log( 53 | web3 54 | .fromWei(await web3.eth.getBalanceAsync(config.dp1Acc0), "ether") 55 | .toString() 56 | ); 57 | console.log( 58 | web3 59 | .fromWei(await web3.eth.getBalanceAsync(config.dp1Acc1), "ether") 60 | .toString() 61 | ); 62 | } 63 | 64 | main().catch(err => { 65 | console.log(err); 66 | process.exit(1); 67 | }); 68 | -------------------------------------------------------------------------------- /test-e2e/test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-alert, no-console */ 2 | 3 | const Web3 = require("web3"); 4 | const ProviderEngine = require("web3-provider-engine"); 5 | const FetchSubprovider = require("web3-provider-engine/subproviders/fetch"); 6 | const HookedWalletSubprovider = require("web3-provider-engine/subproviders/hooked-wallet"); 7 | const { promisify } = require("bluebird"); 8 | 9 | const LedgerWallet = require("../src/LedgerWallet"); 10 | const { config } = require("./config"); 11 | 12 | const isNode = typeof window === "undefined"; 13 | 14 | // Public keys of accounts setup in ganache-cli.sh 15 | const ganacheAcc1 = isNode 16 | ? "0x7bd1e022bdfbd45d77913bec1582dc095cb5fa31" 17 | : "0x627d795782f653c8ea5e7a63b9cdfe5cb6846d9f"; 18 | const ganacheAcc2 = isNode 19 | ? "0x2e6d01625685281a1e3d10e4b41a61b4e6acb55f" 20 | : "0xf40011040398947b3c6b7532ed23fbc8c19c9654"; 21 | 22 | const rpcUrl = isNode 23 | ? "http://localhost:8545" 24 | : "https://localhost:8080/node/"; 25 | 26 | async function main() { 27 | const engine = new ProviderEngine(); 28 | const web3 = new Web3(engine); 29 | web3.eth.getAccountsAsync = promisify(web3.eth.getAccounts); 30 | web3.eth.getBalanceAsync = promisify(web3.eth.getBalance); 31 | web3.eth.sendTransactionAsync = promisify(web3.eth.sendTransaction); 32 | web3.eth.signAsync = promisify(web3.eth.sign); 33 | 34 | const ledger = new LedgerWallet(() => 44, config.dp0); 35 | await ledger.init(); 36 | engine.addProvider(new HookedWalletSubprovider(ledger)); 37 | engine.addProvider(new FetchSubprovider({ rpcUrl })); 38 | 39 | engine.start(); 40 | 41 | console.log('Signing message "hello world"'); 42 | const sha3 = web3.sha3("hello world"); 43 | const signedMsg = await web3.eth.signAsync(config.dp0Acc0, sha3); 44 | console.log(signedMsg); 45 | 46 | const ledgerDp0Acc0 = (await web3.eth.getAccountsAsync())[0]; 47 | console.log( 48 | `First account from ledger: ${ledgerDp0Acc0} on derivation path: ${ 49 | config.dp0 50 | }` 51 | ); 52 | if (config.dp0Acc0.toLowerCase() !== ledgerDp0Acc0.toLowerCase()) { 53 | engine.stop(); 54 | throw new Error("Account dp0Acc0 mismatch when using web3.eth.getAccounts"); 55 | } 56 | 57 | const ledgerDp0Accs = await ledger.getMultipleAccounts(config.dp0, 0, 2); 58 | const ledgerDp0Acc1 = ledgerDp0Accs[Object.keys(ledgerDp0Accs)[1]]; 59 | console.log( 60 | `Second account from ledger: ${ledgerDp0Acc1} on derivation path: ${ 61 | config.dp0 62 | }` 63 | ); 64 | if (config.dp0Acc1.toLowerCase() !== ledgerDp0Acc1.toLowerCase()) { 65 | engine.stop(); 66 | throw new Error( 67 | "Account dp1Acc1 mismatch when using ledger.MultipleAccounts" 68 | ); 69 | } 70 | 71 | await web3.eth.sendTransactionAsync({ 72 | from: ledgerDp0Acc0, 73 | to: ganacheAcc1, 74 | value: web3.toWei(0.5, "ether") 75 | }); 76 | 77 | ledger.setDerivationPath(config.dp1); 78 | 79 | const ledgerDp1Acc0 = (await web3.eth.getAccountsAsync())[0]; 80 | console.log( 81 | `First account from ledger: ${ledgerDp1Acc0} on derivation path: ${ 82 | config.dp1 83 | }` 84 | ); 85 | if (config.dp1Acc0.toLowerCase() !== ledgerDp1Acc0.toLowerCase()) { 86 | engine.stop(); 87 | throw new Error("Account dp1Acc0 mismatch when using web3.eth.getAccounts"); 88 | } 89 | 90 | const ledgerDp1Accs = await ledger.getMultipleAccounts(config.dp1, 0, 2); 91 | const ledgerDp1Acc1 = ledgerDp1Accs[Object.keys(ledgerDp1Accs)[1]]; 92 | console.log( 93 | `Second account from ledger: ${ledgerDp1Acc1} on derivation path: ${ 94 | config.dp1 95 | }` 96 | ); 97 | if (config.dp1Acc1.toLowerCase() !== ledgerDp1Acc1.toLowerCase()) { 98 | engine.stop(); 99 | throw new Error( 100 | "Account dp1Acc1 mismatch when using ledger.MultipleAccounts" 101 | ); 102 | } 103 | 104 | await web3.eth.sendTransactionAsync({ 105 | from: ledgerDp1Acc0, 106 | to: ganacheAcc2, 107 | value: web3.toWei(0.5, "ether") 108 | }); 109 | 110 | console.log( 111 | web3 112 | .fromWei(await web3.eth.getBalanceAsync(ganacheAcc1), "ether") 113 | .toString() 114 | ); 115 | console.log( 116 | web3 117 | .fromWei(await web3.eth.getBalanceAsync(ganacheAcc2), "ether") 118 | .toString() 119 | ); 120 | 121 | engine.stop(); 122 | } 123 | 124 | main().catch(err => { 125 | console.log(err); 126 | if (isNode) { 127 | process.exit(1); 128 | } 129 | }); 130 | -------------------------------------------------------------------------------- /test-e2e/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | test 6 | 7 | 8 | 9 |

10 | Open development console to see test output 11 |

12 | 13 | -------------------------------------------------------------------------------- /test/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "rules": { 6 | "no-unused-expressions": "off", 7 | "no-restricted-syntax": "off" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/LedgerWallet.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | 3 | import { obtainPathComponentsFromDerivationPath } from "../src/LedgerWallet"; 4 | 5 | describe("LedgerWallet", () => { 6 | describe("obtainPathComponentsFromDerivationPath()", () => { 7 | it("should parse correctly", () => { 8 | const correctDps = [ 9 | ["44'/60'/0'/0/", 0], 10 | ["44'/61'/0'/0/", 0], 11 | ["44'/60'/0/0/", 0], 12 | ["44'/60'/0'/1/", 0], 13 | ["44'/60'/0'/0/", 999], 14 | ["44'/60'/999'/0/", 0], 15 | ["44'/60'/0'/", 0], 16 | ["44'/61'/0'/", 0], 17 | ["44'/60'/0'/", 999], 18 | ["44'/60'/0/", 0], 19 | ["44'/60'/32'/", 0] 20 | ]; 21 | 22 | for (const correctDp of correctDps) { 23 | const pathComponents = obtainPathComponentsFromDerivationPath( 24 | correctDp[0] + correctDp[1] 25 | ); 26 | expect(pathComponents.basePath).to.be.equal(correctDp[0]); 27 | expect(pathComponents.index).to.be.equal(correctDp[1]); 28 | } 29 | }); 30 | it("should fail for derivation path not matching pattern", () => { 31 | const incorrectDps = [ 32 | "44'/60'/0'/2/0", 33 | "44'/62'/0'/0", 34 | "44'/60'/0'/", 35 | "44'/60'/0'/0'", 36 | "44'/60'/0'/0a", 37 | "a44'/60'/0'/0", 38 | "44'/60'/0'/0'/s", 39 | "44'/60'/0'/0/" 40 | ]; 41 | 42 | for (const incorrectDp of incorrectDps) { 43 | const f = () => obtainPathComponentsFromDerivationPath(incorrectDp); 44 | expect(f, `should throw on ${incorrectDp}`).to.throw(); 45 | } 46 | }); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = { 4 | entry: "./test-e2e/test.js", 5 | output: { 6 | filename: "bundle.js" 7 | }, 8 | 9 | module: { 10 | rules: [ 11 | { 12 | test: /\.js$/, 13 | exclude: /(node_modules)/, 14 | loader: "babel-loader" 15 | } 16 | ] 17 | }, 18 | 19 | devServer: { 20 | contentBase: path.join(__dirname, "/test-e2e/web"), 21 | host: "localhost", 22 | port: 8080, 23 | proxy: { 24 | "/node": { 25 | target: "http://localhost:8545", 26 | pathRewrite: { "^/node": "" } 27 | } 28 | }, 29 | staticOptions: { 30 | extensions: ["html"] 31 | } 32 | } 33 | }; 34 | --------------------------------------------------------------------------------