UNPKG

6.38 kBPlain TextView Raw
1import Transport from "@ledgerhq/hw-transport";
2import type {
3 Observer,
4 DescriptorEvent,
5 Subscription,
6} from "@ledgerhq/hw-transport";
7import hidFraming from "@ledgerhq/devices/lib/hid-framing";
8import { identifyUSBProductId } from "@ledgerhq/devices";
9import type { DeviceModel } from "@ledgerhq/devices";
10import { log } from "@ledgerhq/logs";
11import {
12 TransportOpenUserCancelled,
13 TransportInterfaceNotAvailable,
14 TransportWebUSBGestureRequired,
15 DisconnectedDeviceDuringOperation,
16 DisconnectedDevice,
17} from "@ledgerhq/errors";
18import {
19 getLedgerDevices,
20 getFirstLedgerDevice,
21 requestLedgerDevice,
22 isSupported,
23} from "./webusb";
24
25const configurationValue = 1;
26const endpointNumber = 3;
27
28/**
29 * WebUSB Transport implementation
30 * @example
31 * import TransportWebUSB from "@ledgerhq/hw-transport-webusb";
32 * ...
33 * TransportWebUSB.create().then(transport => ...)
34 */
35export default class TransportWebUSB extends Transport {
36 device: USBDevice;
37 deviceModel: DeviceModel | null | undefined;
38 channel = Math.floor(Math.random() * 0xffff);
39 packetSize = 64;
40 interfaceNumber: number;
41
42 constructor(device: USBDevice, interfaceNumber: number) {
43 super();
44 this.device = device;
45 this.interfaceNumber = interfaceNumber;
46 this.deviceModel = identifyUSBProductId(device.productId);
47 }
48
49 /**
50 * Check if WebUSB transport is supported.
51 */
52 static isSupported = isSupported;
53
54 /**
55 * List the WebUSB devices that was previously authorized by the user.
56 */
57 static list = getLedgerDevices;
58
59 /**
60 * Actively listen to WebUSB devices and emit ONE device
61 * that was either accepted before, if not it will trigger the native permission UI.
62 *
63 * Important: it must be called in the context of a UI click!
64 */
65 static listen = (
66 observer: Observer<DescriptorEvent<USBDevice>>
67 ): Subscription => {
68 let unsubscribed = false;
69 getFirstLedgerDevice().then(
70 (device) => {
71 if (!unsubscribed) {
72 const deviceModel = identifyUSBProductId(device.productId);
73 observer.next({
74 type: "add",
75 descriptor: device,
76 deviceModel,
77 });
78 observer.complete();
79 }
80 },
81 (error) => {
82 if (
83 window.DOMException &&
84 error instanceof window.DOMException &&
85 error.code === 18
86 ) {
87 observer.error(new TransportWebUSBGestureRequired(error.message));
88 } else {
89 observer.error(new TransportOpenUserCancelled(error.message));
90 }
91 }
92 );
93
94 function unsubscribe() {
95 unsubscribed = true;
96 }
97
98 return {
99 unsubscribe,
100 };
101 };
102
103 /**
104 * Similar to create() except it will always display the device permission (even if some devices are already accepted).
105 */
106 static async request() {
107 const device = await requestLedgerDevice();
108 return TransportWebUSB.open(device);
109 }
110
111 /**
112 * Similar to create() except it will never display the device permission (it returns a Promise<?Transport>, null if it fails to find a device).
113 */
114 static async openConnected() {
115 const devices = await getLedgerDevices();
116 if (devices.length === 0) return null;
117 return TransportWebUSB.open(devices[0]);
118 }
119
120 /**
121 * Create a Ledger transport with a USBDevice
122 */
123 static async open(device: USBDevice) {
124 await device.open();
125
126 if (device.configuration === null) {
127 await device.selectConfiguration(configurationValue);
128 }
129
130 await gracefullyResetDevice(device);
131 const iface = device.configurations[0].interfaces.find(({ alternates }) =>
132 alternates.some((a) => a.interfaceClass === 255)
133 );
134
135 if (!iface) {
136 throw new TransportInterfaceNotAvailable(
137 "No WebUSB interface found for your Ledger device. Please upgrade firmware or contact techsupport."
138 );
139 }
140
141 const interfaceNumber = iface.interfaceNumber;
142
143 try {
144 await device.claimInterface(interfaceNumber);
145 } catch (e: any) {
146 await device.close();
147 throw new TransportInterfaceNotAvailable(e.message);
148 }
149
150 const transport = new TransportWebUSB(device, interfaceNumber);
151
152 const onDisconnect = (e) => {
153 if (device === e.device) {
154 // $FlowFixMe
155 navigator.usb.removeEventListener("disconnect", onDisconnect);
156
157 transport._emitDisconnect(new DisconnectedDevice());
158 }
159 };
160
161 // $FlowFixMe
162 navigator.usb.addEventListener("disconnect", onDisconnect);
163 return transport;
164 }
165
166 _disconnectEmitted = false;
167 _emitDisconnect = (e: Error) => {
168 if (this._disconnectEmitted) return;
169 this._disconnectEmitted = true;
170 this.emit("disconnect", e);
171 };
172
173 /**
174 * Release the transport device
175 */
176 async close(): Promise<void> {
177 await this.exchangeBusyPromise;
178 await this.device.releaseInterface(this.interfaceNumber);
179 await gracefullyResetDevice(this.device);
180 await this.device.close();
181 }
182
183 /**
184 * Exchange with the device using APDU protocol.
185 * @param apdu
186 * @returns a promise of apdu response
187 */
188 async exchange(apdu: Buffer): Promise<Buffer> {
189 const b = await this.exchangeAtomicImpl(async () => {
190 const { channel, packetSize } = this;
191 log("apdu", "=> " + apdu.toString("hex"));
192 const framing = hidFraming(channel, packetSize);
193 // Write...
194 const blocks = framing.makeBlocks(apdu);
195
196 for (let i = 0; i < blocks.length; i++) {
197 await this.device.transferOut(endpointNumber, blocks[i]);
198 }
199
200 // Read...
201 let result;
202 let acc;
203
204 while (!(result = framing.getReducedResult(acc))) {
205 const r = await this.device.transferIn(endpointNumber, packetSize);
206 // eslint-disable-next-line @typescript-eslint/ban-ts-comment
207 // @ts-ignore
208 const buffer = Buffer.from(r.data.buffer);
209 acc = framing.reduceResponse(acc, buffer);
210 }
211
212 log("apdu", "<= " + result.toString("hex"));
213 return result;
214 }).catch((e) => {
215 if (e && e.message && e.message.includes("disconnected")) {
216 this._emitDisconnect(e);
217
218 throw new DisconnectedDeviceDuringOperation(e.message);
219 }
220
221 throw e;
222 });
223
224 return b as Buffer;
225 }
226
227 setScrambleKey() {}
228}
229
230async function gracefullyResetDevice(device: USBDevice) {
231 try {
232 await device.reset();
233 } catch (err) {
234 console.warn(err);
235 }
236}