From 6d0f3a0b26e49a7be9c2f1e3b02bf26d091e44ca Mon Sep 17 00:00:00 2001 From: OGPoyraz Date: Wed, 26 Apr 2023 17:02:33 +0200 Subject: [PATCH] Consume Decrypt Message Manager from @metamask/message-manager (#18379) --- app/scripts/background.js | 18 +- .../controllers/decrypt-message.test.ts | 222 ++++++++++ app/scripts/controllers/decrypt-message.ts | 394 ++++++++++++++++++ .../controllers/encryption-public-key.ts | 29 +- app/scripts/lib/decrypt-message-manager.js | 351 ---------------- app/scripts/metamask-controller.js | 133 ++---- .../files-to-convert.json | 1 - jest.config.js | 2 + package.json | 2 +- types/eth-keyring-controller.d.ts | 2 + .../confirm-decrypt-message.component.js | 2 +- ui/selectors/selectors.js | 9 +- yarn.lock | 20 +- 13 files changed, 684 insertions(+), 501 deletions(-) create mode 100644 app/scripts/controllers/decrypt-message.test.ts create mode 100644 app/scripts/controllers/decrypt-message.ts delete mode 100644 app/scripts/lib/decrypt-message-manager.js diff --git a/app/scripts/background.js b/app/scripts/background.js index dceea1b5e..cce8b1585 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -682,7 +682,7 @@ export function setupController( METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE, updateBadge, ); - controller.decryptMessageManager.on( + controller.decryptMessageController.hub.on( METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE, updateBadge, ); @@ -725,14 +725,11 @@ export function setupController( } function getUnapprovedTransactionCount() { - const { unapprovedDecryptMsgCount } = controller.decryptMessageManager; const pendingApprovalCount = controller.approvalController.getTotalApprovalCount(); const waitingForUnlockCount = controller.appStateController.waitingForUnlock.length; - return ( - unapprovedDecryptMsgCount + pendingApprovalCount + waitingForUnlockCount - ); + return pendingApprovalCount + waitingForUnlockCount; } notificationManager.on( @@ -753,14 +750,9 @@ export function setupController( controller.txController.txStateManager.setTxStatusRejected(txId), ); controller.signController.rejectUnapproved(REJECT_NOTIFICATION_CLOSE_SIG); - controller.decryptMessageManager.messages - .filter((msg) => msg.status === 'unapproved') - .forEach((tx) => - controller.decryptMessageManager.rejectMsg( - tx.id, - REJECT_NOTIFICATION_CLOSE, - ), - ); + controller.decryptMessageController.rejectUnapproved( + REJECT_NOTIFICATION_CLOSE, + ); controller.encryptionPublicKeyController.rejectUnapproved( REJECT_NOTIFICATION_CLOSE, ); diff --git a/app/scripts/controllers/decrypt-message.test.ts b/app/scripts/controllers/decrypt-message.test.ts new file mode 100644 index 000000000..032a15289 --- /dev/null +++ b/app/scripts/controllers/decrypt-message.test.ts @@ -0,0 +1,222 @@ +import { DecryptMessageManager } from '@metamask/message-manager'; +import { AbstractMessage } from '@metamask/message-manager/dist/AbstractMessageManager'; +import { MetaMetricsEventCategory } from '../../../shared/constants/metametrics'; +import DecryptMessageController, { + DecryptMessageControllerMessenger, + DecryptMessageControllerOptions, + getDefaultState, +} from './decrypt-message'; + +const messageIdMock = '12345'; +const messageMock = { + metamaskId: messageIdMock, + time: 123, + status: 'unapproved', + type: 'testType', + rawSig: undefined, +} as any as AbstractMessage; + +const mockExtState = {}; + +jest.mock('@metamask/message-manager', () => ({ + DecryptMessageManager: jest.fn(), +})); + +const createKeyringControllerMock = () => ({ + decryptMessage: jest.fn(), +}); + +const createMessengerMock = () => + ({ + registerActionHandler: jest.fn(), + publish: jest.fn(), + call: jest.fn(), + } as any as jest.Mocked); + +const createDecryptMessageManagerMock = () => + ({ + getUnapprovedMessages: jest.fn(), + getUnapprovedMessagesCount: jest.fn(), + getMessage: jest.fn(), + addUnapprovedMessageAsync: jest.fn(), + approveMessage: jest.fn(), + setMessageStatusAndResult: jest.fn(), + rejectMessage: jest.fn(), + update: jest.fn(), + subscribe: jest.fn(), + updateMessage: jest.fn(), + updateMessageErrorInline: jest.fn(), + setResult: jest.fn(), + hub: { + on: jest.fn(), + }, + } as any as jest.Mocked); + +describe('EncryptionPublicKeyController', () => { + let decryptMessageController: DecryptMessageController; + + const decryptMessageManagerConstructorMock = + DecryptMessageManager as jest.MockedClass; + const getStateMock = jest.fn(); + const keyringControllerMock = createKeyringControllerMock(); + const messengerMock = createMessengerMock(); + const metricsEventMock = jest.fn(); + + const decryptMessageManagerMock = + createDecryptMessageManagerMock(); + + beforeEach(() => { + jest.resetAllMocks(); + + decryptMessageManagerConstructorMock.mockReturnValue( + decryptMessageManagerMock, + ); + + decryptMessageController = new DecryptMessageController({ + getState: getStateMock as any, + keyringController: keyringControllerMock as any, + messenger: messengerMock as any, + metricsEvent: metricsEventMock as any, + } as DecryptMessageControllerOptions); + }); + + it('should return unapprovedMsgCount', () => { + decryptMessageManagerMock.getUnapprovedMessagesCount.mockReturnValue(5); + expect(decryptMessageController.unapprovedDecryptMsgCount).toBe(5); + }); + + it('should reset state', () => { + decryptMessageController.update(() => ({ + unapprovedDecryptMsgs: { + [messageIdMock]: messageMock, + } as any, + unapprovedDecryptMsgCount: 1, + })); + decryptMessageController.resetState(); + expect(decryptMessageController.state).toStrictEqual(getDefaultState()); + }); + + it('should clear unapproved messages', () => { + decryptMessageController.clearUnapproved(); + expect(decryptMessageController.state).toStrictEqual(getDefaultState()); + expect(decryptMessageManagerMock.update).toBeCalledTimes(1); + }); + it('should add unapproved messages', async () => { + await decryptMessageController.newRequestDecryptMessage(messageMock); + + expect(decryptMessageManagerMock.addUnapprovedMessageAsync).toBeCalledTimes( + 1, + ); + expect(decryptMessageManagerMock.addUnapprovedMessageAsync).toBeCalledWith( + messageMock, + undefined, + ); + }); + + it('should decrypt message', async () => { + const messageToDecrypt = { + ...messageMock, + data: '0x7b22666f6f223a22626172227d', + }; + decryptMessageManagerMock.approveMessage.mockResolvedValue( + messageToDecrypt, + ); + keyringControllerMock.decryptMessage.mockResolvedValue('decryptedMessage'); + getStateMock.mockReturnValue(mockExtState); + + const result = await decryptMessageController.decryptMessage( + messageToDecrypt, + ); + + expect(decryptMessageManagerMock.approveMessage).toBeCalledTimes(1); + expect(decryptMessageManagerMock.approveMessage).toBeCalledWith( + messageToDecrypt, + ); + expect(keyringControllerMock.decryptMessage).toBeCalledTimes(1); + expect(keyringControllerMock.decryptMessage).toBeCalledWith( + messageToDecrypt, + ); + expect(decryptMessageManagerMock.setMessageStatusAndResult).toBeCalledTimes( + 1, + ); + expect(decryptMessageManagerMock.setMessageStatusAndResult).toBeCalledWith( + messageIdMock, + 'decryptedMessage', + 'decrypted', + ); + expect(result).toBe(mockExtState); + }); + + it('should cancel decrypt request', async () => { + const messageToDecrypt = { + ...messageMock, + data: '0x7b22666f6f223a22626172227d', + }; + decryptMessageManagerMock.approveMessage.mockResolvedValue( + messageToDecrypt, + ); + keyringControllerMock.decryptMessage.mockRejectedValue(new Error('error')); + getStateMock.mockReturnValue(mockExtState); + + return expect( + decryptMessageController.decryptMessage(messageToDecrypt), + ).rejects.toThrow('error'); + }); + + it('should decrypt message inline', async () => { + const messageToDecrypt = { + ...messageMock, + data: '0x7b22666f6f223a22626172227d', + }; + decryptMessageManagerMock.getMessage.mockReturnValue(messageToDecrypt); + keyringControllerMock.decryptMessage.mockResolvedValue('decryptedMessage'); + getStateMock.mockReturnValue(mockExtState); + + const result = await decryptMessageController.decryptMessageInline( + messageToDecrypt, + ); + + expect(decryptMessageManagerMock.setResult).toBeCalledTimes(1); + expect(decryptMessageManagerMock.setResult).toBeCalledWith( + messageMock.metamaskId, + 'decryptedMessage', + ); + expect(result).toBe(mockExtState); + }); + + it('should be able to cancel decrypt message', async () => { + decryptMessageManagerMock.rejectMessage.mockResolvedValue(messageMock); + getStateMock.mockReturnValue(mockExtState); + + const result = await decryptMessageController.cancelDecryptMessage( + messageIdMock, + ); + + expect(decryptMessageManagerMock.rejectMessage).toBeCalledTimes(1); + expect(decryptMessageManagerMock.rejectMessage).toBeCalledWith( + messageIdMock, + ); + expect(result).toBe(mockExtState); + }); + + it('should be able to reject all unapproved messages', async () => { + decryptMessageManagerMock.getUnapprovedMessages.mockReturnValue({ + [messageIdMock]: messageMock, + }); + + await decryptMessageController.rejectUnapproved('reason to cancel'); + + expect(decryptMessageManagerMock.rejectMessage).toBeCalledTimes(1); + expect(decryptMessageManagerMock.rejectMessage).toBeCalledWith( + messageIdMock, + ); + expect(metricsEventMock).toBeCalledTimes(1); + expect(metricsEventMock).toBeCalledWith({ + event: 'reason to cancel', + category: MetaMetricsEventCategory.Messages, + properties: { + action: 'Decrypt Message Request', + }, + }); + }); +}); diff --git a/app/scripts/controllers/decrypt-message.ts b/app/scripts/controllers/decrypt-message.ts new file mode 100644 index 000000000..779b22f78 --- /dev/null +++ b/app/scripts/controllers/decrypt-message.ts @@ -0,0 +1,394 @@ +import EventEmitter from 'events'; +import log from 'loglevel'; +import { + DecryptMessageManager, + DecryptMessageParams, + DecryptMessageParamsMetamask, +} from '@metamask/message-manager'; +import { KeyringController } from '@metamask/eth-keyring-controller'; +import { + AbstractMessage, + AbstractMessageManager, + AbstractMessageParams, + AbstractMessageParamsMetamask, + MessageManagerState, + OriginalRequest, +} from '@metamask/message-manager/dist/AbstractMessageManager'; +import { + BaseControllerV2, + RestrictedControllerMessenger, +} from '@metamask/base-controller'; +import { + AcceptRequest, + AddApprovalRequest, + RejectRequest, +} from '@metamask/approval-controller'; +import { ApprovalType, ORIGIN_METAMASK } from '@metamask/controller-utils'; +import { Patch } from 'immer'; +import { MetaMetricsEventCategory } from '../../../shared/constants/metametrics'; +import { stripHexPrefix } from '../../../shared/modules/hexstring-utils'; + +const controllerName = 'DecryptMessageController'; + +const stateMetadata = { + unapprovedDecryptMsgs: { persist: false, anonymous: false }, + unapprovedDecryptMsgCount: { persist: false, anonymous: false }, +}; + +export const getDefaultState = () => ({ + unapprovedDecryptMsgs: {}, + unapprovedDecryptMsgCount: 0, +}); + +export type CoreMessage = AbstractMessage & { + messageParams: AbstractMessageParams; +}; + +export type StateMessage = Required< + Omit +>; + +export type DecryptMessageControllerState = { + unapprovedDecryptMsgs: Record; + unapprovedDecryptMsgCount: number; +}; + +export type GetDecryptMessageState = { + type: `${typeof controllerName}:getState`; + handler: () => DecryptMessageControllerState; +}; + +export type DecryptMessageStateChange = { + type: `${typeof controllerName}:stateChange`; + payload: [DecryptMessageControllerState, Patch[]]; +}; + +export type DecryptMessageControllerActions = GetDecryptMessageState; + +export type DecryptMessageControllerEvents = DecryptMessageStateChange; + +type AllowedActions = AddApprovalRequest | AcceptRequest | RejectRequest; + +export type DecryptMessageControllerMessenger = RestrictedControllerMessenger< + typeof controllerName, + DecryptMessageControllerActions | AllowedActions, + DecryptMessageControllerEvents, + AllowedActions['type'], + never +>; + +export type DecryptMessageControllerOptions = { + getState: () => any; + keyringController: KeyringController; + messenger: DecryptMessageControllerMessenger; + metricsEvent: (payload: any, options?: any) => void; +}; + +/** + * Controller for decrypt signing requests requiring user approval. + */ +export default class DecryptMessageController extends BaseControllerV2< + typeof controllerName, + DecryptMessageControllerState, + DecryptMessageControllerMessenger +> { + hub: EventEmitter; + + private _getState: () => any; + + private _keyringController: KeyringController; + + private _metricsEvent: (payload: any, options?: any) => void; + + private _decryptMessageManager: DecryptMessageManager; + + /** + * Construct a DecryptMessage controller. + * + * @param options - The controller options. + * @param options.getState - Callback to retrieve all user state. + * @param options.keyringController - An instance of a keyring controller used to decrypt message + * @param options.messenger - A reference to the messaging system. + * @param options.metricsEvent - A function for emitting a metric event. + */ + constructor({ + getState, + keyringController, + metricsEvent, + messenger, + }: DecryptMessageControllerOptions) { + super({ + metadata: stateMetadata, + messenger, + name: controllerName, + state: getDefaultState(), + }); + this._getState = getState; + this._keyringController = keyringController; + this._metricsEvent = metricsEvent; + + this.hub = new EventEmitter(); + + this._decryptMessageManager = new DecryptMessageManager( + undefined, + undefined, + undefined, + ['decrypted'], + ); + + this._decryptMessageManager.hub.on('updateBadge', () => { + this.hub.emit('updateBadge'); + }); + + this._decryptMessageManager.hub.on( + 'unapprovedMessage', + (messageParams: AbstractMessageParamsMetamask) => { + this._requestApproval(messageParams); + }, + ); + + this._subscribeToMessageState( + this._decryptMessageManager, + (state, newMessages, messageCount) => { + state.unapprovedDecryptMsgs = newMessages; + state.unapprovedDecryptMsgCount = messageCount; + }, + ); + } + + /** + * A getter for the number of 'unapproved' Messages in the DecryptMessageManager. + * + * @returns The number of 'unapproved' Messages in the DecryptMessageManager. + */ + get unapprovedDecryptMsgCount(): number { + return this._decryptMessageManager.getUnapprovedMessagesCount(); + } + + /** + * Reset the controller state to the initial state. + */ + resetState() { + this.update(() => getDefaultState()); + } + + /** + * Clears all unapproved messages from memory. + */ + clearUnapproved() { + this._decryptMessageManager.update({ + unapprovedMessages: {}, + unapprovedMessagesCount: 0, + }); + } + + /** + * Called when a dapp uses the eth_decrypt method + * + * @param messageParams - The params passed to eth_decrypt. + * @param req - The original request, containing the origin. + * @returns Promise resolving to the raw data of the signature request. + */ + async newRequestDecryptMessage( + messageParams: DecryptMessageParams, + req: OriginalRequest, + ): Promise { + return this._decryptMessageManager.addUnapprovedMessageAsync( + messageParams, + req, + ); + } + + /** + * Signifies a user's approval to decrypt a message in queue. + * Triggers decrypt, and the callback function from newUnsignedDecryptMessage. + * + * @param messageParams - The params of the message to decrypt & return to the Dapp. + * @returns A full state update. + */ + async decryptMessage(messageParams: DecryptMessageParamsMetamask) { + const messageId = messageParams.metamaskId as string; + try { + const cleanMessageParams = + await this._decryptMessageManager.approveMessage(messageParams); + + cleanMessageParams.data = this._parseMessageData(cleanMessageParams.data); + const rawMessage = await this._keyringController.decryptMessage( + cleanMessageParams, + ); + + this._decryptMessageManager.setMessageStatusAndResult( + messageId, + rawMessage, + 'decrypted', + ); + this._acceptApproval(messageId); + } catch (error) { + log.info('MetaMaskController - eth_decrypt failed.', error); + this._cancelAbstractMessage(this._decryptMessageManager, messageId); + throw error; + } + return this._getState(); + } + + /** + * Only decrypt message and don't touch transaction state + * + * @param messageParams - The params of the message to decrypt. + * @returns A full state update. + */ + async decryptMessageInline(messageParams: DecryptMessageParamsMetamask) { + const messageId = messageParams.metamaskId as string; + messageParams.data = this._parseMessageData(messageParams.data); + const rawMessage = await this._keyringController.decryptMessage( + messageParams, + ); + + this._decryptMessageManager.setResult(messageId, rawMessage); + + return this._getState(); + } + + /** + * Used to cancel a eth_decrypt type message. + * + * @param messageId - The ID of the message to cancel. + * @returns A full state update. + */ + cancelDecryptMessage(messageId: string) { + this._decryptMessageManager.rejectMessage(messageId); + this._rejectApproval(messageId); + return this._getState(); + } + + /** + * Reject all unapproved messages of any type. + * + * @param reason - A message to indicate why. + */ + rejectUnapproved(reason?: string) { + Object.keys(this._decryptMessageManager.getUnapprovedMessages()).forEach( + (messageId) => { + this._cancelAbstractMessage( + this._decryptMessageManager, + messageId, + reason, + ); + }, + ); + } + + private _acceptApproval(messageId: string) { + this.messagingSystem.call('ApprovalController:acceptRequest', messageId); + } + + private _cancelAbstractMessage( + messageManager: AbstractMessageManager< + AbstractMessage, + AbstractMessageParams, + AbstractMessageParamsMetamask + >, + messageId: string, + reason?: string, + ) { + if (reason) { + this._metricsEvent({ + event: reason, + category: MetaMetricsEventCategory.Messages, + properties: { + action: 'Decrypt Message Request', + }, + }); + } + + messageManager.rejectMessage(messageId); + this._rejectApproval(messageId); + + return this._getState(); + } + + private _subscribeToMessageState( + messageManager: AbstractMessageManager< + AbstractMessage, + AbstractMessageParams, + AbstractMessageParamsMetamask + >, + updateState: ( + state: DecryptMessageControllerState, + newMessages: Record, + messageCount: number, + ) => void, + ) { + messageManager.subscribe((state: MessageManagerState) => { + const newMessages = this._migrateMessages( + state.unapprovedMessages as any, + ); + this.update((draftState) => { + updateState(draftState, newMessages, state.unapprovedMessagesCount); + }); + }); + } + + private _migrateMessages( + coreMessages: Record, + ): Record { + const stateMessages: Record = {}; + + for (const messageId of Object.keys(coreMessages)) { + const coreMessage = coreMessages[messageId]; + const stateMessage = this._migrateMessage(coreMessage); + stateMessages[messageId] = stateMessage; + } + + return stateMessages; + } + + private _migrateMessage(coreMessage: CoreMessage): StateMessage { + const { messageParams, ...coreMessageData } = coreMessage; + + const stateMessage = { + ...coreMessageData, + rawSig: coreMessage.rawSig as string, + msgParams: messageParams, + origin: messageParams.origin, + }; + + return stateMessage; + } + + private _requestApproval(messageParams: AbstractMessageParamsMetamask) { + const id = messageParams.metamaskId as string; + const origin = messageParams.origin || ORIGIN_METAMASK; + try { + this.messagingSystem.call( + 'ApprovalController:addRequest', + { + id, + origin, + type: ApprovalType.EthDecrypt, + }, + true, + ); + } catch (error) { + log.info('Error adding request to approval controller', error); + } + } + + private _parseMessageData(data: string) { + const stripped = stripHexPrefix(data); + const buff = Buffer.from(stripped, 'hex'); + return JSON.parse(buff.toString('utf8')); + } + + private _rejectApproval(messageId: string) { + try { + this.messagingSystem.call( + 'ApprovalController:rejectRequest', + messageId, + 'Cancel', + ); + } catch (error) { + log.info('Error rejecting request to approval controller', error); + } + } +} diff --git a/app/scripts/controllers/encryption-public-key.ts b/app/scripts/controllers/encryption-public-key.ts index f4cb5e25e..626413b3f 100644 --- a/app/scripts/controllers/encryption-public-key.ts +++ b/app/scripts/controllers/encryption-public-key.ts @@ -342,36 +342,31 @@ export default class EncryptionPublicKeyController extends BaseControllerV2< messageCount: number, ) => void, ) { - messageManager.subscribe( - async (state: MessageManagerState) => { - const newMessages = await this._migrateMessages( - state.unapprovedMessages as any, - ); - this.update((draftState) => { - updateState(draftState, newMessages, state.unapprovedMessagesCount); - }); - }, - ); + messageManager.subscribe((state: MessageManagerState) => { + const newMessages = this._migrateMessages( + state.unapprovedMessages as any, + ); + this.update((draftState) => { + updateState(draftState, newMessages, state.unapprovedMessagesCount); + }); + }); } - private async _migrateMessages( + private _migrateMessages( coreMessages: Record, - ): Promise> { + ): Record { const stateMessages: Record = {}; for (const messageId of Object.keys(coreMessages)) { const coreMessage = coreMessages[messageId]; - const stateMessage = await this._migrateMessage(coreMessage); - + const stateMessage = this._migrateMessage(coreMessage); stateMessages[messageId] = stateMessage; } return stateMessages; } - private async _migrateMessage( - coreMessage: CoreMessage, - ): Promise { + private _migrateMessage(coreMessage: CoreMessage): StateMessage { const { messageParams, ...coreMessageData } = coreMessage; // Core message managers use messageParams but frontend uses msgParams with lots of references diff --git a/app/scripts/lib/decrypt-message-manager.js b/app/scripts/lib/decrypt-message-manager.js deleted file mode 100644 index 9249c907e..000000000 --- a/app/scripts/lib/decrypt-message-manager.js +++ /dev/null @@ -1,351 +0,0 @@ -import EventEmitter from 'events'; -import { ObservableStore } from '@metamask/obs-store'; -import { bufferToHex } from 'ethereumjs-util'; -import { ethErrors } from 'eth-rpc-errors'; -import log from 'loglevel'; -import { MESSAGE_TYPE } from '../../../shared/constants/app'; -import { MetaMetricsEventCategory } from '../../../shared/constants/metametrics'; -import { METAMASK_CONTROLLER_EVENTS } from '../metamask-controller'; -import createId from '../../../shared/modules/random-id'; -import { stripHexPrefix } from '../../../shared/modules/hexstring-utils'; -import { addHexPrefix } from './util'; - -const hexRe = /^[0-9A-Fa-f]+$/gu; - -/** - * 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. - * - * @param {object} opts - Controller options - * @param {Function} opts.metricEvent - A function for emitting a metric event. - */ - constructor(opts) { - super(); - this.memStore = new ObservableStore({ - unapprovedDecryptMsgs: {}, - unapprovedDecryptMsgCount: 0, - }); - - this.resetState = () => { - this.memStore.updateState({ - unapprovedDecryptMsgs: {}, - unapprovedDecryptMsgCount: 0, - }); - }; - - this.messages = []; - this.metricsEvent = opts.metricsEvent; - } - - /** - * 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] - 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 Decryption: from field is required.')); - return; - } - const msgId = this.addUnapprovedMessage(msgParams, req); - this.once(`${msgId}:finished`, (data) => { - switch (data.status) { - case 'decrypted': - resolve(data.rawData); - return; - case 'rejected': - reject( - ethErrors.provider.userRejectedRequest( - 'MetaMask Decryption: User denied message decryption.', - ), - ); - return; - case 'errored': - reject(new Error('This message cannot be decrypted')); - return; - default: - reject( - new Error( - `MetaMask 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] - 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, - time, - status: 'unapproved', - type: MESSAGE_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 - */ - async prepMsgForDecryption(msgParams) { - delete msgParams.metamaskId; - return msgParams; - } - - /** - * Sets a DecryptMessage status to 'rejected' via a call to this._setMsgStatus. - * - * @param {number} msgId - The id of the DecryptMessage to reject. - * @param reason - */ - rejectMsg(msgId, reason = undefined) { - if (reason) { - this.metricsEvent({ - event: reason, - category: MetaMetricsEventCategory.Messages, - properties: { - action: 'Decrypt Message Request', - }, - }); - } - 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 - * @param error - */ - errorMessage(msgId, error) { - const msg = this.getMsg(msgId); - msg.error = error; - this._updateMsg(msg); - this._setMsgStatus(msgId, 'errored'); - } - - /** - * Clears all unapproved messages from memory. - */ - clearUnapproved() { - this.messages = this.messages.filter((msg) => msg.status !== 'unapproved'); - this._saveMsgList(); - } - - /** - * 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 {DecryptMessage} msg - 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(METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE); - } - - /** - * 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 = stripHexPrefix(data); - if (stripped.match(hexRe)) { - return addHexPrefix(stripped); - } - } catch (e) { - log.debug(`Message was not hex encoded, interpreting as utf8.`); - } - - return bufferToHex(Buffer.from(data, 'utf8')); - } -} diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index a789ae227..e2e52493e 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -90,7 +90,6 @@ import { ///: END:ONLY_INCLUDE_IN } from '../../shared/constants/permissions'; import { UI_NOTIFICATIONS } from '../../shared/notifications'; -import { stripHexPrefix } from '../../shared/modules/hexstring-utils'; import { MILLISECOND, SECOND } from '../../shared/constants/time'; import { ORIGIN_METAMASK, @@ -150,7 +149,7 @@ import AlertController from './controllers/alert'; import OnboardingController from './controllers/onboarding'; import BackupController from './controllers/backup'; import IncomingTransactionsController from './controllers/incoming-transactions'; -import DecryptMessageManager from './lib/decrypt-message-manager'; +import DecryptMessageController from './controllers/decrypt-message'; import TransactionController from './controllers/transactions'; import DetectTokensController from './controllers/detect-tokens'; import SwapsController from './controllers/swaps'; @@ -1156,7 +1155,17 @@ export default class MetamaskController extends EventEmitter { ); this.networkController.lookupNetwork(); - this.decryptMessageManager = new DecryptMessageManager({ + this.decryptMessageController = new DecryptMessageController({ + getState: this.getState.bind(this), + keyringController: this.keyringController, + messenger: this.controllerMessenger.getRestricted({ + name: 'DecryptMessageController', + allowedActions: [ + `${this.approvalController.name}:addRequest`, + `${this.approvalController.name}:acceptRequest`, + `${this.approvalController.name}:rejectRequest`, + ], + }), metricsEvent: this.metaMetricsController.trackEvent.bind( this.metaMetricsController, ), @@ -1256,7 +1265,7 @@ export default class MetamaskController extends EventEmitter { () => { this.txController.txStateManager.clearUnapprovedTxs(); this.encryptionPublicKeyController.clearUnapproved(); - this.decryptMessageManager.clearUnapproved(); + this.decryptMessageController.clearUnapproved(); this.signController.clearUnapproved(); }, ); @@ -1321,11 +1330,14 @@ export default class MetamaskController extends EventEmitter { this.signController.newUnsignedPersonalMessage.bind( this.signController, ), - processDecryptMessage: this.newRequestDecryptMessage.bind(this), processEncryptionPublicKey: this.encryptionPublicKeyController.newRequestEncryptionPublicKey.bind( this.encryptionPublicKeyController, ), + processDecryptMessage: + this.decryptMessageController.newRequestDecryptMessage.bind( + this.decryptMessageController, + ), getPendingNonce: this.getPendingNonce.bind(this), getPendingTransactionByHash: (hash) => this.txController.getTransactions({ @@ -1347,7 +1359,7 @@ export default class MetamaskController extends EventEmitter { AccountTracker: this.accountTracker.store, TxController: this.txController.memStore, TokenRatesController: this.tokenRatesController, - DecryptMessageManager: this.decryptMessageManager.memStore, + DecryptMessageController: this.decryptMessageController, EncryptionPublicKeyController: this.encryptionPublicKeyController, SignController: this.signController, SwapsController: this.swapsController.store, @@ -1433,7 +1445,9 @@ export default class MetamaskController extends EventEmitter { const resetMethods = [ this.accountTracker.resetState, this.txController.resetState, - this.decryptMessageManager.resetState, + this.decryptMessageController.resetState.bind( + this.decryptMessageController, + ), this.encryptionPublicKeyController.resetState.bind( this.encryptionPublicKeyController, ), @@ -2137,10 +2151,18 @@ export default class MetamaskController extends EventEmitter { this.signController, ), - // decryptMessageManager - decryptMessage: this.decryptMessage.bind(this), - decryptMessageInline: this.decryptMessageInline.bind(this), - cancelDecryptMessage: this.cancelDecryptMessage.bind(this), + // decryptMessageController + decryptMessage: this.decryptMessageController.decryptMessage.bind( + this.decryptMessageController, + ), + decryptMessageInline: + this.decryptMessageController.decryptMessageInline.bind( + this.decryptMessageController, + ), + cancelDecryptMessage: + this.decryptMessageController.cancelDecryptMessage.bind( + this.decryptMessageController, + ), // EncryptionPublicKeyController encryptionPublicKey: @@ -3158,95 +3180,6 @@ export default class MetamaskController extends EventEmitter { return await this.txController.newUnapprovedTransaction(txParams, req); } - // 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.showUserConfirmation(); - return promise; - } - - /** - * Only decrypt 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 = 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 = 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. - */ - cancelDecryptMessage(msgId) { - const messageManager = this.decryptMessageManager; - messageManager.rejectMsg(msgId); - return this.getState(); - } - /** * @returns {boolean} true if the keyring type supports EIP-1559 */ diff --git a/development/ts-migration-dashboard/files-to-convert.json b/development/ts-migration-dashboard/files-to-convert.json index 099edb1f1..bf123f1e3 100644 --- a/development/ts-migration-dashboard/files-to-convert.json +++ b/development/ts-migration-dashboard/files-to-convert.json @@ -79,7 +79,6 @@ "app/scripts/lib/createRPCMethodTrackingMiddleware.test.js", "app/scripts/lib/createStreamSink.js", "app/scripts/lib/createTabIdMiddleware.js", - "app/scripts/lib/decrypt-message-manager.js", "app/scripts/lib/ens-ipfs/contracts/registry.js", "app/scripts/lib/ens-ipfs/contracts/resolver.js", "app/scripts/lib/ens-ipfs/resolver.js", diff --git a/jest.config.js b/jest.config.js index bbcb93a4a..a3813632b 100644 --- a/jest.config.js +++ b/jest.config.js @@ -6,6 +6,7 @@ module.exports = { '!/app/scripts/controllers/network/**/test/*.ts', '/app/scripts/controllers/permissions/**/*.js', '/app/scripts/controllers/sign.ts', + '/app/scripts/controllers/decrypt-message.ts', '/app/scripts/flask/**/*.js', '/app/scripts/lib/**/*.js', '/app/scripts/lib/createRPCMethodTrackingMiddleware.js', @@ -44,6 +45,7 @@ module.exports = { '/app/scripts/controllers/network/**/*.test.ts', '/app/scripts/controllers/permissions/**/*.test.js', '/app/scripts/controllers/sign.test.ts', + '/app/scripts/controllers/decrypt-message.test.ts', '/app/scripts/flask/**/*.test.js', '/app/scripts/lib/**/*.test.js', '/app/scripts/lib/**/*.test.ts', diff --git a/package.json b/package.json index d89c7421a..2ae4f8c36 100644 --- a/package.json +++ b/package.json @@ -246,7 +246,7 @@ "@metamask/jazzicon": "^2.0.0", "@metamask/key-tree": "^7.0.0", "@metamask/logo": "^3.1.1", - "@metamask/message-manager": "^3.0.0", + "@metamask/message-manager": "^3.1.1", "@metamask/metamask-eth-abis": "^3.0.0", "@metamask/notification-controller": "^2.0.0", "@metamask/obs-store": "^8.1.0", diff --git a/types/eth-keyring-controller.d.ts b/types/eth-keyring-controller.d.ts index 81145fa60..ee7525bf3 100644 --- a/types/eth-keyring-controller.d.ts +++ b/types/eth-keyring-controller.d.ts @@ -11,5 +11,7 @@ declare module '@metamask/eth-keyring-controller' { }>; getEncryptionPublicKey: (address: string) => Promise; + + decryptMessage: (...any) => any; } } diff --git a/ui/pages/confirm-decrypt-message/confirm-decrypt-message.component.js b/ui/pages/confirm-decrypt-message/confirm-decrypt-message.component.js index d223a3e96..620aa41fc 100644 --- a/ui/pages/confirm-decrypt-message/confirm-decrypt-message.component.js +++ b/ui/pages/confirm-decrypt-message/confirm-decrypt-message.component.js @@ -221,7 +221,7 @@ export default class ConfirmDecryptMessage extends Component { } else { this.setState({ hasDecrypted: true, - rawMessage: result.rawData, + rawMessage: result.rawSig, }); } }); diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index 41f7b6da7..71d8ead6c 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -497,14 +497,9 @@ export function getCurrentCurrency(state) { } export function getTotalUnapprovedCount(state) { - const { unapprovedDecryptMsgCount = 0, pendingApprovalCount = 0 } = - state.metamask; + const { pendingApprovalCount = 0 } = state.metamask; - return ( - unapprovedDecryptMsgCount + - pendingApprovalCount + - getSuggestedAssetCount(state) - ); + return pendingApprovalCount + getSuggestedAssetCount(state); } export function getTotalUnapprovedMessagesCount(state) { diff --git a/yarn.lock b/yarn.lock index 56e38c69d..7edb2d18e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3725,9 +3725,9 @@ __metadata: languageName: node linkType: hard -"@metamask/controller-utils@npm:^3.0.0, @metamask/controller-utils@npm:^3.1.0, @metamask/controller-utils@npm:^3.2.0, @metamask/controller-utils@npm:^3.3.0": - version: 3.3.0 - resolution: "@metamask/controller-utils@npm:3.3.0" +"@metamask/controller-utils@npm:^3.0.0, @metamask/controller-utils@npm:^3.1.0, @metamask/controller-utils@npm:^3.2.0, @metamask/controller-utils@npm:^3.3.0, @metamask/controller-utils@npm:^3.4.0": + version: 3.4.0 + resolution: "@metamask/controller-utils@npm:3.4.0" dependencies: "@metamask/utils": ^5.0.1 "@spruceid/siwe-parser": 1.1.3 @@ -3736,7 +3736,7 @@ __metadata: ethereumjs-util: ^7.0.10 ethjs-unit: ^0.1.6 fast-deep-equal: ^3.1.3 - checksum: 54e19f7bfd7b7762913313877484f0cfe9ac3e66cf43eabc6573e22433a7a36154a1f04fa5834807644a780b4b2200e0cafc06a0f0ef9fcead44304d742b2ad3 + checksum: c483a56a062118ad0b740ca65ec05810226af069bdcd7ff92adc250a9a4e8b9abf347876476ecd005f7890770b5bbf2f621a90b5a3698fdd059127d4337d7c6b languageName: node linkType: hard @@ -4024,18 +4024,18 @@ __metadata: languageName: node linkType: hard -"@metamask/message-manager@npm:^3.0.0": - version: 3.0.0 - resolution: "@metamask/message-manager@npm:3.0.0" +"@metamask/message-manager@npm:^3.1.1": + version: 3.1.1 + resolution: "@metamask/message-manager@npm:3.1.1" dependencies: "@metamask/base-controller": ^2.0.0 - "@metamask/controller-utils": ^3.1.0 + "@metamask/controller-utils": ^3.4.0 "@types/uuid": ^8.3.0 eth-sig-util: ^3.0.0 ethereumjs-util: ^7.0.10 jsonschema: ^1.2.4 uuid: ^8.3.2 - checksum: 14e0a4a398d95ce720e515bd1f35aee7b7b9f5f59367210a9125fe66fb561b630ae51b61f32048767f0bb30dd4a2e442e47c8d850de78f820feda7f72e4dc05e + checksum: 5f2341a67826b04a2b9dafff1bd53f1a7d3748e9617da5360248549eb5c048aebe542d3c364e77a5d4263358f8dfac8109f0f5faaabf0630c3be7d5857ab881d languageName: node linkType: hard @@ -24091,7 +24091,7 @@ __metadata: "@metamask/jazzicon": ^2.0.0 "@metamask/key-tree": ^7.0.0 "@metamask/logo": ^3.1.1 - "@metamask/message-manager": ^3.0.0 + "@metamask/message-manager": ^3.1.1 "@metamask/metamask-eth-abis": ^3.0.0 "@metamask/notification-controller": ^2.0.0 "@metamask/obs-store": ^8.1.0