aboutsummaryrefslogtreecommitdiffstats
path: root/app/scripts
diff options
context:
space:
mode:
authorbitpshr <mail@bitpshr.net>2018-09-11 05:11:57 +0800
committerbitpshr <mail@bitpshr.net>2018-09-14 03:40:57 +0800
commit36dd0354e777e6786ae0d2284ffcb1adbc6d85f7 (patch)
treee5c965249db37ce354e61820800e13bb33d00497 /app/scripts
parentad6e31ec84d4e2c3a0ed7a3a6ceea499db2644b8 (diff)
downloadtangerine-wallet-browser-36dd0354e777e6786ae0d2284ffcb1adbc6d85f7.tar
tangerine-wallet-browser-36dd0354e777e6786ae0d2284ffcb1adbc6d85f7.tar.gz
tangerine-wallet-browser-36dd0354e777e6786ae0d2284ffcb1adbc6d85f7.tar.bz2
tangerine-wallet-browser-36dd0354e777e6786ae0d2284ffcb1adbc6d85f7.tar.lz
tangerine-wallet-browser-36dd0354e777e6786ae0d2284ffcb1adbc6d85f7.tar.xz
tangerine-wallet-browser-36dd0354e777e6786ae0d2284ffcb1adbc6d85f7.tar.zst
tangerine-wallet-browser-36dd0354e777e6786ae0d2284ffcb1adbc6d85f7.zip
Implement latest EIP-712 protocol
Diffstat (limited to 'app/scripts')
-rw-r--r--app/scripts/lib/typed-message-manager.js74
-rw-r--r--app/scripts/metamask-controller.js77
2 files changed, 112 insertions, 39 deletions
diff --git a/app/scripts/lib/typed-message-manager.js b/app/scripts/lib/typed-message-manager.js
index e5e1c94b3..3e97023f5 100644
--- a/app/scripts/lib/typed-message-manager.js
+++ b/app/scripts/lib/typed-message-manager.js
@@ -4,6 +4,7 @@ const createId = require('./random-id')
const assert = require('assert')
const sigUtil = require('eth-sig-util')
const log = require('loglevel')
+const jsonschema = require('jsonschema')
/**
* Represents, and contains data about, an 'eth_signTypedData' type signature request. These are created when a
@@ -17,7 +18,7 @@ const log = require('loglevel')
* @property {Object} msgParams.from The address that is making the signature request.
* @property {string} msgParams.data A hex string conversion of the raw buffer data of the signature request
* @property {number} time The epoch time at which the this message was created
- * @property {string} status Indicates whether the signature request is 'unapproved', 'approved', 'signed' or 'rejected'
+ * @property {string} status Indicates whether the signature request is 'unapproved', 'approved', 'signed', 'rejected', or 'errored'
* @property {string} type The json-prc signing method for which a signature request has been made. A 'Message' will
* always have a 'eth_signTypedData' type.
*
@@ -26,17 +27,10 @@ const log = require('loglevel')
module.exports = class TypedMessageManager extends EventEmitter {
/**
* Controller in charge of managing - storing, adding, removing, updating - TypedMessage.
- *
- * @typedef {Object} TypedMessage
- * @param {Object} opts @deprecated
- * @property {Object} memStore The observable store where TypedMessage are saved.
- * @property {Object} memStore.unapprovedTypedMessages A collection of all TypedMessages in the 'unapproved' state
- * @property {number} memStore.unapprovedTypedMessagesCount The count of all TypedMessages in this.memStore.unapprobedMsgs
- * @property {array} messages Holds all messages that have been created by this TypedMessage
- *
*/
- constructor (opts) {
+ constructor ({ networkController }) {
super()
+ this.networkController = networkController
this.memStore = new ObservableStore({
unapprovedTypedMessages: {},
unapprovedTypedMessagesCount: 0,
@@ -76,15 +70,17 @@ module.exports = class TypedMessageManager extends EventEmitter {
* @returns {promise} When the message has been signed or rejected
*
*/
- addUnapprovedMessageAsync (msgParams, req) {
+ addUnapprovedMessageAsync (msgParams, req, version) {
return new Promise((resolve, reject) => {
- const msgId = this.addUnapprovedMessage(msgParams, req)
+ const msgId = this.addUnapprovedMessage(msgParams, req, version)
this.once(`${msgId}:finished`, (data) => {
switch (data.status) {
case 'signed':
return resolve(data.rawSig)
case 'rejected':
return reject(new Error('MetaMask Message Signature: User denied message signature.'))
+ case 'errored':
+ return reject(new Error(`MetaMask Message Signature: ${data.error}`))
default:
return reject(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`))
}
@@ -102,7 +98,8 @@ module.exports = class TypedMessageManager extends EventEmitter {
* @returns {number} The id of the newly created TypedMessage.
*
*/
- addUnapprovedMessage (msgParams, req) {
+ addUnapprovedMessage (msgParams, req, version) {
+ msgParams.version = version
this.validateParams(msgParams)
// add origin from request
if (req) msgParams.origin = req.origin
@@ -132,14 +129,33 @@ module.exports = class TypedMessageManager extends EventEmitter {
*
*/
validateParams (params) {
- assert.equal(typeof params, 'object', 'Params should ben an object.')
- assert.ok('data' in params, 'Params must include a data field.')
- assert.ok('from' in params, 'Params must include a from field.')
- assert.ok(Array.isArray(params.data), 'Data should be an array.')
- assert.equal(typeof params.from, 'string', 'From field must be a string.')
- assert.doesNotThrow(() => {
- sigUtil.typedSignatureHash(params.data)
- }, 'Expected EIP712 typed data')
+ switch (params.version) {
+ case 'V1':
+ assert.equal(typeof params, 'object', 'Params should ben an object.')
+ assert.ok('data' in params, 'Params must include a data field.')
+ assert.ok('from' in params, 'Params must include a from field.')
+ assert.ok(Array.isArray(params.data), 'Data should be an array.')
+ assert.equal(typeof params.from, 'string', 'From field must be a string.')
+ assert.doesNotThrow(() => {
+ sigUtil.typedSignatureHash(params.data)
+ }, 'Expected EIP712 typed data')
+ break
+ case 'V2':
+ let data
+ assert.equal(typeof params, 'object', 'Params should be an object.')
+ assert.ok('data' in params, 'Params must include a data field.')
+ assert.ok('from' in params, 'Params must include a from field.')
+ assert.equal(typeof params.from, 'string', 'From field must be a string.')
+ assert.equal(typeof params.data, 'string', 'Data must be passed as a valid JSON string.')
+ assert.doesNotThrow(() => { data = JSON.parse(params.data) }, 'Data must be passed as a valid JSON string.')
+ const validation = jsonschema.validate(data, sigUtil.TYPED_MESSAGE_SCHEMA)
+ assert.ok(data.primaryType in data.types, `Primary type of "${data.primaryType}" has no type definition.`)
+ assert.equal(validation.errors.length, 0, 'Data must conform to EIP-712 schema. See https://git.io/fNtcx.')
+ const chainId = data.domain.chainId
+ const activeChainId = parseInt(this.networkController.getNetworkState())
+ chainId && assert.equal(chainId, activeChainId, `Provided chainId (${activeChainId}) must match the active chainId (${activeChainId})`)
+ break
+ }
}
/**
@@ -214,6 +230,7 @@ module.exports = class TypedMessageManager extends EventEmitter {
*/
prepMsgForSigning (msgParams) {
delete msgParams.metamaskId
+ delete msgParams.version
return Promise.resolve(msgParams)
}
@@ -227,6 +244,19 @@ module.exports = class TypedMessageManager extends EventEmitter {
this._setMsgStatus(msgId, 'rejected')
}
+ /**
+ * Sets a TypedMessage status to 'errored' via a call to this._setMsgStatus.
+ *
+ * @param {number} msgId The id of the TypedMessage to error
+ *
+ */
+ errorMessage (msgId, error) {
+ const msg = this.getMsg(msgId)
+ msg.error = error
+ this._updateMsg(msg)
+ this._setMsgStatus(msgId, 'errored')
+ }
+
//
// PRIVATE METHODS
//
@@ -250,7 +280,7 @@ module.exports = class TypedMessageManager extends EventEmitter {
msg.status = status
this._updateMsg(msg)
this.emit(`${msgId}:${status}`, msg)
- if (status === 'rejected' || status === 'signed') {
+ if (status === 'rejected' || status === 'signed' || status === 'errored') {
this.emit(`${msgId}:finished`, msg)
}
}
diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js
index f9a12628b..d5005d977 100644
--- a/app/scripts/metamask-controller.js
+++ b/app/scripts/metamask-controller.js
@@ -49,6 +49,8 @@ const log = require('loglevel')
const TrezorKeyring = require('eth-trezor-keyring')
const LedgerBridgeKeyring = require('eth-ledger-bridge-keyring')
const EthQuery = require('eth-query')
+const ethUtil = require('ethereumjs-util')
+const sigUtil = require('eth-sig-util')
module.exports = class MetamaskController extends EventEmitter {
@@ -205,7 +207,7 @@ module.exports = class MetamaskController extends EventEmitter {
this.networkController.lookupNetwork()
this.messageManager = new MessageManager()
this.personalMessageManager = new PersonalMessageManager()
- this.typedMessageManager = new TypedMessageManager()
+ this.typedMessageManager = new TypedMessageManager({ networkController: this.networkController })
this.publicConfigStore = this.initPublicConfigStore()
this.store.updateStructure({
@@ -266,7 +268,6 @@ module.exports = class MetamaskController extends EventEmitter {
// msg signing
processEthSignMessage: this.newUnsignedMessage.bind(this),
processPersonalMessage: this.newUnsignedPersonalMessage.bind(this),
- processTypedMessage: this.newUnsignedTypedMessage.bind(this),
}
const providerProxy = this.networkController.initializeProvider(providerOpts)
return providerProxy
@@ -975,22 +976,31 @@ module.exports = class MetamaskController extends EventEmitter {
* @param {Object} msgParams - The params passed to eth_signTypedData.
* @returns {Object} Full state update.
*/
- signTypedMessage (msgParams) {
- log.info('MetaMaskController - signTypedMessage')
+ async signTypedMessage (msgParams) {
+ log.info('MetaMaskController - eth_signTypedData')
const msgId = msgParams.metamaskId
- // sets the status op the message to 'approved'
- // and removes the metamaskId for signing
- return this.typedMessageManager.approveMessage(msgParams)
- .then((cleanMsgParams) => {
- // signs the message
- return this.keyringController.signTypedMessage(cleanMsgParams)
- })
- .then((rawSig) => {
- // tells the listener that the message has been signed
- // and can be returned to the dapp
- this.typedMessageManager.setMsgStatusSigned(msgId, rawSig)
- return this.getState()
- })
+ const version = msgParams.version
+ try {
+ const cleanMsgParams = await this.typedMessageManager.approveMessage(msgParams)
+ const address = sigUtil.normalize(cleanMsgParams.from)
+ const keyring = await this.keyringController.getKeyringForAccount(address)
+ const wallet = keyring._getWalletForAccount(address)
+ const privKey = ethUtil.toBuffer(wallet.getPrivateKey())
+ let signature
+ switch (version) {
+ case 'V1':
+ signature = sigUtil.signTypedDataLegacy(privKey, { data: cleanMsgParams.data })
+ break
+ case 'V2':
+ signature = sigUtil.signTypedData(privKey, { data: JSON.parse(cleanMsgParams.data) })
+ break
+ }
+ this.typedMessageManager.setMsgStatusSigned(msgId, signature)
+ return this.getState()
+ } catch (error) {
+ log.info('MetaMaskController - eth_signTypedData failed.', error)
+ this.typedMessageManager.errorMessage(msgId, error)
+ }
}
/**
@@ -1241,6 +1251,9 @@ module.exports = class MetamaskController extends EventEmitter {
engine.push(createLoggerMiddleware({ origin }))
engine.push(filterMiddleware)
engine.push(this.preferencesController.requestWatchAsset.bind(this.preferencesController))
+ engine.push(this.createTypedDataMiddleware('eth_signTypedData', 'V1').bind(this))
+ engine.push(this.createTypedDataMiddleware('eth_signTypedData_v1', 'V1').bind(this))
+ engine.push(this.createTypedDataMiddleware('eth_signTypedData_v2', 'V2').bind(this))
engine.push(createProviderMiddleware({ provider: this.provider }))
// setup connection
@@ -1474,4 +1487,34 @@ module.exports = class MetamaskController extends EventEmitter {
set isClientOpenAndUnlocked (active) {
this.tokenRatesController.isActive = active
}
+
+ /**
+ * Creates RPC engine middleware for processing eth_signTypedData requests
+ *
+ * @param {Object} req - request object
+ * @param {Object} res - response object
+ * @param {Function} - next
+ * @param {Function} - end
+ */
+ createTypedDataMiddleware (methodName, version) {
+ return async (req, res, next, end) => {
+ const { method, params } = req
+ if (method === methodName) {
+ const promise = this.typedMessageManager.addUnapprovedMessageAsync({
+ data: params.length >= 1 && params[0],
+ from: params.length >= 2 && params[1],
+ }, req, version)
+ this.sendUpdate()
+ this.opts.showUnconfirmedMessage()
+ try {
+ res.result = await promise
+ end()
+ } catch (error) {
+ end(error)
+ }
+ } else {
+ next()
+ }
+ }
+ }
}