Browse Source

Merge pull request #369 from valpinkman/manager-workflow

Manager workflow
master
Gaëtan Renaudeau 7 years ago
committed by GitHub
parent
commit
0aa2c316f0
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      README.md
  2. 24
      src/commands/getDeviceInfo.js
  3. 19
      src/commands/getFirmwareInfo.js
  4. 15
      src/commands/getIsGenuine.js
  5. 19
      src/commands/getLatestFirmwareForDevice.js
  6. 25
      src/commands/installApp.js
  7. 15
      src/commands/listApps.js
  8. 46
      src/components/ManagerPage/AppsList.js
  9. 93
      src/components/ManagerPage/EnsureDashboard.js
  10. 37
      src/components/ManagerPage/EnsureDevice.js
  11. 87
      src/components/ManagerPage/EnsureGenuine.js
  12. 78
      src/components/ManagerPage/FinalFirmwareUpdate.js
  13. 127
      src/components/ManagerPage/FirmwareUpdate.js
  14. 0
      src/components/ManagerPage/FlashMcu.js
  15. 100
      src/components/ManagerPage/index.js
  16. 16
      src/helpers/apps/installApp.js
  17. 14
      src/helpers/apps/listApps.js
  18. 12
      src/helpers/apps/uninstallApp.js
  19. 236
      src/helpers/common.js
  20. 13
      src/helpers/constants.js
  21. 26
      src/helpers/devices/getDeviceInfo.js
  22. 31
      src/helpers/devices/getFirmwareInfo.js
  23. 5
      src/helpers/devices/getIsGenuine.js
  24. 43
      src/helpers/devices/getLatestFirmwareForDevice.js
  25. 24
      src/helpers/firmware/installFinalFirmware.js
  26. 1
      src/helpers/firmware/installMcu.js
  27. 27
      src/helpers/firmware/installOsuFirmware.js
  28. 15
      src/internals/devices/index.js
  29. 2
      src/internals/manager/constants.js
  30. 70
      src/internals/manager/helpers.js
  31. 8
      src/internals/manager/index.js
  32. 12
      src/internals/manager/installApp.js
  33. 14
      src/internals/manager/listApps.js
  34. 12
      src/internals/manager/uninstallApp.js

3
README.md

@ -37,6 +37,9 @@ yarn
# Where errors will be tracked (you may not want to edit this line)
# SENTRY_URL=
# api base url
API_BASE_URL=http://...
# OPTIONAL ENV VARIABLES
# ----------------------

24
src/commands/getDeviceInfo.js

@ -0,0 +1,24 @@
// @flow
import { createCommand, Command } from 'helpers/ipc'
import { fromPromise } from 'rxjs/observable/fromPromise'
import { withDevice } from 'helpers/deviceAccess'
import getDeviceInfo from 'helpers/devices/getDeviceInfo'
type Input = {
devicePath: string,
}
type Result = {
targetId: number | string,
version: string,
final: boolean,
mcu: boolean,
}
const cmd: Command<Input, Result> = createCommand('devices', 'getDeviceInfo', ({ devicePath }) =>
fromPromise(withDevice(devicePath)(transport => getDeviceInfo(transport))),
)
export default cmd

19
src/commands/getFirmwareInfo.js

@ -0,0 +1,19 @@
// @flow
import { createCommand, Command } from 'helpers/ipc'
import { fromPromise } from 'rxjs/observable/fromPromise'
import getFirmwareInfo from 'helpers/devices/getFirmwareInfo'
type Input = {
targetId: string | number,
version: string,
}
type Result = *
const cmd: Command<Input, Result> = createCommand('devices', 'getFirmwareInfo', data =>
fromPromise(getFirmwareInfo(data)),
)
export default cmd

15
src/commands/getIsGenuine.js

@ -0,0 +1,15 @@
// @flow
import { createCommand, Command } from 'helpers/ipc'
import { fromPromise } from 'rxjs/observable/fromPromise'
import getIsGenuine from 'helpers/devices/getIsGenuine'
type Input = *
type Result = boolean
const cmd: Command<Input, Result> = createCommand('devices', 'getIsGenuine', () =>
fromPromise(getIsGenuine()),
)
export default cmd

19
src/commands/getLatestFirmwareForDevice.js

@ -0,0 +1,19 @@
// @flow
import { createCommand, Command } from 'helpers/ipc'
import { fromPromise } from 'rxjs/observable/fromPromise'
import getLatestFirmwareForDevice from '../helpers/devices/getLatestFirmwareForDevice'
type Input = {
targetId: string | number,
version: string,
}
type Result = *
const cmd: Command<Input, Result> = createCommand('devices', 'getLatestFirmwareForDevice', data =>
fromPromise(getLatestFirmwareForDevice(data)),
)
export default cmd

25
src/commands/installApp.js

@ -0,0 +1,25 @@
// @flow
import { createCommand, Command } from 'helpers/ipc'
import { fromPromise } from 'rxjs/observable/fromPromise'
import { withDevice } from 'helpers/deviceAccess'
import installApp from 'helpers/apps/installApp'
import type { LedgerScriptParams } from 'helpers/common'
type Input = {
appParams: LedgerScriptParams,
devicePath: string,
}
type Result = *
const cmd: Command<Input, Result> = createCommand(
'devices',
'installApp',
({ devicePath, ...rest }) =>
fromPromise(withDevice(devicePath)(transport => installApp(transport, rest))),
)
export default cmd

