From a3759580f1872c2b32a9dc3627e9428cba638b90 Mon Sep 17 00:00:00 2001 From: Sergey Date: Fri, 12 Nov 2021 10:17:19 +0300 Subject: [PATCH] Sign messages by Trezor (#10903) --- .../browser/brave_wallet_constants.h | 6 ++- .../brave_wallet_ui/common/async/hardware.ts | 12 ++++- .../common/hardware_operations.ts | 10 +++- .../eth_ledger_bridge_keyring.test.ts | 16 ++---- .../ledgerjs/eth_ledger_bridge_keyring.ts | 50 ++++++++---------- .../common/trezor/trezor-messages.ts | 49 ++++++++++------- .../common/trezor/trezor_bridge_keyring.ts | 33 ++++++++++-- components/brave_wallet_ui/constants/types.ts | 6 --- .../panel/async/wallet_panel_async_handler.ts | 52 ++++++++++--------- .../panel/constants/action_types.ts | 4 +- components/brave_wallet_ui/trezor/trezor.ts | 13 ++++- components/resources/wallet_strings.grdp | 2 + 12 files changed, 154 insertions(+), 99 deletions(-) diff --git a/components/brave_wallet/browser/brave_wallet_constants.h b/components/brave_wallet/browser/brave_wallet_constants.h index ae24a9449a5a..fe366a8171a8 100644 --- a/components/brave_wallet/browser/brave_wallet_constants.h +++ b/components/brave_wallet/browser/brave_wallet_constants.h @@ -505,7 +505,11 @@ constexpr webui::LocalizedString kLocalizedStrings[] = { {"braveWalletSignOnDeviceError", IDS_BRAVE_WALLET_HARDWARE_TRANSACTION_DEVICE_ERROR}, {"braveWalletNoMessageToSignError", - IDS_BRAVE_WALLET_HARDWARE_TRANSACTION_NO_MESSAGE_TO_SIGN_ERROR}}; + IDS_BRAVE_WALLET_HARDWARE_TRANSACTION_NO_MESSAGE_TO_SIGN_ERROR}, + {"braveWalletProcessMessageError", + IDS_BRAVE_WALLET_HARDWARE_SIGN_MESSAGE_ERROR}, + {"braveWalletUnknownKeyringError", + IDS_BRAVE_WALLET_HARDWARE_UNKNOWN_KEYRING_ERROR}}; const char kRopstenSwapBaseAPIURL[] = "https://ropsten.api.0x.org/"; const char kRopstenBuyTokenPercentageFee[] = "0.00875"; diff --git a/components/brave_wallet_ui/common/async/hardware.ts b/components/brave_wallet_ui/common/async/hardware.ts index 653da46d392e..d889da61d2ae 100644 --- a/components/brave_wallet_ui/common/async/hardware.ts +++ b/components/brave_wallet_ui/common/async/hardware.ts @@ -4,7 +4,7 @@ // you can obtain one at http://mozilla.org/MPL/2.0/. import * as BraveWallet from 'gen/brave/components/brave_wallet/common/brave_wallet.mojom.m.js' -import { SignHardwareTransactionType } from '../hardware_operations' +import { SignHardwareTransactionType, SignHardwareMessageOperationResult } from '../hardware_operations' import { getLocale } from '../../../common/locale' import WalletApiProxy from '../../common/wallet_api_proxy' import LedgerBridgeKeyring from '../../common/ledgerjs/eth_ledger_bridge_keyring' @@ -53,3 +53,13 @@ export async function signLedgerTransaction (apiProxy: WalletApiProxy, path: str } return { success: result.status } } + +export async function signMessageWithHardwareKeyring (apiProxy: WalletApiProxy, vendor: string, path: string, address: string, message: string): Promise { + const deviceKeyring = await apiProxy.getKeyringsByType(vendor) + if (deviceKeyring instanceof LedgerBridgeKeyring) { + return deviceKeyring.signPersonalMessage(path, address, message) + } else if (deviceKeyring instanceof TrezorBridgeKeyring) { + return deviceKeyring.signPersonalMessage(path, message) + } + return { success: false, error: getLocale('braveWalletUnknownKeyringError') } +} diff --git a/components/brave_wallet_ui/common/hardware_operations.ts b/components/brave_wallet_ui/common/hardware_operations.ts index 66c98af962a8..0aa300eab147 100644 --- a/components/brave_wallet_ui/common/hardware_operations.ts +++ b/components/brave_wallet_ui/common/hardware_operations.ts @@ -4,7 +4,11 @@ export interface SignHardwareTransactionType { success: boolean error?: string } - +export interface SignatureVRS { + v: number + r: string + s: string +} export type HardwareOperationResult = { success: boolean error?: string @@ -14,3 +18,7 @@ export type HardwareOperationResult = { export type SignHardwareTransactionOperationResult = HardwareOperationResult & { payload?: EthereumSignedTx } + +export type SignHardwareMessageOperationResult = HardwareOperationResult & { + payload?: string +} diff --git a/components/brave_wallet_ui/common/ledgerjs/eth_ledger_bridge_keyring.test.ts b/components/brave_wallet_ui/common/ledgerjs/eth_ledger_bridge_keyring.test.ts index 16a18262720b..3c064dc3f743 100644 --- a/components/brave_wallet_ui/common/ledgerjs/eth_ledger_bridge_keyring.test.ts +++ b/components/brave_wallet_ui/common/ledgerjs/eth_ledger_bridge_keyring.test.ts @@ -10,9 +10,9 @@ import { } from '../../components/desktop/popup-modals/add-account-modal/hardware-wallet-connect/types' import { - LEDGER_HARDWARE_VENDOR, - SignatureVRS + LEDGER_HARDWARE_VENDOR } from '../../constants/types' +import { SignatureVRS } from '../hardware_operations' class MockApp { signature: SignatureVRS @@ -108,21 +108,15 @@ test('Sign personal message successfully', () => { const ledgerHardwareKeyring = new LedgerBridgeKeyring() ledgerHardwareKeyring.app = new MockApp() ledgerHardwareKeyring.app.signature = { v: 1, r: 'b68983', s: 'r68983' } - ledgerHardwareKeyring._recoverAddressFromSignature = (message: string, signature: string) => { - return '0x111' - } return expect(ledgerHardwareKeyring.signPersonalMessage( 'm/44\'/60\'/0\'/0/0', '0x111', 'message')) - .resolves.toStrictEqual('0xb68983r68983-26') + .resolves.toStrictEqual({ payload: '0xb68983r68983-26', success: true }) }) test('Sign personal message failed', () => { - const ledgerHardwareKeyring = new LedgerBridgeKeyring() + const ledgerHardwareKeyring = createLedgerKeyring() ledgerHardwareKeyring.app = new MockApp() - ledgerHardwareKeyring._recoverAddressFromSignature = (message: string, signature: string) => { - return '0x111' - } return expect(ledgerHardwareKeyring.signPersonalMessage( 'm/44\'/60\'/0\'/0/0', '0x111', 'message')) - .rejects.toThrow() + .resolves.toMatchObject({ success: false }) }) diff --git a/components/brave_wallet_ui/common/ledgerjs/eth_ledger_bridge_keyring.ts b/components/brave_wallet_ui/common/ledgerjs/eth_ledger_bridge_keyring.ts index fa76e321d00f..97221fb6f693 100644 --- a/components/brave_wallet_ui/common/ledgerjs/eth_ledger_bridge_keyring.ts +++ b/components/brave_wallet_ui/common/ledgerjs/eth_ledger_bridge_keyring.ts @@ -4,20 +4,15 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. */ const { EventEmitter } = require('events') - +import Eth from '@ledgerhq/hw-app-eth' +import TransportWebHID from '@ledgerhq/hw-transport-webhid' +import { LEDGER_HARDWARE_VENDOR } from 'gen/brave/components/brave_wallet/common/brave_wallet.mojom.m.js' import { LedgerDerivationPaths } from '../../components/desktop/popup-modals/add-account-modal/hardware-wallet-connect/types' - -import { - LEDGER_HARDWARE_VENDOR, SignatureVRS -} from '../../constants/types' - -import Eth from '@ledgerhq/hw-app-eth' -import TransportWebHID from '@ledgerhq/hw-transport-webhid' import { getLocale } from '../../../common/locale' import { hardwareDeviceIdFromAddress } from '../hardwareDeviceIdFromAddress' -import { SignHardwareTransactionOperationResult } from '../../common/hardware_operations' +import { SignatureVRS, SignHardwareMessageOperationResult, SignHardwareTransactionOperationResult } from '../../common/hardware_operations' export default class LedgerBridgeKeyring extends EventEmitter { constructor () { @@ -70,27 +65,24 @@ export default class LedgerBridgeKeyring extends EventEmitter { return { success: true, payload: signed } } - signPersonalMessage = async (path: string, address: string, message: string) => { - return new Promise(async (resolve, reject) => { - try { - if (!this.isUnlocked() && !(await this.unlock())) { - return new Error(getLocale('braveWalletUnlockError')) - } - return this.app.signPersonalMessage(path, - Buffer.from(message)).then((result: SignatureVRS) => { - const signature = this._createMessageSignature(result, message, address) - if (!signature) { - return reject(new Error(getLocale('braveWalletLedgerValidationError'))) - } - resolve(signature) - }).catch(reject) - } catch (e) { - reject(e) + signPersonalMessage = async (path: string, address: string, message: string): Promise => { + if (!this.isUnlocked() && !(await this.unlock())) { + return { success: false, error: getLocale('braveWalletUnlockError') } + } + try { + const data = await this.app.signPersonalMessage(path, + Buffer.from(message)) + const signature = this.createMessageSignature(data) + if (!signature) { + return { success: false, error: getLocale('braveWalletLedgerValidationError') } } - }) + return { success: true, payload: signature } + } catch (e) { + return { success: false, error: e.message } + } } - _createMessageSignature = (result: SignatureVRS, message: string, address: string) => { + private readonly createMessageSignature = (result: SignatureVRS) => { let v = (result.v - 27).toString() if (v.length < 2) { v = `0${v}` @@ -100,7 +92,7 @@ export default class LedgerBridgeKeyring extends EventEmitter { } /* PRIVATE METHODS */ - _getPathForIndex = (index: number, scheme: string) => { + private readonly getPathForIndex = (index: number, scheme: string) => { if (scheme === LedgerDerivationPaths.LedgerLive) { return `m/44'/60'/${index}'/0/0` } else if (scheme === LedgerDerivationPaths.Legacy) { @@ -120,7 +112,7 @@ export default class LedgerBridgeKeyring extends EventEmitter { _getAccounts = async (from: number, to: number, scheme: string) => { const accounts = [] for (let i = from; i <= to; i++) { - const path = this._getPathForIndex(i, scheme) + const path = this.getPathForIndex(i, scheme) const address = await this._getAddress(path) accounts.push({ address: address.address, diff --git a/components/brave_wallet_ui/common/trezor/trezor-messages.ts b/components/brave_wallet_ui/common/trezor/trezor-messages.ts index 078c53d3906a..ca1bb7fa1a07 100644 --- a/components/brave_wallet_ui/common/trezor/trezor-messages.ts +++ b/components/brave_wallet_ui/common/trezor/trezor-messages.ts @@ -5,26 +5,21 @@ import { loadTimeData } from '../../../common/loadTimeData' import { Unsuccessful, EthereumSignTransaction, CommonParams, Success } from 'trezor-connect' import { HDNodeResponse } from 'trezor-connect/lib/typescript' -import { EthereumSignedTx } from 'trezor-connect/lib/typescript/networks/ethereum' +import { EthereumSignedTx, EthereumSignMessage } from 'trezor-connect/lib/typescript/networks/ethereum' +import { MessageSignature } from 'trezor-connect/lib/typescript/trezor/protobuf' export const kTrezorBridgeUrl = loadTimeData.getString('braveWalletTrezorBridgeUrl') export enum TrezorCommand { Unlock = 'trezor-unlock', GetAccounts = 'trezor-get-accounts', - SignTransaction = 'trezor-sign-treansaction' + SignTransaction = 'trezor-sign-treansaction', + SignMessage = 'trezor-sign-message' } export type CommandMessage = { command: TrezorCommand id: string origin: string } -export type TrezorAccountPath = { - path: string -} -export type GetAccountsCommand = CommandMessage & { - command: TrezorCommand.GetAccounts - paths: TrezorAccountPath[] -} export type UnlockCommand = CommandMessage & { command: TrezorCommand.Unlock } @@ -32,16 +27,9 @@ export type UnlockResponse = CommandMessage & { result: Boolean error?: Unsuccessful } - -export type SignTransactionCommandPayload = CommonParams & EthereumSignTransaction - -export type SignTransactionCommand = CommandMessage & { - command: TrezorCommand.SignTransaction - payload: SignTransactionCommandPayload +export type TrezorAccountPath = { + path: string } - -export type SignTransactionResponse = Unsuccessful | Success - export type TrezorAccount = { publicKey: string serializedPath: string @@ -55,12 +43,33 @@ export type TrezorGetPublicKeyResponse = Unsuccessful | Success export type SignTransactionResponsePayload = CommandMessage & { payload: SignTransactionResponse } -export type TrezorFrameCommand = GetAccountsCommand | UnlockCommand | SignTransactionCommand -export type TrezorFrameResponse = UnlockResponse | GetAccountsResponsePayload | SignTransactionResponsePayload + +export type SignMessageCommandPayload = CommonParams & EthereumSignMessage +export type SignMessageCommand = CommandMessage & { + command: TrezorCommand.SignMessage + payload: SignMessageCommandPayload +} +export type SignMessageResponse = Unsuccessful | Success +export type SignMessageResponsePayload = CommandMessage & { + payload: SignMessageResponse +} + +export type TrezorFrameCommand = GetAccountsCommand | UnlockCommand | SignTransactionCommand | SignMessageCommand +export type TrezorFrameResponse = UnlockResponse | GetAccountsResponsePayload | SignTransactionResponsePayload | SignMessageResponsePayload // Trezor library is loaded inside the chrome-untrusted webui page // and communication is going through posting messages between parent window diff --git a/components/brave_wallet_ui/common/trezor/trezor_bridge_keyring.ts b/components/brave_wallet_ui/common/trezor/trezor_bridge_keyring.ts index a6555c0861dd..527d5e99dd49 100644 --- a/components/brave_wallet_ui/common/trezor/trezor_bridge_keyring.ts +++ b/components/brave_wallet_ui/common/trezor/trezor_bridge_keyring.ts @@ -17,12 +17,14 @@ import { TrezorAccount, SignTransactionCommandPayload, TrezorFrameCommand, - SignTransactionResponsePayload + SignTransactionResponsePayload, + SignMessageCommandPayload, + SignMessageResponsePayload } from '../../common/trezor/trezor-messages' import { sendTrezorCommand } from '../../common/trezor/trezor-bridge-transport' import { getLocale } from '../../../common/locale' import { hardwareDeviceIdFromAddress } from '../hardwareDeviceIdFromAddress' -import { SignHardwareTransactionOperationResult } from '../../common/hardware_operations' +import { SignHardwareMessageOperationResult, SignHardwareTransactionOperationResult } from '../../common/hardware_operations' export default class TrezorBridgeKeyring extends EventEmitter { constructor () { @@ -78,6 +80,27 @@ export default class TrezorBridgeKeyring extends EventEmitter { return { success: true, payload: data.payload.payload } } + signPersonalMessage = async (path: string, message: string): Promise => { + if (!this.isUnlocked() && !(await this.unlock())) { + return { success: false, error: getLocale('braveWalletUnlockError') } + } + const data = await this.sendTrezorCommand({ + command: TrezorCommand.SignMessage, + // @ts-expect-error + id: crypto.randomUUID(), + payload: this.prepareSignMessagePayload(path, message), + origin: window.origin + }) + if (!data) { + return { success: false, error: getLocale('braveWalletProcessMessageError') } + } + if (!data.payload.success) { + const unsuccess = data.payload + return { success: false, error: unsuccess.payload.error, code: unsuccess.payload.code } + } + return { success: true, payload: data.payload.payload.signature } + } + isUnlocked = () => { return this.unlocked_ } @@ -88,7 +111,7 @@ export default class TrezorBridgeKeyring extends EventEmitter { id: crypto.randomUUID(), origin: window.origin, command: TrezorCommand.Unlock -}) + }) if (!data) { return false } @@ -158,6 +181,10 @@ export default class TrezorBridgeKeyring extends EventEmitter { } } + private readonly prepareSignMessagePayload = (path: string, message: string): SignMessageCommandPayload => { + return { path: path, message: message } + } + private readonly publicKeyToAddress = (key: string) => { const buffer = Buffer.from(key, 'hex') const address = publicToAddress(buffer, true).toString('hex') diff --git a/components/brave_wallet_ui/constants/types.ts b/components/brave_wallet_ui/constants/types.ts index f0776096514e..6b2a63f4ee12 100644 --- a/components/brave_wallet_ui/constants/types.ts +++ b/components/brave_wallet_ui/constants/types.ts @@ -34,12 +34,6 @@ export interface AssetOptionType { logo: string } -export interface SignatureVRS { - v: number - r: string - s: string -} - export interface UserAssetOptionType { asset: AssetOptionType assetBalance: number diff --git a/components/brave_wallet_ui/panel/async/wallet_panel_async_handler.ts b/components/brave_wallet_ui/panel/async/wallet_panel_async_handler.ts index fafac9c8faa8..ac64dfb6af15 100644 --- a/components/brave_wallet_ui/panel/async/wallet_panel_async_handler.ts +++ b/components/brave_wallet_ui/panel/async/wallet_panel_async_handler.ts @@ -3,7 +3,14 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this file, // you can obtain one at http://mozilla.org/MPL/2.0/. -import { TransactionInfo, TREZOR_HARDWARE_VENDOR, LEDGER_HARDWARE_VENDOR } from 'gen/brave/components/brave_wallet/common/brave_wallet.mojom.m.js' +import { + TransactionInfo, + SignMessageRequest, + SwitchChainRequest, + TransactionStatus, + TREZOR_HARDWARE_VENDOR, + LEDGER_HARDWARE_VENDOR +} from 'gen/brave/components/brave_wallet/common/brave_wallet.mojom.m.js' import AsyncActionHandler from '../../../common/AsyncActionHandler' import * as PanelActions from '../actions/wallet_panel_actions' import * as WalletActions from '../../common/actions/wallet_actions' @@ -11,10 +18,7 @@ import { TransactionStatusChanged } from '../../common/constants/action_types' import { WalletPanelState, PanelState, - WalletState, - TransactionStatus, - SignMessageRequest, - SwitchChainRequest + WalletState } from '../../constants/types' import { AccountPayloadType, @@ -31,15 +35,14 @@ import { } from '../../common/async/lib' import { signTrezorTransaction, - signLedgerTransaction + signLedgerTransaction, + signMessageWithHardwareKeyring } from '../../common/async/hardware' import { fetchSwapQuoteFactory } from '../../common/async/handlers' import { Store } from '../../common/async/types' import { getLocale } from '../../../common/locale' -import LedgerBridgeKeyring from '../../common/ledgerjs/eth_ledger_bridge_keyring' -import TrezorBridgeKeyring from '../../common/trezor/trezor_bridge_keyring' import getWalletPanelApiProxy from '../wallet_panel_api_proxy' const handler = new AsyncActionHandler() @@ -247,34 +250,35 @@ handler.on(PanelActions.signMessageProcessed.getType(), async (store: Store, pay handler.on(PanelActions.signMessageHardware.getType(), async (store, messageData: SignMessageRequest) => { const apiProxy = getWalletPanelApiProxy() - const braveWalletService = apiProxy.braveWalletService const hardwareAccount = await findHardwareAccountInfo(messageData.address) - if (hardwareAccount && hardwareAccount.hardware) { - let deviceKeyring = await apiProxy.getKeyringsByType(hardwareAccount.hardware.vendor) - if (deviceKeyring instanceof LedgerBridgeKeyring || deviceKeyring instanceof TrezorBridgeKeyring) { - deviceKeyring.signPersonalMessage(hardwareAccount.hardware.path, hardwareAccount.address, messageData.message) - .then(async (signature: string) => { - store.dispatch(PanelActions.signMessageHardwareProcessed({ success: true, id: messageData.id, signature: signature, error: '' })) - }).catch(async (error: any) => { - store.dispatch(PanelActions.signMessageHardwareProcessed({ success: false, id: messageData.id, signature: '', error: error.message })) - }) + if (!hardwareAccount || !hardwareAccount.hardware) { + const braveWalletService = apiProxy.braveWalletService + braveWalletService.notifySignMessageHardwareRequestProcessed(false, messageData.id, + '', getLocale('braveWalletHardwareAccountNotFound')) + const signMessageRequest = await getPendingSignMessageRequest() + if (signMessageRequest) { + store.dispatch(PanelActions.signMessage(signMessageRequest)) + return } + apiProxy.panelHandler.closeUI() return } - braveWalletService.notifySignMessageHardwareRequestProcessed(false, messageData.id, - '', getLocale('braveWalletHardwareAccountNotFound')) - const signMessageRequest = await getPendingSignMessageRequest() - if (signMessageRequest) { - store.dispatch(PanelActions.signMessage(signMessageRequest)) + const info = hardwareAccount.hardware + apiProxy.panelHandler.setCloseOnDeactivate(false) + const signature = await signMessageWithHardwareKeyring(apiProxy, info.vendor, info.path, messageData.address, messageData.message) + apiProxy.panelHandler.setCloseOnDeactivate(true) + if (!signature || !signature.success) { + store.dispatch(PanelActions.signMessageHardwareProcessed({ success: false, id: messageData.id, error: signature.error })) return } + store.dispatch(PanelActions.signMessageHardwareProcessed({ success: true, id: messageData.id, signature: signature.payload })) apiProxy.panelHandler.closeUI() }) handler.on(PanelActions.signMessageHardwareProcessed.getType(), async (store, payload: SignMessageHardwareProcessedPayload) => { const apiProxy = getWalletPanelApiProxy() const braveWalletService = apiProxy.braveWalletService - braveWalletService.notifySignMessageHardwareRequestProcessed(payload.success, payload.id, payload.signature, payload.error) + braveWalletService.notifySignMessageHardwareRequestProcessed(payload.success, payload.id, payload.signature || '', payload.error || '') const signMessageRequest = await getPendingSignMessageRequest() if (signMessageRequest) { store.dispatch(PanelActions.signMessage(signMessageRequest)) diff --git a/components/brave_wallet_ui/panel/constants/action_types.ts b/components/brave_wallet_ui/panel/constants/action_types.ts index 6cdc577424cc..3949400f86cf 100644 --- a/components/brave_wallet_ui/panel/constants/action_types.ts +++ b/components/brave_wallet_ui/panel/constants/action_types.ts @@ -39,8 +39,8 @@ export type SignMessageProcessedPayload = { export type SignMessageHardwareProcessedPayload = { success: boolean id: number - signature: string - error: string + signature?: string + error?: string } export type SwitchEthereumChainProcessedPayload = { diff --git a/components/brave_wallet_ui/trezor/trezor.ts b/components/brave_wallet_ui/trezor/trezor.ts index 88f08aa9a75e..12b5d86eb5dc 100644 --- a/components/brave_wallet_ui/trezor/trezor.ts +++ b/components/brave_wallet_ui/trezor/trezor.ts @@ -13,7 +13,10 @@ import { GetAccountsResponsePayload, TrezorGetPublicKeyResponse, SignTransactionCommand, - SignTransactionResponsePayload + SignTransactionResponsePayload, + SignMessageCommand, + SignMessageResponsePayload, + SignMessageResponse } from '../common/trezor/trezor-messages' import { addTrezorCommandHandler } from '../common/trezor/trezor-command-handler' @@ -58,3 +61,11 @@ addTrezorCommandHandler(TrezorCommand.SignTransaction, (command: SignTransaction }) }) }) + +addTrezorCommandHandler(TrezorCommand.SignMessage, (command: SignMessageCommand, source: Window): Promise => { + return new Promise(async (resolve) => { + TrezorConnect.ethereumSignMessage(command.payload).then((result: SignMessageResponse) => { + resolve({ id: command.id, command: command.command, payload: result, origin: command.origin }) + }) + }) +}) diff --git a/components/resources/wallet_strings.grdp b/components/resources/wallet_strings.grdp index bb62fade3259..2311a8812c29 100644 --- a/components/resources/wallet_strings.grdp +++ b/components/resources/wallet_strings.grdp @@ -347,4 +347,6 @@ Unable to find hardware transaction, please try again Received error from hardware device, probably you need to confirm the transaction on your device Error processing the transaction, please try again + Error signing message, please try again + Error signing message, unknown keyring requested, please try again