Skip to content

Commit

Permalink
Ledger Ethereum chrome://untrusted refactor
Browse files Browse the repository at this point in the history
This change isolates the ledgerjs libraries required to interact with the
Ethereum app on Ledger devices to an iFrame in a chrome untrusted context.
It reuses the framework established in the Ledger Solana Ethereum refactor,
but generalizes some code such that it can be reused for both Solana and
Ethereum. For example:

* Shared logic in SolanaLedgerBridgeKeyring class has been moved to a new
  parent class that LedgerBridgeKeyring that both SolanaLedgerBridgeKeyring
  and EthereumLedgerBridgeKeyring that both inherit from

* Solana specific logic was moved out of LedgerUntrustedMessagingTransport
  and into a new child class, SolanaLedgerUntrustedMessagingTransport.
  Similarly, Ethereum specific logic is now in a new child class,
  EthereumLedgerUntrustedMessagingTransport that inherits from
  LedgerUntrustedMessagingTransport.

* Common message types were kept in ledger-messages.ts, but Solana and
  Ethereum specific message types were separated into their own
  sol-ledger-messages.ts and eth-ledger-messages.ts modules
  respectively.
  • Loading branch information
nvonpentz committed Aug 18, 2022
1 parent 53f188f commit 144d32b
Show file tree
Hide file tree
Showing 25 changed files with 1,424 additions and 728 deletions.
14 changes: 7 additions & 7 deletions components/brave_wallet_ui/common/api/hardware_keyrings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@

import { assert } from 'chrome://resources/js/assert.m.js'
import { BraveWallet } from '../../constants/types'
import LedgerBridgeKeyring from '../../common/hardware/ledgerjs/eth_ledger_bridge_keyring'
import EthereumLedgerBridgeKeyring from '../../common/hardware/ledgerjs/eth_ledger_bridge_keyring'
import TrezorBridgeKeyring from '../../common/hardware/trezor/trezor_bridge_keyring'
import * as HWInterfaces from '../hardware/interfaces'
import FilecoinLedgerKeyring from '../hardware/ledgerjs/filecoin_ledger_keyring'
import SolanaLedgerBridgeKeyring from '../../common/hardware/ledgerjs/sol_ledger_bridge_keyring'

export type HardwareKeyring = LedgerBridgeKeyring | TrezorBridgeKeyring | SolanaLedgerBridgeKeyring
export type HardwareKeyring = HWInterfaces.LedgerEthereumKeyring | HWInterfaces.TrezorKeyring | HWInterfaces.LedgerFilecoinKeyring | HWInterfaces.LedgerSolanaKeyring

