1 |
|
2 |
|
3 | declare var __VERSION__: string;
|
4 |
|
5 | import {debugInOut} from '../debug-decorator';
|
6 |
|
7 | type TrezorDeviceInfo = {path: string};
|
8 |
|
9 | const TREZOR_DESC = {
|
10 | vendorId: 0x534c,
|
11 | productId: 0x0001,
|
12 | };
|
13 |
|
14 | const FORBIDDEN_DESCRIPTORS = [0xf1d0, 0xff01];
|
15 | const REPORT_ID = 63;
|
16 |
|
17 | function deviceToJson(device: ChromeHidDeviceInfo): TrezorDeviceInfo {
|
18 | return {
|
19 | path: device.deviceId.toString(),
|
20 | };
|
21 | }
|
22 |
|
23 | function 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 |
|
42 | function 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 |
|
58 | function 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 |
|
74 | function 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 |
|
91 |
|
92 | function 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 |
|
109 | function 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 |
|
129 | export 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 |
|
151 | export 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 |
|
169 | export 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 |
|
200 |
|
201 |
|
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 | }
|