1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-22 18:00:18 +01:00

Implementation encrypt/decrypt feature (#7831)

Implement `eth_decrypt` and `eth_getEncryptionPublicKey`. This allows decryption backed by the user's private key. The message decryption uses a confirmation flow similar to the messaging signing flow, where the message to be decrypted is also able to be decrypted inline for the user to read directly before confirming.
This commit is contained in:
Konstantin 2020-02-19 21:24:16 +03:00 committed by GitHub
parent cab2f1b769
commit 6f47fece56
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 2272 additions and 38 deletions

View File

@ -385,7 +385,7 @@
"message": "Custom Spend Limit" "message": "Custom Spend Limit"
}, },
"dataBackupFoundInfo": { "dataBackupFoundInfo": {
"message": "Some of your account data was backed up during a previous installation of MetaMask. This could include your settings, contacts and tokens. Would you like to restore this data now?" "message": "Some of your account data was backed up during a previous installation of MetaMask. This could include your settings, contacts, and tokens. Would you like to restore this data now?"
}, },
"decimalsMustZerotoTen": { "decimalsMustZerotoTen": {
"message": "Decimals must be at least 0, and not over 36." "message": "Decimals must be at least 0, and not over 36."
@ -1609,5 +1609,35 @@
}, },
"zeroGasPriceOnSpeedUpError": { "zeroGasPriceOnSpeedUpError": {
"message": "Zero gas price on speed up" "message": "Zero gas price on speed up"
},
"decryptRequest": {
"message": "Decrypt request"
},
"decrypt": {
"message": "Decrypt"
},
"decryptMessageNotice": {
"message": "$1 would like to read this message to complete your action",
"description": "$1 is website or dapp name"
},
"decryptMetamask": {
"message": "Decrypt message"
},
"decryptCopy": {
"message": "Copy encrypted message"
},
"decryptInlineError": {
"message": "This message cannot be decrypted due to error: $1",
"description": "$1 is error message"
},
"provide": {
"message": "Provide"
},
"encryptionPublicKeyRequest": {
"message": "Request encryption public key"
},
"encryptionPublicKeyNotice": {
"message": "$1 would like your public encryption key. By consenting, this site will be able to compose encrypted messages to you.",
"description": "$1 is website or dapp name"
} }
} }

View File

@ -124,6 +124,9 @@
"customRPC": { "customRPC": {
"message": "Пользовательский RPC" "message": "Пользовательский RPC"
}, },
"dataBackupFoundInfo": {
"message": "Некоторые данные вашей учетной записи были экспортированы во время предыдущей установки MetaMask. Это может включать ваши настройки, контакты и токены. Вы хотите импортировать эти данные сейчас?"
},
"decimalsMustZerotoTen": { "decimalsMustZerotoTen": {
"message": "Количество десятичных разрядов должно быть минимум 0 и максимум 36." "message": "Количество десятичных разрядов должно быть минимум 0 и максимум 36."
}, },
@ -962,6 +965,9 @@
"noConversionRateAvailable": { "noConversionRateAvailable": {
"message": "Курсы валют недоступны" "message": "Курсы валют недоступны"
}, },
"noThanks": {
"message": "Нет, спасибо"
},
"notEnoughGas": { "notEnoughGas": {
"message": "Не хватает газа" "message": "Не хватает газа"
}, },
@ -1046,6 +1052,10 @@
"restoreAccountWithSeed": { "restoreAccountWithSeed": {
"message": "Восстановите свой аккаунт с помощью секретной фразы" "message": "Восстановите свой аккаунт с помощью секретной фразы"
}, },
"restoreWalletPreferences": {
"message": "Были найдены данные экспортированные от $1. Вы желаете восстановить настройки вашего кошелька?",
"description": "$1 is the date at which the data was backed up"
},
"requestsAwaitingAcknowledgement": { "requestsAwaitingAcknowledgement": {
"message": "запросы, ожидающие подтверждения" "message": "запросы, ожидающие подтверждения"
}, },
@ -1339,5 +1349,35 @@
}, },
"yourPrivateSeedPhrase": { "yourPrivateSeedPhrase": {
"message": "Ваша сид-фраза" "message": "Ваша сид-фраза"
},
"decryptRequest": {
"message": "Запрос расшифровки"
},
"decrypt": {
"message": "Расшифровать"
},
"decryptMessageNotice": {
"message": "Для $1 необходимо прочитать это сообщение, чтобы завершить Ваше действие",
"description": "$1 is website or dapp name"
},
"decryptMetamask": {
"message": "Расшифровать сообщение"
},
"decryptCopy": {
"message": "Скопировать расшифрованное сообщение"
},
"decryptInlineError": {
"message": "Это сообщение не может быть дешифровано из-за ошибки: $1",
"description": "$1 is error message"
},
"provide": {
"message": "Предоставить"
},
"encryptionPublicKeyRequest": {
"message": "Запрос публичного ключа шифрования"
},
"encryptionPublicKeyNotice": {
"message": "$1 запрашивает ваш открытый ключ шифрования. По согласованию, этот сайт сможет создавать для Вас зашифрованные сообщения.",
"description": "$1 is website or dapp name"
} }
} }

View File

@ -127,6 +127,10 @@ initialize().catch(log.error)
* @property {number} unapprovedMsgCount - The number of messages in unapprovedMsgs. * @property {number} unapprovedMsgCount - The number of messages in unapprovedMsgs.
* @property {Object} unapprovedPersonalMsgs - An object of messages associated with the currently selected account, mapping a unique ID to the options. * @property {Object} unapprovedPersonalMsgs - An object of messages associated with the currently selected account, mapping a unique ID to the options.
* @property {number} unapprovedPersonalMsgCount - The number of messages in unapprovedPersonalMsgs. * @property {number} unapprovedPersonalMsgCount - The number of messages in unapprovedPersonalMsgs.
* @property {Object} EncryptionPublicKeyMsgs - An object of messages associated with the currently selected account, mapping a unique ID to the options.
* @property {number} unapprovedEncryptionPublicKeyMsgCount - The number of messages in EncryptionPublicKeyMsgs.
* @property {Object} unapprovedDecryptMsgs - An object of messages associated with the currently selected account, mapping a unique ID to the options.
* @property {number} unapprovedDecryptMsgCount - The number of messages in unapprovedDecryptMsgs.
* @property {Object} unapprovedTypedMsgs - An object of messages associated with the currently selected account, mapping a unique ID to the options. * @property {Object} unapprovedTypedMsgs - An object of messages associated with the currently selected account, mapping a unique ID to the options.
* @property {number} unapprovedTypedMsgCount - The number of messages in unapprovedTypedMsgs. * @property {number} unapprovedTypedMsgCount - The number of messages in unapprovedTypedMsgs.
* @property {string[]} keyringTypes - An array of unique keyring identifying strings, representing available strategies for creating accounts. * @property {string[]} keyringTypes - An array of unique keyring identifying strings, representing available strategies for creating accounts.
@ -413,6 +417,8 @@ function setupController (initState, initLangCode) {
controller.txController.on('update:badge', updateBadge) controller.txController.on('update:badge', updateBadge)
controller.messageManager.on('updateBadge', updateBadge) controller.messageManager.on('updateBadge', updateBadge)
controller.personalMessageManager.on('updateBadge', updateBadge) controller.personalMessageManager.on('updateBadge', updateBadge)
controller.decryptMessageManager.on('updateBadge', updateBadge)
controller.encryptionPublicKeyManager.on('updateBadge', updateBadge)
controller.typedMessageManager.on('updateBadge', updateBadge) controller.typedMessageManager.on('updateBadge', updateBadge)
controller.permissionsController.permissions.subscribe(updateBadge) controller.permissionsController.permissions.subscribe(updateBadge)
@ -424,10 +430,13 @@ function setupController (initState, initLangCode) {
let label = '' let label = ''
const unapprovedTxCount = controller.txController.getUnapprovedTxCount() const unapprovedTxCount = controller.txController.getUnapprovedTxCount()
const unapprovedMsgCount = controller.messageManager.unapprovedMsgCount const unapprovedMsgCount = controller.messageManager.unapprovedMsgCount
const unapprovedPersonalMsgs = controller.personalMessageManager.unapprovedPersonalMsgCount const unapprovedPersonalMsgCount = controller.personalMessageManager.unapprovedPersonalMsgCount
const unapprovedTypedMsgs = controller.typedMessageManager.unapprovedTypedMessagesCount const unapprovedDecryptMsgCount = controller.decryptMessageManager.unapprovedDecryptMsgCount
const unapprovedEncryptionPublicKeyMsgCount = controller.encryptionPublicKeyManager.unapprovedEncryptionPublicKeyMsgCount
const unapprovedTypedMessagesCount = controller.typedMessageManager.unapprovedTypedMessagesCount
const pendingPermissionRequests = Object.keys(controller.permissionsController.permissions.state.permissionsRequests).length const pendingPermissionRequests = Object.keys(controller.permissionsController.permissions.state.permissionsRequests).length
const count = unapprovedTxCount + unapprovedMsgCount + unapprovedPersonalMsgs + unapprovedTypedMsgs + pendingPermissionRequests const count = unapprovedTxCount + unapprovedMsgCount + unapprovedPersonalMsgCount + unapprovedDecryptMsgCount + unapprovedEncryptionPublicKeyMsgCount +
unapprovedTypedMessagesCount + pendingPermissionRequests
if (count) { if (count) {
label = String(count) label = String(count)
} }

View File

@ -14,6 +14,8 @@ function createMetamaskMiddleware ({
processTypedMessageV3, processTypedMessageV3,
processTypedMessageV4, processTypedMessageV4,
processPersonalMessage, processPersonalMessage,
processDecryptMessage,
processEncryptionPublicKey,
getPendingNonce, getPendingNonce,
getPendingTransactionByHash, getPendingTransactionByHash,
}) { }) {
@ -31,6 +33,8 @@ function createMetamaskMiddleware ({
processTypedMessageV3, processTypedMessageV3,
processTypedMessageV4, processTypedMessageV4,
processPersonalMessage, processPersonalMessage,
processDecryptMessage,
processEncryptionPublicKey,
}), }),
createPendingNonceMiddleware({ getPendingNonce }), createPendingNonceMiddleware({ getPendingNonce }),
createPendingTxMiddleware({ getPendingTransactionByHash }), createPendingTxMiddleware({ getPendingTransactionByHash }),

View File

@ -69,4 +69,6 @@ export const SAFE_METHODS = [
'eth_uninstallFilter', 'eth_uninstallFilter',
'metamask_watchAsset', 'metamask_watchAsset',
'wallet_watchAsset', 'wallet_watchAsset',
'eth_getEncryptionPublicKey',
'eth_decrypt',
] ]

View File