15
src/commands/listApps.js

@ -0,0 +1,15 @@
// @flow
import { createCommand, Command } from 'helpers/ipc'
import { fromPromise } from 'rxjs/observable/fromPromise'
import listApps from 'helpers/apps/listApps'
type Input = *
type Result = *
const cmd: Command<Input, Result> = createCommand('manager', 'listApps', () =>
fromPromise(listApps()),
)
export default cmd

46
src/components/ManagerPage/AppsList.js

@ -4,7 +4,8 @@ import React, { PureComponent } from 'react'
import styled from 'styled-components'
import { translate } from 'react-i18next'
import runJob from 'renderer/runJob'
import listApps from 'commands/listApps'
import installApp from 'commands/installApp'
import Box from 'components/base/Box'
import Modal, { ModalBody } from 'components/base/Modal'
@ -28,12 +29,6 @@ const ICONS_FALLBACK = {
type Status = 'loading' | 'idle' | 'busy' | 'success' | 'error'
type jobHandlerOptions = {
job: string,
successResponse: string,
errorResponse: string,
}
type LedgerApp = {
name: string,
version: string,
@ -64,7 +59,7 @@ class AppsList extends PureComponent<Props, State> {
}
componentDidMount() {
this.fetchList()
this.fetchAppList()
}
componentWillUnmount() {
@ -73,49 +68,32 @@ class AppsList extends PureComponent<Props, State> {
_unmounted = false
async fetchList() {
console.log(`fetching app list`)
const appsList =
CACHED_APPS ||
(await runJob({
channel: 'manager',
job: 'listApps',
successResponse: 'manager.listAppsSuccess',
errorResponse: 'manager.listAppsError',
}))
async fetchAppList() {
const appsList = CACHED_APPS || (await listApps.send().toPromise())
CACHED_APPS = appsList
if (!this._unmounted) {
this.setState({ appsList, status: 'idle' })
}
}
createDeviceJobHandler = (options: jobHandlerOptions) => (args: { app: any }) => async () => {
handleInstallApp = (args: { app: any }) => async () => {
const appParams = args.app
this.setState({ status: 'busy' })
try {
const { job, successResponse, errorResponse } = options
const {
device: { path: devicePath },
} = this.props
const data = { appParams, devicePath }
await runJob({ channel: 'usb', job, successResponse, errorResponse, data })
await installApp.send(data).toPromise()
this.setState({ status: 'success' })
} catch (err) {
this.setState({ status: 'error', error: err.message })
}
}
handleInstall = this.createDeviceJobHandler({
job: 'manager.installApp',
successResponse: 'device.appInstalled',
errorResponse: 'device.appInstallError',
})
handleUninstall = this.createDeviceJobHandler({
job: 'manager.uninstallApp',
successResponse: 'device.appUninstalled',
errorResponse: 'device.appUninstallError',
})
handleUninstallApp = (/* args: { app: any } */) => () => {
/* TODO */
}
handleCloseModal = () => this.setState({ status: 'idle' })
@ -129,8 +107,8 @@ class AppsList extends PureComponent<Props, State> {
name={c.name}
version={`Version ${c.version}`}
icon={ICONS_FALLBACK[c.icon] || c.icon}
onInstall={this.handleInstall(c)}
onUninstall={this.handleUninstall(c)}
onInstall={() => {}}
onUninstall={() => {}}
/>
))}
<Modal

93
src/components/ManagerPage/EnsureDashboard.js

@ -0,0 +1,93 @@
// @flow
import React, { PureComponent, Fragment } from 'react'
import { translate } from 'react-i18next'
import isEqual from 'lodash/isEqual'
// import type { Device, T } from 'types/common'
import type { Device } from 'types/common'
import getDeviceInfo from 'commands/getDeviceInfo'
type DeviceInfo = {
targetId: number | string,
version: string,
final: boolean,
mcu: boolean,
}
type Props = {
// t: T,
device: Device,
children: Function,
}
type State = {
deviceInfo: ?DeviceInfo,
error: ?{
message: string,
stack: string,
},
}
class EnsureDashboard extends PureComponent<Props, State> {
static defaultProps = {
children: null,
device: null,
}
state = {
deviceInfo: null,
error: null,
}
componentDidMount() {
this.checkForDashboard()
}
componentDidUpdate() {
this.checkForDashboard()
}
componentWillUnmount() {
this._unmounting = true
}
_checking = false
_unmounting = false
async checkForDashboard() {
const { device } = this.props
if (device && !this._checking) {
this._checking = true
try {
const deviceInfo = await getDeviceInfo.send({ devicePath: device.path }).toPromise()
if (!isEqual(this.state.deviceInfo, deviceInfo) || this.state.error) {
!this._unmounting && this.setState({ deviceInfo, error: null })
}
} catch (err) {
if (!isEqual(err, this.state.error)) {
!this._unmounting && this.setState({ error: err, deviceInfo: null })
}
}
this._checking = false
}
}
render() {
const { deviceInfo, error } = this.state
const { children } = this.props
if (deviceInfo) {
return children(deviceInfo)
}
return error ? (
<Fragment>
<span>{error.message}</span>
<span>Please make sure your device is on the dashboard screen</span>
</Fragment>
) : null
}
}
export default translate()(EnsureDashboard)

37
src/components/ManagerPage/EnsureDevice.js

@ -0,0 +1,37 @@
// @flow
import React, { PureComponent } from 'react'
import { connect } from 'react-redux'
import { translate } from 'react-i18next'
import { compose } from 'redux'
// import type { Device, T } from 'types/common'
import type { Device } from 'types/common'
import { getCurrentDevice, getDevices } from 'reducers/devices'
const mapStateToProps = state => ({
device: getCurrentDevice(state),
nbDevices: getDevices(state).length,
})
type Props = {
// t: T,
device: ?Device,
nbDevices: number,
children: Function,
}
type State = {}
class EnsureDevice extends PureComponent<Props, State> {
static defaultProps = {
device: null,
}
render() {
const { device, nbDevices, children } = this.props
return device ? children(device, nbDevices) : <span>Please connect your device</span>
}
}
export default compose(translate(), connect(mapStateToProps))(EnsureDevice)

87
src/components/ManagerPage/EnsureGenuine.js

@ -0,0 +1,87 @@
// @flow
import React, { PureComponent, Fragment } from 'react'
import { translate } from 'react-i18next'
import isEqual from 'lodash/isEqual'
import type { Node } from 'react'
// import type { Device, T } from 'types/common'
import type { Device } from 'types/common'
import getIsGenuine from 'commands/getIsGenuine'
type Props = {
// t: T,
device: Device,
children: Node,
}
type State = {
genuine: boolean,
error: ?{
message: string,
stack: string,
},
}
class EnsureGenuine extends PureComponent<Props, State> {
static defaultProps = {
children: null,
firmwareInfo: null,
}
state = {
error: null,
genuine: false,
}
componentDidMount() {
this.checkIsGenuine()
}
componentDidUpdate() {
this.checkIsGenuine()
}
componentWillUnmount() {
this._unmounting = true
}
_checking = false
_unmounting = false
async checkIsGenuine() {
const { device } = this.props
if (device && !this._checking) {
this._checking = true
try {
const isGenuine = await getIsGenuine.send().toPromise()
if (!this.state.genuine || this.state.error) {
!this._unmounting && this.setState({ genuine: isGenuine, error: null })
}
} catch (err) {
if (!isEqual(this.state.error, err)) {
!this._unmounting && this.setState({ genuine: false, error: err })
}
}
this._checking = false
}
}
render() {
const { error, genuine } = this.state
const { children } = this.props
if (genuine) {
return children
}
return error ? (
<Fragment>
<span>{error.message}</span>
<span>You did not approve request on your device or your device is not genuine</span>
</Fragment>
) : null
}
}
export default translate()(EnsureGenuine)

78
src/components/ManagerPage/FinalFirmwareUpdate.js

@ -0,0 +1,78 @@
// @flow
import React, { PureComponent } from 'react'
import { translate } from 'react-i18next'
import type { Device, T } from 'types/common'
// import runJob from 'renderer/runJob'
import Box, { Card } from 'components/base/Box'
// import Button from 'components/base/Button'
type DeviceInfos = {
targetId: number,
version: string,
}
type Props = {
t: T,
device: Device,
infos: DeviceInfos,
}
type State = {
// latestFirmware: ?FirmwareInfos,
}
class FirmwareUpdate extends PureComponent<Props, State> {
state = {
// latestFirmware: null,
}
componentDidMount() {}
componentWillUnmount() {
this._unmounting = true
}
_unmounting = false
// handleInstallFinalFirmware = async () => {
// try {
// const { latestFirmware } = this.state
// this.setState(state => ({ ...state, installing: true }))
// const {
// device: { path: devicePath },
// } = this.props
// await runJob({
// channel: 'manager',
// job: 'installFinalFirmware',
// successResponse: 'device.finalFirmwareInstallSuccess',
// errorResponse: 'device.finalFirmwareInstallError',
// data: {
// devicePath,
// firmware: latestFirmware,
// },
// })
// } catch (err) {
// console.log(err)
// }
// }
render() {
const { t, ...props } = this.props
return (
<Box flow={4} {...props}>
<Box color="dark" ff="Museo Sans" fontSize={6}>
{t('manager:firmwareUpdate')}
</Box>
<Card flow={2} {...props}>
<Box horizontal align="center" flow={2} />
</Card>
</Box>
)
}
}
export default translate()(FirmwareUpdate)

127
src/components/ManagerPage/FirmwareUpdate.js

@ -0,0 +1,127 @@
// @flow
import React, { PureComponent } from 'react'
import isEqual from 'lodash/isEqual'
import isEmpty from 'lodash/isEmpty'
import type { Device, T } from 'types/common'
import runJob from 'renderer/runJob'
import getLatestFirmwareForDevice from 'commands/getLatestFirmwareForDevice'
import Box, { Card } from 'components/base/Box'
import Button from 'components/base/Button'
// let CACHED_LATEST_FIRMWARE = null
type FirmwareInfos = {
name: string,
notes: string,
}
type DeviceInfos = {
targetId: number,
version: string,
}
type Props = {
t: T,
device: Device,
infos: DeviceInfos,
}
type State = {
latestFirmware: ?FirmwareInfos,
}
class FirmwareUpdate extends PureComponent<Props, State> {
state = {
latestFirmware: null,
}
componentDidMount() {
this.fetchLatestFirmware()
}
componentDidUpdate() {
if (/* !CACHED_LATEST_FIRMWARE || */ isEmpty(this.state.latestFirmware)) {
this.fetchLatestFirmware()
}
}
componentWillUnmount() {
this._unmounting = true
}
_unmounting = false
fetchLatestFirmware = async () => {
const { infos } = this.props
const latestFirmware =
// CACHED_LATEST_FIRMWARE ||
await getLatestFirmwareForDevice
.send({ targetId: infos.targetId, version: infos.version })
.toPromise()
if (
!isEmpty(latestFirmware) &&
!isEqual(this.state.latestFirmware, latestFirmware) &&
!this._unmounting
) {
// CACHED_LATEST_FIRMWARE = latestFirmware
this.setState({ latestFirmware })
}
}
installFirmware = async () => {
try {
const { latestFirmware } = this.state
const {
device: { path: devicePath },
} = this.props
await runJob({
channel: 'manager',
job: 'installOsuFirmware',
successResponse: 'device.osuFirmwareInstallSuccess',
errorResponse: 'device.osuFirmwareInstallError',
data: {
devicePath,
firmware: latestFirmware,
},
})
} catch (err) {
console.log(err)
}
}
render() {
const { t, ...props } = this.props
const { latestFirmware } = this.state
if (!latestFirmware) {
return null
}
return (
<Box flow={4} {...props}>
<Box color="dark" ff="Museo Sans" fontSize={6}>
{t('manager:firmwareUpdate')}
</Box>
<Card flow={2} {...props}>
<Box horizontal align="center" flow={2}>
<Box ff="Museo Sans">{`Latest firmware: ${latestFirmware.name}`}</Box>
<Button outline onClick={this.installFirmware}>
{'Install'}
</Button>
</Box>
<Box
fontSize={3}
style={{ whiteSpace: 'pre' }}
dangerouslySetInnerHTML={{ __html: latestFirmware.notes }}
/>
</Card>
</Box>
)
}
}
export default FirmwareUpdate

0
src/components/ManagerPage/FlashMcu.js

100
src/components/ManagerPage/index.js

@ -1,55 +1,47 @@
// @flow
import React, { PureComponent, Fragment } from 'react'
import { connect } from 'react-redux'
import React, { Component, Fragment } from 'react'
import { translate } from 'react-i18next'
import { compose } from 'redux'
import type { Device, T } from 'types/common'
import type { T } from 'types/common'
import { getCurrentDevice, getDevices } from 'reducers/devices'
import Pills from 'components/base/Pills'
// import Pills from 'components/base/Pills'
import AppsList from './AppsList'
import DeviceInfos from './DeviceInfos'
const mapStateToProps = state => ({
device: getCurrentDevice(state),
nbDevices: getDevices(state).length,
})
// import DeviceInfos from './DeviceInfos'
// import FirmwareUpdate from './FirmwareUpdate'
import EnsureDevice from './EnsureDevice'
import EnsureDashboard from './EnsureDashboard'
import EnsureGenuine from './EnsureGenuine'
const TABS = [{ key: 'apps', value: 'apps' }, { key: 'device', value: 'device' }]
type Props = {
t: T,
device: Device,
nbDevices: number,
}
type State = {
currentTab: 'apps' | 'device',
// currentTab: 'apps' | 'device',
}
class ManagerPage extends PureComponent<Props, State> {
state = {
currentTab: 'apps',
}
class ManagerPage extends Component<Props, State> {
// state = {
// currentTab: 'apps',
// }
componentWillReceiveProps(nextProps) {
const { device } = this.props
const { currentTab } = this.state
if (device && !nextProps.device && currentTab === 'device') {
this.setState({ currentTab: 'apps' })
}
}
// componentWillReceiveProps(nextProps) {
// const { device } = this.props
// const { currentTab } = this.state
// if (device && !nextProps.device && currentTab === 'device') {
// this.setState({ currentTab: 'apps' })
// }
// }
handleTabChange = t => this.setState({ currentTab: t.value })
// handleTabChange = t => this.setState({ currentTab: t.value })
render() {
const { device, t, nbDevices } = this.props
const { currentTab } = this.state
const tabs = TABS.map(i => {
createTabs = (device, nbDevices) => {
const { t } = this.props
return TABS.map(i => {
let label = t(`manager:tabs.${i.key}`)
if (i.key === 'device') {
if (!device) {
@ -59,14 +51,50 @@ class ManagerPage extends PureComponent<Props, State> {
}
return { ...i, label }
}).filter(Boolean)
}
render() {
const { t } = this.props
// const { currentTab } = this.state
return (
<Fragment>
<Pills items={tabs} activeKey={currentTab} onChange={this.handleTabChange} mb={6} />
{currentTab === 'apps' && <AppsList device={device} />}
{currentTab === 'device' && <DeviceInfos device={device} />}
<EnsureDevice>
{device => (
<EnsureDashboard device={device}>
{deviceInfo => (
<Fragment>
{/* <Pills
items={this.createTabs(device, nbDevices)}
activeKey={currentTab}
onChange={this.handleTabChange}
mb={6}
/> */}
{deviceInfo.mcu && <span>bootloader mode</span>}
{deviceInfo.final && <span>osu mode</span>}
{!deviceInfo.mcu &&
!deviceInfo.final && (
<EnsureGenuine device={device} t={t}>
{/* <FirmwareUpdate
infos={{
targetId: deviceInfo.targetId,
version: deviceInfo.version,
}}
device={device}
t={t}
/> */}
<AppsList device={device} />
</EnsureGenuine>
)}
</Fragment>
)}
</EnsureDashboard>
)}
</EnsureDevice>
</Fragment>
)
}
}
export default compose(translate(), connect(mapStateToProps))(ManagerPage)
export default translate()(ManagerPage)

16
src/helpers/apps/installApp.js

@ -0,0 +1,16 @@
// @flow
import type Transport from '@ledgerhq/hw-transport'
import { createSocketDialog } from 'helpers/common'
import type { LedgerScriptParams } from 'helpers/common'
/**
* Install an app on the device
*/
export default async function installApp(
transport: Transport<*>,
{ appParams }: { appParams: LedgerScriptParams },
): Promise<void> {
return createSocketDialog(transport, '/update/install', appParams)
}

14
src/helpers/apps/listApps.js

@ -0,0 +1,14 @@
// @flow
import axios from 'axios'
export default async () => {
try {
const { data } = await axios.get('https://api.ledgerwallet.com/update/applications')
return data['nanos-1.4']
} catch (err) {
const error = Error(err.message)
error.stack = err.stack
throw err
}
}

12
src/helpers/apps/uninstallApp.js

@ -0,0 +1,12 @@
// @flow
import type { IPCSend } from 'types/electron'
// import { createTransportHandler, uninstallApp } from 'helpers/common'
// export default (send: IPCSend, data: any) =>
// createTransportHandler(send, {
// action: uninstallApp,
// successResponse: 'manager.appUninstalled',
// errorResponse: 'manager.appUninstallError',
// })(data)

236
src/helpers/common.js

@ -0,0 +1,236 @@
// @flow
import chalk from 'chalk'
import Websocket from 'ws'
import qs from 'qs'
import type Transport from '@ledgerhq/hw-transport'
import { BASE_SOCKET_URL, APDUS } from './constants'
type WebsocketType = {
send: (string, any) => void,
on: (string, Function) => void,
}
type Message = {
nonce: number,
query?: string,
response?: string,
data: any,
}
export type LedgerScriptParams = {
firmware?: string,
firmwareKey?: string,
delete?: string,
deleteKey?: string,
}
type FirmwareUpdateType = 'osu' | 'final'
// /**
// * Install an app on the device
// */
// export async function installApp(
// transport: Transport<*>,
// { appParams }: { appParams: LedgerScriptParams },
// ): Promise<void> {
// return createSocketDialog(transport, '/update/install', appParams)
// }
/**
* Uninstall an app on the device
*/
export async function uninstallApp(
transport: Transport<*>,
{ appParams }: { appParams: LedgerScriptParams },
): Promise<void> {
return createSocketDialog(transport, '/update/install', {
...appParams,
firmware: appParams.delete,
firmwareKey: appParams.deleteKey,
})
}
export async function getMemInfos(transport: Transport<*>): Promise<Object> {
const { targetId } = await getFirmwareInfo(transport)
// Dont ask me about this `perso_11`: I don't know. But we need it.
return createSocketDialog(transport, '/get-mem-infos', { targetId, perso: 'perso_11' })
}
/**
* Send data through ws
*/
function socketSend(ws: WebsocketType, msg: Message) {
logWS('SEND', msg)
const strMsg = JSON.stringify(msg)
ws.send(strMsg)
}
/**
* Exchange data on transport
*/
export async function exchange(
ws: WebsocketType,
transport: Transport<*>,
msg: Message,
): Promise<void> {
const { data, nonce } = msg
const r: Buffer = await transport.exchange(Buffer.from(data, 'hex'))
const status = r.slice(r.length - 2)
const buffer = r.slice(0, r.length - 2)
const strStatus = status.toString('hex')
socketSend(ws, {
nonce,
response: strStatus === '9000' ? 'success' : 'error',
data: buffer.toString('hex'),
})
}
/**
* Bulk update on transport
*/
export async function bulk(ws: WebsocketType, transport: Transport<*>, msg: Message) {
const { data, nonce } = msg
// Execute all apdus and collect last status
let lastStatus = null
for (const apdu of data) {
const r: Buffer = await transport.exchange(Buffer.from(apdu, 'hex'))
lastStatus = r.slice(r.length - 2)
}
if (!lastStatus) {
throw new Error('No status collected from bulk')
}
const strStatus = lastStatus.toString('hex')
socketSend(ws, {
nonce,
response: strStatus === '9000' ? 'success' : 'error',
data: strStatus === '9000' ? '' : strStatus,
})
}
/**
* Open socket connection with firmware api, and init a dialog
* with the device
*/
export async function createSocketDialog(
transport: Transport<*>,
endpoint: string,
params: LedgerScriptParams,
) {
return new Promise(async (resolve, reject) => {
try {
let lastData
const url = `${BASE_SOCKET_URL}${endpoint}?${qs.stringify(params)}`
log('WS CONNECTING', url)
const ws: WebsocketType = new Websocket(url)
ws.on('open', () => log('WS CONNECTED'))
ws.on('close', () => {
log('WS CLOSED')
resolve(lastData)
})
ws.on('message', async rawMsg => {
const handlers = {
exchange: msg => exchange(ws, transport, msg),
bulk: msg => bulk(ws, transport, msg),
success: msg => {
if (msg.data) {
lastData = msg.data
}
},
error: msg => {
log('WS ERROR', ':(')
throw new Error(msg.data)
},
}
try {
const msg = JSON.parse(rawMsg)
if (!(msg.query in handlers)) {
throw new Error(`Cannot handle msg of type ${msg.query}`)
}
logWS('RECEIVE', msg)
await handlers[msg.query](msg)
} catch (err) {
log('ERROR', err.toString())
reject(err)
}
})
} catch (err) {
reject(err)
}
})
}
/**
* Retrieve targetId and firmware version from device
*/
export async function getFirmwareInfo(transport: Transport<*>) {
try {
const res = await transport.send(...APDUS.GET_FIRMWARE)
const byteArray = [...res]
const data = byteArray.slice(0, byteArray.length - 2)
const targetIdStr = Buffer.from(data.slice(0, 4))
const targetId = targetIdStr.readUIntBE(0, 4)
const versionLength = data[4]
const version = Buffer.from(data.slice(5, 5 + versionLength)).toString()
return { targetId, version }
} catch (err) {
const error = new Error(err.message)
error.stack = err.stack
throw error
}
}
/**
* Debug helper
*/
export function log(namespace: string, str: string = '', color?: string) {
namespace = namespace.padEnd(15)
// $FlowFixMe
const coloredNamespace = color ? chalk[color](namespace) : namespace
if (__DEV__) {
console.log(`${chalk.bold(`> ${coloredNamespace}`)} ${str}`) // eslint-disable-line no-console
}
}
/**
* Log a socket send/receive
*/
export function logWS(type: string, msg: Message) {
const arrow = type === 'SEND' ? '↑' : '↓'
const namespace = `${arrow} WS ${type}`
const color = type === 'SEND' ? 'blue' : 'red'
if (msg.nonce) {
let d = ''
if (msg.query === 'exchange') {
d = msg.data.length > 100 ? `${msg.data.substr(0, 97)}...` : msg.data
} else if (msg.query === 'bulk') {
d = `[bulk x ${msg.data.length}]`
}
log(
namespace,
`${String(msg.nonce).padEnd(2)} ${(msg.response || msg.query || '').padEnd(10)} ${d}`,
color,
)
} else {
log(namespace, JSON.stringify(msg), color)
}
}
/**
* Helpers to build OSU and Final firmware params
*/
export const buildParamsFromFirmware = (type: FirmwareUpdateType): Function => (
data: any,
): LedgerScriptParams => ({
firmware: data[`${type}_firmware`],
firmwareKey: data[`${type}_firmware_key`],
perso: data[`${type}_perso`],
targetId: data[`${type}_target_id`],
})

13
src/helpers/constants.js

@ -0,0 +1,13 @@
// Socket endpoint
export const BASE_SOCKET_URL = 'ws://api.ledgerwallet.com'
// If you want to test locally with https://github.com/LedgerHQ/ledger-update-python-api
// export const BASE_SOCKET_URL = 'ws://localhost:3001/update'
// List of APDUS
export const APDUS = {
GET_FIRMWARE: [0xe0, 0x01, 0x00, 0x00],
// we dont have common call that works inside app & dashboard
// TODO: this should disappear.
GET_FIRMWARE_FALLBACK: [0xe0, 0xc4, 0x00, 0x00],
}

26
src/helpers/devices/getDeviceInfo.js

@ -0,0 +1,26 @@
// @flow
import type Transport from '@ledgerhq/hw-transport'
import { getFirmwareInfo } from 'helpers/common'
type Result = {
targetId: string | number,
version: string,
mcu: boolean,
final: boolean,
}
export default async (transport: Transport<*>): Promise<Result> => {
try {
const { targetId, version } = await getFirmwareInfo(transport)
const finalReady = version.endsWith('-osu')
const mcuReady = targetId === 0x01000001
return { targetId, version, final: finalReady, mcu: mcuReady }
} catch (err) {
const error = Error(err.message)
error.stack = err.stack
throw error
}
}

31
src/helpers/devices/getFirmwareInfo.js

@ -0,0 +1,31 @@
// @flow
import axios from 'axios'
import isEmpty from 'lodash/isEmpty'
const { API_BASE_URL } = process.env
type Input = {
version: string,
targetId: string | number,
}
let error
export default async (data: Input) => {
try {
const { data: seFirmwareVersion } = await axios.post(`${API_BASE_URL}/firmware_versions_name`, {
se_firmware_name: data.version,
target_id: data.targetId,
})
if (!isEmpty(seFirmwareVersion)) {
return seFirmwareVersion
}
error = Error('could not retrieve firmware informations, try again later')
throw error
} catch (err) {
error = Error(err.message)
error.stack = err.stack
throw error
}
}

5
src/helpers/devices/getIsGenuine.js

@ -0,0 +1,5 @@
// @flow
// import type Transport from '@ledgerhq/hw-transport'
export default async (/* transport: Transport<*> */) => new Promise(resolve => resolve(true))

43
src/helpers/devices/getLatestFirmwareForDevice.js

@ -0,0 +1,43 @@
// @flow
import axios from 'axios'
import isEmpty from 'lodash/isEmpty'
import getFirmwareInfo from './getFirmwareInfo'
const { API_BASE_URL } = process.env
type Input = {
targetId: string | number,
version: string,
}
export default async (data: Input) => {
try {
// Get firmware infos with firmware name and device version
const seFirmwareVersion = await getFirmwareInfo(data)
// Get device infos from targetId
const { data: deviceVersion } = await axios.get(
`${API_BASE_URL}/device_versions_target_id/${data.targetId}`,
)
// Fetch next possible firmware
const { data: serverData } = await axios.post(`${API_BASE_URL}/get_latest_firmware`, {
current_se_firmware_version: seFirmwareVersion.id,
device_version: deviceVersion.id,
providers: [1],
})
const { se_firmware_version } = serverData
if (!isEmpty(se_firmware_version)) {
return se_firmware_version
}
return null
} catch (err) {
const error = Error(err.message)
error.stack = err.stack
throw error
}
}

24
src/helpers/firmware/installFinalFirmware.js

@ -0,0 +1,24 @@
// @flow
import CommNodeHid from '@ledgerhq/hw-transport-node-hid'
import type { IPCSend } from 'types/electron'
import { createSocketDialog, buildParamsFromFirmware } from 'helpers/common'
type DataType = {
devicePath: string,
firmware: Object,
}
const buildFinalParams = buildParamsFromFirmware('final')
export default async (send: IPCSend, data: DataType) => {
try {
const transport = await CommNodeHid.open(data.devicePath)
const finalData = buildFinalParams(data.firmware)
await createSocketDialog(transport, '/update/install', finalData)
send('device.finalFirmwareInstallSuccess', { success: true })
} catch (err) {
send('device.finalFirmwareInstallError', { success: false })
}
}

1
src/helpers/firmware/installMcu.js

@ -0,0 +1 @@
// flow

27
src/helpers/firmware/installOsuFirmware.js

@ -0,0 +1,27 @@
// @flow
import type Transport from '@ledgerhq/hw-transport'
import { createSocketDialog, buildParamsFromFirmware } from 'helpers/common'
type Input = {
devicePath: string,
firmware: Object,
}
type Result = *
const buildOsuParams = buildParamsFromFirmware('osu')
export default async (transport: Transport<*>, data: Input): Result => {
try {
const osuData = buildOsuParams(data.firmware)
await createSocketDialog(transport, '/update/install', osuData)
return { success: true }
} catch (err) {
const error = Error(err.message)
error.stack = err.stack
const result = { success: false, error }
throw result
}
}

15
src/internals/devices/index.js

@ -3,9 +3,22 @@ import type { Command } from 'helpers/ipc'
import getAddress from 'commands/getAddress'
import signTransaction from 'commands/signTransaction'
import getDeviceInfo from 'commands/getDeviceInfo'
import getFirmwareInfo from 'commands/getFirmwareInfo'
import getIsGenuine from 'commands/getIsGenuine'
import getLatestFirmwareForDevice from 'commands/getLatestFirmwareForDevice'
import installApp from 'commands/installApp'
import listen from './listen'
// TODO port these to commands
export { listen }
export const commands: Array<Command<any, any>> = [getAddress, signTransaction]
export const commands: Array<Command<any, any>> = [
getAddress,
signTransaction,
getDeviceInfo,
getFirmwareInfo,
getIsGenuine,
getLatestFirmwareForDevice,
installApp,
]

2
src/internals/manager/constants.js

@ -1,6 +1,6 @@
// Socket endpoint
export const BASE_SOCKET_URL = 'ws://api.ledgerwallet.com/update'
export const BASE_SOCKET_URL = 'ws://api.ledgerwallet.com'
// If you want to test locally with https://github.com/LedgerHQ/ledger-update-python-api
// export const BASE_SOCKET_URL = 'ws://localhost:3001/update'

70
src/internals/manager/helpers.js

@ -9,6 +9,8 @@ import type Transport from '@ledgerhq/hw-transport'
import type { IPCSend } from 'types/electron'
import { BASE_SOCKET_URL, APDUS } from './constants'
// TODO: REMOVE FILE WHEN REFACTO IS OVER
type WebsocketType = {
send: (string, any) => void,
on: (string, Function) => void,
@ -21,7 +23,7 @@ type Message = {
data: any,
}
type LedgerAppParams = {
type LedgerScriptParams = {
firmware?: string,
firmwareKey?: string,
delete?: string,
@ -63,30 +65,6 @@ export function createTransportHandler(
}
}
/**
* Install an app on the device
*/
export async function installApp(
transport: Transport<*>,
{ appParams }: { appParams: LedgerAppParams },
): Promise<void> {
return createSocketDialog(transport, '/install', appParams)
}
/**
* Uninstall an app on the device
*/
export async function uninstallApp(
transport: Transport<*>,
{ appParams }: { appParams: LedgerAppParams },
): Promise<void> {
return createSocketDialog(transport, '/install', {
...appParams,
firmware: appParams.delete,
firmwareKey: appParams.deleteKey,
})
}
export async function getMemInfos(transport: Transport<*>): Promise<Object> {
const { targetId } = await getFirmwareInfo(transport)
// Dont ask me about this `perso_11`: I don't know. But we need it.
@ -105,7 +83,11 @@ function socketSend(ws: WebsocketType, msg: Message) {
/**
* Exchange data on transport
*/
async function exchange(ws: WebsocketType, transport: Transport<*>, msg: Message): Promise<void> {
export async function exchange(
ws: WebsocketType,
transport: Transport<*>,
msg: Message,
): Promise<void> {
const { data, nonce } = msg
const r: Buffer = await transport.exchange(Buffer.from(data, 'hex'))
const status = r.slice(r.length - 2)
@ -121,7 +103,7 @@ async function exchange(ws: WebsocketType, transport: Transport<*>, msg: Message
/**
* Bulk update on transport
*/
async function bulk(ws: WebsocketType, transport: Transport<*>, msg: Message) {
export async function bulk(ws: WebsocketType, transport: Transport<*>, msg: Message) {
const { data, nonce } = msg
// Execute all apdus and collect last status
@ -146,11 +128,15 @@ async function bulk(ws: WebsocketType, transport: Transport<*>, msg: Message) {
* Open socket connection with firmware api, and init a dialog
* with the device
*/
function createSocketDialog(transport: Transport<*>, endpoint: string, appParams: LedgerAppParams) {
export async function createSocketDialog(
transport: Transport<*>,
endpoint: string,
params: LedgerScriptParams,
) {
return new Promise(async (resolve, reject) => {
try {
let lastData
const url = `${BASE_SOCKET_URL}${endpoint}?${qs.stringify(appParams)}`
const url = `${BASE_SOCKET_URL}${endpoint}?${qs.stringify(params)}`
log('WS CONNECTING', url)
const ws: WebsocketType = new Websocket(url)
@ -198,20 +184,26 @@ function createSocketDialog(transport: Transport<*>, endpoint: string, appParams
* Retrieve targetId and firmware version from device
*/
export async function getFirmwareInfo(transport: Transport<*>) {
const res = await transport.send(...APDUS.GET_FIRMWARE)
const byteArray = [...res]
const data = byteArray.slice(0, byteArray.length - 2)
const targetIdStr = Buffer.from(data.slice(0, 4))
const targetId = targetIdStr.readUIntBE(0, 4)
const versionLength = data[4]
const version = Buffer.from(data.slice(5, 5 + versionLength)).toString()
return { targetId, version }
try {
const res = await transport.send(...APDUS.GET_FIRMWARE)
const byteArray = [...res]
const data = byteArray.slice(0, byteArray.length - 2)
const targetIdStr = Buffer.from(data.slice(0, 4))
const targetId = targetIdStr.readUIntBE(0, 4)
const versionLength = data[4]
const version = Buffer.from(data.slice(5, 5 + versionLength)).toString()
return { targetId, version }
} catch (err) {
const error = new Error(err.message)
error.stack = err.stack
throw error
}
}
/**
* Debug helper
*/
function log(namespace: string, str: string = '', color?: string) {
export function log(namespace: string, str: string = '', color?: string) {
namespace = namespace.padEnd(15)
// $FlowFixMe
const coloredNamespace = color ? chalk[color](namespace) : namespace
@ -223,7 +215,7 @@ function log(namespace: string, str: string = '', color?: string) {
/**
* Log a socket send/receive
*/
function logWS(type: string, msg: Message) {
export function logWS(type: string, msg: Message) {
const arrow = type === 'SEND' ? '↑' : '↓'
const namespace = `${arrow} WS ${type}`
const color = type === 'SEND' ? 'blue' : 'red'

8
src/internals/manager/index.js

@ -1,4 +1,7 @@
// @flow
import type { Command } from 'helpers/ipc'
import listApps from 'commands/listApps'
/**
* Manager
@ -17,6 +20,5 @@
*/
export { default as getMemInfos } from './getMemInfos'
export { default as installApp } from './installApp'
export { default as listApps } from './listApps'
export { default as uninstallApp } from './uninstallApp'
export const commands: Array<Command<any, any>> = [listApps]

12
src/internals/manager/installApp.js

@ -1,12 +0,0 @@
// @flow
import type { IPCSend } from 'types/electron'
import { createTransportHandler, installApp } from './helpers'
export default (send: IPCSend, data: any) =>
createTransportHandler(send, {
action: installApp,
successResponse: 'manager.appInstalled',
errorResponse: 'manager.appInstallError',
})(data)

14
src/internals/manager/listApps.js

@ -1,14 +0,0 @@
// @flow
import axios from 'axios'
import type { IPCSend } from 'types/electron'
export default async (send: IPCSend) => {
try {
const { data } = await axios.get('https://api.ledgerwallet.com/update/applications')
send('manager.listAppsSuccess', data['nanos-1.4'])
} catch (err) {
send('manager.listAppsError', { message: err.message, stack: err.stack })
}
}

12
src/internals/manager/uninstallApp.js

@ -1,12 +0,0 @@
// @flow
import type { IPCSend } from 'types/electron'
import { createTransportHandler, uninstallApp } from './helpers'
export default (send: IPCSend, data: any) =>
createTransportHandler(send, {
action: uninstallApp,
successResponse: 'manager.appUninstalled',
errorResponse: 'manager.appUninstallError',
})(data)
Loading…
Cancel
Save