diff --git a/.travis.yml b/.travis.yml index 30f2115c..5ac7cb17 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,7 +14,8 @@ before_install: - export DISPLAY=:99.0 - sh -e /etc/init.d/xvfb start - docker pull dternyak/eth-priv-to-addr:latest - + - sudo apt-get install libusb-1.0 + install: - npm install --silent diff --git a/common/components/BalanceSidebar/AccountInfo.scss b/common/components/BalanceSidebar/AccountInfo.scss index cfc42bac..de16ff6a 100644 --- a/common/components/BalanceSidebar/AccountInfo.scss +++ b/common/components/BalanceSidebar/AccountInfo.scss @@ -1,5 +1,5 @@ -@import "common/sass/variables"; -@import "common/sass/mixins"; +@import 'common/sass/variables'; +@import 'common/sass/mixins'; .AccountInfo { &-section { @@ -22,18 +22,35 @@ &-address { @include clearfix; + &-section { + display: flex; + max-width: 100%; + flex-direction: row; + flex-wrap: nowrap; + } + + &-wrapper { + max-width: calc(100% - 44px - 24px); + } + &-icon { - float: left; width: 44px; height: 44px; margin-right: $space-md; } &-addr { - width: 100%; word-wrap: break-word; @include mono; } + + &-confirm { + display: flex; + align-items: center; + .Spinner { + margin-right: 16px; + } + } } &-list { @@ -53,8 +70,6 @@ .account-info { padding-left: 1em; margin: 0; - li { - } table { font-weight: 200; border-bottom: 0; diff --git a/common/components/BalanceSidebar/AccountInfo.tsx b/common/components/BalanceSidebar/AccountInfo.tsx index e363b4e3..059d1c6f 100644 --- a/common/components/BalanceSidebar/AccountInfo.tsx +++ b/common/components/BalanceSidebar/AccountInfo.tsx @@ -1,6 +1,6 @@ import { Identicon, UnitDisplay } from 'components/ui'; import { NetworkConfig } from 'config/data'; -import { IWallet, Balance } from 'libs/wallet'; +import { IWallet, Balance, TrezorWallet, LedgerWallet } from 'libs/wallet'; import React from 'react'; import translate from 'translations'; import './AccountInfo.scss'; @@ -15,11 +15,13 @@ interface Props { interface State { showLongBalance: boolean; address: string; + confirmAddr: boolean; } export default class AccountInfo extends React.Component { public state = { showLongBalance: false, - address: '' + address: '', + confirmAddr: false }; public async setAddressFromWallet() { @@ -37,6 +39,12 @@ export default class AccountInfo extends React.Component { this.setAddressFromWallet(); } + public toggleConfirmAddr = () => { + this.setState(state => { + return { confirmAddr: !state.confirmAddr }; + }); + }; + public toggleShowLongBalance = (e: React.FormEvent) => { e.preventDefault(); this.setState(state => { @@ -48,21 +56,46 @@ export default class AccountInfo extends React.Component { public render() { const { network, balance } = this.props; + const { address, showLongBalance, confirmAddr } = this.state; const { blockExplorer, tokenExplorer } = network; - const { address, showLongBalance } = this.state; - + const wallet = this.props.wallet as LedgerWallet | TrezorWallet; return (
-
-
{translate('sidebar_AccountAddr')}
-
-
- -
+
{translate('sidebar_AccountAddr')}
+
+
+ +
+
{address}
+ {typeof wallet.displayAddress === 'function' && ( + + )} +
{translate('sidebar_AccountBal')}
    diff --git a/common/components/WalletDecrypt/components/LedgerNano.tsx b/common/components/WalletDecrypt/components/LedgerNano.tsx index 7d97c49e..7347ac45 100644 --- a/common/components/WalletDecrypt/components/LedgerNano.tsx +++ b/common/components/WalletDecrypt/components/LedgerNano.tsx @@ -3,8 +3,7 @@ import React, { Component } from 'react'; import translate, { translateRaw } from 'translations'; import DeterministicWalletsModal from './DeterministicWalletsModal'; import { LedgerWallet } from 'libs/wallet'; -import Ledger3 from 'vendor/ledger3'; -import LedgerEth from 'vendor/ledger-eth'; +import ledger from 'ledgerco'; import DPATHS from 'config/dpaths'; import { Spinner } from 'components/ui'; @@ -136,35 +135,26 @@ export class LedgerNanoSDecrypt extends Component { showTip: false }); - const ledger = new Ledger3('w0w'); - const ethApp = new LedgerEth(ledger); - - ethApp.getAddress( - dPath, - (res, err) => { - if (err) { - if (err.errorCode === 5) { - this.showTip(); - } - err = ethApp.getError(err); - } - - if (res) { + ledger.comm_u2f.create_async().then(comm => { + new ledger.eth(comm) + .getAddress_async(dPath, false, true) + .then(res => { this.setState({ publicKey: res.publicKey, chainCode: res.chainCode, isLoading: false }); - } else { + }) + .catch(err => { + if (err.metaData.code === 5) { + this.showTip(); + } this.setState({ - error: err, + error: err.metaData.type, isLoading: false }); - } - }, - false, - true - ); + }); + }); }; private handleCancel = () => { diff --git a/common/components/WalletDecrypt/components/Trezor.tsx b/common/components/WalletDecrypt/components/Trezor.tsx index 26971a5b..1344711a 100644 --- a/common/components/WalletDecrypt/components/Trezor.tsx +++ b/common/components/WalletDecrypt/components/Trezor.tsx @@ -97,8 +97,7 @@ export class TrezorDecrypt extends Component { error: null }); - // TODO: type vendor file - (TrezorConnect as any).getXPubKey( + TrezorConnect.getXPubKey( dPath, res => { if (res.success) { diff --git a/common/libs/wallet/deterministic/deterministic.ts b/common/libs/wallet/deterministic/deterministic.ts index 6d6ef113..0805bf02 100644 --- a/common/libs/wallet/deterministic/deterministic.ts +++ b/common/libs/wallet/deterministic/deterministic.ts @@ -1,7 +1,7 @@ export class DeterministicWallet { - private address: string; - private dPath: string; - private index: number; + protected address: string; + protected dPath: string; + protected index: number; constructor(address: string, dPath: string, index: number) { this.address = address; diff --git a/common/libs/wallet/deterministic/ledger.ts b/common/libs/wallet/deterministic/ledger.ts index eb511b86..97d76280 100644 --- a/common/libs/wallet/deterministic/ledger.ts +++ b/common/libs/wallet/deterministic/ledger.ts @@ -1,19 +1,19 @@ -import Ledger3 from 'vendor/ledger3'; -import LedgerEth from 'vendor/ledger-eth'; +import ledger from 'ledgerco'; import EthTx, { TxObj } from 'ethereumjs-tx'; import { addHexPrefix, bufferToHex, toBuffer } from 'ethereumjs-util'; import { DeterministicWallet } from './deterministic'; import { getTransactionFields } from 'libs/transaction'; import { IFullWallet } from '../IWallet'; +import { translateRaw } from 'translations'; export class LedgerWallet extends DeterministicWallet implements IFullWallet { - private ledger: any; private ethApp: any; constructor(address: string, dPath: string, index: number) { super(address, dPath, index); - this.ledger = new Ledger3('w0w'); - this.ethApp = new LedgerEth(this.ledger); + ledger.comm_u2f.create_async().then(comm => { + this.ethApp = new ledger.eth(comm); + }); } // modeled after @@ -24,16 +24,10 @@ export class LedgerWallet extends DeterministicWallet implements IFullWallet { t.s = toBuffer(0); return new Promise((resolve, reject) => { - this.ethApp.signTransaction( - this.getPath(), - t.serialize().toString('hex'), - (result, error) => { - if (error) { - const errorMessage = this.ethApp.getError(error); - return reject(Error(errorMessage)); - } + this.ethApp + .signTransaction_async(this.getPath(), t.serialize().toString('hex')) + .then(result => { const strTx = getTransactionFields(t); - const txToSerialize: TxObj = { ...strTx, v: addHexPrefix(result.v), @@ -43,8 +37,10 @@ export class LedgerWallet extends DeterministicWallet implements IFullWallet { const serializedTx = new EthTx(txToSerialize).serialize(); resolve(serializedTx); - } - ); + }) + .catch(err => { + return reject(Error(err + '. Check to make sure contract data is on')); + }); }); } @@ -68,4 +64,25 @@ export class LedgerWallet extends DeterministicWallet implements IFullWallet { }); }); } + + public displayAddress = ( + dPath?: string, + index?: number + ): Promise<{ + publicKey: string; + address: string; + chainCode?: string; + }> => { + if (!dPath) { + dPath = this.dPath; + } + if (!index) { + index = this.index; + } + return this.ethApp.getAddress_async(dPath + '/' + index, true, false); + }; + + public getWalletType(): string { + return translateRaw('x_Ledger'); + } } diff --git a/common/libs/wallet/deterministic/trezor.ts b/common/libs/wallet/deterministic/trezor.ts index a49a4be9..aa6960e1 100644 --- a/common/libs/wallet/deterministic/trezor.ts +++ b/common/libs/wallet/deterministic/trezor.ts @@ -8,6 +8,7 @@ import { getTransactionFields } from 'libs/transaction'; import mapValues from 'lodash/mapValues'; import { IFullWallet } from '../IWallet'; +import { translateRaw } from 'translations'; export class TrezorWallet extends DeterministicWallet implements IFullWallet { public signRawTransaction(tx: EthTx): Promise { @@ -16,7 +17,7 @@ export class TrezorWallet extends DeterministicWallet implements IFullWallet { // stripHexPrefixAndLower identical to ethFuncs.getNakedAddress const cleanedTx = mapValues(mapValues(strTx, stripHexPrefixAndLower), padLeftEven); - (TrezorConnect as any).ethereumSignTx( + TrezorConnect.ethereumSignTx( // Args this.getPath(), cleanedTx.nonce, @@ -50,11 +51,26 @@ export class TrezorWallet extends DeterministicWallet implements IFullWallet { public signMessage = () => Promise.reject(new Error('Signing via Trezor not yet supported.')); + // trezor-connect.js doesn't provide the promise return type + public displayAddress = (dPath?: string, index?: number): Promise => { + if (!dPath) { + dPath = this.dPath; + } + if (!index) { + index = this.index; + } + return TrezorConnect.ethereumGetAddress(dPath + '/' + index); + }; + + public getWalletType(): string { + return translateRaw('x_Trezor'); + } + // works, but returns a signature that can only be verified with a Trezor device /* public signMessage = (message: string): Promise => { return new Promise((resolve, reject) => { - (TrezorConnect as any).ethereumSignMessage( + TrezorConnect.ethereumSignMessage( this.getPath(), message, response => { diff --git a/common/vendor/ledger-eth.js b/common/vendor/ledger-eth.js deleted file mode 100644 index 42a2f84f..00000000 --- a/common/vendor/ledger-eth.js +++ /dev/null @@ -1,283 +0,0 @@ -/* prettier-ignore */ - -/******************************************************************************** - * Ledger Communication toolkit - * (c) 2016 Ledger - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - ********************************************************************************/ - -'use strict'; - -// MEW - Require u2f instead of expecting it in global scope -var u2f = require('./u2f-api'); - -var LedgerEth = function(comm) { - this.comm = comm; -}; - -//MEW - Add error handling method -LedgerEth.prototype.getError = function(err) { - return err.errorCode - ? u2f.getErrorByCode(err.errorCode) - : err; -}; - -LedgerEth.splitPath = function(path) { - var result = []; - var components = path.split('/'); - components.forEach(function(element, index) { - var number = parseInt(element, 10); - if (isNaN(number)) { - return; - } - if (element.length > 1 && element[element.length - 1] == "'") { - number += 0x80000000; - } - result.push(number); - }); - return result; -}; - -// callback is function(response, error) -LedgerEth.prototype.getAddress = function( - path, - callback, - boolDisplay, - boolChaincode -) { - var splitPath = LedgerEth.splitPath(path); - var buffer = new Buffer(5 + 1 + splitPath.length * 4); - buffer[0] = 0xe0; - buffer[1] = 0x02; - buffer[2] = boolDisplay ? 0x01 : 0x00; - buffer[3] = boolChaincode ? 0x01 : 0x00; - buffer[4] = 1 + splitPath.length * 4; - buffer[5] = splitPath.length; - splitPath.forEach(function(element, index) { - buffer.writeUInt32BE(element, 6 + 4 * index); - }); - var self = this; - var localCallback = function(response, error) { - if (typeof error != 'undefined') { - callback(undefined, error); - } else { - var result = {}; - response = new Buffer(response, 'hex'); - var sw = response.readUInt16BE(response.length - 2); - if (sw != 0x9000) { - callback( - undefined, - 'Invalid status ' + - sw.toString(16) + - '. Check to make sure the right application is selected ?' - ); - return; - } - var publicKeyLength = response[0]; - var addressLength = response[1 + publicKeyLength]; - result['publicKey'] = response - .slice(1, 1 + publicKeyLength) - .toString('hex'); - result['address'] = - '0x' + - response - .slice( - 1 + publicKeyLength + 1, - 1 + publicKeyLength + 1 + addressLength - ) - .toString('ascii'); - if (boolChaincode) { - result['chainCode'] = response - .slice( - 1 + publicKeyLength + 1 + addressLength, - 1 + publicKeyLength + 1 + addressLength + 32 - ) - .toString('hex'); - } - callback(result); - } - }; - this.comm.exchange(buffer.toString('hex'), localCallback); -}; - -// callback is function(response, error) -LedgerEth.prototype.signTransaction = function(path, rawTxHex, callback) { - var splitPath = LedgerEth.splitPath(path); - var offset = 0; - var rawTx = new Buffer(rawTxHex, 'hex'); - var apdus = []; - while (offset != rawTx.length) { - var maxChunkSize = offset == 0 ? 150 - 1 - splitPath.length * 4 : 150; - var chunkSize = - offset + maxChunkSize > rawTx.length - ? rawTx.length - offset - : maxChunkSize; - var buffer = new Buffer( - offset == 0 ? 5 + 1 + splitPath.length * 4 + chunkSize : 5 + chunkSize - ); - buffer[0] = 0xe0; - buffer[1] = 0x04; - buffer[2] = offset == 0 ? 0x00 : 0x80; - buffer[3] = 0x00; - buffer[4] = offset == 0 ? 1 + splitPath.length * 4 + chunkSize : chunkSize; - if (offset == 0) { - buffer[5] = splitPath.length; - splitPath.forEach(function(element, index) { - buffer.writeUInt32BE(element, 6 + 4 * index); - }); - rawTx.copy(buffer, 6 + 4 * splitPath.length, offset, offset + chunkSize); - } else { - rawTx.copy(buffer, 5, offset, offset + chunkSize); - } - apdus.push(buffer.toString('hex')); - offset += chunkSize; - } - var self = this; - var localCallback = function(response, error) { - if (typeof error != 'undefined') { - callback(undefined, error); - } else { - response = new Buffer(response, 'hex'); - var sw = response.readUInt16BE(response.length - 2); - if (sw != 0x9000) { - callback( - undefined, - 'Invalid status ' + - sw.toString(16) + - '. Check to make sure contract data is on ?' - ); - return; - } - if (apdus.length == 0) { - var result = {}; - result['v'] = response.slice(0, 1).toString('hex'); - result['r'] = response.slice(1, 1 + 32).toString('hex'); - result['s'] = response.slice(1 + 32, 1 + 32 + 32).toString('hex'); - callback(result); - } else { - self.comm.exchange(apdus.shift(), localCallback); - } - } - }; - self.comm.exchange(apdus.shift(), localCallback); -}; - -// callback is function(response, error) -LedgerEth.prototype.getAppConfiguration = function(callback) { - var buffer = new Buffer(5); - buffer[0] = 0xe0; - buffer[1] = 0x06; - buffer[2] = 0x00; - buffer[3] = 0x00; - buffer[4] = 0x00; - var localCallback = function(response, error) { - if (typeof error != 'undefined') { - callback(undefined, error); - } else { - response = new Buffer(response, 'hex'); - var result = {}; - var sw = response.readUInt16BE(response.length - 2); - if (sw != 0x9000) { - callback( - undefined, - 'Invalid status ' + - sw.toString(16) + - '. Check to make sure the right application is selected ?' - ); - return; - } - result['arbitraryDataEnabled'] = response[0] & 0x01; - result['version'] = - '' + response[1] + '.' + response[2] + '.' + response[3]; - callback(result); - } - }; - this.comm.exchange(buffer.toString('hex'), localCallback); -}; - -LedgerEth.prototype.signPersonalMessage_async = function( - path, - messageHex, - callback -) { - var splitPath = LedgerEth.splitPath(path); - var offset = 0; - var message = new Buffer(messageHex, 'hex'); - var apdus = []; - var response = []; - var self = this; - while (offset != message.length) { - var maxChunkSize = offset == 0 ? 150 - 1 - splitPath.length * 4 - 4 : 150; - var chunkSize = - offset + maxChunkSize > message.length - ? message.length - offset - : maxChunkSize; - var buffer = new Buffer( - offset == 0 ? 5 + 1 + splitPath.length * 4 + 4 + chunkSize : 5 + chunkSize - ); - buffer[0] = 0xe0; - buffer[1] = 0x08; - buffer[2] = offset == 0 ? 0x00 : 0x80; - buffer[3] = 0x00; - buffer[4] = - offset == 0 ? 1 + splitPath.length * 4 + 4 + chunkSize : chunkSize; - if (offset == 0) { - buffer[5] = splitPath.length; - splitPath.forEach(function(element, index) { - buffer.writeUInt32BE(element, 6 + 4 * index); - }); - buffer.writeUInt32BE(message.length, 6 + 4 * splitPath.length); - message.copy( - buffer, - 6 + 4 * splitPath.length + 4, - offset, - offset + chunkSize - ); - } else { - message.copy(buffer, 5, offset, offset + chunkSize); - } - apdus.push(buffer.toString('hex')); - offset += chunkSize; - } - var self = this; - var localCallback = function(response, error) { - if (typeof error != 'undefined') { - callback(undefined, error); - } else { - response = new Buffer(response, 'hex'); - var sw = response.readUInt16BE(response.length - 2); - if (sw != 0x9000) { - callback( - undefined, - 'Invalid status ' + - sw.toString(16) + - '. Check to make sure the right application is selected ?' - ); - return; - } - if (apdus.length == 0) { - var result = {}; - result['v'] = response.slice(0, 1).toString('hex'); - result['r'] = response.slice(1, 1 + 32).toString('hex'); - result['s'] = response.slice(1 + 32, 1 + 32 + 32).toString('hex'); - callback(result); - } else { - self.comm.exchange(apdus.shift(), localCallback); - } - } - }; - self.comm.exchange(apdus.shift(), localCallback); -}; - -module.exports = LedgerEth; diff --git a/common/vendor/ledger3.js b/common/vendor/ledger3.js deleted file mode 100644 index 86d3ca23..00000000 --- a/common/vendor/ledger3.js +++ /dev/null @@ -1,87 +0,0 @@ -/* prettier-ignore */ - -/******************************************************************************** - * Ledger Communication toolkit - * (c) 2016 Ledger - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - ********************************************************************************/ - -'use strict'; - -// MEW - Require u2f instead of expecting it in global scope -var u2f = require('./u2f-api'); - -var Ledger3 = function(scrambleKey, timeoutSeconds) { - this.scrambleKey = new Buffer(scrambleKey, 'ascii'); - this.timeoutSeconds = timeoutSeconds; -}; - -Ledger3.wrapApdu = function(apdu, key) { - var result = new Buffer(apdu.length); - for (var i = 0; i < apdu.length; i++) { - result[i] = apdu[i] ^ key[i % key.length]; - } - return result; -}; - -// Convert from normal to web-safe, strip trailing "="s -Ledger3.webSafe64 = function(base64) { - return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); -}; - -// Convert from web-safe to normal, add trailing "="s -Ledger3.normal64 = function(base64) { - return ( - base64.replace(/\-/g, '+').replace(/_/g, '/') + - '=='.substring(0, 3 * base64.length % 4) - ); -}; - -Ledger3.prototype.u2fCallback = function(response, callback) { - if (typeof response['signatureData'] != 'undefined') { - var data = new Buffer( - Ledger3.normal64(response['signatureData']), - 'base64' - ); - callback(data.toString('hex', 5)); - } else { - callback(undefined, response); - } -}; - -// callback is function(response, error) -Ledger3.prototype.exchange = function(apduHex, callback) { - var apdu = new Buffer(apduHex, 'hex'); - var keyHandle = Ledger3.wrapApdu(apdu, this.scrambleKey); - var challenge = new Buffer( - '0000000000000000000000000000000000000000000000000000000000000000', - 'hex' - ); - var key = {}; - key['version'] = 'U2F_V2'; - key['keyHandle'] = Ledger3.webSafe64(keyHandle.toString('base64')); - var self = this; - var localCallback = function(result) { - self.u2fCallback(result, callback); - }; - u2f.sign( - location.origin, - Ledger3.webSafe64(challenge.toString('base64')), - [key], - localCallback, - this.timeoutSeconds - ); -}; - -module.exports = Ledger3; diff --git a/common/vendor/trezor-connect.js b/common/vendor/trezor-connect.js index 5317ef9e..42d65378 100644 --- a/common/vendor/trezor-connect.js +++ b/common/vendor/trezor-connect.js @@ -38,10 +38,8 @@ var ERR_CHROME_NOT_CONNECTED = 'Internal Chrome popup is not responding.'; var DISABLE_LOGIN_BUTTONS = window.TREZOR_DISABLE_LOGIN_BUTTONS || false; var CHROME_URL = window.TREZOR_CHROME_URL || './chrome/wrapper.html'; var POPUP_URL = - window.TREZOR_POPUP_URL || - 'https://connect.trezor.io/' + VERSION + '/popup/popup.html'; -var POPUP_PATH = - window.TREZOR_POPUP_PATH || 'https://connect.trezor.io/' + VERSION; + window.TREZOR_POPUP_URL || 'https://connect.trezor.io/' + VERSION + '/popup/popup.html'; +var POPUP_PATH = window.TREZOR_POPUP_PATH || 'https://connect.trezor.io/' + VERSION; var POPUP_ORIGIN = window.TREZOR_POPUP_ORIGIN || 'https://connect.trezor.io'; var INSIGHT_URLS = window.TREZOR_INSIGHT_URLS || [ @@ -52,27 +50,39 @@ var INSIGHT_URLS = window.TREZOR_INSIGHT_URLS || [ var POPUP_INIT_TIMEOUT = 15000; /** - * Public API. - */ -function TrezorConnect() { - var manager = new PopupManager(); + * Public API. + */ + +class TrezorConnect { + constructor() { + this.manager = new PopupManager(); + this.LOGIN_CSS = ''; + this.LOGIN_ONCLICK = + 'TrezorConnect.requestLogin(' + + "'@hosticon@','@challenge_hidden@','@challenge_visual@','@callback@'" + + ')'; + this.LOGIN_HTML = + '
    ' + + ' ' + + ' ' + + ' @text@' + + ' ' + + ' ' + + ' What is TREZOR?' + + ' ' + + '
    '; + } /** - * Popup errors. - */ - this.ERR_TIMED_OUT = ERR_TIMED_OUT; - this.ERR_WINDOW_CLOSED = ERR_WINDOW_CLOSED; - this.ERR_WINDOW_BLOCKED = ERR_WINDOW_BLOCKED; - this.ERR_ALREADY_WAITING = ERR_ALREADY_WAITING; - this.ERR_CHROME_NOT_CONNECTED = ERR_CHROME_NOT_CONNECTED; - - /** - * Open the popup for further communication. All API functions open the - * popup automatically, but if you need to generate some parameters - * asynchronously, use `open` first to avoid popup blockers. - * @param {function(?Error)} callback - */ - this.open = function(callback) { + * Open the popup for further communication. All API functions open the + * popup automatically, but if you need to generate some parameters + * asynchronously, use `open` first to avoid popup blockers. + * @param {function(?Error)} callback + */ + open(callback) { var onchannel = function(result) { if (result instanceof Error) { callback(result); @@ -80,93 +90,93 @@ function TrezorConnect() { callback(); } }; - manager.waitForChannel(onchannel); - }; + this.manager.waitForChannel(onchannel); + } /** - * Close the opened popup, if any. - */ - this.close = function() { - manager.close(); - }; + * Close the opened popup, if any. + */ + close() { + this.manager.close(); + } /** - * Enable or disable closing the opened popup after a successful call. - * @param {boolean} value - */ - this.closeAfterSuccess = function(value) { - manager.closeAfterSuccess = value; - }; + * Enable or disable closing the opened popup after a successful call. + * @param {boolean} value + */ + closeAfterSuccess(value) { + this.manager.closeAfterSuccess = value; + } /** - * Enable or disable closing the opened popup after a failed call. - * @param {boolean} value - */ - this.closeAfterFailure = function(value) { - manager.closeAfterFailure = value; - }; + * Enable or disable closing the opened popup after a failed call. + * @param {boolean} value + */ + closeAfterFailure(value) { + this.manager.closeAfterFailure = value; + } /** - * Set bitcore server - * @param {string|Array} value - */ - this.setBitcoreURLS = function(value) { + * Set bitcore server + * @param {string|Array} value + */ + setBitcoreURLS(value) { if (typeof value === 'string') { - manager.bitcoreURLS = [value]; + this.manager.bitcoreURLS = [value]; } else if (value instanceof Array) { - manager.bitcoreURLS = value; + this.manager.bitcoreURLS = value; } - }; + } /** - * Set max. limit for account discovery - * @param {number} value - */ - this.setAccountDiscoveryLimit = function(value) { - if (!isNaN(value)) manager.accountDiscoveryLimit = value; - }; + * Set max. limit for account discovery + * @param {number} value + */ + setAccountDiscoveryLimit(value) { + if (!isNaN(value)) this.manager.accountDiscoveryLimit = value; + } /** - * Set max. gap for account discovery - * @param {number} value - */ - this.setAccountDiscoveryGapLength = function(value) { - if (!isNaN(value)) manager.accountDiscoveryGapLength = value; - }; + * Set max. gap for account discovery + * @param {number} value + */ + setAccountDiscoveryGapLength(value) { + if (!isNaN(value)) this.manager.accountDiscoveryGapLength = value; + } /** - * Set discovery BIP44 coin type - * @param {number} value - */ - this.setAccountDiscoveryBip44CoinType = function(value) { - if (!isNaN(value)) manager.accountDiscoveryBip44CoinType = value; - }; + * Set discovery BIP44 coin type + * @param {number} value + */ + setAccountDiscoveryBip44CoinType(value) { + if (!isNaN(value)) this.manager.accountDiscoveryBip44CoinType = value; + } /** - * @typedef XPubKeyResult - * @param {boolean} success - * @param {?string} error - * @param {?string} xpubkey serialized extended public key - * @param {?string} path BIP32 serializd path of the key - */ + * @typedef XPubKeyResult + * @param {boolean} success + * @param {?string} error + * @param {?string} xpubkey serialized extended public key + * @param {?string} path BIP32 serializd path of the key + */ /** - * Load BIP32 extended public key by path. - * - * Path can be specified either in the string form ("m/44'/1/0") or as - * raw integer array. In case you omit the path, user is asked to select - * a BIP32 account to export, and the result contains m/44'/0'/x' node - * of the account. - * - * @param {?(string|array)} path - * @param {function(XPubKeyResult)} callback - * @param {?(string|array)} requiredFirmware - */ - this.getXPubKey = function(path, callback, requiredFirmware) { + * Load BIP32 extended public key by path. + * + * Path can be specified either in the string form ("m/44'/1/0") or as + * raw integer array. In case you omit the path, user is asked to select + * a BIP32 account to export, and the result contains m/44'/0'/x' node + * of the account. + * + * @param {?(string|array)} path + * @param {function(XPubKeyResult)} callback + * @param {?(string|array)} requiredFirmware + */ + getXPubKey(path, callback, requiredFirmware) { if (typeof path === 'string') { path = parseHDPath(path); } - manager.sendWithChannel( + this.manager.sendWithChannel( _fwStrFix( { type: 'xpubkey', @@ -176,9 +186,9 @@ function TrezorConnect() { ), callback ); - }; + } - this.getFreshAddress = function(callback, requiredFirmware) { + getFreshAddress(callback, requiredFirmware) { var wrapperCallback = function(result) { if (result.success) { callback({ success: true, address: result.freshAddress }); @@ -187,7 +197,7 @@ function TrezorConnect() { } }; - manager.sendWithChannel( + this.manager.sendWithChannel( _fwStrFix( { type: 'accountinfo' @@ -196,12 +206,12 @@ function TrezorConnect() { ), wrapperCallback ); - }; + } - this.getAccountInfo = function(input, callback, requiredFirmware) { + getAccountInfo(input, callback, requiredFirmware) { try { var description = parseAccountInfoInput(input); - manager.sendWithChannel( + this.manager.sendWithChannel( _fwStrFix( { type: 'accountinfo', @@ -214,11 +224,11 @@ function TrezorConnect() { } catch (e) { callback({ success: false, error: e }); } - }; + } - this.getAllAccountsInfo = function(callback, requiredFirmware) { + getAllAccountsInfo(callback, requiredFirmware) { try { - manager.sendWithChannel( + this.manager.sendWithChannel( _fwStrFix( { type: 'allaccountsinfo', @@ -231,10 +241,10 @@ function TrezorConnect() { } catch (e) { callback({ success: false, error: e }); } - }; + } - this.getBalance = function(callback, requiredFirmware) { - manager.sendWithChannel( + getBalance(callback, requiredFirmware) { + this.manager.sendWithChannel( _fwStrFix( { type: 'accountinfo' @@ -243,29 +253,29 @@ function TrezorConnect() { ), callback ); - }; + } /** - * @typedef SignTxResult - * @param {boolean} success - * @param {?string} error - * @param {?string} serialized_tx serialized tx, in hex, including signatures - * @param {?array} signatures array of input signatures, in hex - */ + * @typedef SignTxResult + * @param {boolean} success + * @param {?string} error + * @param {?string} serialized_tx serialized tx, in hex, including signatures + * @param {?array} signatures array of input signatures, in hex + */ /** - * Sign a transaction in the device and return both serialized - * transaction and the signatures. - * - * @param {array} inputs - * @param {array} outputs - * @param {function(SignTxResult)} callback - * @param {?(string|array)} requiredFirmware - * - * @see https://github.com/trezor/trezor-common/blob/master/protob/types.proto - */ - this.signTx = function(inputs, outputs, callback, requiredFirmware, coin) { - manager.sendWithChannel( + * Sign a transaction in the device and return both serialized + * transaction and the signatures. + * + * @param {array} inputs + * @param {array} outputs + * @param {function(SignTxResult)} callback + * @param {?(string|array)} requiredFirmware + * + * @see https://github.com/trezor/trezor-common/blob/master/protob/types.proto + */ + signTx(inputs, outputs, callback, requiredFirmware, coin) { + this.manager.sendWithChannel( _fwStrFix( { type: 'signtx', @@ -277,15 +287,15 @@ function TrezorConnect() { ), callback ); - }; + } // new implementation with ethereum at beginnig - this.ethereumSignTx = function() { + ethereumSignTx() { this.signEthereumTx.apply(this, arguments); - }; + } // old fallback - this.signEthereumTx = function( + signEthereumTx( address_n, nonce, gas_price, @@ -303,7 +313,7 @@ function TrezorConnect() { if (typeof address_n === 'string') { address_n = parseHDPath(address_n); } - manager.sendWithChannel( + this.manager.sendWithChannel( _fwStrFix( { type: 'signethtx', @@ -320,26 +330,26 @@ function TrezorConnect() { ), callback ); - }; + } /** - * @typedef TxRecipient - * @param {number} amount the amount to send, in satoshis - * @param {string} address the address of the recipient - */ + * @typedef TxRecipient + * @param {number} amount the amount to send, in satoshis + * @param {string} address the address of the recipient + */ /** - * Compose a transaction by doing BIP-0044 discovery, letting the user - * select an account, and picking UTXO by internal preferences. - * Transaction is then signed and returned in the same format as - * `signTx`. Only supports BIP-0044 accounts (single-signature). - * - * @param {array} recipients - * @param {function(SignTxResult)} callback - * @param {?(string|array)} requiredFirmware - */ - this.composeAndSignTx = function(recipients, callback, requiredFirmware) { - manager.sendWithChannel( + * Compose a transaction by doing BIP-0044 discovery, letting the user + * select an account, and picking UTXO by internal preferences. + * Transaction is then signed and returned in the same format as + * `signTx`. Only supports BIP-0044 accounts (single-signature). + * + * @param {array} recipients + * @param {function(SignTxResult)} callback + * @param {?(string|array)} requiredFirmware + */ + composeAndSignTx(recipients, callback, requiredFirmware) { + this.manager.sendWithChannel( _fwStrFix( { type: 'composetx', @@ -349,34 +359,28 @@ function TrezorConnect() { ), callback ); - }; + } /** - * @typedef RequestLoginResult - * @param {boolean} success - * @param {?string} error - * @param {?string} public_key public key used for signing, in hex - * @param {?string} signature signature, in hex - */ + * @typedef RequestLoginResult + * @param {boolean} success + * @param {?string} error + * @param {?string} public_key public key used for signing, in hex + * @param {?string} signature signature, in hex + */ /** - * Sign a login challenge for active origin. - * - * @param {?string} hosticon - * @param {string} challenge_hidden - * @param {string} challenge_visual - * @param {string|function(RequestLoginResult)} callback - * @param {?(string|array)} requiredFirmware - * - * @see https://github.com/trezor/trezor-common/blob/master/protob/messages.proto - */ - this.requestLogin = function( - hosticon, - challenge_hidden, - challenge_visual, - callback, - requiredFirmware - ) { + * Sign a login challenge for active origin. + * + * @param {?string} hosticon + * @param {string} challenge_hidden + * @param {string} challenge_visual + * @param {string|function(RequestLoginResult)} callback + * @param {?(string|array)} requiredFirmware + * + * @see https://github.com/trezor/trezor-common/blob/master/protob/messages.proto + */ + requestLogin(hosticon, challenge_hidden, challenge_visual, callback, requiredFirmware) { if (typeof callback === 'string') { // special case for a login through button. // `callback` is name of global var @@ -385,7 +389,7 @@ function TrezorConnect() { if (!callback) { throw new TypeError('TrezorConnect: login callback not found'); } - manager.sendWithChannel( + this.manager.sendWithChannel( _fwStrFix( { type: 'login', @@ -397,33 +401,27 @@ function TrezorConnect() { ), callback ); - }; + } /** - * @typedef SignMessageResult - * @param {boolean} success - * @param {?string} error - * @param {?string} address address (in base58check) - * @param {?string} signature signature, in base64 - */ + * @typedef SignMessageResult + * @param {boolean} success + * @param {?string} error + * @param {?string} address address (in base58check) + * @param {?string} signature signature, in base64 + */ /** - * Sign a message - * - * @param {string|array} path - * @param {string} message to sign (ascii) - * @param {string|function(SignMessageResult)} callback - * @param {?string} opt_coin - (optional) name of coin (default Bitcoin) - * @param {?(string|array)} requiredFirmware - * - */ - this.signMessage = function( - path, - message, - callback, - opt_coin, - requiredFirmware - ) { + * Sign a message + * + * @param {string|array} path + * @param {string} message to sign (ascii) + * @param {string|function(SignMessageResult)} callback + * @param {?string} opt_coin - (optional) name of coin (default Bitcoin) + * @param {?(string|array)} requiredFirmware + * + */ + signMessage(path, message, callback, opt_coin, requiredFirmware) { if (typeof path === 'string') { path = parseHDPath(path); } @@ -433,7 +431,7 @@ function TrezorConnect() { if (!callback) { throw new TypeError('TrezorConnect: callback not found'); } - manager.sendWithChannel( + this.manager.sendWithChannel( _fwStrFix( { type: 'signmsg', @@ -445,30 +443,25 @@ function TrezorConnect() { ), callback ); - }; + } /** - * Sign an Ethereum message - * - * @param {string|array} path - * @param {string} message to sign (ascii) - * @param {string|function(SignMessageResult)} callback - * @param {?(string|array)} requiredFirmware - * - */ - this.ethereumSignMessage = function( - path, - message, - callback, - requiredFirmware - ) { + * Sign an Ethereum message + * + * @param {string|array} path + * @param {string} message to sign (ascii) + * @param {string|function(SignMessageResult)} callback + * @param {?(string|array)} requiredFirmware + * + */ + ethereumSignMessage(path, message, callback, requiredFirmware) { if (typeof path === 'string') { path = parseHDPath(path); } if (!callback) { throw new TypeError('TrezorConnect: callback not found'); } - manager.sendWithChannel( + this.manager.sendWithChannel( _fwStrFix( { type: 'signethmsg', @@ -479,34 +472,27 @@ function TrezorConnect() { ), callback ); - }; + } /** - * Verify message - * - * @param {string} address - * @param {string} signature (base64) - * @param {string} message (string) - * @param {string|function()} callback - * @param {?string} opt_coin - (optional) name of coin (default Bitcoin) - * @param {?(string|array)} requiredFirmware - * - */ - this.verifyMessage = function( - address, - signature, - message, - callback, - opt_coin, - requiredFirmware - ) { + * Verify message + * + * @param {string} address + * @param {string} signature (base64) + * @param {string} message (string) + * @param {string|function()} callback + * @param {?string} opt_coin - (optional) name of coin (default Bitcoin) + * @param {?(string|array)} requiredFirmware + * + */ + verifyMessage(address, signature, message, callback, opt_coin, requiredFirmware) { if (!opt_coin) { opt_coin = 'Bitcoin'; } if (!callback) { throw new TypeError('TrezorConnect: callback not found'); } - manager.sendWithChannel( + this.manager.sendWithChannel( _fwStrFix( { type: 'verifymsg', @@ -519,29 +505,23 @@ function TrezorConnect() { ), callback ); - }; + } /** - * Verify ethereum message - * - * @param {string} address - * @param {string} signature (base64) - * @param {string} message (string) - * @param {string|function()} callback - * @param {?(string|array)} requiredFirmware - * - */ - this.ethereumVerifyMessage = function( - address, - signature, - message, - callback, - requiredFirmware - ) { + * Verify ethereum message + * + * @param {string} address + * @param {string} signature (base64) + * @param {string} message (string) + * @param {string|function()} callback + * @param {?(string|array)} requiredFirmware + * + */ + ethereumVerifyMessage(address, signature, message, callback, requiredFirmware) { if (!callback) { throw new TypeError('TrezorConnect: callback not found'); } - manager.sendWithChannel( + this.manager.sendWithChannel( _fwStrFix( { type: 'verifyethmsg', @@ -553,22 +533,22 @@ function TrezorConnect() { ), callback ); - }; + } /** - * Symmetric key-value encryption - * - * @param {string|array} path - * @param {string} key to show on device display - * @param {string} value hexadecimal value, length a multiple of 16 bytes - * @param {boolean} encrypt / decrypt direction - * @param {boolean} ask_on_encrypt (should user confirm on encrypt?) - * @param {boolean} ask_on_decrypt (should user confirm on decrypt?) - * @param {string|function()} callback - * @param {?(string|array)} requiredFirmware - * - */ - this.cipherKeyValue = function( + * Symmetric key-value encryption + * + * @param {string|array} path + * @param {string} key to show on device display + * @param {string} value hexadecimal value, length a multiple of 16 bytes + * @param {boolean} encrypt / decrypt direction + * @param {boolean} ask_on_encrypt (should user confirm on encrypt?) + * @param {boolean} ask_on_decrypt (should user confirm on decrypt?) + * @param {string|function()} callback + * @param {?(string|array)} requiredFirmware + * + */ + cipherKeyValue( path, key, value, @@ -589,14 +569,12 @@ function TrezorConnect() { } if (value.length % 32 !== 0) { // 1 byte == 2 hex strings - throw new TypeError( - 'TrezorConnect: Value length must be multiple of 16 bytes' - ); + throw new TypeError('TrezorConnect: Value length must be multiple of 16 bytes'); } if (!callback) { throw new TypeError('TrezorConnect: callback not found'); } - manager.sendWithChannel( + this.manager.sendWithChannel( _fwStrFix( { type: 'cipherkeyvalue', @@ -611,9 +589,9 @@ function TrezorConnect() { ), callback ); - }; + } - this.pushTransaction = function(rawTx, callback) { + pushTransaction(rawTx, callback) { if (!/^[0-9A-Fa-f]*$/.test(rawTx)) { throw new TypeError('TrezorConnect: Transaction must be hexadecimal'); } @@ -650,29 +628,23 @@ function TrezorConnect() { }; tryUrl(0); - }; + } /** - * Display address on device - * - * @param {array} address - * @param {string} coin - * @param {boolean} segwit - * @param {?(string|array)} requiredFirmware - * - */ - this.getAddress = function( - address, - coin, - segwit, - callback, - requiredFirmware - ) { + * Display address on device + * + * @param {array} address + * @param {string} coin + * @param {boolean} segwit + * @param {?(string|array)} requiredFirmware + * + */ + getAddress(address, coin, segwit, callback, requiredFirmware) { if (typeof address === 'string') { address = parseHDPath(address); } - manager.sendWithChannel( + this.manager.sendWithChannel( _fwStrFix( { type: 'getaddress', @@ -684,60 +656,46 @@ function TrezorConnect() { ), callback ); - }; + } /** - * Display ethereum address on device - * - * @param {array} address - * @param {?(string|array)} requiredFirmware - * - */ - this.ethereumGetAddress = function(address, callback, requiredFirmware) { + * Display ethereum address on device + * + * @param {string|array} address + * @param {string|array} requiredFirmware + * @param {function()} callback + * + */ + ethereumGetAddress(address, requiredFirmware = undefined) { if (typeof address === 'string') { address = parseHDPath(address); } - - manager.sendWithChannel( - _fwStrFix( - { - type: 'ethgetaddress', - address_n: address - }, - requiredFirmware - ), - callback - ); - }; - - var LOGIN_CSS = - ''; - - var LOGIN_ONCLICK = - 'TrezorConnect.requestLogin(' + - "'@hosticon@','@challenge_hidden@','@challenge_visual@','@callback@'" + - ')'; - - var LOGIN_HTML = - '
    ' + - ' ' + - ' ' + - ' @text@' + - ' ' + - ' ' + - ' What is TREZOR?' + - ' ' + - '
    '; + return new Promise((resolve, reject) => { + this.manager.sendWithChannel( + _fwStrFix( + { + type: 'ethgetaddress', + address_n: address + }, + requiredFirmware + ), + response => { + if (response.error) { + reject(response.error.message); + } else { + resolve(response); + } + } + ); + }); + } /** - * Find elements and replace them with login buttons. - * It's not required to use these special elements, feel free to call - * `TrezorConnect.requestLogin` directly. - */ - this.renderLoginButtons = function() { + * Find elements and replace them with login buttons. + * It's not required to use these special elements, feel free to call + * `TrezorConnect.requestLogin` directly. + */ + renderLoginButtons() { var elements = document.getElementsByTagName('trezor:login'); for (var i = 0; i < elements.length; i++) { @@ -751,7 +709,7 @@ function TrezorConnect() { // it's not valid to put markup into attributes, so let users // supply a raw text and make TREZOR bold text = text.replace('TREZOR', 'TREZOR'); - e.outerHTML = (LOGIN_CSS + LOGIN_HTML) + e.outerHTML = (this.LOGIN_CSS + this.LOGIN_HTML) .replace('@text@', text) .replace('@callback@', callback) .replace('@hosticon@', hosticon) @@ -759,7 +717,7 @@ function TrezorConnect() { .replace('@challenge_visual@', challenge_visual) .replace('@connect_path@', POPUP_PATH); } - }; + } } /* @@ -1079,8 +1037,7 @@ function PopupManager() { message.bitcoreURLS = this.bitcoreURLS || null; message.accountDiscoveryLimit = this.accountDiscoveryLimit || null; message.accountDiscoveryGapLength = this.accountDiscoveryGapLength || null; - message.accountDiscoveryBip44CoinType = - this.accountDiscoveryBip44CoinType || null; + message.accountDiscoveryBip44CoinType = this.accountDiscoveryBip44CoinType || null; var respond = function(response) { var succ = response.success && this.closeAfterSuccess; diff --git a/common/vendor/u2f-api.js b/common/vendor/u2f-api.js deleted file mode 100644 index 1fdfe41c..00000000 --- a/common/vendor/u2f-api.js +++ /dev/null @@ -1,832 +0,0 @@ -/* prettier-ignore */ - -//Copyright 2014-2015 Google Inc. All rights reserved. - -//Use of this source code is governed by a BSD-style -//license that can be found in the LICENSE file or at -//https://developers.google.com/open-source/licenses/bsd - -/** - * @fileoverview The U2F api. - */ -'use strict'; - -/** - * Namespace for the U2F api. - * @type {Object} - */ -var u2f = u2f || {}; - -/** - * FIDO U2F Javascript API Version - * @number - */ -var js_api_version; - -/** - * The U2F extension id - * @const {string} - */ -// The Chrome packaged app extension ID. -// Uncomment this if you want to deploy a server instance that uses -// the package Chrome app and does not require installing the U2F Chrome extension. -u2f.EXTENSION_ID = 'kmendfapggjehodndflmmgagdbamhnfd'; -// The U2F Chrome extension ID. -// Uncomment this if you want to deploy a server instance that uses -// the U2F Chrome extension to authenticate. -// u2f.EXTENSION_ID = 'pfboblefjcgdjicmnffhdgionmgcdmne'; - -/** - * Message types for messsages to/from the extension - * @const - * @enum {string} - */ -u2f.MessageTypes = { - U2F_REGISTER_REQUEST: 'u2f_register_request', - U2F_REGISTER_RESPONSE: 'u2f_register_response', - U2F_SIGN_REQUEST: 'u2f_sign_request', - U2F_SIGN_RESPONSE: 'u2f_sign_response', - U2F_GET_API_VERSION_REQUEST: 'u2f_get_api_version_request', - U2F_GET_API_VERSION_RESPONSE: 'u2f_get_api_version_response' -}; - -/** - * Response status codes - * @const - * @enum {number} - */ -u2f.ErrorCodes = { - OK: 0, - OTHER_ERROR: 1, - BAD_REQUEST: 2, - CONFIGURATION_UNSUPPORTED: 3, - DEVICE_INELIGIBLE: 4, - TIMEOUT: 5 -}; - -u2f.getErrorByCode = function(code) { - for (var prop in u2f.ErrorCodes) { - if (u2f.ErrorCodes.hasOwnProperty(prop)) { - if (u2f.ErrorCodes[prop] === code) return prop; - } - } -}; - -/** - * A message for registration requests - * @typedef {{ - * type: u2f.MessageTypes, - * appId: ?string, - * timeoutSeconds: ?number, - * requestId: ?number - * }} - */ -u2f.U2fRequest; - -/** - * A message for registration responses - * @typedef {{ - * type: u2f.MessageTypes, - * responseData: (u2f.Error | u2f.RegisterResponse | u2f.SignResponse), - * requestId: ?number - * }} - */ -u2f.U2fResponse; - -/** - * An error object for responses - * @typedef {{ - * errorCode: u2f.ErrorCodes, - * errorMessage: ?string - * }} - */ -u2f.Error; - -/** - * Data object for a single sign request. - * @typedef {enum {BLUETOOTH_RADIO, BLUETOOTH_LOW_ENERGY, USB, NFC}} - */ -u2f.Transport; - -/** - * Data object for a single sign request. - * @typedef {Array} - */ -u2f.Transports; - -/** - * Data object for a single sign request. - * @typedef {{ - * version: string, - * challenge: string, - * keyHandle: string, - * appId: string - * }} - */ -u2f.SignRequest; - -/** - * Data object for a sign response. - * @typedef {{ - * keyHandle: string, - * signatureData: string, - * clientData: string - * }} - */ -u2f.SignResponse; - -/** - * Data object for a registration request. - * @typedef {{ - * version: string, - * challenge: string - * }} - */ -u2f.RegisterRequest; - -/** - * Data object for a registration response. - * @typedef {{ - * version: string, - * keyHandle: string, - * transports: Transports, - * appId: string - * }} - */ -u2f.RegisterResponse; - -/** - * Data object for a registered key. - * @typedef {{ - * version: string, - * keyHandle: string, - * transports: ?Transports, - * appId: ?string - * }} - */ -u2f.RegisteredKey; - -/** - * Data object for a get API register response. - * @typedef {{ - * js_api_version: number - * }} - */ -u2f.GetJsApiVersionResponse; - -//Low level MessagePort API support - -/** - * Sets up a MessagePort to the U2F extension using the - * available mechanisms. - * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback - */ -u2f.getMessagePort = function(callback) { - if (typeof chrome != 'undefined' && chrome.runtime) { - // The actual message here does not matter, but we need to get a reply - // for the callback to run. Thus, send an empty signature request - // in order to get a failure response. - var msg = { - type: u2f.MessageTypes.U2F_SIGN_REQUEST, - signRequests: [] - }; - chrome.runtime.sendMessage(u2f.EXTENSION_ID, msg, function() { - if (!chrome.runtime.lastError) { - // We are on a whitelisted origin and can talk directly - // with the extension. - u2f.getChromeRuntimePort_(callback); - } else { - // chrome.runtime was available, but we couldn't message - // the extension directly, use iframe - u2f.getIframePort_(callback); - } - }); - } else if (u2f.isAndroidChrome_()) { - u2f.getAuthenticatorPort_(callback); - } else if (u2f.isIosChrome_()) { - u2f.getIosPort_(callback); - } else { - // chrome.runtime was not available at all, which is normal - // when this origin doesn't have access to any extensions. - u2f.getIframePort_(callback); - } -}; - -/** - * Detect chrome running on android based on the browser's useragent. - * @private - */ -u2f.isAndroidChrome_ = function() { - var userAgent = navigator.userAgent; - return ( - userAgent.indexOf('Chrome') != -1 && userAgent.indexOf('Android') != -1 - ); -}; - -/** - * Detect chrome running on iOS based on the browser's platform. - * @private - */ -u2f.isIosChrome_ = function() { - return $.inArray(navigator.platform, ['iPhone', 'iPad', 'iPod']) > -1; -}; - -/** - * Connects directly to the extension via chrome.runtime.connect. - * @param {function(u2f.WrappedChromeRuntimePort_)} callback - * @private - */ -u2f.getChromeRuntimePort_ = function(callback) { - var port = chrome.runtime.connect(u2f.EXTENSION_ID, { - includeTlsChannelId: true - }); - setTimeout(function() { - callback(new u2f.WrappedChromeRuntimePort_(port)); - }, 0); -}; - -/** - * Return a 'port' abstraction to the Authenticator app. - * @param {function(u2f.WrappedAuthenticatorPort_)} callback - * @private - */ -u2f.getAuthenticatorPort_ = function(callback) { - setTimeout(function() { - callback(new u2f.WrappedAuthenticatorPort_()); - }, 0); -}; - -/** - * Return a 'port' abstraction to the iOS client app. - * @param {function(u2f.WrappedIosPort_)} callback - * @private - */ -u2f.getIosPort_ = function(callback) { - setTimeout(function() { - callback(new u2f.WrappedIosPort_()); - }, 0); -}; - -/** - * A wrapper for chrome.runtime.Port that is compatible with MessagePort. - * @param {Port} port - * @constructor - * @private - */ -u2f.WrappedChromeRuntimePort_ = function(port) { - this.port_ = port; -}; - -/** - * Format and return a sign request compliant with the JS API version supported by the extension. - * @param {Array} signRequests - * @param {number} timeoutSeconds - * @param {number} reqId - * @return {Object} - */ -u2f.formatSignRequest_ = function( - appId, - challenge, - registeredKeys, - timeoutSeconds, - reqId -) { - if (js_api_version === undefined || js_api_version < 1.1) { - // Adapt request to the 1.0 JS API - var signRequests = []; - for (var i = 0; i < registeredKeys.length; i++) { - signRequests[i] = { - version: registeredKeys[i].version, - challenge: challenge, - keyHandle: registeredKeys[i].keyHandle, - appId: appId - }; - } - return { - type: u2f.MessageTypes.U2F_SIGN_REQUEST, - signRequests: signRequests, - timeoutSeconds: timeoutSeconds, - requestId: reqId - }; - } - // JS 1.1 API - return { - type: u2f.MessageTypes.U2F_SIGN_REQUEST, - appId: appId, - challenge: challenge, - registeredKeys: registeredKeys, - timeoutSeconds: timeoutSeconds, - requestId: reqId - }; -}; - -/** - * Format and return a register request compliant with the JS API version supported by the extension.. - * @param {Array} signRequests - * @param {Array} signRequests - * @param {number} timeoutSeconds - * @param {number} reqId - * @return {Object} - */ -u2f.formatRegisterRequest_ = function( - appId, - registeredKeys, - registerRequests, - timeoutSeconds, - reqId -) { - if (js_api_version === undefined || js_api_version < 1.1) { - // Adapt request to the 1.0 JS API - for (var i = 0; i < registerRequests.length; i++) { - registerRequests[i].appId = appId; - } - var signRequests = []; - for (var i = 0; i < registeredKeys.length; i++) { - signRequests[i] = { - version: registeredKeys[i].version, - challenge: registerRequests[0], - keyHandle: registeredKeys[i].keyHandle, - appId: appId - }; - } - return { - type: u2f.MessageTypes.U2F_REGISTER_REQUEST, - signRequests: signRequests, - registerRequests: registerRequests, - timeoutSeconds: timeoutSeconds, - requestId: reqId - }; - } - // JS 1.1 API - return { - type: u2f.MessageTypes.U2F_REGISTER_REQUEST, - appId: appId, - registerRequests: registerRequests, - registeredKeys: registeredKeys, - timeoutSeconds: timeoutSeconds, - requestId: reqId - }; -}; - -/** - * Posts a message on the underlying channel. - * @param {Object} message - */ -u2f.WrappedChromeRuntimePort_.prototype.postMessage = function(message) { - this.port_.postMessage(message); -}; - -/** - * Emulates the HTML 5 addEventListener interface. Works only for the - * onmessage event, which is hooked up to the chrome.runtime.Port.onMessage. - * @param {string} eventName - * @param {function({data: Object})} handler - */ -u2f.WrappedChromeRuntimePort_.prototype.addEventListener = function( - eventName, - handler -) { - var name = eventName.toLowerCase(); - if (name == 'message' || name == 'onmessage') { - this.port_.onMessage.addListener(function(message) { - // Emulate a minimal MessageEvent object - handler({ data: message }); - }); - } else { - console.error('WrappedChromeRuntimePort only supports onMessage'); - } -}; - -/** - * Wrap the Authenticator app with a MessagePort interface. - * @constructor - * @private - */ -u2f.WrappedAuthenticatorPort_ = function() { - this.requestId_ = -1; - this.requestObject_ = null; -}; - -/** - * Launch the Authenticator intent. - * @param {Object} message - */ -u2f.WrappedAuthenticatorPort_.prototype.postMessage = function(message) { - var intentUrl = - u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ + - ';S.request=' + - encodeURIComponent(JSON.stringify(message)) + - ';end'; - document.location = intentUrl; -}; - -/** - * Tells what type of port this is. - * @return {String} port type - */ -u2f.WrappedAuthenticatorPort_.prototype.getPortType = function() { - return 'WrappedAuthenticatorPort_'; -}; - -/** - * Emulates the HTML 5 addEventListener interface. - * @param {string} eventName - * @param {function({data: Object})} handler - */ -u2f.WrappedAuthenticatorPort_.prototype.addEventListener = function( - eventName, - handler -) { - var name = eventName.toLowerCase(); - if (name == 'message') { - var self = this; - /* Register a callback to that executes when - * chrome injects the response. */ - window.addEventListener( - 'message', - self.onRequestUpdate_.bind(self, handler), - false - ); - } else { - console.error('WrappedAuthenticatorPort only supports message'); - } -}; - -/** - * Callback invoked when a response is received from the Authenticator. - * @param function({data: Object}) callback - * @param {Object} message message Object - */ -u2f.WrappedAuthenticatorPort_.prototype.onRequestUpdate_ = function( - callback, - message -) { - var messageObject = JSON.parse(message.data); - var intentUrl = messageObject['intentURL']; - - var errorCode = messageObject['errorCode']; - var responseObject = null; - if (messageObject.hasOwnProperty('data')) { - responseObject /** @type {Object} */ = JSON.parse(messageObject['data']); - } - - callback({ data: responseObject }); -}; - -/** - * Base URL for intents to Authenticator. - * @const - * @private - */ -u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ = - 'intent:#Intent;action=com.google.android.apps.authenticator.AUTHENTICATE'; - -/** - * Wrap the iOS client app with a MessagePort interface. - * @constructor - * @private - */ -u2f.WrappedIosPort_ = function() {}; - -/** - * Launch the iOS client app request - * @param {Object} message - */ -u2f.WrappedIosPort_.prototype.postMessage = function(message) { - var str = JSON.stringify(message); - var url = 'u2f://auth?' + encodeURI(str); - location.replace(url); -}; - -/** - * Tells what type of port this is. - * @return {String} port type - */ -u2f.WrappedIosPort_.prototype.getPortType = function() { - return 'WrappedIosPort_'; -}; - -/** - * Emulates the HTML 5 addEventListener interface. - * @param {string} eventName - * @param {function({data: Object})} handler - */ -u2f.WrappedIosPort_.prototype.addEventListener = function(eventName, handler) { - var name = eventName.toLowerCase(); - if (name !== 'message') { - console.error('WrappedIosPort only supports message'); - } -}; - -/** - * Sets up an embedded trampoline iframe, sourced from the extension. - * @param {function(MessagePort)} callback - * @private - */ -u2f.getIframePort_ = function(callback) { - // Create the iframe - var iframeOrigin = 'chrome-extension://' + u2f.EXTENSION_ID; - var iframe = document.createElement('iframe'); - iframe.src = iframeOrigin + '/u2f-comms.html'; - iframe.setAttribute('style', 'display:none'); - document.body.appendChild(iframe); - - var channel = new MessageChannel(); - var ready = function(message) { - if (message.data == 'ready') { - channel.port1.removeEventListener('message', ready); - callback(channel.port1); - } else { - console.error('First event on iframe port was not "ready"'); - } - }; - channel.port1.addEventListener('message', ready); - channel.port1.start(); - - iframe.addEventListener('load', function() { - // Deliver the port to the iframe and initialize - iframe.contentWindow.postMessage('init', iframeOrigin, [channel.port2]); - }); -}; - -//High-level JS API - -/** - * Default extension response timeout in seconds. - * @const - */ -u2f.EXTENSION_TIMEOUT_SEC = 30; - -/** - * A singleton instance for a MessagePort to the extension. - * @type {MessagePort|u2f.WrappedChromeRuntimePort_} - * @private - */ -u2f.port_ = null; - -/** - * Callbacks waiting for a port - * @type {Array} - * @private - */ -u2f.waitingForPort_ = []; - -/** - * A counter for requestIds. - * @type {number} - * @private - */ -u2f.reqCounter_ = 0; - -/** - * A map from requestIds to client callbacks - * @type {Object.} - * @private - */ -u2f.callbackMap_ = {}; - -/** - * Creates or retrieves the MessagePort singleton to use. - * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback - * @private - */ -u2f.getPortSingleton_ = function(callback) { - if (u2f.port_) { - callback(u2f.port_); - } else { - if (u2f.waitingForPort_.length == 0) { - u2f.getMessagePort(function(port) { - u2f.port_ = port; - u2f.port_.addEventListener( - 'message', - /** @type {function(Event)} */ u2f.responseHandler_ - ); - - // Careful, here be async callbacks. Maybe. - while (u2f.waitingForPort_.length) - u2f.waitingForPort_.shift()(u2f.port_); - }); - } - u2f.waitingForPort_.push(callback); - } -}; - -/** - * Handles response messages from the extension. - * @param {MessageEvent.} message - * @private - */ -u2f.responseHandler_ = function(message) { - var response = message.data; - var reqId = response['requestId']; - if (!reqId || !u2f.callbackMap_[reqId]) { - console.error('Unknown or missing requestId in response.'); - return; - } - var cb = u2f.callbackMap_[reqId]; - delete u2f.callbackMap_[reqId]; - cb(response['responseData']); -}; - -/** - * Dispatches an array of sign requests to available U2F tokens. - * If the JS API version supported by the extension is unknown, it first sends a - * message to the extension to find out the supported API version and then it sends - * the sign request. - * @param {string=} appId - * @param {string=} challenge - * @param {Array} registeredKeys - * @param {function((u2f.Error|u2f.SignResponse))} callback - * @param {number=} opt_timeoutSeconds - */ -u2f.sign = function( - appId, - challenge, - registeredKeys, - callback, - opt_timeoutSeconds -) { - if (js_api_version === undefined) { - // Send a message to get the extension to JS API version, then send the actual sign request. - u2f.getApiVersion(function(response) { - js_api_version = - response['js_api_version'] === undefined - ? 0 - : response['js_api_version']; - console.log('Extension JS API Version: ', js_api_version); - u2f.sendSignRequest( - appId, - challenge, - registeredKeys, - callback, - opt_timeoutSeconds - ); - }); - } else { - // We know the JS API version. Send the actual sign request in the supported API version. - u2f.sendSignRequest( - appId, - challenge, - registeredKeys, - callback, - opt_timeoutSeconds - ); - } -}; - -/** - * Dispatches an array of sign requests to available U2F tokens. - * @param {string=} appId - * @param {string=} challenge - * @param {Array} registeredKeys - * @param {function((u2f.Error|u2f.SignResponse))} callback - * @param {number=} opt_timeoutSeconds - */ -u2f.sendSignRequest = function( - appId, - challenge, - registeredKeys, - callback, - opt_timeoutSeconds -) { - u2f.getPortSingleton_(function(port) { - var reqId = ++u2f.reqCounter_; - u2f.callbackMap_[reqId] = callback; - var timeoutSeconds = - typeof opt_timeoutSeconds !== 'undefined' - ? opt_timeoutSeconds - : u2f.EXTENSION_TIMEOUT_SEC; - var req = u2f.formatSignRequest_( - appId, - challenge, - registeredKeys, - timeoutSeconds, - reqId - ); - port.postMessage(req); - }); -}; - -/** - * Dispatches register requests to available U2F tokens. An array of sign - * requests identifies already registered tokens. - * If the JS API version supported by the extension is unknown, it first sends a - * message to the extension to find out the supported API version and then it sends - * the register request. - * @param {string=} appId - * @param {Array} registerRequests - * @param {Array} registeredKeys - * @param {function((u2f.Error|u2f.RegisterResponse))} callback - * @param {number=} opt_timeoutSeconds - */ -u2f.register = function( - appId, - registerRequests, - registeredKeys, - callback, - opt_timeoutSeconds -) { - if (js_api_version === undefined) { - // Send a message to get the extension to JS API version, then send the actual register request. - u2f.getApiVersion(function(response) { - js_api_version = - response['js_api_version'] === undefined - ? 0 - : response['js_api_version']; - console.log('Extension JS API Version: ', js_api_version); - u2f.sendRegisterRequest( - appId, - registerRequests, - registeredKeys, - callback, - opt_timeoutSeconds - ); - }); - } else { - // We know the JS API version. Send the actual register request in the supported API version. - u2f.sendRegisterRequest( - appId, - registerRequests, - registeredKeys, - callback, - opt_timeoutSeconds - ); - } -}; - -/** - * Dispatches register requests to available U2F tokens. An array of sign - * requests identifies already registered tokens. - * @param {string=} appId - * @param {Array} registerRequests - * @param {Array} registeredKeys - * @param {function((u2f.Error|u2f.RegisterResponse))} callback - * @param {number=} opt_timeoutSeconds - */ -u2f.sendRegisterRequest = function( - appId, - registerRequests, - registeredKeys, - callback, - opt_timeoutSeconds -) { - u2f.getPortSingleton_(function(port) { - var reqId = ++u2f.reqCounter_; - u2f.callbackMap_[reqId] = callback; - var timeoutSeconds = - typeof opt_timeoutSeconds !== 'undefined' - ? opt_timeoutSeconds - : u2f.EXTENSION_TIMEOUT_SEC; - var req = u2f.formatRegisterRequest_( - appId, - registeredKeys, - registerRequests, - timeoutSeconds, - reqId - ); - port.postMessage(req); - }); -}; - -/** - * Dispatches a message to the extension to find out the supported - * JS API version. - * If the user is on a mobile phone and is thus using Google Authenticator instead - * of the Chrome extension, don't send the request and simply return 0. - * @param {function((u2f.Error|u2f.GetJsApiVersionResponse))} callback - * @param {number=} opt_timeoutSeconds - */ -u2f.getApiVersion = function(callback, opt_timeoutSeconds) { - u2f.getPortSingleton_(function(port) { - // If we are using Android Google Authenticator or iOS client app, - // do not fire an intent to ask which JS API version to use. - if (port.getPortType) { - var apiVersion; - switch (port.getPortType()) { - case 'WrappedIosPort_': - case 'WrappedAuthenticatorPort_': - apiVersion = 1.1; - break; - - default: - apiVersion = 0; - break; - } - callback({ js_api_version: apiVersion }); - return; - } - var reqId = ++u2f.reqCounter_; - u2f.callbackMap_[reqId] = callback; - var req = { - type: u2f.MessageTypes.U2F_GET_API_VERSION_REQUEST, - timeoutSeconds: - typeof opt_timeoutSeconds !== 'undefined' - ? opt_timeoutSeconds - : u2f.EXTENSION_TIMEOUT_SEC, - requestId: reqId - }; - port.postMessage(req); - }); -}; -module.exports = u2f; diff --git a/package.json b/package.json index c523b79e..5a994ebe 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "hdkey": "0.7.1", "idna-uts46": "1.1.0", "jsonschema": "1.2.2", + "ledgerco": "1.2.1", "lodash": "4.17.4", "moment": "2.20.1", "normalizr": "3.2.4", @@ -128,7 +129,8 @@ "db": "nodemon ./db", "build": "rimraf dist && webpack --config webpack_config/webpack.prod.js", "prebuild": "check-node-version --package", - "build:downloadable": "BUILD_DOWNLOADABLE=true rimraf dist && webpack --config webpack_config/webpack.prod.js", + "build:downloadable": + "BUILD_DOWNLOADABLE=true rimraf dist && webpack --config webpack_config/webpack.prod.js", "prebuild:demo": "check-node-version --package", "test:coverage": "jest --config=jest_config/jest.config.json --coverage", "test": "jest --config=jest_config/jest.config.json", @@ -144,8 +146,10 @@ "tscheck": "tsc --noEmit", "start": "npm run dev", "precommit": "lint-staged", - "formatAll": "find ./common/ -name '*.ts*' | xargs prettier --write --config ./.prettierrc --config-precedence file-override", - "prettier:diff": "prettier --write --config ./.prettierrc --list-different \"common/**/*.ts\" \"common/**/*.tsx\"", + "formatAll": + "find ./common/ -name '*.ts*' | xargs prettier --write --config ./.prettierrc --config-precedence file-override", + "prettier:diff": + "prettier --write --config ./.prettierrc --list-different \"common/**/*.ts\" \"common/**/*.tsx\"", "prepush": "npm run tslint && npm run tscheck" }, "lint-staged": {