UNPKG

8.05 kBJavaScriptView Raw
1/* @flow */
2
3declare var __VERSION__: string;
4
5import {debugInOut} from '../debug-decorator';
6
7type TrezorDeviceInfo = {path: string};
8
9const TREZOR_DESC = {
10 vendorId: 0x534c,
11 productId: 0x0001,
12};
13
14const FORBIDDEN_DESCRIPTORS = [0xf1d0, 0xff01];
15const REPORT_ID = 63;
16
17function deviceToJson(device: ChromeHidDeviceInfo): TrezorDeviceInfo {
18 return {
19 path: device.deviceId.toString(),
20 };
21}
22
23function hidEnumerate(): Promise<Array<ChromeHidDeviceInfo>> {
24 return new Promise((resolve, reject) => {
25 try {
26 chrome.hid.getDevices(
27 TREZOR_DESC,
28 (devices: Array<ChromeHidDeviceInfo>): void => {
29 if (chrome.runtime.lastError) {
30 reject(new Error(chrome.runtime.lastError));
31 } else {
32 resolve(devices);
33 }
34 }
35 );
36 } catch (e) {
37 reject(e);
38 }
39 });
40}
41
42function hidSend(id: number, reportId: number, data: ArrayBuffer): Promise<void> {
43 return new Promise((resolve, reject) => {
44 try {
45 chrome.hid.send(id, reportId, data, () => {
46 if (chrome.runtime.lastError) {
47 reject(new Error(chrome.runtime.lastError));
48 } else {
49 resolve();
50 }
51 });
52 } catch (e) {
53 reject(e);
54 }
55 });
56}
57
58function hidReceive(id: number): Promise<{data: ArrayBuffer, reportId: number}> {
59 return new Promise((resolve, reject) => {
60 try {
61 chrome.hid.receive(id, (reportId: number, data: ArrayBuffer) => {
62 if (chrome.runtime.lastError) {
63 reject(chrome.runtime.lastError);
64 } else {
65 resolve({data, reportId});
66 }
67 });
68 } catch (e) {
69 reject(e);
70 }
71 });
72}
73
74function hidConnect(id: number): Promise<number> {
75 return new Promise((resolve, reject) => {
76 try {
77 chrome.hid.connect(id, (connection) => {
78 if (chrome.runtime.lastError) {
79 reject(chrome.runtime.lastError);
80 } else {
81 resolve(connection.connectionId);
82 }
83 });
84 } catch (e) {
85 reject(e);
86 }
87 });
88}
89
90// Disconnects from trezor.
91// First parameter is connection ID (*not* device ID!)
92function hidDisconnect(id: number): Promise<void> {
93 return new Promise((resolve, reject) => {
94 try {
95 chrome.hid.disconnect(id, () => {
96 if (chrome.runtime.lastError) {
97 reject(chrome.runtime.lastError);
98 } else {
99 resolve();
100 }
101 });
102 } catch (e) {
103 reject(e);
104 }
105 });
106}
107
108// encapsulating chrome's platform info into Promise API
109function platformInfo(): Promise<ChromePlatformInfo> {
110 return new Promise((resolve, reject) => {
111 try {
112 chrome.runtime.getPlatformInfo((info: ChromePlatformInfo) => {
113 if (chrome.runtime.lastError) {
114 reject(chrome.runtime.lastError);
115 } else {
116 if (info == null) {
117 reject(new Error(`info is null`));
118 } else {
119 resolve(info);
120 }
121 }
122 });
123 } catch (e) {
124 reject(e);
125 }
126 });
127}
128
129export function storageGet(key: string): Promise<any> {
130 return new Promise((resolve, reject) => {
131 try {
132 chrome.storage.local.get(key, (items) => {
133 if (chrome.runtime.lastError) {
134 reject(chrome.runtime.lastError);
135 } else {
136 if (items[key] === null || items[key] === undefined) {
137 resolve(null);
138 } else {
139 resolve(items[key]);
140 }
141 resolve(items);
142 }
143 });
144 } catch (e) {
145 reject(e);
146 }
147 });
148}
149
150// Set to storage
151export function storageSet(key:string, value:any): Promise<void> {
152 return new Promise((resolve, reject) => {
153 try {
154 const obj: {} = {};
155 obj[key] = value;
156 chrome.storage.local.set(obj, () => {
157 if (chrome.runtime.lastError) {
158 reject(chrome.runtime.lastError);
159 } else {
160 resolve(undefined);
161 }
162 });
163 } catch (e) {
164 reject(e);
165 }
166 });
167}
168
169export default class ChromeHidPlugin {
170 name: string = `ChromeHidPlugin`;
171
172 _hasReportId: {[id: string]: boolean} = {};
173
174 _udevError: boolean = false;
175 _isLinuxCached: ?boolean;
176
177 version: string = __VERSION__;
178 debug: boolean = false;
179
180 @debugInOut
181 init(debug: ?boolean): Promise<void> {
182 this.debug = !!debug;
183 try {
184 if (chrome.hid.getDevices == null) {
185 return Promise.reject(new Error(`chrome.hid.getDevices is null`));
186 } else {
187 return Promise.resolve();
188 }
189 } catch (e) {
190 return Promise.reject(e);
191 }
192 }
193
194 _catchUdevError(error: Error) {
195 let errMessage = error;
196 if (errMessage.message !== undefined) {
197 errMessage = error.message;
198 }
199 // A little heuristics.
200 // If error message is one of these and the type of original message is initialization, it's
201 // probably udev error.
202 if (errMessage === `Failed to open HID device.` || errMessage === `Transfer failed.`) {
203 this._udevError = true;
204 }
205 throw error;
206 }
207
208 _isLinux(): Promise<boolean> {
209 if (this._isLinuxCached != null) {
210 return Promise.resolve(this._isLinuxCached);
211 }
212 return platformInfo().then(info => {
213 const linux = info.os === `linux`;
214 this._isLinuxCached = linux;
215 return linux;
216 });
217 }
218
219 _isAfterInstall(): Promise<boolean> {
220 return storageGet(`afterInstall`).then((afterInstall) => {
221 return (afterInstall !== false);
222 });
223 }
224
225 showUdevError(): Promise<boolean> {
226 return this._isLinux().then(isLinux => {
227 if (!isLinux) {
228 return false;
229 }
230 return this._isAfterInstall().then(isAfterInstall => {
231 if (isAfterInstall) {
232 return true;
233 } else {
234 return this._udevError;
235 }
236 });
237 });
238 }
239
240 clearUdevError(): Promise<void> {
241 this._udevError = false;
242 return storageSet(`afterInstall`, true);
243 }
244
245 enumerate(): Promise<Array<TrezorDeviceInfo>> {
246 return hidEnumerate().then(devices => devices.filter(
247 device => !FORBIDDEN_DESCRIPTORS.some(des => des === device.collections[0].usagePage))
248 ).then(devices => {
249 this._hasReportId = {};
250
251 devices.forEach(device => {
252 this._hasReportId[device.deviceId.toString()] = device.collections[0].reportIds.length !== 0;
253 });
254
255 return devices;
256 }).then(devices => devices.map(device => deviceToJson(device)));
257 }
258
259 send(device: string, session: string, data: ArrayBuffer): Promise<void> {
260 const sessionNu = parseInt(session);
261 if (isNaN(sessionNu)) {
262 return Promise.reject(new Error(`Session ${session} is not a number`));
263 }
264 const hasReportId = this._hasReportId[device.toString()];
265 const reportId = hasReportId ? REPORT_ID : 0;
266
267 let ab: ArrayBuffer = data;
268 if (!hasReportId) {
269 const newArray: Uint8Array = new Uint8Array(64);
270 newArray[0] = 63;
271 newArray.set(new Uint8Array(data), 1);
272 ab = newArray.buffer;
273 }
274
275 return hidSend(sessionNu, reportId, ab).catch(e => this._catchUdevError(e));
276 }
277
278 receive(device: string, session: string): Promise<ArrayBuffer> {
279 const sessionNu = parseInt(session);
280 if (isNaN(sessionNu)) {
281 return Promise.reject(new Error(`Session ${session} is not a number`));
282 }
283 return hidReceive(sessionNu).then(({data, reportId}) => {
284 if (reportId !== 0) {
285 return data;
286 } else {
287 return data.slice(1);
288 }
289 }).then(res =>
290 this.clearUdevError().then(() => res)
291 ).catch(e => this._catchUdevError(e));
292 }
293
294 @debugInOut
295 connect(device: string): Promise<string> {
296 const deviceNu = parseInt(device);
297 if (isNaN(deviceNu)) {
298 return Promise.reject(new Error(`Device ${deviceNu} is not a number`));
299 }
300 return hidConnect(deviceNu).then(d => d.toString()).catch(e => this._catchUdevError(e));
301 }
302
303 @debugInOut
304 disconnect(path: string, session: string): Promise<void> {
305 const sessionNu = parseInt(session);
306 if (isNaN(sessionNu)) {
307 return Promise.reject(new Error(`Session ${session} is not a number`));
308 }
309 return hidDisconnect(sessionNu);
310 }
311}