export function getCoinName (coin: BraveWallet.CoinType) {
switch (coin) {
Expand All @@ -30,7 +30,7 @@ const VendorTypes = [
export type HardwareVendor = typeof VendorTypes[number]

// Lazy instances for keyrings
let ethereumHardwareKeyring: LedgerBridgeKeyring
let ethereumHardwareKeyring: EthereumLedgerBridgeKeyring
let filecoinHardwareKeyring: FilecoinLedgerKeyring
let solanaHardwareKeyring: SolanaLedgerBridgeKeyring
let trezorHardwareKeyring: TrezorBridgeKeyring
Expand All @@ -48,10 +48,10 @@ export function getHardwareKeyring (
type: HardwareVendor,
coin: BraveWallet.CoinType = BraveWallet.CoinType.ETH,
onAuthorized?: () => void
): LedgerBridgeKeyring | HWInterfaces.TrezorKeyring | FilecoinLedgerKeyring | SolanaLedgerBridgeKeyring {
): EthereumLedgerBridgeKeyring | HWInterfaces.TrezorKeyring | FilecoinLedgerKeyring | SolanaLedgerBridgeKeyring {
if (type === BraveWallet.LEDGER_HARDWARE_VENDOR) {
if (coin === BraveWallet.CoinType.ETH) {
return getLedgerEthereumHardwareKeyring()
return getLedgerEthereumHardwareKeyring(onAuthorized)
} else if (coin === BraveWallet.CoinType.FIL) {
return getLedgerFilecoinHardwareKeyring()
} else if (coin === BraveWallet.CoinType.SOL) {
Expand All @@ -64,9 +64,9 @@ export function getHardwareKeyring (
return trezorKeyring
}

export function getLedgerEthereumHardwareKeyring (): LedgerBridgeKeyring {
export function getLedgerEthereumHardwareKeyring (onAuthorized?: () => void): EthereumLedgerBridgeKeyring {
if (!ethereumHardwareKeyring) {
ethereumHardwareKeyring = new LedgerBridgeKeyring()
ethereumHardwareKeyring = new EthereumLedgerBridgeKeyring(onAuthorized)
}
return ethereumHardwareKeyring
}
Expand Down
11 changes: 4 additions & 7 deletions components/brave_wallet_ui/common/async/hardware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
import { TrezorErrorsCodes } from '../hardware/trezor/trezor-messages'
import FilecoinLedgerKeyring from '../hardware/ledgerjs/filecoin_ledger_keyring'
import TrezorBridgeKeyring from '../hardware/trezor/trezor_bridge_keyring'
import LedgerBridgeKeyring from '../hardware/ledgerjs/eth_ledger_bridge_keyring'
import EthereumLedgerBridgeKeyring from '../hardware/ledgerjs/eth_ledger_bridge_keyring'
import SolanaLedgerBridgeKeyring from '../hardware/ledgerjs/sol_ledger_bridge_keyring'
import { BraveWallet } from '../../constants/types'
import { LedgerEthereumKeyring, LedgerFilecoinKeyring, LedgerSolanaKeyring } from '../hardware/interfaces'
Expand Down Expand Up @@ -109,9 +109,6 @@ export async function signLedgerEthereumTransaction (
if (!signed || !signed.success || !signed.payload) {
const error = signed?.error ?? getLocale('braveWalletSignOnDeviceError')
const code = signed?.code ?? ''
if (code === 'DisconnectedDeviceDuringOperation') {
await deviceKeyring.makeApp()
}
return { success: false, error: error, code: code }
}
const { v, r, s } = signed.payload as EthereumSignedTx
Expand Down Expand Up @@ -185,7 +182,7 @@ export async function signLedgerSolanaTransaction (

export async function signMessageWithHardwareKeyring (vendor: HardwareVendor, path: string, messageData: BraveWallet.SignMessageRequest): Promise<SignHardwareOperationResult> {
const deviceKeyring = getHardwareKeyring(vendor, messageData.coin)
if (deviceKeyring instanceof LedgerBridgeKeyring) {
if (deviceKeyring instanceof EthereumLedgerBridgeKeyring) {
if (messageData.isEip712) {
if (!messageData.domainHash || !messageData.primaryHash) {
return { success: false, error: getLocale('braveWalletUnknownInternalError') }
Expand Down Expand Up @@ -213,7 +210,7 @@ export async function signRawTransactionWithHardwareKeyring (vendor: HardwareVen

if (deviceKeyring instanceof SolanaLedgerBridgeKeyring && message.bytes) {
return deviceKeyring.signTransaction(path, Buffer.from(message.bytes))
} else if (deviceKeyring instanceof TrezorBridgeKeyring || deviceKeyring instanceof LedgerBridgeKeyring || deviceKeyring instanceof FilecoinLedgerKeyring) {
} else if (deviceKeyring instanceof TrezorBridgeKeyring || deviceKeyring instanceof EthereumLedgerBridgeKeyring || deviceKeyring instanceof FilecoinLedgerKeyring) {
return { success: false, error: getLocale('braveWalletHardwareOperationUnsupportedError') }
}

Expand All @@ -222,7 +219,7 @@ export async function signRawTransactionWithHardwareKeyring (vendor: HardwareVen

export async function cancelHardwareOperation (vendor: HardwareVendor, coin: BraveWallet.CoinType) {
const deviceKeyring = getHardwareKeyring(vendor, coin)
if (deviceKeyring instanceof LedgerBridgeKeyring || deviceKeyring instanceof TrezorBridgeKeyring || deviceKeyring instanceof SolanaLedgerBridgeKeyring) {
if (deviceKeyring instanceof EthereumLedgerBridgeKeyring || deviceKeyring instanceof TrezorBridgeKeyring || deviceKeyring instanceof SolanaLedgerBridgeKeyring) {
return deviceKeyring.cancelOperation()
}
}
4 changes: 2 additions & 2 deletions components/brave_wallet_ui/common/async/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import getAPIProxy from './bridge'
import { Dispatch, State, Store } from './types'
import { getHardwareKeyring } from '../api/hardware_keyrings'
import { GetAccountsHardwareOperationResult } from '../hardware/types'
import LedgerBridgeKeyring from '../hardware/ledgerjs/eth_ledger_bridge_keyring'
import EthereumLedgerBridgeKeyring from '../hardware/ledgerjs/eth_ledger_bridge_keyring'
import TrezorBridgeKeyring from '../hardware/trezor/trezor_bridge_keyring'
import { AllNetworksOption } from '../../options/network-filter-options'
import FilecoinLedgerKeyring from '../hardware/ledgerjs/filecoin_ledger_keyring'
Expand Down Expand Up @@ -64,7 +64,7 @@ export const getERC20Allowance = (
export const onConnectHardwareWallet = (opts: HardwareWalletConnectOpts): Promise<BraveWallet.HardwareWalletAccount[]> => {
return new Promise(async (resolve, reject) => {
const keyring = getHardwareKeyring(opts.hardware, opts.coin, opts.onAuthorized)
if ((keyring instanceof LedgerBridgeKeyring || keyring instanceof TrezorBridgeKeyring) && opts.scheme) {
if ((keyring instanceof EthereumLedgerBridgeKeyring || keyring instanceof TrezorBridgeKeyring) && opts.scheme) {
keyring.getAccounts(opts.startIndex, opts.stopIndex, opts.scheme)
.then((result: GetAccountsHardwareOperationResult) => {
if (result.payload) {
Expand Down
1 change: 0 additions & 1 deletion components/brave_wallet_ui/common/hardware/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ export abstract class LedgerEthereumKeyring extends HardwareKeyring {
abstract signPersonalMessage (path: string, address: string, message: string): Promise<SignHardwareOperationResult>
abstract signTransaction (path: string, rawTxHex: string): Promise<SignHardwareOperationResult>
abstract signEip712Message (path: string, domainSeparatorHex: string, hashStructMessageHex: string): Promise<SignHardwareOperationResult>
abstract makeApp (): Promise<void>
}

export abstract class LedgerFilecoinKeyring extends HardwareKeyring {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/* Copyright (c) 2022 The Brave Authors. All rights reserved.
* This Source Code Form is subject to the terms of the Mozilla Public
* 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 type {
CommandMessage,
LedgerCommand,
LedgerError,
LedgerResponsePayload
} from './ledger-messages'

// GetAccounts command
export type EthGetAccountResponsePayload = LedgerResponsePayload & {
publicKey: string
address: string
chainCode?: string
}

export type EthGetAccountResponse = CommandMessage & {
payload: EthGetAccountResponsePayload | LedgerError
}

export type EthGetAccountCommand = CommandMessage & {
command: LedgerCommand.GetAccount
path: string
}

// SignTransaction command
export type EthereumSignedTx = {
v: string
r: string
s: string
}
export type EthSignTransactionResponsePayload = LedgerResponsePayload & EthereumSignedTx

export type EthSignTransactionResponse = CommandMessage & {
payload: EthSignTransactionResponsePayload | LedgerError
}

export type EthSignTransactionCommand = CommandMessage & {
command: LedgerCommand.SignTransaction
path: string
rawTxHex: string
}

// SignPersonalMessage command
export type EthSignPersonalMessageResponsePayload = LedgerResponsePayload & {
v: number
r: string
s: string
}

export type EthSignPersonalMessageResponse = CommandMessage & {
payload: EthSignPersonalMessageResponsePayload | LedgerError
}

export type EthSignPersonalMessageCommand = CommandMessage & {
command: LedgerCommand.SignPersonalMessage
path: string
messageHex: string
}

// SignEip712Message command
export type EthSignEip712MessageResponsePayload = EthSignPersonalMessageResponsePayload

export type EthSignEip712MessageResponse = CommandMessage & {
payload: EthSignEip712MessageResponsePayload | LedgerError
}

export type EthSignEip712MessageCommand = CommandMessage & {
command: LedgerCommand.SignEip712Message
path: string
domainSeparatorHex: string
hashStructMessageHex: string
}

export type EthLedgerFrameCommand = EthGetAccountCommand | EthSignTransactionCommand | EthSignPersonalMessageCommand | EthSignEip712MessageCommand
export type EthLedgerFrameResponse = EthGetAccountResponse | EthSignTransactionResponse | EthSignPersonalMessageResponse | EthSignEip712MessageResponse
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/* Copyright (c) 2022 The Brave Authors. All rights reserved.
* This Source Code Form is subject to the terms of the Mozilla Public
* 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 TransportWebHID from '@ledgerhq/hw-transport-webhid'
import Eth from '@ledgerhq/hw-app-eth'
import {
LedgerCommand,
UnlockResponse
} from './ledger-messages'
import {
EthGetAccountCommand,
EthGetAccountResponse,
EthGetAccountResponsePayload,
EthSignTransactionCommand,
EthSignTransactionResponsePayload,
EthSignTransactionResponse,
EthSignPersonalMessageCommand,
EthSignPersonalMessageResponsePayload,
EthSignPersonalMessageResponse,
EthSignEip712MessageCommand,
EthSignEip712MessageResponsePayload,
EthSignEip712MessageResponse
} from './eth-ledger-messages'
import { LedgerUntrustedMessagingTransport } from './ledger-untrusted-transport'

// EthereumLedgerUntrustedMessagingTransport makes calls to the Ethereum app on a Ledger device
export class EthereumLedgerUntrustedMessagingTransport extends LedgerUntrustedMessagingTransport {
constructor (targetWindow: Window, targetUrl: string) {
super(targetWindow, targetUrl)
this.addCommandHandler<UnlockResponse>(LedgerCommand.Unlock, this.handleUnlock)
this.addCommandHandler<EthGetAccountResponse>(LedgerCommand.GetAccount, this.handleGetAccount)
this.addCommandHandler<EthSignTransactionResponse>(LedgerCommand.SignTransaction, this.handleSignTransaction)
this.addCommandHandler<EthSignPersonalMessageResponse>(LedgerCommand.SignPersonalMessage, this.handleSignPersonalMessage)
this.addCommandHandler<EthSignEip712MessageResponse>(LedgerCommand.SignEip712Message, this.handleSignEip712Message)
}

private handleGetAccount = async (command: EthGetAccountCommand): Promise<EthGetAccountResponse> => {
const transport = await TransportWebHID.create()
const app = new Eth(transport)
try {
const result = await app.getAddress(command.path)
const getAccountResponsePayload: EthGetAccountResponsePayload = {
success: true,
publicKey: result.publicKey,
address: result.address,
chainCode: result.chainCode
}
const response: EthGetAccountResponse = {
id: command.id,
command: command.command,
payload: getAccountResponsePayload,
origin: command.origin
}
return response
} catch (error) {
const response: EthGetAccountResponse = {
id: command.id,
command: command.command,
payload: error,
origin: command.origin
}
return response
} finally {
await transport.close()
}
}

private handleSignTransaction = async (command: EthSignTransactionCommand): Promise<EthSignTransactionResponse> => {
const transport = await TransportWebHID.create()
const app = new Eth(transport)
try {
const result = await app.signTransaction(command.path, command.rawTxHex)
const signTransactionResponsePayload: EthSignTransactionResponsePayload = {
success: true,
v: result.v,
r: result.r,
s: result.s
}
const response: EthSignTransactionResponse = {
id: command.id,
command: command.command,
payload: signTransactionResponsePayload,
origin: command.origin
}
return response
} catch (error) {
const response: EthSignTransactionResponse = {
id: command.id,
command: command.command,
payload: error,
origin: command.origin
}
return response
} finally {
await transport.close()
}
}

private handleSignPersonalMessage = async (command: EthSignPersonalMessageCommand): Promise<EthSignPersonalMessageResponse> => {
const transport = await TransportWebHID.create()
const app = new Eth(transport)
try {
const result = await app.signPersonalMessage(command.path, command.messageHex)
const signPersonalMessageResponsePayload: EthSignPersonalMessageResponsePayload = {
success: true,
v: result.v,
r: result.r,
s: result.s
}
const response: EthSignPersonalMessageResponse = {
id: command.id,
command: command.command,
payload: signPersonalMessageResponsePayload,
origin: command.origin
}
return response
} catch (error) {
const response: EthSignPersonalMessageResponse = {
id: command.id,
command: command.command,
payload: error,
origin: command.origin
}
return response
} finally {
await transport.close()
}
}

private handleSignEip712Message = async (command: EthSignEip712MessageCommand): Promise<EthSignEip712MessageResponse> => {
const transport = await TransportWebHID.create()
const app = new Eth(transport)
try {
const result = await app.signEIP712HashedMessage(command.path, command.domainSeparatorHex, command.hashStructMessageHex)
const signEip712MessageResponsePayload: EthSignEip712MessageResponsePayload = {
success: true,
v: result.v,
r: result.r,
s: result.s
}
const response: EthSignEip712MessageResponse = {
id: command.id,
command: command.command,
payload: signEip712MessageResponsePayload,
origin: command.origin
}
return response
} catch (error) {
const response: EthSignEip712MessageResponse = {
id: command.id,
command: command.command,
payload: error,
origin: command.origin
}
return response
} finally {
await transport.close()
}
}
}
Loading

0 comments on commit 144d32b

Please sign in to comment.