@ -0,0 +1,311 @@
import EventEmitter from 'events'
import ObservableStore from 'obs-store'
import ethUtil from 'ethereumjs-util'
import { ethErrors } from 'eth-json-rpc-errors'
import createId from './random-id'
const hexRe = /^[0-9A-Fa-f]+$/g
import log from 'loglevel'
/**
* Represents, and contains data about, an 'eth_decrypt' type decryption request. These are created when a
* decryption for an eth_decrypt call is requested.
*
* @typedef {Object} DecryptMessage
* @property {number} id An id to track and identify the message object
* @property {Object} msgParams The parameters to pass to the decryptMessage method once the decryption request is
* approved.
* @property {Object} msgParams.metamaskId Added to msgParams for tracking and identification within MetaMask.
* @property {string} msgParams.data A hex string conversion of the raw buffer data of the decryption request
* @property {number} time The epoch time at which the this message was created
* @property {string} status Indicates whether the decryption request is 'unapproved', 'approved', 'decrypted' or 'rejected'
* @property {string} type The json-prc decryption method for which a decryption request has been made. A 'Message' will
* always have a 'eth_decrypt' type.
*
*/
export default class DecryptMessageManager extends EventEmitter {
/**
* Controller in charge of managing - storing, adding, removing, updating - DecryptMessage.
*
* @typedef {Object} DecryptMessageManager
* @property {Object} memStore The observable store where DecryptMessage are saved with persistance.
* @property {Object} memStore.unapprovedDecryptMsgs A collection of all DecryptMessages in the 'unapproved' state
* @property {number} memStore.unapprovedDecryptMsgCount The count of all DecryptMessages in this.memStore.unapprobedMsgs
* @property {array} messages Holds all messages that have been created by this DecryptMessageManager
*
*/
constructor () {
super()
this.memStore = new ObservableStore({
unapprovedDecryptMsgs: {},
unapprovedDecryptMsgCount: 0,
})
this.messages = []
}
/**
* A getter for the number of 'unapproved' DecryptMessages in this.messages
*
* @returns {number} The number of 'unapproved' DecryptMessages in this.messages
*
*/
get unapprovedDecryptMsgCount () {
return Object.keys(this.getUnapprovedMsgs()).length
}
/**
* A getter for the 'unapproved' DecryptMessages in this.messages
*
* @returns {Object} An index of DecryptMessage ids to DecryptMessages, for all 'unapproved' DecryptMessages in
* this.messages
*
*/
getUnapprovedMsgs () {
return this.messages.filter((msg) => msg.status === 'unapproved')
.reduce((result, msg) => {
result[msg.id] = msg; return result
}, {})
}
/**
* Creates a new DecryptMessage with an 'unapproved' status using the passed msgParams. this.addMsg is called to add
* the new DecryptMessage to this.messages, and to save the unapproved DecryptMessages from that list to
* this.memStore.
*
* @param {Object} msgParams The params for the eth_decrypt call to be made after the message is approved.
* @param {Object} req (optional) The original request object possibly containing the origin
* @returns {Promise<Buffer>} The raw decrypted message contents
*
*/
addUnapprovedMessageAsync (msgParams, req) {
return new Promise((resolve, reject) => {
if (!msgParams.from) {
reject(new Error('MetaMask Message for Decryption: from field is required.'))
}
const msgId = this.addUnapprovedMessage(msgParams, req)
this.once(`${msgId}:finished`, (data) => {
switch (data.status) {
case 'decrypted':
return resolve(data.rawData)
case 'rejected':
return reject(ethErrors.provider.userRejectedRequest('MetaMask Message for Decryption: User denied message decryption.'))
case 'errored':
return reject(new Error('This message cannot be decrypted'))
default:
return reject(new Error(`MetaMask Message for Decryption: Unknown problem: ${JSON.stringify(msgParams)}`))
}
})
})
}
/**
* Creates a new DecryptMessage with an 'unapproved' status using the passed msgParams. this.addMsg is called to add
* the new DecryptMessage to this.messages, and to save the unapproved DecryptMessages from that list to
* this.memStore.
*
* @param {Object} msgParams The params for the eth_decryptMsg call to be made after the message is approved.
* @param {Object} req (optional) The original request object possibly containing the origin
* @returns {number} The id of the newly created DecryptMessage.
*
*/
addUnapprovedMessage (msgParams, req) {
log.debug(`DecryptMessageManager addUnapprovedMessage: ${JSON.stringify(msgParams)}`)
// add origin from request
if (req) {
msgParams.origin = req.origin
}
msgParams.data = this.normalizeMsgData(msgParams.data)
// create txData obj with parameters and meta data
const time = (new Date()).getTime()
const msgId = createId()
const msgData = {
id: msgId,
msgParams: msgParams,
time: time,
status: 'unapproved',
type: 'eth_decrypt',
}
this.addMsg(msgData)
// signal update
this.emit('update')
return msgId
}
/**
* Adds a passed DecryptMessage to this.messages, and calls this._saveMsgList() to save the unapproved DecryptMessages from that
* list to this.memStore.
*
* @param {Message} msg The DecryptMessage to add to this.messages
*
*/
addMsg (msg) {
this.messages.push(msg)
this._saveMsgList()
}
/**
* Returns a specified DecryptMessage.
*
* @param {number} msgId The id of the DecryptMessage to get
* @returns {DecryptMessage|undefined} The DecryptMessage with the id that matches the passed msgId, or undefined
* if no DecryptMessage has that id.
*
*/
getMsg (msgId) {
return this.messages.find((msg) => msg.id === msgId)
}
/**
* Approves a DecryptMessage. Sets the message status via a call to this.setMsgStatusApproved, and returns a promise
* with the message params modified for proper decryption.
*
* @param {Object} msgParams The msgParams to be used when eth_decryptMsg is called, plus data added by MetaMask.
* @param {Object} msgParams.metamaskId Added to msgParams for tracking and identification within MetaMask.
* @returns {Promise<object>} Promises the msgParams object with metamaskId removed.
*
*/
approveMessage (msgParams) {
this.setMsgStatusApproved(msgParams.metamaskId)
return this.prepMsgForDecryption(msgParams)
}
/**
* Sets a DecryptMessage status to 'approved' via a call to this._setMsgStatus.
*
* @param {number} msgId The id of the DecryptMessage to approve.
*
*/
setMsgStatusApproved (msgId) {
this._setMsgStatus(msgId, 'approved')
}
/**
* Sets a DecryptMessage status to 'decrypted' via a call to this._setMsgStatus and updates that DecryptMessage in
* this.messages by adding the raw decryption data of the decryption request to the DecryptMessage
*
* @param {number} msgId The id of the DecryptMessage to decrypt.
* @param {buffer} rawData The raw data of the message request
*
*/
setMsgStatusDecrypted (msgId, rawData) {
const msg = this.getMsg(msgId)
msg.rawData = rawData
this._updateMsg(msg)
this._setMsgStatus(msgId, 'decrypted')
}
/**
* Removes the metamaskId property from passed msgParams and returns a promise which resolves the updated msgParams
*
* @param {Object} msgParams The msgParams to modify
* @returns {Promise<object>} Promises the msgParams with the metamaskId property removed
*
*/
prepMsgForDecryption (msgParams) {
delete msgParams.metamaskId
return Promise.resolve(msgParams)
}
/**
* Sets a DecryptMessage status to 'rejected' via a call to this._setMsgStatus.
*
* @param {number} msgId The id of the DecryptMessage to reject.
*
*/
rejectMsg (msgId) {
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')
}
/**
* Updates the status of a DecryptMessage in this.messages via a call to this._updateMsg
*
* @private
* @param {number} msgId The id of the DecryptMessage to update.
* @param {string} status The new status of the DecryptMessage.
* @throws A 'DecryptMessageManager - DecryptMessage not found for id: "${msgId}".' if there is no DecryptMessage
* in this.messages with an id equal to the passed msgId
* @fires An event with a name equal to `${msgId}:${status}`. The DecryptMessage is also fired.
* @fires If status is 'rejected' or 'decrypted', an event with a name equal to `${msgId}:finished` is fired along
* with the DecryptMessage
*
*/
_setMsgStatus (msgId, status) {
const msg = this.getMsg(msgId)
if (!msg) {
throw new Error('DecryptMessageManager - Message not found for id: "${msgId}".')
}
msg.status = status
this._updateMsg(msg)
this.emit(`${msgId}:${status}`, msg)
if (status === 'rejected' || status === 'decrypted' || status === 'errored') {
this.emit(`${msgId}:finished`, msg)
}
}
/**
* Sets a DecryptMessage in this.messages to the passed DecryptMessage if the ids are equal. Then saves the
* unapprovedDecryptMsgs index to storage via this._saveMsgList
*
* @private
* @param {msg} DecryptMessage A DecryptMessage that will replace an existing DecryptMessage (with the same
* id) in this.messages
*
*/
_updateMsg (msg) {
const index = this.messages.findIndex((message) => message.id === msg.id)
if (index !== -1) {
this.messages[index] = msg
}
this._saveMsgList()
}
/**
* Saves the unapproved DecryptMessages, and their count, to this.memStore
*
* @private
* @fires 'updateBadge'
*
*/
_saveMsgList () {
const unapprovedDecryptMsgs = this.getUnapprovedMsgs()
const unapprovedDecryptMsgCount = Object.keys(unapprovedDecryptMsgs).length
this.memStore.updateState({ unapprovedDecryptMsgs, unapprovedDecryptMsgCount })
this.emit('updateBadge')
}
/**
* A helper function that converts raw buffer data to a hex, or just returns the data if it is already formatted as a hex.
*
* @param {any} data The buffer data to convert to a hex
* @returns {string} A hex string conversion of the buffer data
*
*/
normalizeMsgData (data) {
try {
const stripped = ethUtil.stripHexPrefix(data)
if (stripped.match(hexRe)) {
return ethUtil.addHexPrefix(stripped)
}
} catch (e) {
log.debug(`Message was not hex encoded, interpreting as utf8.`)
}
return ethUtil.bufferToHex(Buffer.from(data, 'utf8'))
}
}

View File

