diff options
initial trezor support
Diffstat (limited to 'app/scripts/lib/trezorKeyring.js')
-rw-r--r-- | app/scripts/lib/trezorKeyring.js | 255 |
1 files changed, 255 insertions, 0 deletions
diff --git a/app/scripts/lib/trezorKeyring.js b/app/scripts/lib/trezorKeyring.js new file mode 100644 index 000000000..d99f384f2 --- /dev/null +++ b/app/scripts/lib/trezorKeyring.js @@ -0,0 +1,255 @@ +const { EventEmitter } = require('events') +const ethUtil = require('ethereumjs-util') +// const sigUtil = require('eth-sig-util') +//const { Lock } = require('semaphore-async-await') + +const hdPathString = `m/44'/60'/0'/0` +const keyringType = 'Trezor Hardware Keyring' + +const TrezorConnect = require('./trezor-connect.js') +const HDKey = require('hdkey') +const TREZOR_FIRMWARE_VERSION = '1.4.0' +const log = require('loglevel') + +class TrezorKeyring extends EventEmitter { + constructor (opts = {}) { + super() + this.type = keyringType + //this.lock = new Lock() + this.accounts = [] + this.hdk = new HDKey() + this.deserialize(opts) + this.page = 0 + this.perPage = 5 + } + + serialize () { + return Promise.resolve({ hdPath: this.hdPath, accounts: this.accounts }) + } + + deserialize (opts = {}) { + this.hdPath = opts.hdPath || hdPathString + this.accounts = opts.accounts || [] + return Promise.resolve() + } + + unlock () { + if (this.hdk.publicKey) return Promise.resolve() + + return new Promise((resolve, reject) => { + TrezorConnect.getXPubKey( + this.hdPath, + response => { + log.debug('TREZOR CONNECT RESPONSE: ') + log.debug(response) + if (response.success) { + this.hdk.publicKey = new Buffer(response.publicKey, 'hex') + this.hdk.chainCode = new Buffer(response.chainCode, 'hex') + resolve() + } else { + reject(response.error || 'Unknown error') + } + }, + TREZOR_FIRMWARE_VERSION + ) + }) + } + + addAccounts (n = 1) { + return new Promise((resolve, reject) => { + return this.unlock() + .then(_ => { + const pathBase = 'm' + const from = n + const to = n + 1 + + this.accounts = [] + + for (let i = from; i < to; i++) { + const dkey = this.hdk.derive(`${pathBase}/${i}`) + const address = ethUtil + .publicToAddress(dkey.publicKey, true) + .toString('hex') + this.accounts.push(ethUtil.toChecksumAddress(address)) + this.page = 0 + } + resolve(this.accounts) + }) + .catch(e => { + reject(e) + }) + }) + } + + async getPage () { + return new Promise((resolve, reject) => { + return this.unlock() + .then(_ => { + const pathBase = 'm' + const from = this.page === 0 ? 0 : (this.page - 1) * this.perPage + const to = from + this.perPage + + const accounts = [] + + for (let i = from; i < to; i++) { + const dkey = this.hdk.derive(`${pathBase}/${i}`) + const address = ethUtil + .publicToAddress(dkey.publicKey, true) + .toString('hex') + accounts.push({ + address: ethUtil.toChecksumAddress(address), + balance: 0, + index: i, + }) + } + resolve(accounts) + }) + .catch(e => { + reject(e) + }) + }) + } + + async getPrevAccountSet () { + this.page-- + return await this.getPage() + } + + async getNextAccountSet () { + this.page++ + return await this.getPage() + } + + getAccounts () { + return Promise.resolve(this.accounts.slice()) + } + + // tx is an instance of the ethereumjs-transaction class. + async signTransaction (address, tx) { + throw new Error('Not supported on this device') + /* + await this.lock.acquire() + try { + + // Look before we leap + await this._checkCorrectTrezorAttached() + + let accountId = await this._findAddressId(address) + let eth = await this._getEth() + tx.v = tx._chainId + let TrezorSig = await eth.signTransaction( + this._derivePath(accountId), + tx.serialize().toString('hex') + ) + tx.v = parseInt(TrezorSig.v, 16) + tx.r = '0x' + TrezorSig.r + tx.s = '0x' + TrezorSig.s + + // Since look before we leap check is racy, also check that signature is for account expected + let addressSignedWith = ethUtil.bufferToHex(tx.getSenderAddress()) + if (addressSignedWith.toLowerCase() !== address.toLowerCase()) { + throw new Error( + `Signature is for ${addressSignedWith} but expected ${address} - is the correct Trezor device attached?` + ) + } + + return tx + + } finally { + await this.lock.release() + }*/ + } + + async signMessage (withAccount, data) { + throw new Error('Not supported on this device') + } + + // For personal_sign, we need to prefix the message: + async signPersonalMessage (withAccount, message) { + throw new Error('Not supported on this device') + /* + await this.lock.acquire() + try { + // Look before we leap + await this._checkCorrectTrezorAttached() + + let accountId = await this._findAddressId(withAccount) + let eth = await this._getEth() + let msgHex = ethUtil.stripHexPrefix(message) + let TrezorSig = await eth.signPersonalMessage( + this._derivePath(accountId), + msgHex + ) + let signature = this._personalToRawSig(TrezorSig) + + // Since look before we leap check is racy, also check that signature is for account expected + let addressSignedWith = sigUtil.recoverPersonalSignature({ + data: message, + sig: signature, + }) + if (addressSignedWith.toLowerCase() !== withAccount.toLowerCase()) { + throw new Error( + `Signature is for ${addressSignedWith} but expected ${withAccount} - is the correct Trezor device attached?` + ) + } + + return signature + + } finally { + await this.lock.release() + } */ + } + + async signTypedData (withAccount, typedData) { + throw new Error('Not supported on this device') + } + + async exportAccount (address) { + throw new Error('Not supported on this device') + } + + async _findAddressId (addr) { + const result = this.accounts.indexOf(addr) + if (result === -1) throw new Error('Unknown address') + else return result + } + + async _addressFromId (i) { + /* Must be called with lock acquired + const eth = await this._getEth() + return (await eth.getAddress(this._derivePath(i))).address*/ + const result = this.accounts[i] + if (!result) throw new Error('Unknown address') + else return result + } + + async _checkCorrectTrezorAttached () { + return true + /* Must be called with lock acquired + if (this.accounts.length > 0) { + const expectedFirstAccount = this.accounts[0] + let actualFirstAccount = await this._addressFromId(0) + if (expectedFirstAccount !== actualFirstAccount) { + throw new Error( + `Incorrect Trezor device attached - expected device containg account ${expectedFirstAccount}, but found ${actualFirstAccount}` + ) + } + }*/ + } + + _derivePath (i) { + return this.hdPath + '/' + i + } + + _personalToRawSig (TrezorSig) { + var v = TrezorSig['v'] - 27 + v = v.toString(16) + if (v.length < 2) { + v = '0' + v + } + return '0x' + TrezorSig['r'] + TrezorSig['s'] + v + } +} + +TrezorKeyring.type = keyringType +module.exports = TrezorKeyring
\ No newline at end of file |