diff --git a/README.md b/README.md index f63d9880c..75c47fc5a 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ Welcome to Ledger's JavaScript libraries. - [![npm](https://img.shields.io/npm/v/@ledgerhq/hw-transport-webusb.svg)](https://www.npmjs.com/package/@ledgerhq/hw-transport-webusb) [@ledgerhq/hw-transport-webusb](./packages/hw-transport-webusb) **[Web]** **(WebUSB)** (experimental) – WebUSB [check browser support](https://caniuse.com/webusb). - [![npm](https://img.shields.io/npm/v/@ledgerhq/hw-transport-web-ble.svg)](https://www.npmjs.com/package/@ledgerhq/hw-transport-web-ble) [@ledgerhq/hw-transport-web-ble](./packages/hw-transport-web-ble) **[Web]** **(Bluetooth)** – [check browser support](https://caniuse.com/web-bluetooth). - [![npm](https://img.shields.io/npm/v/@ledgerhq/hw-transport-node-hid.svg)](https://www.npmjs.com/package/@ledgerhq/hw-transport-node-hid) [@ledgerhq/hw-transport-node-hid](./packages/hw-transport-node-hid) **[Node]**/Electron **(HID)** – uses `node-hid` and `usb`. +- [![npm](https://img.shields.io/npm/v/@ledgerhq/hw-transport-node-hid-noevents.svg)](https://www.npmjs.com/package/@ledgerhq/hw-transport-node-hid-noevents) [@ledgerhq/hw-transport-node-hid-noevents](./packages/hw-transport-node-hid-noevents) **[Node]**/Electron **(HID)** – uses **only** `node-hid`. Does not provide USB events. - [![npm](https://img.shields.io/npm/v/@ledgerhq/react-native-hw-transport-ble.svg)](https://www.npmjs.com/package/@ledgerhq/react-native-hw-transport-ble) [@ledgerhq/react-native-hw-transport-ble](./packages/react-native-hw-transport-ble) **[React Native]** **(Bluetooth)** – uses `react-native-ble-plx` - [![npm](https://img.shields.io/npm/v/@ledgerhq/react-native-hid.svg)](https://www.npmjs.com/package/@ledgerhq/react-native-hid) [@ledgerhq/react-native-hid](./packages/react-native-hid) **[React Native]** **(HID)** _Android_ – Ledger's native implementation - [![npm](https://img.shields.io/npm/v/@ledgerhq/hw-transport-http.svg)](https://www.npmjs.com/package/@ledgerhq/hw-transport-http) [@ledgerhq/hw-transport-http](./packages/hw-transport-http) **[DEV only]** universal HTTP channel. **NOT for PROD**. diff --git a/packages/hw-transport-node-hid-noevents/.flowconfig b/packages/hw-transport-node-hid-noevents/.flowconfig new file mode 100644 index 000000000..43c5d124f --- /dev/null +++ b/packages/hw-transport-node-hid-noevents/.flowconfig @@ -0,0 +1,13 @@ +[ignore] +/lib + +[include] + +[libs] +flow-typed + +[lints] + +[options] + +[strict] diff --git a/packages/hw-transport-node-hid-noevents/README.md b/packages/hw-transport-node-hid-noevents/README.md new file mode 100644 index 000000000..a03cbbfd3 --- /dev/null +++ b/packages/hw-transport-node-hid-noevents/README.md @@ -0,0 +1,88 @@ + + +[Github](https://github.com/LedgerHQ/ledgerjs/), +[API Doc](http://ledgerhq.github.io/ledgerjs/), +[Ledger Devs Slack](https://ledger-dev.slack.com/) + +## @ledgerhq/hw-transport-node-hid-noevents + +Allows to communicate with Ledger Hardware Wallets. + +**[Node]**/Electron **(HID)** – uses **only** `node-hid`. Does not provide USB events. + +## API + + + +#### Table of Contents + +- [TransportNodeHidNoEvents](#transportnodehidnoevents) + - [Parameters](#parameters) + - [Examples](#examples) + - [exchange](#exchange) + - [Parameters](#parameters-1) + - [close](#close) + - [isSupported](#issupported) + - [list](#list) + - [listen](#listen) + - [Parameters](#parameters-2) + - [open](#open) + - [Parameters](#parameters-3) + +### TransportNodeHidNoEvents + +**Extends Transport** + +node-hid Transport minimal implementation + +#### Parameters + +- `device` **HID.HID** + +#### Examples + +```javascript +import TransportNodeHid from "@ledgerhq/hw-transport-node-hid-noevents"; +... +TransportNodeHid.create().then(transport => ...) +``` + +#### exchange + +Exchange with the device using APDU protocol. + +##### Parameters + +- `apdu` **[Buffer](https://nodejs.org/api/buffer.html)** + +Returns **[Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)<[Buffer](https://nodejs.org/api/buffer.html)>** a promise of apdu response + +#### close + +release the USB device. + +Returns **[Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)<void>** + +#### isSupported + +Returns **[Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)<[boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)>** + +#### list + +Returns **[Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)<[Array](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array)<[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)?>>** + +#### listen + +##### Parameters + +- `observer` **Observer<DescriptorEvent<[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)?>>** + +Returns **Subscription** + +#### open + +if path="" is not provided, the library will take the first device + +##### Parameters + +- `path` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)?** diff --git a/packages/hw-transport-node-hid-noevents/package.json b/packages/hw-transport-node-hid-noevents/package.json new file mode 100644 index 000000000..f65cd1c44 --- /dev/null +++ b/packages/hw-transport-node-hid-noevents/package.json @@ -0,0 +1,44 @@ +{ + "name": "@ledgerhq/hw-transport-node-hid-noevents", + "version": "4.48.0", + "description": "Ledger Hardware Wallet Node implementation of the communication layer, using node-hid. without usb events", + "keywords": [ + "Ledger", + "LedgerWallet", + "hid", + "node-hid", + "NanoS", + "Blue", + "Hardware Wallet" + ], + "repository": { + "type": "git", + "url": "https://github.com/LedgerHQ/ledgerjs" + }, + "bugs": { + "url": "https://github.com/LedgerHQ/ledgerjs/issues" + }, + "homepage": "https://github.com/LedgerHQ/ledgerjs", + "publishConfig": { + "access": "public" + }, + "main": "lib/TransportNodeHid.js", + "license": "Apache-2.0", + "dependencies": { + "@ledgerhq/devices": "^4.48.0", + "@ledgerhq/errors": "^4.48.0", + "@ledgerhq/hw-transport": "^4.48.0", + "node-hid": "^0.7.7" + }, + "devDependencies": { + "flow-bin": "^0.94.0", + "flow-typed": "^2.5.1" + }, + "scripts": { + "flow": "flow", + "clean": "bash ../../script/clean.sh", + "build": "bash ../../script/build.sh", + "watch": "bash ../../script/watch.sh", + "doc": "bash ../../script/doc.sh" + } +} diff --git a/packages/hw-transport-node-hid-noevents/src/TransportNodeHid.js b/packages/hw-transport-node-hid-noevents/src/TransportNodeHid.js new file mode 100644 index 000000000..e4beefb09 --- /dev/null +++ b/packages/hw-transport-node-hid-noevents/src/TransportNodeHid.js @@ -0,0 +1,185 @@ +//@flow + +import HID from "node-hid"; +import Transport from "@ledgerhq/hw-transport"; +import type { + Observer, + DescriptorEvent, + Subscription +} from "@ledgerhq/hw-transport"; +import { ledgerUSBVendorId } from "@ledgerhq/devices"; +import hidFraming from "@ledgerhq/devices/lib/hid-framing"; +import { identifyUSBProductId } from "@ledgerhq/devices"; +import type { DeviceModel } from "@ledgerhq/devices"; +import { TransportError, DisconnectedDevice } from "@ledgerhq/errors"; + +const filterInterface = device => + ["win32", "darwin"].includes(process.platform) + ? // $FlowFixMe + device.usagePage === 0xffa0 + : device.interface === 0; + +function getDevices(): Array<*> { + // $FlowFixMe + return HID.devices(ledgerUSBVendorId, 0x0).filter(filterInterface); +} + +const isDisconnectedError = e => + e && e.message && e.message.indexOf("HID") >= 0; + +/** + * node-hid Transport minimal implementation + * @example + * import TransportNodeHid from "@ledgerhq/hw-transport-node-hid-noevents"; + * ... + * TransportNodeHid.create().then(transport => ...) + */ +export default class TransportNodeHidNoEvents extends Transport { + /** + * + */ + static isSupported = (): Promise => + Promise.resolve(typeof HID.HID === "function"); + + /** + * + */ + static list = (): Promise<(?string)[]> => + Promise.resolve(getDevices().map(d => d.path)); + + /** + */ + static listen = ( + observer: Observer> + ): Subscription => { + getDevices().forEach(device => { + const deviceModel = identifyUSBProductId(device.productId); + observer.next({ + type: "add", + descriptor: device.path, + deviceModel, + device + }); + }); + observer.complete(); + return { unsubscribe: () => {} }; + }; + + /** + * if path="" is not provided, the library will take the first device + */ + static async open(path: ?string) { + if (path) { + return Promise.resolve(new TransportNodeHidNoEvents(new HID.HID(path))); + } + const device = getDevices()[0]; + if (!device) throw new TransportError("NoDevice", "NoDevice"); + return Promise.resolve( + new TransportNodeHidNoEvents(new HID.HID(device.path)) + ); + } + + device: HID.HID; + deviceModel: ?DeviceModel; + + channel = Math.floor(Math.random() * 0xffff); + packetSize = 64; + disconnected = false; + + constructor(device: HID.HID) { + super(); + this.device = device; + const info = device.getDeviceInfo(); + this.deviceModel = + info && info.serialNumber + ? identifyUSBProductId(parseInt(info.serialNumber, 16)) + : null; + } + + setDisconnected = () => { + if (!this.disconnected) { + this.emit("disconnect"); + this.disconnected = true; + } + }; + + writeHID = (content: Buffer): Promise => { + const data = [0x00]; + for (let i = 0; i < content.length; i++) { + data.push(content[i]); + } + try { + this.device.write(data); + return Promise.resolve(); + } catch (e) { + if (isDisconnectedError(e)) { + this.setDisconnected(); + return Promise.reject(new DisconnectedDevice(e.message)); + } + return Promise.reject(e); + } + }; + + readHID = (): Promise => + new Promise((resolve, reject) => + this.device.read((e, res) => { + if (!res) { + return reject(new DisconnectedDevice()); + } + if (e) { + if (isDisconnectedError(e)) { + this.setDisconnected(); + return reject(new DisconnectedDevice(e.message)); + } + reject(e); + } else { + const buffer = Buffer.from(res); + resolve(buffer); + } + }) + ); + + /** + * Exchange with the device using APDU protocol. + * @param apdu + * @returns a promise of apdu response + */ + exchange = (apdu: Buffer): Promise => + this.exchangeAtomicImpl(async () => { + const { debug, channel, packetSize } = this; + if (debug) { + debug("=>" + apdu.toString("hex")); + } + + const framing = hidFraming(channel, packetSize); + + // Write... + const blocks = framing.makeBlocks(apdu); + for (let i = 0; i < blocks.length; i++) { + await this.writeHID(blocks[i]); + } + + // Read... + let result; + let acc; + while (!(result = framing.getReducedResult(acc))) { + const buffer = await this.readHID(); + acc = framing.reduceResponse(acc, buffer); + } + + if (debug) { + debug("<=" + result.toString("hex")); + } + return result; + }); + + setScrambleKey() {} + + /** + * release the USB device. + */ + async close(): Promise { + await this.exchangeBusyPromise; + this.device.close(); + } +}