Wallet Decrypt - Ledger (#238)
* add static vendor js libraries * add ledger config * add ledger components * add ledger wallet * bugfix: trezor, set dPath on change * add rlp type package, update types * change address to public * update tslint script to exclude all files in directory * revert to private address and use getAddress() * remove unnecessary eslint line out of library files * remove IWallet import * Fix ts errors * Remove version controlled vendor files from DLL
This commit is contained in:
parent
81c817600f
commit
aac0176ca2
|
@ -0,0 +1,26 @@
|
|||
.LedgerDecrypt {
|
||||
text-align: center;
|
||||
padding-top: 30px;
|
||||
|
||||
&-decrypt {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&-help {
|
||||
margin-top: 10px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
&-error {
|
||||
opacity: 0;
|
||||
transition: none;
|
||||
|
||||
&.is-showing {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&-buy {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
|
@ -1,29 +1,150 @@
|
|||
import './LedgerNano.scss';
|
||||
import React, { Component } from 'react';
|
||||
import translate from 'translations';
|
||||
import translate, { translateRaw } from 'translations';
|
||||
import DeterministicWalletsModal from './DeterministicWalletsModal';
|
||||
import LedgerWallet from 'libs/wallet/ledger';
|
||||
import Ledger3 from 'vendor/ledger3';
|
||||
import LedgerEth from 'vendor/ledger-eth';
|
||||
import DPATHS from 'config/dpaths';
|
||||
|
||||
const DEFAULT_PATH = DPATHS.LEDGER[0].value;
|
||||
|
||||
interface Props {
|
||||
onUnlock(param: any): void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
publicKey: string;
|
||||
chainCode: string;
|
||||
dPath: string;
|
||||
error: string | null;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export default class LedgerNanoSDecrypt extends Component<Props, State> {
|
||||
public state: State = {
|
||||
publicKey: '',
|
||||
chainCode: '',
|
||||
dPath: DEFAULT_PATH,
|
||||
error: null,
|
||||
isLoading: false
|
||||
};
|
||||
|
||||
export default class LedgerNanoSDecrypt extends Component {
|
||||
public render() {
|
||||
const { dPath, publicKey, chainCode, error, isLoading } = this.state;
|
||||
const showErr = error ? 'is-showing' : '';
|
||||
|
||||
return (
|
||||
<section className="col-md-4 col-sm-6">
|
||||
<div id="selectedUploadKey">
|
||||
<h4>
|
||||
{translate('ADD_Radio_2_alt')}
|
||||
</h4>
|
||||
|
||||
<div className="form-group">
|
||||
<input type="file" id="fselector" />
|
||||
<section className="LedgerDecrypt col-md-4 col-sm-6">
|
||||
<button
|
||||
className="LedgerDecrypt-decrypt btn btn-primary btn-lg"
|
||||
onClick={this.handleNullConnect}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? 'Unlocking...' : translate('ADD_Ledger_scan')}
|
||||
</button>
|
||||
|
||||
<div className="LedgerDecrypt-help">
|
||||
Guides:
|
||||
<div>
|
||||
<a
|
||||
className="btn-file marg-v-sm"
|
||||
id="aria1"
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
href="http://support.ledgerwallet.com/knowledge_base/topics/how-to-use-myetherwallet-with-ledger"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
{translate('ADD_Radio_2_short')}
|
||||
How to use MyEtherWallet with your Nano S
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href="https://ledger.groovehq.com/knowledge_base/topics/how-to-secure-your-eth-tokens-augur-rep-dot-dot-dot-with-your-nano-s"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
How to secure your tokens with your Nano S
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`LedgerDecrypt-error alert alert-danger ${showErr}`}>
|
||||
{error || '-'}
|
||||
</div>
|
||||
|
||||
<a
|
||||
className="LedgerDecrypt-buy btn btn-sm btn-default"
|
||||
href="https://www.ledgerwallet.com/r/fa4b?path=/products/"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
{translate('Don’t have a Ledger? Order one now!')}
|
||||
</a>
|
||||
|
||||
<DeterministicWalletsModal
|
||||
isOpen={!!publicKey && !!chainCode}
|
||||
publicKey={publicKey}
|
||||
chainCode={chainCode}
|
||||
dPath={dPath}
|
||||
dPaths={DPATHS.LEDGER}
|
||||
onCancel={this.handleCancel}
|
||||
onConfirmAddress={this.handleUnlock}
|
||||
onPathChange={this.handlePathChange}
|
||||
walletType={translateRaw('x_Ledger')}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
private handlePathChange = (dPath: string) => {
|
||||
this.handleConnect(dPath);
|
||||
};
|
||||
|
||||
private handleConnect = (dPath: string = this.state.dPath) => {
|
||||
this.setState({
|
||||
isLoading: true,
|
||||
error: null
|
||||
});
|
||||
|
||||
const ledger = new Ledger3('w0w');
|
||||
const ethApp = new LedgerEth(ledger);
|
||||
|
||||
ethApp.getAddress(
|
||||
dPath,
|
||||
(res, err) => {
|
||||
if (err) {
|
||||
err = ethApp.getError(err);
|
||||
}
|
||||
|
||||
if (res) {
|
||||
this.setState({
|
||||
publicKey: res.publicKey,
|
||||
chainCode: res.chainCode,
|
||||
isLoading: false
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
error: err,
|
||||
isLoading: false
|
||||
});
|
||||
}
|
||||
},
|
||||
false,
|
||||
true
|
||||
);
|
||||
};
|
||||
|
||||
private handleCancel = () => {
|
||||
this.setState({
|
||||
publicKey: '',
|
||||
chainCode: '',
|
||||
dPath: DEFAULT_PATH
|
||||
});
|
||||
};
|
||||
|
||||
private handleUnlock = (address: string, index: number) => {
|
||||
this.props.onUnlock(new LedgerWallet(address, this.state.dPath, index));
|
||||
};
|
||||
|
||||
private handleNullConnect = (): void => {
|
||||
return this.handleConnect();
|
||||
};
|
||||
}
|
||||
|
|
|
@ -81,6 +81,7 @@ export default class TrezorDecrypt extends Component<Props, State> {
|
|||
}
|
||||
|
||||
private handlePathChange = (dPath: string) => {
|
||||
this.setState({ dPath });
|
||||
this.handleConnect(dPath);
|
||||
};
|
||||
|
||||
|
|
|
@ -51,7 +51,9 @@ const WALLETS = {
|
|||
'ledger-nano-s': {
|
||||
lid: 'x_Ledger',
|
||||
component: LedgerNanoSDecrypt,
|
||||
disabled: true
|
||||
initialParams: {},
|
||||
unlock: setWallet,
|
||||
disabled: false
|
||||
},
|
||||
trezor: {
|
||||
lid: 'x_Trezor',
|
||||
|
@ -120,9 +122,7 @@ export class WalletDecrypt extends Component<Props, State> {
|
|||
onChange={this.handleDecryptionChoiceChange}
|
||||
disabled={wallet.disabled}
|
||||
/>
|
||||
<span id={`${key}-label`}>
|
||||
{translate(wallet.lid)}
|
||||
</span>
|
||||
<span id={`${key}-label`}>{translate(wallet.lid)}</span>
|
||||
</label>
|
||||
);
|
||||
});
|
||||
|
@ -149,19 +149,15 @@ export class WalletDecrypt extends Component<Props, State> {
|
|||
return (
|
||||
<article className="Tab-content-pane row">
|
||||
<section className="col-md-4 col-sm-6">
|
||||
<h4>
|
||||
{translate('decrypt_Access')}
|
||||
</h4>
|
||||
<h4>{translate('decrypt_Access')}</h4>
|
||||
|
||||
{this.buildWalletOptions()}
|
||||
</section>
|
||||
|
||||
{decryptionComponent}
|
||||
{!!(this.state.value as PrivateKeyValue).valid &&
|
||||
{!!(this.state.value as PrivateKeyValue).valid && (
|
||||
<section className="col-md-4 col-sm-6">
|
||||
<h4 id="uploadbtntxt-wallet">
|
||||
{translate('ADD_Label_6')}
|
||||
</h4>
|
||||
<h4 id="uploadbtntxt-wallet">{translate('ADD_Label_6')}</h4>
|
||||
<div className="form-group">
|
||||
<a
|
||||
tabIndex={0}
|
||||
|
@ -172,7 +168,8 @@ export class WalletDecrypt extends Component<Props, State> {
|
|||
{translate('ADD_Label_6_short')}
|
||||
</a>
|
||||
</div>
|
||||
</section>}
|
||||
</section>
|
||||
)}
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -44,7 +44,10 @@ const MNEMONIC = [
|
|||
EXPANSE
|
||||
];
|
||||
|
||||
const LEDGER = [ETH_LEDGER, ETC_LEDGER, TESTNET];
|
||||
|
||||
export default {
|
||||
TREZOR,
|
||||
MNEMONIC
|
||||
MNEMONIC,
|
||||
LEDGER
|
||||
};
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import { IWallet } from './IWallet';
|
||||
|
||||
export default class DeterministicWallet {
|
||||
private address: string;
|
||||
private dPath: string;
|
||||
|
|
|
@ -5,3 +5,4 @@ export { default as PresaleWallet } from './presale';
|
|||
export { default as MewV1Wallet } from './mewv1';
|
||||
export { default as UtcWallet } from './utc';
|
||||
export { default as MnemonicWallet } from './mnemonic';
|
||||
export { default as LedgerWallet } from './ledger';
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
import Ledger3 from 'vendor/ledger3';
|
||||
import LedgerEth from 'vendor/ledger-eth';
|
||||
import EthTx from 'ethereumjs-tx';
|
||||
import { addHexPrefix, rlp } from 'ethereumjs-util';
|
||||
import DeterministicWallet from './deterministic';
|
||||
import { IWallet } from './IWallet';
|
||||
import { RawTransaction } from 'libs/transaction';
|
||||
|
||||
export default class LedgerWallet extends DeterministicWallet
|
||||
implements IWallet {
|
||||
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);
|
||||
}
|
||||
|
||||
// modeled after
|
||||
// https://github.com/kvhnuke/etherwallet/blob/3f7ff809e5d02d7ea47db559adaca1c930025e24/app/scripts/uiFuncs.js#L58
|
||||
public signRawTransaction(rawTx: RawTransaction): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const eTx = new EthTx({
|
||||
...rawTx,
|
||||
v: Buffer.from([rawTx.chainId]),
|
||||
r: 0,
|
||||
s: 0
|
||||
});
|
||||
|
||||
this.ethApp.signTransaction(
|
||||
this.getPath(),
|
||||
rlp.encode(eTx.raw).toString('hex'),
|
||||
(result, error) => {
|
||||
if (error) {
|
||||
return reject(this.ethApp.getError(error));
|
||||
}
|
||||
|
||||
const txToSerialize = {
|
||||
...rawTx,
|
||||
v: addHexPrefix(result.v),
|
||||
r: addHexPrefix(result.r),
|
||||
s: addHexPrefix(result.s)
|
||||
};
|
||||
|
||||
const serializedTx = new EthTx(txToSerialize)
|
||||
.serialize()
|
||||
.toString('hex');
|
||||
|
||||
resolve(addHexPrefix(serializedTx));
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// modeled after
|
||||
// https://github.com/kvhnuke/etherwallet/blob/3f7ff809e5d02d7ea47db559adaca1c930025e24/app/scripts/controllers/signMsgCtrl.js#L53
|
||||
public signMessage(msg: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const msgHex = Buffer.from(msg).toString('hex');
|
||||
|
||||
this.ethApp.signPersonalMessage_async(
|
||||
this.getPath(),
|
||||
msgHex,
|
||||
async (signed, error) => {
|
||||
if (error) {
|
||||
return reject(this.ethApp.getError(error));
|
||||
}
|
||||
|
||||
try {
|
||||
const combined = signed.r + signed.s + signed.v;
|
||||
const combinedHex = combined.toString('hex');
|
||||
const signedMsg = JSON.stringify(
|
||||
{
|
||||
address: await this.getAddress(),
|
||||
msg,
|
||||
sig: addHexPrefix(combinedHex),
|
||||
version: '2'
|
||||
},
|
||||
null,
|
||||
2
|
||||
);
|
||||
resolve(signedMsg);
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -40,6 +40,8 @@ declare module 'ethereumjs-tx' {
|
|||
|
||||
export = ITx;
|
||||
class ITx {
|
||||
public raw: Buffer;
|
||||
|
||||
constructor(data: Data);
|
||||
/**
|
||||
* If the tx's `to` is to the creation address
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
declare module 'ethereumjs-util' {
|
||||
import { Buffer } from 'buffer';
|
||||
import BN = require('bn.js');
|
||||
export import rlp = require('rlp');
|
||||
|
||||
interface Signature {
|
||||
v: number;
|
||||
|
|
|
@ -0,0 +1,283 @@
|
|||
/* 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;
|
|
@ -0,0 +1,87 @@
|
|||
/* 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;
|
|
@ -0,0 +1,832 @@
|
|||
/* 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.Transport>}
|
||||
*/
|
||||
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<u2f.SignRequest>} 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<u2f.SignRequest>} signRequests
|
||||
* @param {Array<u2f.RegisterRequest>} 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<function((MessagePort|u2f.WrappedChromeRuntimePort_))>}
|
||||
* @private
|
||||
*/
|
||||
u2f.waitingForPort_ = [];
|
||||
|
||||
/**
|
||||
* A counter for requestIds.
|
||||
* @type {number}
|
||||
* @private
|
||||
*/
|
||||
u2f.reqCounter_ = 0;
|
||||
|
||||
/**
|
||||
* A map from requestIds to client callbacks
|
||||
* @type {Object.<number,(function((u2f.Error|u2f.RegisterResponse))
|
||||
* |function((u2f.Error|u2f.SignResponse)))>}
|
||||
* @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.<u2f.Response>} 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<u2f.RegisteredKey>} 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<u2f.RegisteredKey>} 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<u2f.RegisterRequest>} registerRequests
|
||||
* @param {Array<u2f.RegisteredKey>} 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<u2f.RegisterRequest>} registerRequests
|
||||
* @param {Array<u2f.RegisteredKey>} 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;
|
|
@ -24,4 +24,3 @@ require('qrcode');
|
|||
require('qrcode.react');
|
||||
require('bignumber.js');
|
||||
require('classnames');
|
||||
require('./vendor/trezor-connect');
|
||||
|
|
|
@ -101,6 +101,7 @@
|
|||
"tslint-config-prettier": "^1.5.0",
|
||||
"tslint-react": "^3.2.0",
|
||||
"types-bn": "0.0.1",
|
||||
"types-rlp": "0.0.1",
|
||||
"typescript": "^2.5.2",
|
||||
"url-loader": "^0.5.8",
|
||||
"webpack": "^3.6.0",
|
||||
|
@ -120,7 +121,7 @@
|
|||
"dev:https": "HTTPS=true node webpack_config/server.js",
|
||||
"predev:https": "check-node-version --package",
|
||||
"derivation-checker": "webpack --config=./webpack_config/webpack.derivation-checker.js && node ./dist/derivation-checker.js",
|
||||
"tslint": "tslint --project . --exclude common/vendor/*",
|
||||
"tslint": "tslint --project . --exclude common/vendor/**/*",
|
||||
"postinstall": "webpack --config=./webpack_config/webpack.dll.js",
|
||||
"start": "npm run dev",
|
||||
"precommit": "lint-staged"
|
||||
|
|
|
@ -8,16 +8,14 @@
|
|||
"target": "es5",
|
||||
"allowJs": true,
|
||||
"baseUrl": "./common/",
|
||||
"lib": [
|
||||
"es2017",
|
||||
"dom"
|
||||
],
|
||||
"lib": ["es2017", "dom"],
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"moduleResolution": "node",
|
||||
"noEmitOnError": false
|
||||
},
|
||||
"include": [
|
||||
"./common/",
|
||||
"./node_modules/types-rlp/index.d.ts",
|
||||
"./node_modules/types-bn/index.d.ts"
|
||||
],
|
||||
"awesomeTypescriptLoaderOptions": {
|
||||
|
|
Loading…
Reference in New Issue