1 | import Transport from "@ledgerhq/hw-transport";
|
2 | import type {
|
3 | Observer,
|
4 | DescriptorEvent,
|
5 | Subscription,
|
6 | } from "@ledgerhq/hw-transport";
|
7 | import hidFraming from "@ledgerhq/devices/lib/hid-framing";
|
8 | import { identifyUSBProductId } from "@ledgerhq/devices";
|
9 | import type { DeviceModel } from "@ledgerhq/devices";
|
10 | import { log } from "@ledgerhq/logs";
|
11 | import {
|
12 | TransportOpenUserCancelled,
|
13 | TransportInterfaceNotAvailable,
|
14 | TransportWebUSBGestureRequired,
|
15 | DisconnectedDeviceDuringOperation,
|
16 | DisconnectedDevice,
|
17 | } from "@ledgerhq/errors";
|
18 | import {
|
19 | getLedgerDevices,
|
20 | getFirstLedgerDevice,
|
21 | requestLedgerDevice,
|
22 | isSupported,
|
23 | } from "./webusb";
|
24 |
|
25 | const configurationValue = 1;
|
26 | const endpointNumber = 3;
|
27 |
|
28 |
|
29 |
|
30 |
|
31 |
|
32 |
|
33 |
|
34 |
|
35 | export 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 |
|
51 |
|
52 | static isSupported = isSupported;
|
53 |
|
54 | |
55 |
|
56 |
|
57 | static list = getLedgerDevices;
|
58 |
|
59 | |
60 |
|
61 |
|
62 |
|
63 |
|
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 |
|
105 |
|
106 | static async request() {
|
107 | const device = await requestLedgerDevice();
|
108 | return TransportWebUSB.open(device);
|
109 | }
|
110 |
|
111 | |
112 |
|
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 |
|
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 |
|
155 | navigator.usb.removeEventListener("disconnect", onDisconnect);
|
156 |
|
157 | transport._emitDisconnect(new DisconnectedDevice());
|
158 | }
|
159 | };
|
160 |
|
161 |
|
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 |
|
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 |
|
185 |
|
186 |
|
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 |
|
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 |
|
201 | let result;
|
202 | let acc;
|
203 |
|
204 | while (!(result = framing.getReducedResult(acc))) {
|
205 | const r = await this.device.transferIn(endpointNumber, packetSize);
|
206 |
|
207 |
|
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 |
|
230 | async function gracefullyResetDevice(device: USBDevice) {
|
231 | try {
|
232 | await device.reset();
|
233 | } catch (err) {
|
234 | console.warn(err);
|
235 | }
|
236 | }
|