@ -0,0 +1,286 @@
import EventEmitter from 'events'
import ObservableStore from 'obs-store'
import { ethErrors } from 'eth-json-rpc-errors'
import createId from './random-id'
import log from 'loglevel'
/**
* Represents, and contains data about, an 'eth_getEncryptionPublicKey' type request. These are created when
* an eth_getEncryptionPublicKey call is requested.
*
* @typedef {Object} EncryptionPublicKey
* @property {number} id An id to track and identify the message object
* @property {Object} msgParams The parameters to pass to the encryptionPublicKey method once the request is
* approved.
* @property {Object} msgParams.metamaskId Added to msgParams for tracking and identification within MetaMask.
* @property {string} msgParams.data A hex string conversion of the raw buffer data of the request
* @property {number} time The epoch time at which the this message was created
* @property {string} status Indicates whether the request is 'unapproved', 'approved', 'received' or 'rejected'
* @property {string} type The json-prc method for which a request has been made. A 'Message' will
* always have a 'eth_getEncryptionPublicKey' type.
*
*/
export default class EncryptionPublicKeyManager extends EventEmitter {
/**
* Controller in charge of managing - storing, adding, removing, updating - EncryptionPublicKey.
*
* @typedef {Object} EncryptionPublicKeyManager
* @property {Object} memStore The observable store where EncryptionPublicKey are saved with persistance.
* @property {Object} memStore.unapprovedEncryptionPublicKeyMsgs A collection of all EncryptionPublicKeys in the 'unapproved' state
* @property {number} memStore.unapprovedEncryptionPublicKeyMsgCount The count of all EncryptionPublicKeys in this.memStore.unapprobedMsgs
* @property {array} messages Holds all messages that have been created by this EncryptionPublicKeyManager
*
*/
constructor () {
super()
this.memStore = new ObservableStore({
unapprovedEncryptionPublicKeyMsgs: {},
unapprovedEncryptionPublicKeyMsgCount: 0,
})
this.messages = []
}
/**
* A getter for the number of 'unapproved' EncryptionPublicKeys in this.messages
*
* @returns {number} The number of 'unapproved' EncryptionPublicKeys in this.messages
*
*/
get unapprovedEncryptionPublicKeyMsgCount () {
return Object.keys(this.getUnapprovedMsgs()).length
}
/**
* A getter for the 'unapproved' EncryptionPublicKeys in this.messages
*
* @returns {Object} An index of EncryptionPublicKey ids to EncryptionPublicKeys, for all 'unapproved' EncryptionPublicKeys in
* this.messages
*
*/
getUnapprovedMsgs () {
return this.messages.filter((msg) => msg.status === 'unapproved')
.reduce((result, msg) => {
result[msg.id] = msg; return result
}, {})
}
/**
* Creates a new EncryptionPublicKey with an 'unapproved' status using the passed msgParams. this.addMsg is called to add
* the new EncryptionPublicKey to this.messages, and to save the unapproved EncryptionPublicKeys from that list to
* this.memStore.
*
* @param {Object} address The param for the eth_getEncryptionPublicKey call to be made after the message is approved.
* @param {Object} req (optional) The original request object possibly containing the origin
* @returns {Promise<Buffer>} The raw public key contents
*
*/
addUnapprovedMessageAsync (address, req) {
return new Promise((resolve, reject) => {
if (!address) {
reject(new Error('MetaMask Message for EncryptionPublicKey: address field is required.'))
}
const msgId = this.addUnapprovedMessage(address, req)
this.once(`${msgId}:finished`, (data) => {
switch (data.status) {
case 'received':
return resolve(data.rawData)
case 'rejected':
return reject(ethErrors.provider.userRejectedRequest('MetaMask Message for EncryptionPublicKey: User denied message EncryptionPublicKey.'))
default:
return reject(new Error(`MetaMask Message for EncryptionPublicKey: Unknown problem: ${JSON.stringify(address)}`))
}
})
})
}
/**
* Creates a new EncryptionPublicKey with an 'unapproved' status using the passed msgParams. this.addMsg is called to add
* the new EncryptionPublicKey to this.messages, and to save the unapproved EncryptionPublicKeys from that list to
* this.memStore.
*
* @param {Object} address The param for the eth_getEncryptionPublicKey call to be made after the message is approved.
* @param {Object} _req (optional) The original request object possibly containing the origin
* @returns {number} The id of the newly created EncryptionPublicKey.
*
*/
addUnapprovedMessage (address, _req) {
log.debug(`EncryptionPublicKeyManager addUnapprovedMessage: address`)
// create txData obj with parameters and meta data
const time = (new Date()).getTime()
const msgId = createId()
const msgData = {
id: msgId,
msgParams: address,
time: time,
status: 'unapproved',
type: 'eth_getEncryptionPublicKey',
}
if (_req) {
msgData.origin = _req.origin
}
this.addMsg(msgData)
// signal update
this.emit('update')
return msgId
}
/**
* Adds a passed EncryptionPublicKey to this.messages, and calls this._saveMsgList() to save the unapproved EncryptionPublicKeys from that
* list to this.memStore.
*
* @param {Message} msg The EncryptionPublicKey to add to this.messages
*
*/
addMsg (msg) {
this.messages.push(msg)
this._saveMsgList()
}
/**
* Returns a specified EncryptionPublicKey.
*
* @param {number} msgId The id of the EncryptionPublicKey to get
* @returns {EncryptionPublicKey|undefined} The EncryptionPublicKey with the id that matches the passed msgId, or undefined
* if no EncryptionPublicKey has that id.
*
*/
getMsg (msgId) {
return this.messages.find((msg) => msg.id === msgId)
}
/**
* Approves a EncryptionPublicKey. Sets the message status via a call to this.setMsgStatusApproved, and returns a promise
* with any the message params modified for proper providing.
*
* @param {Object} msgParams The msgParams to be used when eth_getEncryptionPublicKey is called, plus data added by MetaMask.
* @param {Object} msgParams.metamaskId Added to msgParams for tracking and identification within MetaMask.
* @returns {Promise<object>} Promises the msgParams object with metamaskId removed.
*
*/
approveMessage (msgParams) {
this.setMsgStatusApproved(msgParams.metamaskId)
return this.prepMsgForEncryptionPublicKey(msgParams)
}
/**
* Sets a EncryptionPublicKey status to 'approved' via a call to this._setMsgStatus.
*
* @param {number} msgId The id of the EncryptionPublicKey to approve.
*
*/
setMsgStatusApproved (msgId) {
this._setMsgStatus(msgId, 'approved')
}
/**
* Sets a EncryptionPublicKey status to 'received' via a call to this._setMsgStatus and updates that EncryptionPublicKey in
* this.messages by adding the raw data of request to the EncryptionPublicKey
*
* @param {number} msgId The id of the EncryptionPublicKey.
* @param {buffer} rawData The raw data of the message request
*
*/
setMsgStatusReceived (msgId, rawData) {
const msg = this.getMsg(msgId)
msg.rawData = rawData
this._updateMsg(msg)
this._setMsgStatus(msgId, 'received')
}
/**
* Removes the metamaskId property from passed msgParams and returns a promise which resolves the updated msgParams
*
* @param {Object} msgParams The msgParams to modify
* @returns {Promise<object>} Promises the msgParams with the metamaskId property removed
*
*/
prepMsgForEncryptionPublicKey (msgParams) {
delete msgParams.metamaskId
return Promise.resolve(msgParams)
}
/**
* Sets a EncryptionPublicKey status to 'rejected' via a call to this._setMsgStatus.
*
* @param {number} msgId The id of the EncryptionPublicKey to reject.
*
*/
rejectMsg (msgId) {
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')
}
/**
* Updates the status of a EncryptionPublicKey in this.messages via a call to this._updateMsg
*
* @private
* @param {number} msgId The id of the EncryptionPublicKey to update.
* @param {string} status The new status of the EncryptionPublicKey.
* @throws A 'EncryptionPublicKeyManager - EncryptionPublicKey not found for id: "${msgId}".' if there is no EncryptionPublicKey
* in this.messages with an id equal to the passed msgId
* @fires An event with a name equal to `${msgId}:${status}`. The EncryptionPublicKey is also fired.
* @fires If status is 'rejected' or 'received', an event with a name equal to `${msgId}:finished` is fired along
* with the EncryptionPublicKey
*
*/
_setMsgStatus (msgId, status) {
const msg = this.getMsg(msgId)
if (!msg) {
throw new Error('EncryptionPublicKeyManager - Message not found for id: "${msgId}".')
}
msg.status = status
this._updateMsg(msg)
this.emit(`${msgId}:${status}`, msg)
if (status === 'rejected' || status === 'received') {
this.emit(`${msgId}:finished`, msg)
}
}
/**
* Sets a EncryptionPublicKey in this.messages to the passed EncryptionPublicKey if the ids are equal. Then saves the
* unapprovedEncryptionPublicKeyMsgs index to storage via this._saveMsgList
*
* @private
* @param {msg} EncryptionPublicKey A EncryptionPublicKey that will replace an existing EncryptionPublicKey (with the same
* id) in this.messages
*
*/
_updateMsg (msg) {
const index = this.messages.findIndex((message) => message.id === msg.id)
if (index !== -1) {
this.messages[index] = msg
}
this._saveMsgList()
}
/**
* Saves the unapproved EncryptionPublicKeys, and their count, to this.memStore
*
* @private
* @fires 'updateBadge'
*
*/
_saveMsgList () {
const unapprovedEncryptionPublicKeyMsgs = this.getUnapprovedMsgs()
const unapprovedEncryptionPublicKeyMsgCount = Object.keys(unapprovedEncryptionPublicKeyMsgs).length
this.memStore.updateState({ unapprovedEncryptionPublicKeyMsgs, unapprovedEncryptionPublicKeyMsgCount })
this.emit('updateBadge')
}
}

View File

