diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 7e23fdb93..7879bcc82 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -385,7 +385,7 @@ "message": "Custom Spend Limit" }, "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": { "message": "Decimals must be at least 0, and not over 36." @@ -1609,5 +1609,35 @@ }, "zeroGasPriceOnSpeedUpError": { "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" } } diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index addc3178e..09313c81d 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -124,6 +124,9 @@ "customRPC": { "message": "Пользовательский RPC" }, + "dataBackupFoundInfo": { + "message": "Некоторые данные вашей учетной записи были экспортированы во время предыдущей установки MetaMask. Это может включать ваши настройки, контакты и токены. Вы хотите импортировать эти данные сейчас?" + }, "decimalsMustZerotoTen": { "message": "Количество десятичных разрядов должно быть минимум 0 и максимум 36." }, @@ -962,8 +965,11 @@ "noConversionRateAvailable": { "message": "Курсы валют недоступны" }, + "noThanks": { + "message": "Нет, спасибо" + }, "notEnoughGas": { - "message": "Нехватает газа" + "message": "Не хватает газа" }, "noWebcamFoundTitle": { "message": "Веб-камера не найдена" @@ -1046,6 +1052,10 @@ "restoreAccountWithSeed": { "message": "Восстановите свой аккаунт с помощью секретной фразы" }, + "restoreWalletPreferences": { + "message": "Были найдены данные экспортированные от $1. Вы желаете восстановить настройки вашего кошелька?", + "description": "$1 is the date at which the data was backed up" + }, "requestsAwaitingAcknowledgement": { "message": "запросы, ожидающие подтверждения" }, @@ -1339,5 +1349,35 @@ }, "yourPrivateSeedPhrase": { "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" } } diff --git a/app/scripts/background.js b/app/scripts/background.js index ed2070d9b..997e1461b 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -127,6 +127,10 @@ initialize().catch(log.error) * @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 {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 {number} unapprovedTypedMsgCount - The number of messages in unapprovedTypedMsgs. * @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.messageManager.on('updateBadge', updateBadge) controller.personalMessageManager.on('updateBadge', updateBadge) + controller.decryptMessageManager.on('updateBadge', updateBadge) + controller.encryptionPublicKeyManager.on('updateBadge', updateBadge) controller.typedMessageManager.on('updateBadge', updateBadge) controller.permissionsController.permissions.subscribe(updateBadge) @@ -424,10 +430,13 @@ function setupController (initState, initLangCode) { let label = '' const unapprovedTxCount = controller.txController.getUnapprovedTxCount() const unapprovedMsgCount = controller.messageManager.unapprovedMsgCount - const unapprovedPersonalMsgs = controller.personalMessageManager.unapprovedPersonalMsgCount - const unapprovedTypedMsgs = controller.typedMessageManager.unapprovedTypedMessagesCount + const unapprovedPersonalMsgCount = controller.personalMessageManager.unapprovedPersonalMsgCount + 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 count = unapprovedTxCount + unapprovedMsgCount + unapprovedPersonalMsgs + unapprovedTypedMsgs + pendingPermissionRequests + const count = unapprovedTxCount + unapprovedMsgCount + unapprovedPersonalMsgCount + unapprovedDecryptMsgCount + unapprovedEncryptionPublicKeyMsgCount + + unapprovedTypedMessagesCount + pendingPermissionRequests if (count) { label = String(count) } diff --git a/app/scripts/controllers/network/createMetamaskMiddleware.js b/app/scripts/controllers/network/createMetamaskMiddleware.js index e5a0708e0..88c691c30 100644 --- a/app/scripts/controllers/network/createMetamaskMiddleware.js +++ b/app/scripts/controllers/network/createMetamaskMiddleware.js @@ -14,6 +14,8 @@ function createMetamaskMiddleware ({ processTypedMessageV3, processTypedMessageV4, processPersonalMessage, + processDecryptMessage, + processEncryptionPublicKey, getPendingNonce, getPendingTransactionByHash, }) { @@ -31,6 +33,8 @@ function createMetamaskMiddleware ({ processTypedMessageV3, processTypedMessageV4, processPersonalMessage, + processDecryptMessage, + processEncryptionPublicKey, }), createPendingNonceMiddleware({ getPendingNonce }), createPendingTxMiddleware({ getPendingTransactionByHash }), diff --git a/app/scripts/controllers/permissions/enums.js b/app/scripts/controllers/permissions/enums.js index 1e68eddbe..0d30827d6 100644 --- a/app/scripts/controllers/permissions/enums.js +++ b/app/scripts/controllers/permissions/enums.js @@ -69,4 +69,6 @@ export const SAFE_METHODS = [ 'eth_uninstallFilter', 'metamask_watchAsset', 'wallet_watchAsset', + 'eth_getEncryptionPublicKey', + 'eth_decrypt', ] diff --git a/app/scripts/lib/decrypt-message-manager.js b/app/scripts/lib/decrypt-message-manager.js new file mode 100644 index 000000000..24b8e8dd7 --- /dev/null +++ b/app/scripts/lib/decrypt-message-manager.js @@ -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} 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} 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} 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')) + } + +} diff --git a/app/scripts/lib/encryption-public-key-manager.js b/app/scripts/lib/encryption-public-key-manager.js new file mode 100644 index 000000000..4667330ed --- /dev/null +++ b/app/scripts/lib/encryption-public-key-manager.js @@ -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} 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} 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} 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') + } + +} diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 4dfd01900..a4e9e852e 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -35,6 +35,8 @@ import ThreeBoxController from './controllers/threebox' import RecentBlocksController from './controllers/recent-blocks' import IncomingTransactionsController from './controllers/incoming-transactions' 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 TypedMessageManager from './lib/typed-message-manager' import TransactionController from './controllers/transactions' @@ -278,6 +280,8 @@ export default class MetamaskController extends EventEmitter { this.networkController.lookupNetwork() this.messageManager = new MessageManager() this.personalMessageManager = new PersonalMessageManager() + this.decryptMessageManager = new DecryptMessageManager() + this.encryptionPublicKeyManager = new EncryptionPublicKeyManager() this.typedMessageManager = new TypedMessageManager({ networkController: this.networkController }) // ensure isClientOpenAndUnlocked is updated when memState updates @@ -313,6 +317,8 @@ export default class MetamaskController extends EventEmitter { TokenRatesController: this.tokenRatesController.store, MessageManager: this.messageManager.memStore, PersonalMessageManager: this.personalMessageManager.memStore, + DecryptMessageManager: this.decryptMessageManager.memStore, + EncryptionPublicKeyManager: this.encryptionPublicKeyManager.memStore, TypesMessageManager: this.typedMessageManager.memStore, KeyringController: this.keyringController.memStore, PreferencesController: this.preferencesController.store, @@ -363,6 +369,8 @@ export default class MetamaskController extends EventEmitter { processTypedMessageV3: this.newUnsignedTypedMessage.bind(this), processTypedMessageV4: this.newUnsignedTypedMessage.bind(this), processPersonalMessage: this.newUnsignedPersonalMessage.bind(this), + processDecryptMessage: this.newRequestDecryptMessage.bind(this), + processEncryptionPublicKey: this.newRequestEncryptionPublicKey.bind(this), getPendingNonce: this.getPendingNonce.bind(this), getPendingTransactionByHash: (hash) => this.txController.getFilteredTxList({ hash, status: 'submitted' })[0], } @@ -539,6 +547,15 @@ export default class MetamaskController extends EventEmitter { signTypedMessage: nodeify(this.signTypedMessage, 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 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} - 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} - 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} - 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 /** diff --git a/package.json b/package.json index 4a63aad8c..94bcf26f1 100644 --- a/package.json +++ b/package.json @@ -88,7 +88,7 @@ "eth-json-rpc-errors": "^2.0.2", "eth-json-rpc-filters": "^4.1.1", "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-ledger-bridge-keyring": "^0.2.0", "eth-method-registry": "^1.2.0", diff --git a/test/data/2-state.json b/test/data/2-state.json index 9d6dc9af5..8100b7e03 100644 --- a/test/data/2-state.json +++ b/test/data/2-state.json @@ -18,6 +18,8 @@ "unapprovedMsgCount": 0, "unapprovedPersonalMsgs": {}, "unapprovedPersonalMsgCount": 0, + "unapprovedDecryptMsgs": {}, + "unapprovedDecryptMsgCount": 0, "unapprovedTypedMessages": {}, "unapprovedTypedMessagesCount": 0, "isUnlocked": true, diff --git a/test/data/mock-state.json b/test/data/mock-state.json index 2db918522..6b4b732d6 100644 --- a/test/data/mock-state.json +++ b/test/data/mock-state.json @@ -136,6 +136,10 @@ "unapprovedMsgCount": 0, "unapprovedPersonalMsgs": {}, "unapprovedPersonalMsgCount": 0, + "unapprovedDecryptMsgs": {}, + "unapprovedDecryptMsgCount": 0, + "unapprovedEncryptionPublicKeyMsgs": {}, + "unapprovedEncryptionPublicKeyMsgCount": 0, "unapprovedTypedMessages": {}, "unapprovedTypedMessagesCount": 0, "send": { diff --git a/ui/app/css/itcss/components/index.scss b/ui/app/css/itcss/components/index.scss index 01c913a1a..c8cf1d903 100644 --- a/ui/app/css/itcss/components/index.scss +++ b/ui/app/css/itcss/components/index.scss @@ -42,6 +42,10 @@ @import './request-signature.scss'; +@import './request-encryption-public-key.scss'; + +@import './request-decrypt-message.scss'; + @import './account-details-dropdown.scss'; @import './editable-label.scss'; diff --git a/ui/app/css/itcss/components/request-decrypt-message.scss b/ui/app/css/itcss/components/request-decrypt-message.scss new file mode 100644 index 000000000..66acefa0c --- /dev/null +++ b/ui/app/css/itcss/components/request-decrypt-message.scss @@ -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; + } + } + } +} diff --git a/ui/app/css/itcss/components/request-encryption-public-key.scss b/ui/app/css/itcss/components/request-encryption-public-key.scss new file mode 100644 index 000000000..36865a147 --- /dev/null +++ b/ui/app/css/itcss/components/request-encryption-public-key.scss @@ -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; + } + } + } +} diff --git a/ui/app/helpers/constants/routes.js b/ui/app/helpers/constants/routes.js index 799dfc197..19c7ff250 100644 --- a/ui/app/helpers/constants/routes.js +++ b/ui/app/helpers/constants/routes.js @@ -48,6 +48,8 @@ const CONFIRM_APPROVE_PATH = '/approve' const CONFIRM_TRANSFER_FROM_PATH = '/transfer-from' const CONFIRM_TOKEN_METHOD_PATH = '/token-method' 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 { DEFAULT_ROUTE, @@ -81,6 +83,8 @@ export { CONFIRM_TRANSFER_FROM_PATH, CONFIRM_TOKEN_METHOD_PATH, SIGNATURE_REQUEST_PATH, + DECRYPT_MESSAGE_REQUEST_PATH, + ENCRYPTION_PUBLIC_KEY_REQUEST_PATH, INITIALIZE_METAMETRICS_OPT_IN_ROUTE, ADVANCED_ROUTE, SECURITY_ROUTE, diff --git a/ui/app/helpers/constants/transactions.js b/ui/app/helpers/constants/transactions.js index e91e56ddc..e7e4c7fb2 100644 --- a/ui/app/helpers/constants/transactions.js +++ b/ui/app/helpers/constants/transactions.js @@ -18,6 +18,8 @@ export const APPROVE_ACTION_KEY = 'approve' export const SEND_TOKEN_ACTION_KEY = 'sentTokens' export const TRANSFER_FROM_ACTION_KEY = 'transferFrom' 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 CANCEL_ATTEMPT_ACTION_KEY = 'cancelAttempt' export const DEPOSIT_TRANSACTION_KEY = 'deposit' diff --git a/ui/app/helpers/utils/transactions.util.js b/ui/app/helpers/utils/transactions.util.js index 1f8c6e952..49f4f86c0 100644 --- a/ui/app/helpers/utils/transactions.util.js +++ b/ui/app/helpers/utils/transactions.util.js @@ -19,6 +19,8 @@ import { SEND_TOKEN_ACTION_KEY, TRANSFER_FROM_ACTION_KEY, SIGNATURE_REQUEST_KEY, + DECRYPT_REQUEST_KEY, + ENCRYPTION_PUBLIC_KEY_REQUEST_KEY, CONTRACT_INTERACTION_KEY, CANCEL_ATTEMPT_ACTION_KEY, DEPOSIT_TRANSACTION_KEY, @@ -132,7 +134,13 @@ export function getTransactionActionKey (transaction) { } if (msgParams) { - return SIGNATURE_REQUEST_KEY + if (type === 'eth_decrypt') { + return DECRYPT_REQUEST_KEY + } else if (type === 'eth_getEncryptionPublicKey') { + return ENCRYPTION_PUBLIC_KEY_REQUEST_KEY + } else { + return SIGNATURE_REQUEST_KEY + } } if (isConfirmDeployContract(transaction)) { diff --git a/ui/app/pages/confirm-decrypt-message/confirm-decrypt-message.component.js b/ui/app/pages/confirm-decrypt-message/confirm-decrypt-message.component.js new file mode 100644 index 000000000..36bc135d4 --- /dev/null +++ b/ui/app/pages/confirm-decrypt-message/confirm-decrypt-message.component.js @@ -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 ( +
+
+ +
+ { this.context.t('decryptRequest') } +
+ +
+
+
+
+ ) + } + + renderAccount = () => { + const { selectedAccount } = this.state + + return ( +
+
+ { `${this.context.t('account')}:` } +
+ +
+ +
+
+ ) + } + + renderBalance = () => { + const { balance, conversionRate } = this.props + + const balanceInEther = conversionUtil(balance, { + fromNumericBase: 'hex', + toNumericBase: 'dec', + fromDenomination: 'WEI', + numberOfDecimals: 6, + conversionRate, + }) + + return ( +
+
+ { `${this.context.t('balance')}:` } +
+
+ { `${balanceInEther} ETH` } +
+
+ ) + } + + renderRequestIcon = () => { + const { requesterAddress } = this.props + + return ( +
+ +
+ ) + } + + renderAccountInfo = () => { + return ( +
+ { this.renderAccount() } + { this.renderRequestIcon() } + { this.renderBalance() } +
+ ) + } + + 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 ( +
+ { this.renderAccountInfo() } +
+
+ {origin.icon ? ( + + ) : ( + + {origin.name.charAt(0).toUpperCase()} + + )} +
+ { notice } +
+
+
+
+
+ { !hasDecrypted && !hasError ? txData.msgParams.data : rawMessage } + { !hasError ? '' : errorMessage } +
+
+
+
{ + 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]) }) + } + }) + }} + > + +
+ {this.context.t('decryptMetamask')} +
+
+
+ { hasDecrypted ? + ( +
this.copyMessage()} + onMouseDown={() => this.setState({ copyToClipboardPressed: true })} + onMouseUp={() => this.setState({ copyToClipboardPressed: false })} + > + +
+ {this.context.t('decryptCopy')} +
+ +
+
+ ) + : +
+ } +
+ ) + } + + renderFooter = () => { + const { txData } = this.props + + return ( +
+ + +
+ ) + } + + render = () => { + return ( +
+ { this.renderHeader() } + { this.renderBody() } + { this.renderFooter() } +
+ ) + } +} diff --git a/ui/app/pages/confirm-decrypt-message/confirm-decrypt-message.container.js b/ui/app/pages/confirm-decrypt-message/confirm-decrypt-message.container.js new file mode 100644 index 000000000..8bc561a3f --- /dev/null +++ b/ui/app/pages/confirm-decrypt-message/confirm-decrypt-message.container.js @@ -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) diff --git a/ui/app/pages/confirm-decrypt-message/index.js b/ui/app/pages/confirm-decrypt-message/index.js new file mode 100644 index 000000000..9cc671681 --- /dev/null +++ b/ui/app/pages/confirm-decrypt-message/index.js @@ -0,0 +1 @@ +export { default } from './confirm-decrypt-message.container' diff --git a/ui/app/pages/confirm-encryption-public-key/confirm-encryption-public-key.component.js b/ui/app/pages/confirm-encryption-public-key/confirm-encryption-public-key.component.js new file mode 100644 index 000000000..0b63b57d0 --- /dev/null +++ b/ui/app/pages/confirm-encryption-public-key/confirm-encryption-public-key.component.js @@ -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 ( +
+
+ +
+ { this.context.t('encryptionPublicKeyRequest') } +
+ +
+
+
+
+ ) + } + + renderAccount = () => { + const { selectedAccount } = this.state + + return ( +
+
+ { `${this.context.t('account')}:` } +
+ +
+ +
+
+ ) + } + + renderBalance = () => { + const { balance, conversionRate } = this.props + + const balanceInEther = conversionUtil(balance, { + fromNumericBase: 'hex', + toNumericBase: 'dec', + fromDenomination: 'WEI', + numberOfDecimals: 6, + conversionRate, + }) + + return ( +
+
+ { `${this.context.t('balance')}:` } +
+
+ { `${balanceInEther} ETH` } +
+
+ ) + } + + renderRequestIcon = () => { + const { requesterAddress } = this.props + + return ( +
+ +
+ ) + } + + renderAccountInfo = () => { + return ( +
+ { this.renderAccount() } + { this.renderRequestIcon() } + { this.renderBalance() } +
+ ) + } + + renderBody = () => { + const { txData } = this.props + + const origin = this.props.domainMetadata[txData.origin] + const notice = this.context.t('encryptionPublicKeyNotice', [origin.name]) + + return ( +
+ { this.renderAccountInfo() } +
+
+ {origin.icon ? ( + + ) : ( + + {origin.name.charAt(0).toUpperCase()} + + )} +
+ { notice } +
+
+
+
+ ) + } + + renderFooter = () => { + const { txData } = this.props + + return ( +
+ + +
+ ) + } + + render = () => { + return ( +
+ { this.renderHeader() } + { this.renderBody() } + { this.renderFooter() } +
+ ) + } +} diff --git a/ui/app/pages/confirm-encryption-public-key/confirm-encryption-public-key.container.js b/ui/app/pages/confirm-encryption-public-key/confirm-encryption-public-key.container.js new file mode 100644 index 000000000..b70d06b6a --- /dev/null +++ b/ui/app/pages/confirm-encryption-public-key/confirm-encryption-public-key.container.js @@ -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) diff --git a/ui/app/pages/confirm-encryption-public-key/index.js b/ui/app/pages/confirm-encryption-public-key/index.js new file mode 100644 index 000000000..9eb370e52 --- /dev/null +++ b/ui/app/pages/confirm-encryption-public-key/index.js @@ -0,0 +1 @@ +export { default } from './confirm-encryption-public-key.container' diff --git a/ui/app/pages/confirm-transaction-switch/confirm-transaction-switch.component.js b/ui/app/pages/confirm-transaction-switch/confirm-transaction-switch.component.js index 268751643..2ed2ab1e6 100644 --- a/ui/app/pages/confirm-transaction-switch/confirm-transaction-switch.component.js +++ b/ui/app/pages/confirm-transaction-switch/confirm-transaction-switch.component.js @@ -11,6 +11,8 @@ import { CONFIRM_TRANSFER_FROM_PATH, CONFIRM_TOKEN_METHOD_PATH, SIGNATURE_REQUEST_PATH, + DECRYPT_MESSAGE_REQUEST_PATH, + ENCRYPTION_PUBLIC_KEY_REQUEST_PATH, } from '../../helpers/constants/routes' import { TOKEN_METHOD_TRANSFER, @@ -68,11 +70,15 @@ export default class ConfirmTransactionSwitch extends Component { render () { const { txData } = this.props - if (txData.txParams) { return this.redirectToTransaction() } 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 } diff --git a/ui/app/pages/confirm-transaction/confirm-transaction.component.js b/ui/app/pages/confirm-transaction/confirm-transaction.component.js index 66d367b4d..334b6f585 100644 --- a/ui/app/pages/confirm-transaction/confirm-transaction.component.js +++ b/ui/app/pages/confirm-transaction/confirm-transaction.component.js @@ -10,6 +10,9 @@ import ConfirmDeployContract from '../confirm-deploy-contract' import ConfirmApprove from '../confirm-approve' import ConfirmTokenTransactionBaseContainer from '../confirm-token-transaction-base' import ConfTx from './conf-tx' +import ConfirmDecryptMessage from '../confirm-decrypt-message' +import ConfirmEncryptionPublicKey from '../confirm-encryption-public-key' + import { DEFAULT_ROUTE, CONFIRM_TRANSACTION_ROUTE, @@ -20,6 +23,8 @@ import { CONFIRM_TRANSFER_FROM_PATH, CONFIRM_TOKEN_METHOD_PATH, SIGNATURE_REQUEST_PATH, + DECRYPT_MESSAGE_REQUEST_PATH, + ENCRYPTION_PUBLIC_KEY_REQUEST_PATH, } from '../../helpers/constants/routes' export default class ConfirmTransaction extends Component { @@ -155,6 +160,16 @@ export default class ConfirmTransaction extends Component { path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${SIGNATURE_REQUEST_PATH}`} component={ConfTx} /> + + ) diff --git a/ui/app/pages/send/tests/send-selectors-test-data.js b/ui/app/pages/send/tests/send-selectors-test-data.js index 24a5a52f9..842cd3fb3 100644 --- a/ui/app/pages/send/tests/send-selectors-test-data.js +++ b/ui/app/pages/send/tests/send-selectors-test-data.js @@ -130,6 +130,10 @@ export default { 'unapprovedMsgCount': 0, 'unapprovedPersonalMsgs': {}, 'unapprovedPersonalMsgCount': 0, + 'unapprovedDecryptMsgs': {}, + 'unapprovedDecryptMsgCount': 0, + 'unapprovedEncryptionPublicKeyMsgs': {}, + 'unapprovedEncryptionPublicKeyMsgCount': 0, 'keyringTypes': [ 'Simple Key Pair', 'HD Key Tree', diff --git a/ui/app/selectors/confirm-transaction.js b/ui/app/selectors/confirm-transaction.js index e10ac70ad..de29db568 100644 --- a/ui/app/selectors/confirm-transaction.js +++ b/ui/app/selectors/confirm-transaction.js @@ -16,6 +16,8 @@ import { const unapprovedTxsSelector = (state) => state.metamask.unapprovedTxs const unapprovedMsgsSelector = (state) => state.metamask.unapprovedMsgs 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 networkSelector = (state) => state.metamask.network @@ -23,18 +25,24 @@ export const unconfirmedTransactionsListSelector = createSelector( unapprovedTxsSelector, unapprovedMsgsSelector, unapprovedPersonalMsgsSelector, + unapprovedDecryptMsgsSelector, + unapprovedEncryptionPublicKeyMsgsSelector, unapprovedTypedMessagesSelector, networkSelector, ( unapprovedTxs = {}, unapprovedMsgs = {}, unapprovedPersonalMsgs = {}, + unapprovedDecryptMsgs = {}, + unapprovedEncryptionPublicKeyMsgs = {}, unapprovedTypedMessages = {}, network ) => txHelper( unapprovedTxs, unapprovedMsgs, unapprovedPersonalMsgs, + unapprovedDecryptMsgs, + unapprovedEncryptionPublicKeyMsgs, unapprovedTypedMessages, network ) || [] @@ -44,12 +52,16 @@ export const unconfirmedTransactionsHashSelector = createSelector( unapprovedTxsSelector, unapprovedMsgsSelector, unapprovedPersonalMsgsSelector, + unapprovedDecryptMsgsSelector, + unapprovedEncryptionPublicKeyMsgsSelector, unapprovedTypedMessagesSelector, networkSelector, ( unapprovedTxs = {}, unapprovedMsgs = {}, unapprovedPersonalMsgs = {}, + unapprovedDecryptMsgs = {}, + unapprovedEncryptionPublicKeyMsgs = {}, unapprovedTypedMessages = {}, network ) => { @@ -68,6 +80,8 @@ export const unconfirmedTransactionsHashSelector = createSelector( ...filteredUnapprovedTxs, ...unapprovedMsgs, ...unapprovedPersonalMsgs, + ...unapprovedDecryptMsgs, + ...unapprovedEncryptionPublicKeyMsgs, ...unapprovedTypedMessages, } } @@ -75,18 +89,24 @@ export const unconfirmedTransactionsHashSelector = createSelector( const unapprovedMsgCountSelector = (state) => state.metamask.unapprovedMsgCount const unapprovedPersonalMsgCountSelector = (state) => state.metamask.unapprovedPersonalMsgCount +const unapprovedDecryptMsgCountSelector = (state) => state.metamask.unapprovedDecryptMsgCount +const unapprovedEncryptionPublicKeyMsgCountSelector = (state) => state.metamask.unapprovedEncryptionPublicKeyMsgCount const unapprovedTypedMessagesCountSelector = (state) => state.metamask.unapprovedTypedMessagesCount export const unconfirmedTransactionsCountSelector = createSelector( unapprovedTxsSelector, unapprovedMsgCountSelector, unapprovedPersonalMsgCountSelector, + unapprovedDecryptMsgCountSelector, + unapprovedEncryptionPublicKeyMsgCountSelector, unapprovedTypedMessagesCountSelector, networkSelector, ( unapprovedTxs = {}, unapprovedMsgCount = 0, unapprovedPersonalMsgCount = 0, + unapprovedDecryptMsgCount = 0, + unapprovedEncryptionPublicKeyMsgCount = 0, unapprovedTypedMessagesCount = 0, network ) => { @@ -96,7 +116,7 @@ export const unconfirmedTransactionsCountSelector = createSelector( }) return filteredUnapprovedTxIds.length + unapprovedTypedMessagesCount + unapprovedMsgCount + - unapprovedPersonalMsgCount + unapprovedPersonalMsgCount + unapprovedDecryptMsgCount + unapprovedEncryptionPublicKeyMsgCount } ) diff --git a/ui/app/selectors/selectors.js b/ui/app/selectors/selectors.js index 7995a6e69..52fe9e35e 100644 --- a/ui/app/selectors/selectors.js +++ b/ui/app/selectors/selectors.js @@ -326,11 +326,13 @@ export function getTotalUnapprovedCount ({ metamask }) { unapprovedTxs = {}, unapprovedMsgCount, unapprovedPersonalMsgCount, + unapprovedDecryptMsgCount, + unapprovedEncryptionPublicKeyMsgCount, unapprovedTypedMessagesCount, } = metamask return Object.keys(unapprovedTxs).length + unapprovedMsgCount + unapprovedPersonalMsgCount + - unapprovedTypedMessagesCount + unapprovedTypedMessagesCount + unapprovedDecryptMsgCount + unapprovedEncryptionPublicKeyMsgCount } export function getIsMainnet (state) { diff --git a/ui/app/selectors/tests/selectors-test-data.js b/ui/app/selectors/tests/selectors-test-data.js index 894082a53..02eec51fe 100644 --- a/ui/app/selectors/tests/selectors-test-data.js +++ b/ui/app/selectors/tests/selectors-test-data.js @@ -132,6 +132,10 @@ export default { 'unapprovedMsgCount': 0, 'unapprovedPersonalMsgs': {}, 'unapprovedPersonalMsgCount': 0, + 'unapprovedDecryptMsgs': {}, + 'unapprovedDecryptMsgCount': 0, + 'unapprovedEncryptionPublicKeyMsgs': {}, + 'unapprovedEncryptionPublicKeyMsgCount': 0, 'keyringTypes': [ 'Simple Key Pair', 'HD Key Tree', diff --git a/ui/app/selectors/transactions.js b/ui/app/selectors/transactions.js index ede432914..dd687ce3f 100644 --- a/ui/app/selectors/transactions.js +++ b/ui/app/selectors/transactions.js @@ -33,23 +33,31 @@ export const incomingTxListSelector = (state) => { export const unapprovedMsgsSelector = (state) => state.metamask.unapprovedMsgs export const selectedAddressTxListSelector = (state) => state.metamask.selectedAddressTxList 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 networkSelector = (state) => state.metamask.network export const unapprovedMessagesSelector = createSelector( unapprovedMsgsSelector, unapprovedPersonalMsgsSelector, + unapprovedDecryptMsgsSelector, + unapprovedEncryptionPublicKeyMsgsSelector, unapprovedTypedMessagesSelector, networkSelector, ( unapprovedMsgs = {}, unapprovedPersonalMsgs = {}, + unapprovedDecryptMsgs = {}, + unapprovedEncryptionPublicKeyMsgs = {}, unapprovedTypedMessages = {}, network ) => txHelper( {}, unapprovedMsgs, unapprovedPersonalMsgs, + unapprovedDecryptMsgs, + unapprovedEncryptionPublicKeyMsgs, unapprovedTypedMessages, network ) || [] diff --git a/ui/app/store/actions.js b/ui/app/store/actions.js index e8fdc0a6c..45113ff2a 100644 --- a/ui/app/store/actions.js +++ b/ui/app/store/actions.js @@ -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) { log.debug('action - signTypedMsg') 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) { return (dispatch) => { dispatch(showLoadingIndication()) diff --git a/ui/index.js b/ui/index.js index d3e348f04..d36941f5a 100644 --- a/ui/index.js +++ b/ui/index.js @@ -59,7 +59,7 @@ async function startApp (metamaskState, backgroundConnection, opts) { }) // 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 if (numberOfUnapprivedTx > 0) { store.dispatch(actions.showConfTxPage({ diff --git a/ui/lib/tx-helper.js b/ui/lib/tx-helper.js index 005bd0769..03a7b3e1e 100644 --- a/ui/lib/tx-helper.js +++ b/ui/lib/tx-helper.js @@ -1,9 +1,9 @@ import { valuesFor } from '../app/helpers/utils/util' 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({ 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) 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`) 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) log.debug(`tx helper found ${typedValues.length} unsigned typed messages`) allValues = allValues.concat(typedValues) diff --git a/yarn.lock b/yarn.lock index 9fda558d2..cf4de07de 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10337,30 +10337,10 @@ eth-json-rpc-middleware@^1.5.0: promise-to-callback "^1.0.0" tape "^4.6.3" -eth-json-rpc-middleware@^4.1.4, eth-json-rpc-middleware@^4.1.5: - version "4.2.0" - resolved "https://registry.yarnpkg.com/eth-json-rpc-middleware/-/eth-json-rpc-middleware-4.2.0.tgz#cfb77c5056cb8001548c6c7d54f4af5fce04d489" - integrity sha512-90LljqRyJhkg7fOwKunh1lu1Mr5bspXMBDitaTGyGPPNiFTbMrhtfbf9fteYlXRFCbq+aIFWwl/X+P7nkrdkLg== - 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== +eth-json-rpc-middleware@^4.1.4, eth-json-rpc-middleware@^4.1.5, eth-json-rpc-middleware@^4.4.1: + version "4.4.1" + resolved "https://registry.yarnpkg.com/eth-json-rpc-middleware/-/eth-json-rpc-middleware-4.4.1.tgz#07d3dd0724c24a8d31e4a172ee96271da71b4228" + integrity sha512-yoSuRgEYYGFdVeZg3poWOwAlRI+MoBIltmOB86MtpoZjvLbou9EB/qWMOWSmH2ryCWLW97VYY6NWsmWm3OAA7A== dependencies: btoa "^1.2.1" clone "^2.1.1"