From 82983e5effb236474c7eba2ac2ba62ea3fe58f5f Mon Sep 17 00:00:00 2001 From: kumavis Date: Thu, 28 Apr 2016 14:16:24 -0700 Subject: idmgmt - eth_sign support + notifications --- app/scripts/background.js | 20 +++++++- app/scripts/lib/config-manager.js | 82 +++++++++++++++++++++++++++++-- app/scripts/lib/idStore.js | 99 ++++++++++++++++++++++++++++++++++++-- app/scripts/lib/notifications.js | 75 +++++++++++++++++++++++++++++ app/scripts/lib/tx-notification.js | 49 ------------------- 5 files changed, 266 insertions(+), 59 deletions(-) create mode 100644 app/scripts/lib/notifications.js delete mode 100644 app/scripts/lib/tx-notification.js (limited to 'app') diff --git a/app/scripts/background.js b/app/scripts/background.js index 83d0f575a..3c46f4693 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -7,7 +7,8 @@ const EthStore = require('eth-store') const PortStream = require('./lib/port-stream.js') const MetaMaskProvider = require('web3-provider-engine/zero.js') const IdentityStore = require('./lib/idStore') -const createTxNotification = require('./lib/tx-notification.js') +const createTxNotification = require('./lib/notifications.js').createTxNotification +const createMsgNotification = require('./lib/notifications.js').createMsgNotification const configManager = require('./lib/config-manager-singleton') const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex const HostStore = require('./lib/remote-store.js').HostStore @@ -55,13 +56,18 @@ var idStore = new IdentityStore() var providerOpts = { rpcUrl: configManager.getCurrentRpcAddress(), + // account mgmt getAccounts: function(cb){ var selectedAddress = idStore.getSelectedAddress() var result = selectedAddress ? [selectedAddress] : [] cb(null, result) }, + // tx signing approveTransaction: addUnconfirmedTx, signTransaction: idStore.signTransaction.bind(idStore), + // msg signing + approveMessage: addUnconfirmedMsg, + signMessage: idStore.signMessage.bind(idStore), } var provider = MetaMaskProvider(providerOpts) var web3 = new Web3(provider) @@ -209,7 +215,7 @@ function updateBadge(state){ } // -// Add unconfirmed Tx +// Add unconfirmed Tx + Msg // function addUnconfirmedTx(txParams, cb){ @@ -222,6 +228,16 @@ function addUnconfirmedTx(txParams, cb){ }) } +function addUnconfirmedMsg(msgParams, cb){ + var msgId = idStore.addUnconfirmedMessage(msgParams, cb) + createMsgNotification({ + title: 'New Unsigned Message', + msgParams: msgParams, + confirm: idStore.approveMessage.bind(idStore, msgId, noop), + cancel: idStore.cancelMessage.bind(idStore, msgId), + }) +} + // // config // diff --git a/app/scripts/lib/config-manager.js b/app/scripts/lib/config-manager.js index 102327c2d..0e7454dfd 100644 --- a/app/scripts/lib/config-manager.js +++ b/app/scripts/lib/config-manager.js @@ -145,15 +145,25 @@ ConfigManager.prototype.setData = function(data) { this.migrator.saveData(data) } +// +// Tx +// + ConfigManager.prototype.getTxList = function() { var data = this.migrator.getData() - if ('transactions' in data) { + if (data.transactions !== undefined) { return data.transactions } else { return [] } } +ConfigManager.prototype.unconfirmedTxs = function() { + var transactions = this.getTxList() + return transactions.filter(tx => tx.status === 'unconfirmed') + .reduce((result, tx) => { result[tx.id] = tx; return result }, {}) +} + ConfigManager.prototype._saveTxList = function(txList) { var data = this.migrator.getData() data.transactions = txList @@ -201,12 +211,74 @@ ConfigManager.prototype.updateTx = function(tx) { this._saveTxList(transactions) } -ConfigManager.prototype.unconfirmedTxs = function() { - var transactions = this.getTxList() - return transactions.filter(tx => tx.status === 'unconfirmed') - .reduce((result, tx) => { result[tx.id] = tx; return result }, {}) +// +// Msg +// + +ConfigManager.prototype.getMsgList = function() { + var data = this.migrator.getData() + if (data.messages !== undefined) { + return data.messages + } else { + return [] + } +} + +ConfigManager.prototype.unconfirmedMsgs = function() { + var messages = this.getMsgList() + return messages.filter(msg => msg.status === 'unconfirmed') + .reduce((result, msg) => { result[msg.id] = msg; return result }, {}) +} + +ConfigManager.prototype._saveMsgList = function(msgList) { + var data = this.migrator.getData() + data.messages = msgList + this.setData(data) } +ConfigManager.prototype.addMsg = function(msg) { + var messages = this.getMsgList() + messages.push(msg) + this._saveMsgList(messages) +} + +ConfigManager.prototype.getMsg = function(msgId) { + var messages = this.getMsgList() + var matching = messages.filter(msg => msg.id === msgId) + return matching.length > 0 ? matching[0] : null +} + +ConfigManager.prototype.confirmMsg = function(msgId) { + this._setMsgStatus(msgId, 'confirmed') +} + +ConfigManager.prototype.rejectMsg = function(msgId) { + this._setMsgStatus(msgId, 'rejected') +} + +ConfigManager.prototype._setMsgStatus = function(msgId, status) { + var msg = this.getMsg(msgId) + msg.status = status + this.updateMsg(msg) +} + +ConfigManager.prototype.updateMsg = function(msg) { + var messages = this.getMsgList() + var found, index + messages.forEach((otherMsg, i) => { + if (otherMsg.id === msg.id) { + found = true + index = i + } + }) + if (found) { + messages[index] = msg + } + this._saveMsgList(messages) +} + + + // observable ConfigManager.prototype.subscribe = function(fn){ diff --git a/app/scripts/lib/idStore.js b/app/scripts/lib/idStore.js index b462d4ad5..c25d83c9d 100644 --- a/app/scripts/lib/idStore.js +++ b/app/scripts/lib/idStore.js @@ -34,6 +34,7 @@ function IdentityStore(opts = {}) { } // not part of serilized metamask state - only kept in memory this._unconfTxCbs = {} + this._unconfMsgCbs = {} } // @@ -140,6 +141,10 @@ IdentityStore.prototype.exportAccount = function(address, cb) { cb(null, privateKey) } +// +// Transactions +// + // comes from dapp via zero-client hooked-wallet provider IdentityStore.prototype.addUnconfirmedTransaction = function(txParams, cb){ @@ -170,7 +175,6 @@ IdentityStore.prototype.addUnconfirmedTransaction = function(txParams, cb){ // comes from metamask ui IdentityStore.prototype.approveTransaction = function(txId, cb){ var txData = configManager.getTx(txId) - var txParams = txData.txParams var approvalCb = this._unconfTxCbs[txId] || noop // accept tx @@ -206,6 +210,73 @@ IdentityStore.prototype.signTransaction = function(txParams, cb){ } } +// +// Messages +// + +// comes from dapp via zero-client hooked-wallet provider +IdentityStore.prototype.addUnconfirmedMessage = function(msgParams, cb){ + + // create txData obj with parameters and meta data + var time = (new Date()).getTime() + var msgId = createId() + var msgData = { + id: msgId, + msgParams: msgParams, + time: time, + status: 'unconfirmed', + } + configManager.addMsg(msgData) + console.log('addUnconfirmedMessage:', msgData) + + // keep the cb around for after approval (requires user interaction) + // This cb fires completion to the Dapp's write operation. + this._unconfMsgCbs[msgId] = cb + + // signal update + this._didUpdate() + + return msgId +} + +// comes from metamask ui +IdentityStore.prototype.approveMessage = function(msgId, cb){ + var msgData = configManager.getMsg(msgId) + var approvalCb = this._unconfMsgCbs[msgId] || noop + + // accept msg + cb() + approvalCb(null, true) + // clean up + configManager.confirmMsg(msgId) + delete this._unconfMsgCbs[msgId] + this._didUpdate() +} + +// comes from metamask ui +IdentityStore.prototype.cancelMessage = function(msgId){ + var txData = configManager.getMsg(msgId) + var approvalCb = this._unconfMsgCbs[msgId] || noop + + // reject tx + approvalCb(null, false) + // clean up + configManager.rejectMsg(msgId) + delete this._unconfTxCbs[msgId] + this._didUpdate() +} + +// performs the actual signing, no autofill of params +IdentityStore.prototype.signMessage = function(msgParams, cb){ + try { + console.log('signing msg...', msgParams.data) + var rawMsg = this._idmgmt.signMsg(msgParams.from, msgParams.data) + cb(null, rawMsg) + } catch (err) { + cb(err) + } +} + // // private // @@ -352,7 +423,7 @@ function IdManagement(opts) { var tx = new Transaction(txParams) // sign tx - var privKeyHex = ethUtil.addHexPrefix(this.keyStore.exportPrivateKey(txParams.from, this.derivedKey, this.hdPathString)) + var privKeyHex = this.exportPrivateKey(txParams.from) var privKey = ethUtil.toBuffer(privKeyHex) tx.sign(privKey) @@ -367,12 +438,23 @@ function IdManagement(opts) { return rawTx } + this.signMsg = function(address, message){ + // sign message + var privKeyHex = this.exportPrivateKey(address) + var privKey = ethUtil.toBuffer(privKeyHex) + var msgHash = ethUtil.sha3(message) + var msgSig = ethUtil.ecsign(msgHash, privKey) + var rawMsgSig = ethUtil.bufferToHex(concatSig(msgSig.v, msgSig.r, msgSig.s)) + return rawMsgSig + } + this.getSeed = function(){ return this.keyStore.getSeed(this.derivedKey) } this.exportPrivateKey = function(address) { - return this.keyStore.exportPrivateKey(address, this.derivedKey, this.hdPathString) + var privKeyHex = ethUtil.addHexPrefix(this.keyStore.exportPrivateKey(address, this.derivedKey, this.hdPathString)) + return privKeyHex } } @@ -380,3 +462,14 @@ function IdManagement(opts) { // util function noop(){} + + +function concatSig(v, r, s) { + r = ethUtil.fromSigned(r) + s = ethUtil.fromSigned(s) + v = ethUtil.bufferToInt(v) + r = ethUtil.toUnsigned(r).toString('hex') + s = ethUtil.toUnsigned(s).toString('hex') + v = ethUtil.stripHexPrefix(ethUtil.intToHex(v)) + return ethUtil.addHexPrefix(r.concat(s, v).toString("hex")) +} \ No newline at end of file diff --git a/app/scripts/lib/notifications.js b/app/scripts/lib/notifications.js new file mode 100644 index 000000000..2b7cbfe66 --- /dev/null +++ b/app/scripts/lib/notifications.js @@ -0,0 +1,75 @@ +const createId = require('hat') +const uiUtils = require('../../../ui/app/util') +var notificationHandlers = {} + +module.exports = { + createTxNotification: createTxNotification, + createMsgNotification: createMsgNotification, +} + +// notification button press +chrome.notifications.onButtonClicked.addListener(function(notificationId, buttonIndex){ + var handlers = notificationHandlers[notificationId] + if (buttonIndex === 0) { + handlers.confirm() + } else { + handlers.cancel() + } + chrome.notifications.clear(notificationId) +}) + +// notification teardown +chrome.notifications.onClosed.addListener(function(notificationId){ + delete notificationHandlers[notificationId] +}) + +// creation helper +function createTxNotification(opts){ + var message = [ + 'to: '+uiUtils.addressSummary(opts.txParams.to), + 'from: '+uiUtils.addressSummary(opts.txParams.from), + 'value: '+uiUtils.formatBalance(opts.txParams.value), + 'data: '+uiUtils.dataSize(opts.txParams.data), + ].join('\n') + + var id = createId() + chrome.notifications.create(id, { + type: 'basic', + iconUrl: '/images/icon-128.png', + title: opts.title, + message: message, + buttons: [{ + title: 'confirm', + },{ + title: 'cancel', + }] + }) + notificationHandlers[id] = { + confirm: opts.confirm, + cancel: opts.cancel, + } +} + +function createMsgNotification(opts){ + var message = [ + 'to be signed by: '+uiUtils.addressSummary(opts.msgParams.from), + 'message:\n'+opts.msgParams.data, + ].join('\n') + + var id = createId() + chrome.notifications.create(id, { + type: 'basic', + iconUrl: '/images/icon-128.png', + title: opts.title, + message: message, + buttons: [{ + title: 'confirm', + },{ + title: 'cancel', + }] + }) + notificationHandlers[id] = { + confirm: opts.confirm, + cancel: opts.cancel, + } +} \ No newline at end of file diff --git a/app/scripts/lib/tx-notification.js b/app/scripts/lib/tx-notification.js deleted file mode 100644 index 75985dee1..000000000 --- a/app/scripts/lib/tx-notification.js +++ /dev/null @@ -1,49 +0,0 @@ -const createId = require('hat') -const uiUtils = require('../../../ui/app/util') -var notificationHandlers = {} - -module.exports = createTxNotification - - -// notification button press -chrome.notifications.onButtonClicked.addListener(function(notificationId, buttonIndex){ - var handlers = notificationHandlers[notificationId] - if (buttonIndex === 0) { - handlers.confirm() - } else { - handlers.cancel() - } - chrome.notifications.clear(notificationId) -}) - -// notification teardown -chrome.notifications.onClosed.addListener(function(notificationId){ - delete notificationHandlers[notificationId] -}) - -// creation helper -function createTxNotification(opts){ - var message = [ - 'to: '+uiUtils.addressSummary(opts.txParams.to), - 'from: '+uiUtils.addressSummary(opts.txParams.from), - 'value: '+uiUtils.formatBalance(opts.txParams.value), - 'data: '+uiUtils.dataSize(opts.txParams.data), - ].join('\n') - - var id = createId() - chrome.notifications.create(id, { - type: 'basic', - iconUrl: '/images/icon-128.png', - title: opts.title, - message: message, - buttons: [{ - title: 'confirm', - },{ - title: 'cancel', - }] - }) - notificationHandlers[id] = { - confirm: opts.confirm, - cancel: opts.cancel, - } -} -- cgit