@ -35,6 +35,8 @@ import ThreeBoxController from './controllers/threebox'
import RecentBlocksController from './controllers/recent-blocks' import RecentBlocksController from './controllers/recent-blocks'
import IncomingTransactionsController from './controllers/incoming-transactions' import IncomingTransactionsController from './controllers/incoming-transactions'
import MessageManager from './lib/message-manager' import MessageManager from './lib/message-manager'
import DecryptMessageManager from './lib/decrypt-message-manager'
import EncryptionPublicKeyManager from './lib/encryption-public-key-manager'
import PersonalMessageManager from './lib/personal-message-manager' import PersonalMessageManager from './lib/personal-message-manager'
import TypedMessageManager from './lib/typed-message-manager' import TypedMessageManager from './lib/typed-message-manager'
import TransactionController from './controllers/transactions' import TransactionController from './controllers/transactions'
@ -278,6 +280,8 @@ export default class MetamaskController extends EventEmitter {
this.networkController.lookupNetwork() this.networkController.lookupNetwork()
this.messageManager = new MessageManager() this.messageManager = new MessageManager()
this.personalMessageManager = new PersonalMessageManager() this.personalMessageManager = new PersonalMessageManager()
this.decryptMessageManager = new DecryptMessageManager()
this.encryptionPublicKeyManager = new EncryptionPublicKeyManager()
this.typedMessageManager = new TypedMessageManager({ networkController: this.networkController }) this.typedMessageManager = new TypedMessageManager({ networkController: this.networkController })
// ensure isClientOpenAndUnlocked is updated when memState updates // ensure isClientOpenAndUnlocked is updated when memState updates
@ -313,6 +317,8 @@ export default class MetamaskController extends EventEmitter {
TokenRatesController: this.tokenRatesController.store, TokenRatesController: this.tokenRatesController.store,
MessageManager: this.messageManager.memStore, MessageManager: this.messageManager.memStore,
PersonalMessageManager: this.personalMessageManager.memStore, PersonalMessageManager: this.personalMessageManager.memStore,
DecryptMessageManager: this.decryptMessageManager.memStore,
EncryptionPublicKeyManager: this.encryptionPublicKeyManager.memStore,
TypesMessageManager: this.typedMessageManager.memStore, TypesMessageManager: this.typedMessageManager.memStore,
KeyringController: this.keyringController.memStore, KeyringController: this.keyringController.memStore,
PreferencesController: this.preferencesController.store, PreferencesController: this.preferencesController.store,
@ -363,6 +369,8 @@ export default class MetamaskController extends EventEmitter {
processTypedMessageV3: this.newUnsignedTypedMessage.bind(this), processTypedMessageV3: this.newUnsignedTypedMessage.bind(this),
processTypedMessageV4: this.newUnsignedTypedMessage.bind(this), processTypedMessageV4: this.newUnsignedTypedMessage.bind(this),
processPersonalMessage: this.newUnsignedPersonalMessage.bind(this), processPersonalMessage: this.newUnsignedPersonalMessage.bind(this),
processDecryptMessage: this.newRequestDecryptMessage.bind(this),
processEncryptionPublicKey: this.newRequestEncryptionPublicKey.bind(this),
getPendingNonce: this.getPendingNonce.bind(this), getPendingNonce: this.getPendingNonce.bind(this),
getPendingTransactionByHash: (hash) => this.txController.getFilteredTxList({ hash, status: 'submitted' })[0], getPendingTransactionByHash: (hash) => this.txController.getFilteredTxList({ hash, status: 'submitted' })[0],
} }
@ -539,6 +547,15 @@ export default class MetamaskController extends EventEmitter {
signTypedMessage: nodeify(this.signTypedMessage, this), signTypedMessage: nodeify(this.signTypedMessage, this),
cancelTypedMessage: this.cancelTypedMessage.bind(this), cancelTypedMessage: this.cancelTypedMessage.bind(this),
// decryptMessageManager
decryptMessage: nodeify(this.decryptMessage, this),
decryptMessageInline: nodeify(this.decryptMessageInline, this),
cancelDecryptMessage: this.cancelDecryptMessage.bind(this),
// EncryptionPublicKeyManager
encryptionPublicKey: nodeify(this.encryptionPublicKey, this),
cancelEncryptionPublicKey: this.cancelEncryptionPublicKey.bind(this),
// onboarding controller // onboarding controller
setSeedPhraseBackedUp: nodeify(onboardingController.setSeedPhraseBackedUp, onboardingController), setSeedPhraseBackedUp: nodeify(onboardingController.setSeedPhraseBackedUp, onboardingController),
@ -1168,6 +1185,147 @@ export default class MetamaskController extends EventEmitter {
} }
} }
// eth_decrypt methods
/**
* Called when a dapp uses the eth_decrypt method.
*
* @param {Object} msgParams - The params of the message to sign & return to the Dapp.
* @param {Object} req - (optional) the original request, containing the origin
* Passed back to the requesting Dapp.
*/
async newRequestDecryptMessage (msgParams, req) {
const promise = this.decryptMessageManager.addUnapprovedMessageAsync(msgParams, req)
this.sendUpdate()
this.opts.showUnconfirmedMessage()
return promise
}
/**
* Only decypt message and don't touch transaction state
*
* @param {Object} msgParams - The params of the message to decrypt.
* @returns {Promise<Object>} - A full state update.
*/
async decryptMessageInline (msgParams) {
log.info('MetaMaskController - decryptMessageInline')
// decrypt the message inline
const msgId = msgParams.metamaskId
const msg = this.decryptMessageManager.getMsg(msgId)
try {
const stripped = ethUtil.stripHexPrefix(msgParams.data)
const buff = Buffer.from(stripped, 'hex')
msgParams.data = JSON.parse(buff.toString('utf8'))
msg.rawData = await this.keyringController.decryptMessage(msgParams)
} catch (e) {
msg.error = e.message
}
this.decryptMessageManager._updateMsg(msg)
return this.getState()
}
/**
* Signifies a user's approval to decrypt a message in queue.
* Triggers decrypt, and the callback function from newUnsignedDecryptMessage.
*
* @param {Object} msgParams - The params of the message to decrypt & return to the Dapp.
* @returns {Promise<Object>} - A full state update.
*/
async decryptMessage (msgParams) {
log.info('MetaMaskController - decryptMessage')
const msgId = msgParams.metamaskId
// sets the status op the message to 'approved'
// and removes the metamaskId for decryption
try {
const cleanMsgParams = await this.decryptMessageManager.approveMessage(msgParams)
const stripped = ethUtil.stripHexPrefix(cleanMsgParams.data)
const buff = Buffer.from(stripped, 'hex')
cleanMsgParams.data = JSON.parse(buff.toString('utf8'))
// decrypt the message
const rawMess = await this.keyringController.decryptMessage(cleanMsgParams)
// tells the listener that the message has been decrypted and can be returned to the dapp
this.decryptMessageManager.setMsgStatusDecrypted(msgId, rawMess)
} catch (error) {
log.info('MetaMaskController - eth_decrypt failed.', error)
this.decryptMessageManager.errorMessage(msgId, error)
}
return this.getState()
}
/**
* Used to cancel a eth_decrypt type message.
* @param {string} msgId - The ID of the message to cancel.
* @param {Function} cb - The callback function called with a full state update.
*/
cancelDecryptMessage (msgId, cb) {
const messageManager = this.decryptMessageManager
messageManager.rejectMsg(msgId)
if (cb && typeof cb === 'function') {
cb(null, this.getState())
}
}
// eth_getEncryptionPublicKey methods
/**
* Called when a dapp uses the eth_getEncryptionPublicKey method.
*
* @param {Object} msgParams - The params of the message to sign & return to the Dapp.
* @param {Object} req - (optional) the original request, containing the origin
* Passed back to the requesting Dapp.
*/
async newRequestEncryptionPublicKey (msgParams, req) {
const promise = this.encryptionPublicKeyManager.addUnapprovedMessageAsync(msgParams, req)
this.sendUpdate()
this.opts.showUnconfirmedMessage()
return promise
}
/**
* Signifies a user's approval to receiving encryption public key in queue.
* Triggers receiving, and the callback function from newUnsignedEncryptionPublicKey.
*
* @param {Object} msgParams - The params of the message to receive & return to the Dapp.
* @returns {Promise<Object>} - A full state update.
*/
async encryptionPublicKey (msgParams) {
log.info('MetaMaskController - encryptionPublicKey')
const msgId = msgParams.metamaskId
// sets the status op the message to 'approved'
// and removes the metamaskId for decryption
try {
const params = await this.encryptionPublicKeyManager.approveMessage(msgParams)
// EncryptionPublicKey message
const publicKey = await this.keyringController.getEncryptionPublicKey(params.data)
// tells the listener that the message has been processed
// and can be returned to the dapp
this.encryptionPublicKeyManager.setMsgStatusReceived(msgId, publicKey)
} catch (error) {
log.info('MetaMaskController - eth_getEncryptionPublicKey failed.', error)
this.encryptionPublicKeyManager.errorMessage(msgId, error)
}
return this.getState()
}
/**
* Used to cancel a eth_getEncryptionPublicKey type message.
* @param {string} msgId - The ID of the message to cancel.
* @param {Function} cb - The callback function called with a full state update.
*/
cancelEncryptionPublicKey (msgId, cb) {
const messageManager = this.encryptionPublicKeyManager
messageManager.rejectMsg(msgId)
if (cb && typeof cb === 'function') {
cb(null, this.getState())
}
}
// eth_signTypedData methods // eth_signTypedData methods
/** /**

View File

@ -88,7 +88,7 @@
"eth-json-rpc-errors": "^2.0.2", "eth-json-rpc-errors": "^2.0.2",
"eth-json-rpc-filters": "^4.1.1", "eth-json-rpc-filters": "^4.1.1",
"eth-json-rpc-infura": "^4.0.2", "eth-json-rpc-infura": "^4.0.2",
"eth-json-rpc-middleware": "^4.4.0", "eth-json-rpc-middleware": "^4.4.1",
"eth-keyring-controller": "^5.5.0", "eth-keyring-controller": "^5.5.0",
"eth-ledger-bridge-keyring": "^0.2.0", "eth-ledger-bridge-keyring": "^0.2.0",
"eth-method-registry": "^1.2.0", "eth-method-registry": "^1.2.0",

View File

@ -18,6 +18,8 @@
"unapprovedMsgCount": 0, "unapprovedMsgCount": 0,
"unapprovedPersonalMsgs": {}, "unapprovedPersonalMsgs": {},
"unapprovedPersonalMsgCount": 0, "unapprovedPersonalMsgCount": 0,
"unapprovedDecryptMsgs": {},
"unapprovedDecryptMsgCount": 0,
"unapprovedTypedMessages": {}, "unapprovedTypedMessages": {},
"unapprovedTypedMessagesCount": 0, "unapprovedTypedMessagesCount": 0,
"isUnlocked": true, "isUnlocked": true,

View File

@ -136,6 +136,10 @@
"unapprovedMsgCount": 0, "unapprovedMsgCount": 0,
"unapprovedPersonalMsgs": {}, "unapprovedPersonalMsgs": {},
"unapprovedPersonalMsgCount": 0, "unapprovedPersonalMsgCount": 0,
"unapprovedDecryptMsgs": {},
"unapprovedDecryptMsgCount": 0,
"unapprovedEncryptionPublicKeyMsgs": {},
"unapprovedEncryptionPublicKeyMsgCount": 0,
"unapprovedTypedMessages": {}, "unapprovedTypedMessages": {},
"unapprovedTypedMessagesCount": 0, "unapprovedTypedMessagesCount": 0,
"send": { "send": {

View File

@ -42,6 +42,10 @@
@import './request-signature.scss'; @import './request-signature.scss';
@import './request-encryption-public-key.scss';
@import './request-decrypt-message.scss';
@import './account-details-dropdown.scss'; @import './account-details-dropdown.scss';
@import './editable-label.scss'; @import './editable-label.scss';

View File

@ -0,0 +1,293 @@
.request-decrypt-message {
&__container {
width: 380px;
border-radius: 8px;
background-color: $white;
box-shadow: 0 2px 4px 0 rgba(0,0,0,0.08);
display: flex;
flex-flow: column nowrap;
z-index: 25;
align-items: center;
font-family: Roboto;
position: relative;
height: 100%;
@media screen and (max-width: $break-small) {
width: 100%;
top: 0;
box-shadow: none;
}
@media screen and (min-width: $break-large) {
height: 620px;
}
}
&__typed-container {
padding: 17px;
h1 {
font-weight: 900;
margin-bottom: 5px;
}
* {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
> div {
margin-bottom: 10px;
}
}
&__header {
height: 64px;
width: 100%;
position: relative;
display: flex;
flex-flow: column;
justify-content: center;
align-items: center;
flex: 0 0 auto;
}
&__header-background {
position: absolute;
background-color: $athens-grey;
z-index: 2;
width: 100%;
height: 100%;
}
&__header__text {
color: #5B5D67;
font-family: Roboto;
font-size: 22px;
line-height: 29px;
z-index: 3;
text-align: center;
}
&__header__tip-container {
width: 100%;
display: flex;
justify-content: center;
}
&__header__tip {
height: 25px;
width: 25px;
background: $athens-grey;
transform: rotate(45deg);
position: absolute;
bottom: -8px;
z-index: 1;
}
&__account-info {
display: flex;
justify-content: space-between;
margin-top: 18px;
margin-bottom: 20px;
}
&__account {
color: $dusty-gray;
margin-left: 17px;
}
&__account-text {
font-size: 14px;
}
&__account-item {
height: 22px;
background-color: $white;
font-family: Roboto;
line-height: 16px;
font-size: 12px;
width: 124px;
.account-list-item {
margin-top: 6px;
}
.account-list-item__account-name {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
width: 80px;
}
.account-list-item__top-row {
margin: 0;
}
}
&__balance {
color: $dusty-gray;
margin-right: 17px;
width: 124px;
}
&__balance-text {
text-align: right;
font-size: 14px;
}
&__balance-value {
text-align: right;
margin-top: 2.5px;
}
&__request-icon {
margin-top: 25px;
}
&__body {
width: 100%;
height: 100%;
display: flex;
flex-flow: column;
flex: 1 1 auto;
height: 0;
}
&__notice {
font-family: "Avenir Next";
font-size: 14px;
line-height: 19px;
text-align: center;
margin-top: 15px;
margin-bottom: 11px;
width: 100%;
}
&__message {
overflow-wrap: break-word;
margin: 20px;
overflow: hidden;
border: 1px solid #dedede;
padding: 5px;
border-radius: 5px;
position: relative;
&-text {
font-size: 0.7em;
height: 115px;
}
&-cover {
background-color: white;
opacity: 0.75;
position: absolute;
height: 100%;
width: 100%;
top: 0px;
}
&-lock {
position: absolute;
height: 100%;
width: 100%;
top: 0px;
cursor: pointer;
img {
padding: 5px;
background-color: #fff;
left: calc(50% - 24px);
position: absolute;
top: calc(50% - 34px);
border-radius: 3px;
}
&--pressed {
display: none;
}
}
&-lock-text {
width: 200px;
font-size: 0.75em;
position: absolute;
top: calc(50% + 5px);
text-align: center;
left: calc(50% - 100px);
background-color: white;
line-height: 1em;
border-radius: 3px;
}
&-copy {
justify-content: space-evenly;
font-size: 0.75em;
margin-left: 20px;
margin-right: 20px;
display: flex;
cursor: pointer;
}
&-copy-text {
margin-right: 10px;
display: inline;
}
&-copy-tooltip {
float: right;
}
}
&__footer {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
position: relative;
flex: 0 0 auto;
border-top: 1px solid $geyser;
padding: 1.6rem;
button {
width: 165px;
}
&__cancel-button {
margin-right: 1.2rem;
}
}
&__visual {
display: flex;
flex-direction: row;
justify-content: space-evenly;
position: relative;
margin: 0 20px;
section {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
}
&-identicon {
width: 48px;
height: 48px;
&--default {
background-color: #777A87;
color: white;
width: 48px;
height: 48px;
border-radius: 24px;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
}
}
}
}

View File

@ -0,0 +1,222 @@
.request-encryption-public-key {
&__container {
width: 380px;
border-radius: 8px;
background-color: $white;
box-shadow: 0 2px 4px 0 rgba(0,0,0,0.08);
display: flex;
flex-flow: column nowrap;
z-index: 25;
align-items: center;
font-family: Roboto;
position: relative;
height: 100%;
@media screen and (max-width: $break-small) {
width: 100%;
top: 0;
box-shadow: none;
}
@media screen and (min-width: $break-large) {
height: 620px;
}
}
&__typed-container {
padding: 17px;
h1 {
font-weight: 900;
margin-bottom: 5px;
}
* {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
> div {
margin-bottom: 10px;
}
}
&__header {
height: 64px;
width: 100%;
position: relative;
display: flex;
flex-flow: column;
justify-content: center;
align-items: center;
flex: 0 0 auto;
}
&__header-background {
position: absolute;
background-color: $athens-grey;
z-index: 2;
width: 100%;
height: 100%;
}
&__header__text {
color: #5B5D67;
font-family: Roboto;
font-size: 22px;
line-height: 29px;
z-index: 3;
text-align: center;
}
&__header__tip-container {
width: 100%;
display: flex;
justify-content: center;
}
&__header__tip {
height: 25px;
width: 25px;
background: $athens-grey;
transform: rotate(45deg);
position: absolute;
bottom: -8px;
z-index: 1;
}
&__account-info {
display: flex;
justify-content: space-between;
margin-top: 18px;
margin-bottom: 20px;
}
&__account {
color: $dusty-gray;
margin-left: 17px;
}
&__account-text {
font-size: 14px;
}
&__account-item {
height: 22px;
background-color: $white;
font-family: Roboto;
line-height: 16px;
font-size: 12px;
width: 124px;
.account-list-item {
margin-top: 6px;
}
.account-list-item__account-name {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
width: 80px;
}
.account-list-item__top-row {
margin: 0;
}
}
&__balance {
color: $dusty-gray;
margin-right: 17px;
width: 124px;
}
&__balance-text {
text-align: right;
font-size: 14px;
}
&__balance-value {
text-align: right;
margin-top: 2.5px;
}
&__request-icon {
margin-top: 25px;
}
&__body {
width: 100%;
height: 100%;
display: flex;
flex-flow: column;
flex: 1 1 auto;
height: 0;
}
&__notice {
font-family: "Avenir Next";
font-size: 14px;
line-height: 19px;
text-align: center;
margin-top: 41px;
margin-bottom: 11px;
width: 100%;
padding-left: 20px;
padding-right: 20px;
color: $dusty-gray;
}
&__footer {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
position: relative;
flex: 0 0 auto;
border-top: 1px solid $geyser;
padding: 1.6rem;
button {
width: 165px;
}
&__cancel-button {
margin-right: 1.2rem;
}
}
&__visual {
display: flex;
flex-direction: row;
justify-content: space-evenly;
position: relative;
margin: 0 20px;
section {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
}
&-identicon {
width: 48px;
height: 48px;
&--default {
background-color: #777A87;
color: white;
width: 48px;
height: 48px;
border-radius: 24px;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
}
}
}
}

View File

@ -48,6 +48,8 @@ const CONFIRM_APPROVE_PATH = '/approve'
const CONFIRM_TRANSFER_FROM_PATH = '/transfer-from' const CONFIRM_TRANSFER_FROM_PATH = '/transfer-from'
const CONFIRM_TOKEN_METHOD_PATH = '/token-method' const CONFIRM_TOKEN_METHOD_PATH = '/token-method'
const SIGNATURE_REQUEST_PATH = '/signature-request' const SIGNATURE_REQUEST_PATH = '/signature-request'
const DECRYPT_MESSAGE_REQUEST_PATH = '/decrypt-message-request'
const ENCRYPTION_PUBLIC_KEY_REQUEST_PATH = '/encryption-public-key-request'
export { export {
DEFAULT_ROUTE, DEFAULT_ROUTE,
@ -81,6 +83,8 @@ export {
CONFIRM_TRANSFER_FROM_PATH, CONFIRM_TRANSFER_FROM_PATH,
CONFIRM_TOKEN_METHOD_PATH, CONFIRM_TOKEN_METHOD_PATH,
SIGNATURE_REQUEST_PATH, SIGNATURE_REQUEST_PATH,
DECRYPT_MESSAGE_REQUEST_PATH,
ENCRYPTION_PUBLIC_KEY_REQUEST_PATH,
INITIALIZE_METAMETRICS_OPT_IN_ROUTE, INITIALIZE_METAMETRICS_OPT_IN_ROUTE,
ADVANCED_ROUTE, ADVANCED_ROUTE,
SECURITY_ROUTE, SECURITY_ROUTE,

View File

@ -18,6 +18,8 @@ export const APPROVE_ACTION_KEY = 'approve'
export const SEND_TOKEN_ACTION_KEY = 'sentTokens' export const SEND_TOKEN_ACTION_KEY = 'sentTokens'
export const TRANSFER_FROM_ACTION_KEY = 'transferFrom' export const TRANSFER_FROM_ACTION_KEY = 'transferFrom'
export const SIGNATURE_REQUEST_KEY = 'signatureRequest' export const SIGNATURE_REQUEST_KEY = 'signatureRequest'
export const DECRYPT_REQUEST_KEY = 'decryptRequest'
export const ENCRYPTION_PUBLIC_KEY_REQUEST_KEY = 'encryptionPublicKeyRequest'
export const CONTRACT_INTERACTION_KEY = 'contractInteraction' export const CONTRACT_INTERACTION_KEY = 'contractInteraction'
export const CANCEL_ATTEMPT_ACTION_KEY = 'cancelAttempt' export const CANCEL_ATTEMPT_ACTION_KEY = 'cancelAttempt'
export const DEPOSIT_TRANSACTION_KEY = 'deposit' export const DEPOSIT_TRANSACTION_KEY = 'deposit'

View File

@ -19,6 +19,8 @@ import {
SEND_TOKEN_ACTION_KEY, SEND_TOKEN_ACTION_KEY,
TRANSFER_FROM_ACTION_KEY, TRANSFER_FROM_ACTION_KEY,
SIGNATURE_REQUEST_KEY, SIGNATURE_REQUEST_KEY,
DECRYPT_REQUEST_KEY,
ENCRYPTION_PUBLIC_KEY_REQUEST_KEY,
CONTRACT_INTERACTION_KEY, CONTRACT_INTERACTION_KEY,
CANCEL_ATTEMPT_ACTION_KEY, CANCEL_ATTEMPT_ACTION_KEY,
DEPOSIT_TRANSACTION_KEY, DEPOSIT_TRANSACTION_KEY,
@ -132,8 +134,14 @@ export function getTransactionActionKey (transaction) {
} }
if (msgParams) { if (msgParams) {
if (type === 'eth_decrypt') {
return DECRYPT_REQUEST_KEY
} else if (type === 'eth_getEncryptionPublicKey') {
return ENCRYPTION_PUBLIC_KEY_REQUEST_KEY
} else {
return SIGNATURE_REQUEST_KEY return SIGNATURE_REQUEST_KEY
} }
}
if (isConfirmDeployContract(transaction)) { if (isConfirmDeployContract(transaction)) {
return DEPLOY_CONTRACT_ACTION_KEY return DEPLOY_CONTRACT_ACTION_KEY

View File

@ -0,0 +1,333 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import Tooltip from '../../components/ui/tooltip-v2'
import copyToClipboard from 'copy-to-clipboard'
import classnames from 'classnames'
import { ENVIRONMENT_TYPE_NOTIFICATION } from '../../../../app/scripts/lib/enums'
import { getEnvironmentType } from '../../../../app/scripts/lib/util'
import Identicon from '../../components/ui/identicon'
import AccountListItem from '../send/account-list-item/account-list-item.component'
import { conversionUtil } from '../../helpers/utils/conversion-util'
import Button from '../../components/ui/button'
import { DEFAULT_ROUTE } from '../../helpers/constants/routes'
export default class ConfirmDecryptMessage extends Component {
static contextTypes = {
t: PropTypes.func.isRequired,
metricsEvent: PropTypes.func.isRequired,
}
static propTypes = {
balance: PropTypes.string,
clearConfirmTransaction: PropTypes.func.isRequired,
cancelDecryptMessage: PropTypes.func.isRequired,
decryptMessage: PropTypes.func.isRequired,
decryptMessageInline: PropTypes.func.isRequired,
conversionRate: PropTypes.number,
history: PropTypes.object.isRequired,
requesterAddress: PropTypes.string,
selectedAccount: PropTypes.object,
txData: PropTypes.object,
domainMetadata: PropTypes.object,
}
state = {
selectedAccount: this.props.selectedAccount,
hasCopied: false,
copyToClipboardPressed: false,
}
componentDidMount = () => {
if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_NOTIFICATION) {
window.addEventListener('beforeunload', this._beforeUnload)
}
}
componentWillUnmount = () => {
this._removeBeforeUnload()
}
_beforeUnload = (event) => {
const { clearConfirmTransaction, cancelDecryptMessage } = this.props
const { metricsEvent } = this.context
metricsEvent({
eventOpts: {
category: 'Messages',
action: 'Decrypt Message Request',
name: 'Cancel Via Notification Close',
},
})
clearConfirmTransaction()
cancelDecryptMessage(event)
}
_removeBeforeUnload = () => {
if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_NOTIFICATION) {
window.removeEventListener('beforeunload', this._beforeUnload)
}
}
copyMessage = () => {
copyToClipboard(this.state.rawMessage)
this.context.metricsEvent({
eventOpts: {
category: 'Messages',
action: 'Decrypt Message Copy',
name: 'Copy',
},
})
this.setState({ hasCopied: true })
setTimeout(() => this.setState({ hasCopied: false }), 3000)
}
renderHeader = () => {
return (
<div className="request-decrypt-message__header">
<div className="request-decrypt-message__header-background" />
<div className="request-decrypt-message__header__text">
{ this.context.t('decryptRequest') }
</div>
<div className="request-decrypt-message__header__tip-container">
<div className="request-decrypt-message__header__tip" />
</div>
</div>
)
}
renderAccount = () => {
const { selectedAccount } = this.state
return (
<div className="request-decrypt-message__account">
<div className="request-decrypt-message__account-text">
{ `${this.context.t('account')}:` }
</div>
<div className="request-decrypt-message__account-item">
<AccountListItem
account={selectedAccount}
displayBalance={false}
/>
</div>
</div>
)
}
renderBalance = () => {
const { balance, conversionRate } = this.props
const balanceInEther = conversionUtil(balance, {
fromNumericBase: 'hex',
toNumericBase: 'dec',
fromDenomination: 'WEI',
numberOfDecimals: 6,
conversionRate,
})
return (
<div className="request-decrypt-message__balance">
<div className="request-decrypt-message__balance-text">
{ `${this.context.t('balance')}:` }
</div>
<div className="request-decrypt-message__balance-value">
{ `${balanceInEther} ETH` }
</div>
</div>
)
}
renderRequestIcon = () => {
const { requesterAddress } = this.props
return (
<div className="request-decrypt-message__request-icon">
<Identicon
diameter={40}
address={requesterAddress}
/>
</div>
)
}
renderAccountInfo = () => {
return (
<div className="request-decrypt-message__account-info">
{ this.renderAccount() }
{ this.renderRequestIcon() }
{ this.renderBalance() }
</div>
)
}
renderBody = () => {
const { txData } = this.props
const origin = this.props.domainMetadata[txData.msgParams.origin]
const notice = this.context.t('decryptMessageNotice', [origin.name])
const {
hasCopied,
hasDecrypted,
hasError,
rawMessage,
errorMessage,
copyToClipboardPressed,
} = this.state
return (
<div className="request-decrypt-message__body">
{ this.renderAccountInfo() }
<div
className="request-decrypt-message__visual"
>
<section>
{origin.icon ? (
<img
className="request-decrypt-message__visual-identicon"
src={origin.icon}
/>
) : (
<i className="request-decrypt-message__visual-identicon--default">
{origin.name.charAt(0).toUpperCase()}
</i>
)}
<div
className="request-decrypt-message__notice"
>
{ notice }
</div>
</section>
</div>
<div
className="request-decrypt-message__message"
>
<div
className="request-decrypt-message__message-text"
>
{ !hasDecrypted && !hasError ? txData.msgParams.data : rawMessage }
{ !hasError ? '' : errorMessage }
</div>
<div
className={classnames({
'request-decrypt-message__message-cover': true,
'request-decrypt-message__message-lock--pressed': hasDecrypted || hasError,
})}
>
</div>
<div
className={classnames({
'request-decrypt-message__message-lock': true,
'request-decrypt-message__message-lock--pressed': hasDecrypted || hasError,
})}
onClick={(event) => {
this.props.decryptMessageInline(txData, event).then((result) => {
if (!result.error) {
this.setState({ hasDecrypted: true, rawMessage: result.rawData })
} else {
this.setState({ hasError: true, errorMessage: this.context.t('decryptInlineError', [result.error]) })
}
})
}}
>
<img src="images/lock.svg" />
<div
className="request-decrypt-message__message-lock-text"
>
{this.context.t('decryptMetamask')}
</div>
</div>
</div>
{ hasDecrypted ?
(
<div
className={classnames({
'request-decrypt-message__message-copy': true,
'request-decrypt-message__message-copy--pressed': copyToClipboardPressed,
})}
onClick={() => this.copyMessage()}
onMouseDown={() => this.setState({ copyToClipboardPressed: true })}
onMouseUp={() => this.setState({ copyToClipboardPressed: false })}
>
<Tooltip
position="bottom"
title={hasCopied ? this.context.t('copiedExclamation') : this.context.t('copyToClipboard')}
wrapperClassName="request-decrypt-message__message-copy-tooltip"
>
<div
className="request-decrypt-message__message-copy-text"
>
{this.context.t('decryptCopy')}
</div>
<img src="images/copy-to-clipboard.svg" />
</Tooltip>
</div>
)
:
<div></div>
}
</div>
)
}
renderFooter = () => {
const { txData } = this.props
return (
<div className="request-decrypt-message__footer">
<Button
type="default"
large
className="request-decrypt-message__footer__cancel-button"
onClick={async (event) => {
this._removeBeforeUnload()
await this.props.cancelDecryptMessage(txData, event)
this.context.metricsEvent({
eventOpts: {
category: 'Messages',
action: 'Decrypt Message Request',
name: 'Cancel',
},
})
this.props.clearConfirmTransaction()
this.props.history.push(DEFAULT_ROUTE)
}}
>
{ this.context.t('cancel') }
</Button>
<Button
type="secondary"
large
className="request-decrypt-message__footer__sign-button"
onClick={async (event) => {
this._removeBeforeUnload()
await this.props.decryptMessage(txData, event)
this.context.metricsEvent({
eventOpts: {
category: 'Messages',
action: 'Decrypt Message Request',
name: 'Confirm',
},
})
this.props.clearConfirmTransaction()
this.props.history.push(DEFAULT_ROUTE)
}}
>
{ this.context.t('decrypt') }
</Button>
</div>
)
}
render = () => {
return (
<div className="request-decrypt-message__container">
{ this.renderHeader() }
{ this.renderBody() }
{ this.renderFooter() }
</div>
)
}
}

View File

@ -0,0 +1,62 @@
import { connect } from 'react-redux'
import { compose } from 'recompose'
import { withRouter } from 'react-router-dom'
import { goHome, decryptMsg, cancelDecryptMsg, decryptMsgInline } from '../../store/actions'
import {
getSelectedAccount,
getCurrentAccountWithSendEtherInfo,
getSelectedAddress,
conversionRateSelector,
} from '../../selectors/selectors.js'
import { clearConfirmTransaction } from '../../ducks/confirm-transaction/confirm-transaction.duck'
import ConfirmDecryptMessage from './confirm-decrypt-message.component'
function mapStateToProps (state) {
const { confirmTransaction,
metamask: { domainMetadata = {} },
} = state
const {
txData = {},
} = confirmTransaction
return {
txData: txData,
domainMetadata: domainMetadata,
balance: getSelectedAccount(state).balance,
selectedAccount: getCurrentAccountWithSendEtherInfo(state),
selectedAddress: getSelectedAddress(state),
requester: null,
requesterAddress: null,
conversionRate: conversionRateSelector(state),
}
}
function mapDispatchToProps (dispatch) {
return {
goHome: () => dispatch(goHome()),
clearConfirmTransaction: () => dispatch(clearConfirmTransaction()),
decryptMessage: (msgData, event) => {
const params = msgData.msgParams
params.metamaskId = msgData.id
event.stopPropagation(event)
return dispatch(decryptMsg(params))
},
cancelDecryptMessage: (msgData, event) => {
event.stopPropagation(event)
return dispatch(cancelDecryptMsg(msgData))
},
decryptMessageInline: (msgData, event) => {
const params = msgData.msgParams
params.metamaskId = msgData.id
event.stopPropagation(event)
return dispatch(decryptMsgInline(params))
},
}
}
export default compose(
withRouter,
connect(mapStateToProps, mapDispatchToProps)
)(ConfirmDecryptMessage)

View File

@ -0,0 +1 @@
export { default } from './confirm-decrypt-message.container'

View File

@ -0,0 +1,238 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { ENVIRONMENT_TYPE_NOTIFICATION } from '../../../../app/scripts/lib/enums'
import { getEnvironmentType } from '../../../../app/scripts/lib/util'
import Identicon from '../../components/ui/identicon'
import AccountListItem from '../send/account-list-item/account-list-item.component'
import { conversionUtil } from '../../helpers/utils/conversion-util'
import Button from '../../components/ui/button'
import { DEFAULT_ROUTE } from '../../helpers/constants/routes'
export default class ConfirmEncryptionPublicKey extends Component {
static contextTypes = {
t: PropTypes.func.isRequired,
metricsEvent: PropTypes.func.isRequired,
}
static propTypes = {
balance: PropTypes.string,
clearConfirmTransaction: PropTypes.func.isRequired,
cancelEncryptionPublicKey: PropTypes.func.isRequired,
encryptionPublicKey: PropTypes.func.isRequired,
conversionRate: PropTypes.number,
history: PropTypes.object.isRequired,
requesterAddress: PropTypes.string,
selectedAccount: PropTypes.object,
txData: PropTypes.object,
domainMetadata: PropTypes.object,
}
state = {
selectedAccount: this.props.selectedAccount,
}
componentDidMount = () => {
if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_NOTIFICATION) {
window.addEventListener('beforeunload', this._beforeUnload)
}
}
componentWillUnmount = () => {
this._removeBeforeUnload()
}
_beforeUnload = (event) => {
const { clearConfirmTransaction, cancelEncryptionPublicKey } = this.props
const { metricsEvent } = this.context
metricsEvent({
eventOpts: {
category: 'Messages',
action: 'Encryption public key Request',
name: 'Cancel Via Notification Close',
},
})
clearConfirmTransaction()
cancelEncryptionPublicKey(event)
}
_removeBeforeUnload = () => {
if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_NOTIFICATION) {
window.removeEventListener('beforeunload', this._beforeUnload)
}
}
renderHeader = () => {
return (
<div className="request-encryption-public-key__header">
<div className="request-encryption-public-key__header-background" />
<div className="request-encryption-public-key__header__text">
{ this.context.t('encryptionPublicKeyRequest') }
</div>
<div className="request-encryption-public-key__header__tip-container">
<div className="request-encryption-public-key__header__tip" />
</div>
</div>
)
}
renderAccount = () => {
const { selectedAccount } = this.state
return (
<div className="request-encryption-public-key__account">
<div className="request-encryption-public-key__account-text">
{ `${this.context.t('account')}:` }
</div>
<div className="request-encryption-public-key__account-item">
<AccountListItem
account={selectedAccount}
displayBalance={false}
/>
</div>
</div>
)
}
renderBalance = () => {
const { balance, conversionRate } = this.props
const balanceInEther = conversionUtil(balance, {
fromNumericBase: 'hex',
toNumericBase: 'dec',
fromDenomination: 'WEI',
numberOfDecimals: 6,
conversionRate,
})
return (
<div className="request-encryption-public-key__balance">
<div className="request-encryption-public-key__balance-text">
{ `${this.context.t('balance')}:` }
</div>
<div className="request-encryption-public-key__balance-value">
{ `${balanceInEther} ETH` }
</div>
</div>
)
}
renderRequestIcon = () => {
const { requesterAddress } = this.props
return (
<div className="request-encryption-public-key__request-icon">
<Identicon
diameter={40}
address={requesterAddress}
/>
</div>
)
}
renderAccountInfo = () => {
return (
<div className="request-encryption-public-key__account-info">
{ this.renderAccount() }
{ this.renderRequestIcon() }
{ this.renderBalance() }
</div>
)
}
renderBody = () => {
const { txData } = this.props
const origin = this.props.domainMetadata[txData.origin]
const notice = this.context.t('encryptionPublicKeyNotice', [origin.name])
return (
<div className="request-encryption-public-key__body">
{ this.renderAccountInfo() }
<div
className="request-encryption-public-key__visual"
>
<section>
{origin.icon ? (
<img
className="request-encryption-public-key__visual-identicon"
src={origin.icon}
/>
) : (
<i className="request-encryption-public-key__visual-identicon--default">
{origin.name.charAt(0).toUpperCase()}
</i>
)}
<div
className="request-encryption-public-key__notice"
>
{ notice }
</div>
</section>
</div>
</div>
)
}
renderFooter = () => {
const { txData } = this.props
return (
<div className="request-encryption-public-key__footer">
<Button
type="default"
large
className="request-encryption-public-key__footer__cancel-button"
onClick={async (event) => {
this._removeBeforeUnload()
await this.props.cancelEncryptionPublicKey(txData, event)
this.context.metricsEvent({
eventOpts: {
category: 'Messages',
action: 'Encryption public key Request',
name: 'Cancel',
},
})
this.props.clearConfirmTransaction()
this.props.history.push(DEFAULT_ROUTE)
}}
>
{ this.context.t('cancel') }
</Button>
<Button
type="secondary"
large
className="request-encryption-public-key__footer__sign-button"
onClick={async (event) => {
this._removeBeforeUnload()
await this.props.encryptionPublicKey(txData, event)
this.context.metricsEvent({
eventOpts: {
category: 'Messages',
action: 'Encryption public key Request',
name: 'Confirm',
},
})
this.props.clearConfirmTransaction()
this.props.history.push(DEFAULT_ROUTE)
}}
>
{ this.context.t('provide') }
</Button>
</div>
)
}
render = () => {
return (
<div className="request-encryption-public-key__container">
{ this.renderHeader() }
{ this.renderBody() }
{ this.renderFooter() }
</div>
)
}
}

View File

@ -0,0 +1,55 @@
import { connect } from 'react-redux'
import { compose } from 'recompose'
import { withRouter } from 'react-router-dom'
import { goHome, encryptionPublicKeyMsg, cancelEncryptionPublicKeyMsg } from '../../store/actions'
import {
getSelectedAccount,
getCurrentAccountWithSendEtherInfo,
getSelectedAddress,
conversionRateSelector,
} from '../../selectors/selectors.js'
import { clearConfirmTransaction } from '../../ducks/confirm-transaction/confirm-transaction.duck'
import ConfirmEncryptionPublicKey from './confirm-encryption-public-key.component'
function mapStateToProps (state) {
const { confirmTransaction,
metamask: { domainMetadata = {} },
} = state
const {
txData = {},
} = confirmTransaction
return {
txData: txData,
domainMetadata: domainMetadata,
balance: getSelectedAccount(state).balance,
selectedAccount: getCurrentAccountWithSendEtherInfo(state),
selectedAddress: getSelectedAddress(state),
requester: null,
requesterAddress: null,
conversionRate: conversionRateSelector(state),
}
}
function mapDispatchToProps (dispatch) {
return {
goHome: () => dispatch(goHome()),
clearConfirmTransaction: () => dispatch(clearConfirmTransaction()),
encryptionPublicKey: (msgData, event) => {
const params = { data: msgData.msgParams, metamaskId: msgData.id }
event.stopPropagation()
return dispatch(encryptionPublicKeyMsg(params))
},
cancelEncryptionPublicKey: (msgData, event) => {
event.stopPropagation()
return dispatch(cancelEncryptionPublicKeyMsg(msgData))
},
}
}
export default compose(
withRouter,
connect(mapStateToProps, mapDispatchToProps)
)(ConfirmEncryptionPublicKey)

View File

@ -0,0 +1 @@
export { default } from './confirm-encryption-public-key.container'

View File

@ -11,6 +11,8 @@ import {
CONFIRM_TRANSFER_FROM_PATH, CONFIRM_TRANSFER_FROM_PATH,
CONFIRM_TOKEN_METHOD_PATH, CONFIRM_TOKEN_METHOD_PATH,
SIGNATURE_REQUEST_PATH, SIGNATURE_REQUEST_PATH,
DECRYPT_MESSAGE_REQUEST_PATH,
ENCRYPTION_PUBLIC_KEY_REQUEST_PATH,
} from '../../helpers/constants/routes' } from '../../helpers/constants/routes'
import { import {
TOKEN_METHOD_TRANSFER, TOKEN_METHOD_TRANSFER,
@ -68,11 +70,15 @@ export default class ConfirmTransactionSwitch extends Component {
render () { render () {
const { txData } = this.props const { txData } = this.props
if (txData.txParams) { if (txData.txParams) {
return this.redirectToTransaction() return this.redirectToTransaction()
} else if (txData.msgParams) { } else if (txData.msgParams) {
const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${txData.id}${SIGNATURE_REQUEST_PATH}` let pathname = `${CONFIRM_TRANSACTION_ROUTE}/${txData.id}${SIGNATURE_REQUEST_PATH}`
if (txData.type === 'eth_decrypt') {
pathname = `${CONFIRM_TRANSACTION_ROUTE}/${txData.id}${DECRYPT_MESSAGE_REQUEST_PATH}`
} else if (txData.type === 'eth_getEncryptionPublicKey') {
pathname = `${CONFIRM_TRANSACTION_ROUTE}/${txData.id}${ENCRYPTION_PUBLIC_KEY_REQUEST_PATH}`
}
return <Redirect to={{ pathname }} /> return <Redirect to={{ pathname }} />
} }

View File

@ -10,6 +10,9 @@ import ConfirmDeployContract from '../confirm-deploy-contract'
import ConfirmApprove from '../confirm-approve' import ConfirmApprove from '../confirm-approve'
import ConfirmTokenTransactionBaseContainer from '../confirm-token-transaction-base' import ConfirmTokenTransactionBaseContainer from '../confirm-token-transaction-base'
import ConfTx from './conf-tx' import ConfTx from './conf-tx'
import ConfirmDecryptMessage from '../confirm-decrypt-message'
import ConfirmEncryptionPublicKey from '../confirm-encryption-public-key'
import { import {
DEFAULT_ROUTE, DEFAULT_ROUTE,
CONFIRM_TRANSACTION_ROUTE, CONFIRM_TRANSACTION_ROUTE,
@ -20,6 +23,8 @@ import {
CONFIRM_TRANSFER_FROM_PATH, CONFIRM_TRANSFER_FROM_PATH,
CONFIRM_TOKEN_METHOD_PATH, CONFIRM_TOKEN_METHOD_PATH,
SIGNATURE_REQUEST_PATH, SIGNATURE_REQUEST_PATH,
DECRYPT_MESSAGE_REQUEST_PATH,
ENCRYPTION_PUBLIC_KEY_REQUEST_PATH,
} from '../../helpers/constants/routes' } from '../../helpers/constants/routes'
export default class ConfirmTransaction extends Component { export default class ConfirmTransaction extends Component {
@ -155,6 +160,16 @@ export default class ConfirmTransaction extends Component {
path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${SIGNATURE_REQUEST_PATH}`} path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${SIGNATURE_REQUEST_PATH}`}
component={ConfTx} component={ConfTx}
/> />
<Route
exact
path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${DECRYPT_MESSAGE_REQUEST_PATH}`}
component={ConfirmDecryptMessage}
/>
<Route
exact
path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${ENCRYPTION_PUBLIC_KEY_REQUEST_PATH}`}
component={ConfirmEncryptionPublicKey}
/>
<Route path="*" component={ConfirmTransactionSwitch} /> <Route path="*" component={ConfirmTransactionSwitch} />
</Switch> </Switch>
) )

View File

@ -130,6 +130,10 @@ export default {
'unapprovedMsgCount': 0, 'unapprovedMsgCount': 0,
'unapprovedPersonalMsgs': {}, 'unapprovedPersonalMsgs': {},
'unapprovedPersonalMsgCount': 0, 'unapprovedPersonalMsgCount': 0,
'unapprovedDecryptMsgs': {},
'unapprovedDecryptMsgCount': 0,
'unapprovedEncryptionPublicKeyMsgs': {},
'unapprovedEncryptionPublicKeyMsgCount': 0,
'keyringTypes': [ 'keyringTypes': [
'Simple Key Pair', 'Simple Key Pair',
'HD Key Tree', 'HD Key Tree',

View File

@ -16,6 +16,8 @@ import {
const unapprovedTxsSelector = (state) => state.metamask.unapprovedTxs const unapprovedTxsSelector = (state) => state.metamask.unapprovedTxs
const unapprovedMsgsSelector = (state) => state.metamask.unapprovedMsgs const unapprovedMsgsSelector = (state) => state.metamask.unapprovedMsgs
const unapprovedPersonalMsgsSelector = (state) => state.metamask.unapprovedPersonalMsgs const unapprovedPersonalMsgsSelector = (state) => state.metamask.unapprovedPersonalMsgs
const unapprovedDecryptMsgsSelector = (state) => state.metamask.unapprovedDecryptMsgs
const unapprovedEncryptionPublicKeyMsgsSelector = (state) => state.metamask.unapprovedEncryptionPublicKeyMsgs
const unapprovedTypedMessagesSelector = (state) => state.metamask.unapprovedTypedMessages const unapprovedTypedMessagesSelector = (state) => state.metamask.unapprovedTypedMessages
const networkSelector = (state) => state.metamask.network const networkSelector = (state) => state.metamask.network
@ -23,18 +25,24 @@ export const unconfirmedTransactionsListSelector = createSelector(
unapprovedTxsSelector, unapprovedTxsSelector,
unapprovedMsgsSelector, unapprovedMsgsSelector,
unapprovedPersonalMsgsSelector, unapprovedPersonalMsgsSelector,
unapprovedDecryptMsgsSelector,
unapprovedEncryptionPublicKeyMsgsSelector,
unapprovedTypedMessagesSelector, unapprovedTypedMessagesSelector,
networkSelector, networkSelector,
( (
unapprovedTxs = {}, unapprovedTxs = {},
unapprovedMsgs = {}, unapprovedMsgs = {},
unapprovedPersonalMsgs = {}, unapprovedPersonalMsgs = {},
unapprovedDecryptMsgs = {},
unapprovedEncryptionPublicKeyMsgs = {},
unapprovedTypedMessages = {}, unapprovedTypedMessages = {},
network network
) => txHelper( ) => txHelper(
unapprovedTxs, unapprovedTxs,
unapprovedMsgs, unapprovedMsgs,
unapprovedPersonalMsgs, unapprovedPersonalMsgs,
unapprovedDecryptMsgs,
unapprovedEncryptionPublicKeyMsgs,
unapprovedTypedMessages, unapprovedTypedMessages,
network network
) || [] ) || []
@ -44,12 +52,16 @@ export const unconfirmedTransactionsHashSelector = createSelector(
unapprovedTxsSelector, unapprovedTxsSelector,
unapprovedMsgsSelector, unapprovedMsgsSelector,
unapprovedPersonalMsgsSelector, unapprovedPersonalMsgsSelector,
unapprovedDecryptMsgsSelector,
unapprovedEncryptionPublicKeyMsgsSelector,
unapprovedTypedMessagesSelector, unapprovedTypedMessagesSelector,
networkSelector, networkSelector,
( (
unapprovedTxs = {}, unapprovedTxs = {},
unapprovedMsgs = {}, unapprovedMsgs = {},
unapprovedPersonalMsgs = {}, unapprovedPersonalMsgs = {},
unapprovedDecryptMsgs = {},
unapprovedEncryptionPublicKeyMsgs = {},
unapprovedTypedMessages = {}, unapprovedTypedMessages = {},
network network
) => { ) => {
@ -68,6 +80,8 @@ export const unconfirmedTransactionsHashSelector = createSelector(
...filteredUnapprovedTxs, ...filteredUnapprovedTxs,
...unapprovedMsgs, ...unapprovedMsgs,
...unapprovedPersonalMsgs, ...unapprovedPersonalMsgs,
...unapprovedDecryptMsgs,
...unapprovedEncryptionPublicKeyMsgs,
...unapprovedTypedMessages, ...unapprovedTypedMessages,
} }
} }
@ -75,18 +89,24 @@ export const unconfirmedTransactionsHashSelector = createSelector(
const unapprovedMsgCountSelector = (state) => state.metamask.unapprovedMsgCount const unapprovedMsgCountSelector = (state) => state.metamask.unapprovedMsgCount
const unapprovedPersonalMsgCountSelector = (state) => state.metamask.unapprovedPersonalMsgCount const unapprovedPersonalMsgCountSelector = (state) => state.metamask.unapprovedPersonalMsgCount
const unapprovedDecryptMsgCountSelector = (state) => state.metamask.unapprovedDecryptMsgCount
const unapprovedEncryptionPublicKeyMsgCountSelector = (state) => state.metamask.unapprovedEncryptionPublicKeyMsgCount
const unapprovedTypedMessagesCountSelector = (state) => state.metamask.unapprovedTypedMessagesCount const unapprovedTypedMessagesCountSelector = (state) => state.metamask.unapprovedTypedMessagesCount
export const unconfirmedTransactionsCountSelector = createSelector( export const unconfirmedTransactionsCountSelector = createSelector(
unapprovedTxsSelector, unapprovedTxsSelector,
unapprovedMsgCountSelector, unapprovedMsgCountSelector,
unapprovedPersonalMsgCountSelector, unapprovedPersonalMsgCountSelector,
unapprovedDecryptMsgCountSelector,
unapprovedEncryptionPublicKeyMsgCountSelector,
unapprovedTypedMessagesCountSelector, unapprovedTypedMessagesCountSelector,
networkSelector, networkSelector,
( (
unapprovedTxs = {}, unapprovedTxs = {},
unapprovedMsgCount = 0, unapprovedMsgCount = 0,
unapprovedPersonalMsgCount = 0, unapprovedPersonalMsgCount = 0,
unapprovedDecryptMsgCount = 0,
unapprovedEncryptionPublicKeyMsgCount = 0,
unapprovedTypedMessagesCount = 0, unapprovedTypedMessagesCount = 0,
network network
) => { ) => {
@ -96,7 +116,7 @@ export const unconfirmedTransactionsCountSelector = createSelector(
}) })
return filteredUnapprovedTxIds.length + unapprovedTypedMessagesCount + unapprovedMsgCount + return filteredUnapprovedTxIds.length + unapprovedTypedMessagesCount + unapprovedMsgCount +
unapprovedPersonalMsgCount unapprovedPersonalMsgCount + unapprovedDecryptMsgCount + unapprovedEncryptionPublicKeyMsgCount
} }
) )

View File

@ -326,11 +326,13 @@ export function getTotalUnapprovedCount ({ metamask }) {
unapprovedTxs = {}, unapprovedTxs = {},
unapprovedMsgCount, unapprovedMsgCount,
unapprovedPersonalMsgCount, unapprovedPersonalMsgCount,
unapprovedDecryptMsgCount,
unapprovedEncryptionPublicKeyMsgCount,
unapprovedTypedMessagesCount, unapprovedTypedMessagesCount,
} = metamask } = metamask
return Object.keys(unapprovedTxs).length + unapprovedMsgCount + unapprovedPersonalMsgCount + return Object.keys(unapprovedTxs).length + unapprovedMsgCount + unapprovedPersonalMsgCount +
unapprovedTypedMessagesCount unapprovedTypedMessagesCount + unapprovedDecryptMsgCount + unapprovedEncryptionPublicKeyMsgCount
} }
export function getIsMainnet (state) { export function getIsMainnet (state) {

View File

@ -132,6 +132,10 @@ export default {
'unapprovedMsgCount': 0, 'unapprovedMsgCount': 0,
'unapprovedPersonalMsgs': {}, 'unapprovedPersonalMsgs': {},
'unapprovedPersonalMsgCount': 0, 'unapprovedPersonalMsgCount': 0,
'unapprovedDecryptMsgs': {},
'unapprovedDecryptMsgCount': 0,
'unapprovedEncryptionPublicKeyMsgs': {},
'unapprovedEncryptionPublicKeyMsgCount': 0,
'keyringTypes': [ 'keyringTypes': [
'Simple Key Pair', 'Simple Key Pair',
'HD Key Tree', 'HD Key Tree',

View File

@ -33,23 +33,31 @@ export const incomingTxListSelector = (state) => {
export const unapprovedMsgsSelector = (state) => state.metamask.unapprovedMsgs export const unapprovedMsgsSelector = (state) => state.metamask.unapprovedMsgs
export const selectedAddressTxListSelector = (state) => state.metamask.selectedAddressTxList export const selectedAddressTxListSelector = (state) => state.metamask.selectedAddressTxList
export const unapprovedPersonalMsgsSelector = (state) => state.metamask.unapprovedPersonalMsgs export const unapprovedPersonalMsgsSelector = (state) => state.metamask.unapprovedPersonalMsgs
export const unapprovedDecryptMsgsSelector = (state) => state.metamask.unapprovedDecryptMsgs
export const unapprovedEncryptionPublicKeyMsgsSelector = (state) => state.metamask.unapprovedEncryptionPublicKeyMsgs
export const unapprovedTypedMessagesSelector = (state) => state.metamask.unapprovedTypedMessages export const unapprovedTypedMessagesSelector = (state) => state.metamask.unapprovedTypedMessages
export const networkSelector = (state) => state.metamask.network export const networkSelector = (state) => state.metamask.network
export const unapprovedMessagesSelector = createSelector( export const unapprovedMessagesSelector = createSelector(
unapprovedMsgsSelector, unapprovedMsgsSelector,
unapprovedPersonalMsgsSelector, unapprovedPersonalMsgsSelector,
unapprovedDecryptMsgsSelector,
unapprovedEncryptionPublicKeyMsgsSelector,
unapprovedTypedMessagesSelector, unapprovedTypedMessagesSelector,
networkSelector, networkSelector,
( (
unapprovedMsgs = {}, unapprovedMsgs = {},
unapprovedPersonalMsgs = {}, unapprovedPersonalMsgs = {},
unapprovedDecryptMsgs = {},
unapprovedEncryptionPublicKeyMsgs = {},
unapprovedTypedMessages = {}, unapprovedTypedMessages = {},
network network
) => txHelper( ) => txHelper(
{}, {},
unapprovedMsgs, unapprovedMsgs,
unapprovedPersonalMsgs, unapprovedPersonalMsgs,
unapprovedDecryptMsgs,
unapprovedEncryptionPublicKeyMsgs,
unapprovedTypedMessages, unapprovedTypedMessages,
network network
) || [] ) || []

View File

@ -631,6 +631,80 @@ export function signPersonalMsg (msgData) {
} }
} }
export function decryptMsgInline (decryptedMsgData) {
log.debug('action - decryptMsgInline')
return (dispatch) => {
return new Promise((resolve, reject) => {
log.debug(`actions calling background.decryptMessageInline`)
background.decryptMessageInline(decryptedMsgData, (err, newState) => {
log.debug('decryptMsgInline called back')
dispatch(updateMetamaskState(newState))
if (err) {
log.error(err)
dispatch(displayWarning(err.message))
return reject(err)
}
decryptedMsgData = newState.unapprovedDecryptMsgs[decryptedMsgData.metamaskId]
return resolve(decryptedMsgData)
})
})
}
}
export function decryptMsg (decryptedMsgData) {
log.debug('action - decryptMsg')
return (dispatch) => {
dispatch(showLoadingIndication())
return new Promise((resolve, reject) => {
log.debug(`actions calling background.decryptMessage`)
background.decryptMessage(decryptedMsgData, (err, newState) => {
log.debug('decryptMsg called back')
dispatch(updateMetamaskState(newState))
dispatch(hideLoadingIndication())
if (err) {
log.error(err)
dispatch(displayWarning(err.message))
return reject(err)
}
dispatch(completedTx(decryptedMsgData.metamaskId))
dispatch(closeCurrentNotificationWindow())
console.log(decryptedMsgData)
return resolve(decryptedMsgData)
})
})
}
}
export function encryptionPublicKeyMsg (msgData) {
log.debug('action - encryptionPublicKeyMsg')
return (dispatch) => {
dispatch(showLoadingIndication())
return new Promise((resolve, reject) => {
log.debug(`actions calling background.encryptionPublicKey`)
background.encryptionPublicKey(msgData, (err, newState) => {
log.debug('encryptionPublicKeyMsg called back')
dispatch(updateMetamaskState(newState))
dispatch(hideLoadingIndication())
if (err) {
log.error(err)
dispatch(displayWarning(err.message))
return reject(err)
}
dispatch(completedTx(msgData.metamaskId))
dispatch(closeCurrentNotificationWindow())
return resolve(msgData)
})
})
}
}
export function signTypedMsg (msgData) { export function signTypedMsg (msgData) {
log.debug('action - signTypedMsg') log.debug('action - signTypedMsg')
return (dispatch) => { return (dispatch) => {
@ -1005,6 +1079,50 @@ export function cancelPersonalMsg (msgData) {
} }
} }
export function cancelDecryptMsg (msgData) {
return (dispatch) => {
dispatch(showLoadingIndication())
return new Promise((resolve, reject) => {
const id = msgData.id
background.cancelDecryptMessage(id, (err, newState) => {
dispatch(updateMetamaskState(newState))
dispatch(hideLoadingIndication())
if (err) {
return reject(err)
}
dispatch(completedTx(id))
dispatch(closeCurrentNotificationWindow())
return resolve(msgData)
})
})
}
}
export function cancelEncryptionPublicKeyMsg (msgData) {
return (dispatch) => {
dispatch(showLoadingIndication())
return new Promise((resolve, reject) => {
const id = msgData.id
background.cancelEncryptionPublicKey(id, (err, newState) => {
dispatch(updateMetamaskState(newState))
dispatch(hideLoadingIndication())
if (err) {
return reject(err)
}
dispatch(completedTx(id))
dispatch(closeCurrentNotificationWindow())
return resolve(msgData)
})
})
}
}
export function cancelTypedMsg (msgData) { export function cancelTypedMsg (msgData) {
return (dispatch) => { return (dispatch) => {
dispatch(showLoadingIndication()) dispatch(showLoadingIndication())

View File

@ -59,7 +59,7 @@ async function startApp (metamaskState, backgroundConnection, opts) {
}) })
// if unconfirmed txs, start on txConf page // if unconfirmed txs, start on txConf page
const unapprovedTxsAll = txHelper(metamaskState.unapprovedTxs, metamaskState.unapprovedMsgs, metamaskState.unapprovedPersonalMsgs, metamaskState.unapprovedTypedMessages, metamaskState.network) const unapprovedTxsAll = txHelper(metamaskState.unapprovedTxs, metamaskState.unapprovedMsgs, metamaskState.unapprovedPersonalMsgs, metamaskState.unapprovedDecryptMsgs, metamaskState.unapprovedEncryptionPublicKeyMsgs, metamaskState.unapprovedTypedMessages, metamaskState.network)
const numberOfUnapprivedTx = unapprovedTxsAll.length const numberOfUnapprivedTx = unapprovedTxsAll.length
if (numberOfUnapprivedTx > 0) { if (numberOfUnapprivedTx > 0) {
store.dispatch(actions.showConfTxPage({ store.dispatch(actions.showConfTxPage({

View File

@ -1,9 +1,9 @@
import { valuesFor } from '../app/helpers/utils/util' import { valuesFor } from '../app/helpers/utils/util'
import log from 'loglevel' import log from 'loglevel'
export default function txHelper (unapprovedTxs, unapprovedMsgs, personalMsgs, typedMessages, network) { export default function txHelper (unapprovedTxs, unapprovedMsgs, personalMsgs, decryptMsgs, encryptionPublicKeyMsgs, typedMessages, network) {
log.debug('tx-helper called with params:') log.debug('tx-helper called with params:')
log.debug({ unapprovedTxs, unapprovedMsgs, personalMsgs, typedMessages, network }) log.debug({ unapprovedTxs, unapprovedMsgs, personalMsgs, decryptMsgs, encryptionPublicKeyMsgs, typedMessages, network })
const txValues = network ? valuesFor(unapprovedTxs).filter((txMeta) => txMeta.metamaskNetworkId === network) : valuesFor(unapprovedTxs) const txValues = network ? valuesFor(unapprovedTxs).filter((txMeta) => txMeta.metamaskNetworkId === network) : valuesFor(unapprovedTxs)
log.debug(`tx helper found ${txValues.length} unapproved txs`) log.debug(`tx helper found ${txValues.length} unapproved txs`)
@ -16,6 +16,14 @@ export default function txHelper (unapprovedTxs, unapprovedMsgs, personalMsgs, t
log.debug(`tx helper found ${personalValues.length} unsigned personal messages`) log.debug(`tx helper found ${personalValues.length} unsigned personal messages`)
allValues = allValues.concat(personalValues) allValues = allValues.concat(personalValues)
const decryptValues = valuesFor(decryptMsgs)
log.debug(`tx helper found ${decryptValues.length} decrypt requests`)
allValues = allValues.concat(decryptValues)
const encryptionPublicKeyValues = valuesFor(encryptionPublicKeyMsgs)
log.debug(`tx helper found ${encryptionPublicKeyValues.length} encryptionPublicKey requests`)
allValues = allValues.concat(encryptionPublicKeyValues)
const typedValues = valuesFor(typedMessages) const typedValues = valuesFor(typedMessages)
log.debug(`tx helper found ${typedValues.length} unsigned typed messages`) log.debug(`tx helper found ${typedValues.length} unsigned typed messages`)
allValues = allValues.concat(typedValues) allValues = allValues.concat(typedValues)

View File

@ -10337,30 +10337,10 @@ eth-json-rpc-middleware@^1.5.0:
promise-to-callback "^1.0.0" promise-to-callback "^1.0.0"
tape "^4.6.3" tape "^4.6.3"
eth-json-rpc-middleware@^4.1.4, eth-json-rpc-middleware@^4.1.5: eth-json-rpc-middleware@^4.1.4, eth-json-rpc-middleware@^4.1.5, eth-json-rpc-middleware@^4.4.1:
version "4.2.0" version "4.4.1"
resolved "https://registry.yarnpkg.com/eth-json-rpc-middleware/-/eth-json-rpc-middleware-4.2.0.tgz#cfb77c5056cb8001548c6c7d54f4af5fce04d489" resolved "https://registry.yarnpkg.com/eth-json-rpc-middleware/-/eth-json-rpc-middleware-4.4.1.tgz#07d3dd0724c24a8d31e4a172ee96271da71b4228"
integrity sha512-90LljqRyJhkg7fOwKunh1lu1Mr5bspXMBDitaTGyGPPNiFTbMrhtfbf9fteYlXRFCbq+aIFWwl/X+P7nkrdkLg== integrity sha512-yoSuRgEYYGFdVeZg3poWOwAlRI+MoBIltmOB86MtpoZjvLbou9EB/qWMOWSmH2ryCWLW97VYY6NWsmWm3OAA7A==
dependencies:
btoa "^1.2.1"
clone "^2.1.1"
eth-json-rpc-errors "^1.0.1"
eth-query "^2.1.2"
eth-sig-util "^1.4.2"
ethereumjs-block "^1.6.0"
ethereumjs-tx "^1.3.7"
ethereumjs-util "^5.1.2"
ethereumjs-vm "^2.6.0"
fetch-ponyfill "^4.0.0"
json-rpc-engine "^5.1.3"
json-stable-stringify "^1.0.1"
pify "^3.0.0"
safe-event-emitter "^1.0.1"
eth-json-rpc-middleware@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/eth-json-rpc-middleware/-/eth-json-rpc-middleware-4.4.0.tgz#ef63b783b48dcbea9c1fe25c79e6ea01510e5877"
integrity sha512-IeOsil/XiHsybJO9nFf86+1+YIqGQWPPfiTEp3WLkpLZhJm97kw6tFM7GttIZXIcwtaO3zEXgY6PWAH1jkB3ag==
dependencies: dependencies:
btoa "^1.2.1" btoa "^1.2.1"
clone "^2.1.1" clone "^2.1.1"