• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (C) 2019 The Android Open Source Project
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//      http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15import {_TextDecoder, _TextEncoder} from 'custom_utils';
16
17import {assertExists} from '../base/logging';
18
19import {Adb, AdbMsg, AdbStream, CmdType} from './adb_interfaces';
20
21const textEncoder = new _TextEncoder();
22const textDecoder = new _TextDecoder();
23
24export const VERSION_WITH_CHECKSUM = 0x01000000;
25export const VERSION_NO_CHECKSUM = 0x01000001;
26export const DEFAULT_MAX_PAYLOAD_BYTES = 256 * 1024;
27
28export enum AdbState {
29  DISCONNECTED = 0,
30  // Authentication steps, see AdbOverWebUsb's handleAuthentication().
31  AUTH_STEP1 = 1,
32  AUTH_STEP2 = 2,
33  AUTH_STEP3 = 3,
34
35  CONNECTED = 2,
36}
37
38enum AuthCmd {
39  TOKEN = 1,
40  SIGNATURE = 2,
41  RSAPUBLICKEY = 3,
42}
43
44const DEVICE_NOT_SET_ERROR = 'Device not set.';
45
46// This class is a basic TypeScript implementation of adb that only supports
47// shell commands. It is used to send the start tracing command to the connected
48// android device, and to automatically pull the trace after the end of the
49// recording. It works through the webUSB API. A brief description of how it
50// works is the following:
51// - The connection with the device is initiated by findAndConnect, which shows
52//   a dialog with a list of connected devices. Once one is selected the
53//   authentication begins. The authentication has to pass different steps, as
54//   described in the "handeAuthentication" method.
55// - AdbOverWebUsb tracks the state of the authentication via a state machine
56//   (see AdbState).
57// - A Message handler loop is executed to keep receiving the messages.
58// - All the messages received from the device are passed to "onMessage" that is
59//   implemented as a state machine.
60// - When a new shell is established, it becomes an AdbStream, and is kept in
61//   the "streams" map. Each time a message from the device is for a specific
62//   previously opened stream, the "onMessage" function will forward it to the
63//   stream (identified by a number).
64export class AdbOverWebUsb implements Adb {
65  state: AdbState = AdbState.DISCONNECTED;
66  streams = new Map<number, AdbStream>();
67  devProps = '';
68  maxPayload = DEFAULT_MAX_PAYLOAD_BYTES;
69  key?: CryptoKeyPair;
70  onConnected = () => {};
71
72  // Devices after Dec 2017 don't use checksum. This will be auto-detected
73  // during the connection.
74  useChecksum = true;
75
76  private lastStreamId = 0;
77  private dev?: USBDevice;
78  private usbInterfaceNumber?: number;
79  private usbReadEndpoint = -1;
80  private usbWriteEpEndpoint = -1;
81  private filter = {
82    classCode: 255,    // USB vendor specific code
83    subclassCode: 66,  // Android vendor specific subclass
84    protocolCode: 1,   // Adb protocol
85  };
86
87  async findDevice() {
88    if (!('usb' in navigator)) {
89      throw new Error('WebUSB not supported by the browser (requires HTTPS)');
90    }
91    return navigator.usb.requestDevice({filters: [this.filter]});
92  }
93
94  async getPairedDevices() {
95    try {
96      return navigator.usb.getDevices();
97    } catch (e) {  // WebUSB not available.
98      return Promise.resolve([]);
99    }
100  }
101
102  async connect(device: USBDevice): Promise<void> {
103    // If we are already connected, we are also already authenticated, so we can
104    // skip doing the authentication again.
105    if (this.state === AdbState.CONNECTED) {
106      if (this.dev === device && device.opened) {
107        this.onConnected();
108        this.onConnected = () => {};
109        return;
110      }
111      // Another device was connected.
112      await this.disconnect();
113    }
114
115    this.dev = device;
116    this.useChecksum = true;
117    this.key = await AdbOverWebUsb.initKey();
118
119    await this.dev.open();
120
121    const {configValue, usbInterfaceNumber, endpoints} =
122        this.findInterfaceAndEndpoint();
123    this.usbInterfaceNumber = usbInterfaceNumber;
124
125    this.usbReadEndpoint = this.findEndpointNumber(endpoints, 'in');
126    this.usbWriteEpEndpoint = this.findEndpointNumber(endpoints, 'out');
127
128    console.assert(this.usbReadEndpoint >= 0 && this.usbWriteEpEndpoint >= 0);
129
130    await this.dev.selectConfiguration(configValue);
131    await this.dev.claimInterface(usbInterfaceNumber);
132
133    await this.startAuthentication();
134
135    // This will start a message handler loop.
136    this.receiveDeviceMessages();
137    // The promise will be resolved after the handshake.
138    return new Promise<void>((resolve, _) => this.onConnected = resolve);
139  }
140
141  async disconnect(): Promise<void> {
142    if (this.state === AdbState.DISCONNECTED) {
143      return;
144    }
145    this.state = AdbState.DISCONNECTED;
146
147    if (!this.dev) return;
148
149    new Map(this.streams).forEach((stream, _id) => stream.setClosed());
150    console.assert(this.streams.size === 0);
151
152    await this.dev.releaseInterface(assertExists(this.usbInterfaceNumber));
153    this.dev = undefined;
154    this.usbInterfaceNumber = undefined;
155  }
156
157  async startAuthentication() {
158    // USB connected, now let's authenticate.
159    const VERSION =
160        this.useChecksum ? VERSION_WITH_CHECKSUM : VERSION_NO_CHECKSUM;
161    this.state = AdbState.AUTH_STEP1;
162    await this.send('CNXN', VERSION, this.maxPayload, 'host:1:UsbADB');
163  }
164
165  findInterfaceAndEndpoint() {
166    if (!this.dev) throw Error(DEVICE_NOT_SET_ERROR);
167    for (const config of this.dev.configurations) {
168      for (const interface_ of config.interfaces) {
169        for (const alt of interface_.alternates) {
170          if (alt.interfaceClass === this.filter.classCode &&
171              alt.interfaceSubclass === this.filter.subclassCode &&
172              alt.interfaceProtocol === this.filter.protocolCode) {
173            return {
174              configValue: config.configurationValue,
175              usbInterfaceNumber: interface_.interfaceNumber,
176              endpoints: alt.endpoints,
177            };
178          }  // if (alternate)
179        }    // for (interface.alternates)
180      }      // for (configuration.interfaces)
181    }        // for (configurations)
182
183    throw Error('Cannot find interfaces and endpoints');
184  }
185
186  findEndpointNumber(
187      endpoints: USBEndpoint[], direction: 'out'|'in', type = 'bulk'): number {
188    const ep =
189        endpoints.find((ep) => ep.type === type && ep.direction === direction);
190
191    if (ep) return ep.endpointNumber;
192
193    throw Error(`Cannot find ${direction} endpoint`);
194  }
195
196  receiveDeviceMessages() {
197    this.recv()
198        .then((msg) => {
199          this.onMessage(msg);
200          this.receiveDeviceMessages();
201        })
202        .catch((e) => {
203          // Ignore error with "DEVICE_NOT_SET_ERROR" message since it is always
204          // thrown after the device disconnects.
205          if (e.message !== DEVICE_NOT_SET_ERROR) {
206            console.error(`Exception in recv: ${e.name}. error: ${e.message}`);
207          }
208          this.disconnect();
209        });
210  }
211
212  async onMessage(msg: AdbMsg) {
213    if (!this.key) throw Error('ADB key not initialized');
214
215    if (msg.cmd === 'AUTH' && msg.arg0 === AuthCmd.TOKEN) {
216      this.handleAuthentication(msg);
217    } else if (msg.cmd === 'CNXN') {
218      console.assert(
219          [AdbState.AUTH_STEP2, AdbState.AUTH_STEP3].includes(this.state));
220      this.state = AdbState.CONNECTED;
221      this.handleConnectedMessage(msg);
222    } else if (this.state === AdbState.CONNECTED && [
223                 'OKAY',
224                 'WRTE',
225                 'CLSE',
226               ].indexOf(msg.cmd) >= 0) {
227      const stream = this.streams.get(msg.arg1);
228      if (!stream) {
229        console.warn(`Received message ${msg} for unknown stream ${msg.arg1}`);
230        return;
231      }
232      stream.onMessage(msg);
233    } else {
234      console.error(`Unexpected message `, msg, ` in state ${this.state}`);
235    }
236  }
237
238  async handleAuthentication(msg: AdbMsg) {
239    if (!this.key) throw Error('ADB key not initialized');
240
241    console.assert(msg.cmd === 'AUTH' && msg.arg0 === AuthCmd.TOKEN);
242    const token = msg.data;
243
244    if (this.state === AdbState.AUTH_STEP1) {
245      // During this step, we send back the token received signed with our
246      // private key. If the device has previously received our public key, the
247      // dialog will not be displayed. Otherwise we will receive another message
248      // ending up in AUTH_STEP3.
249      this.state = AdbState.AUTH_STEP2;
250
251      const signedToken =
252          await signAdbTokenWithPrivateKey(this.key.privateKey, token);
253      this.send('AUTH', AuthCmd.SIGNATURE, 0, new Uint8Array(signedToken));
254      return;
255    }
256
257    console.assert(this.state === AdbState.AUTH_STEP2);
258
259    // During this step, we send our public key. The dialog will appear, and
260    // if the user chooses to remember our public key, it will be
261    // saved, so that the next time we will only pass through AUTH_STEP1.
262    this.state = AdbState.AUTH_STEP3;
263    const encodedPubKey = await encodePubKey(this.key.publicKey);
264    this.send('AUTH', AuthCmd.RSAPUBLICKEY, 0, encodedPubKey);
265  }
266
267  private handleConnectedMessage(msg: AdbMsg) {
268    console.assert(msg.cmd === 'CNXN');
269
270    this.maxPayload = msg.arg1;
271    this.devProps = textDecoder.decode(msg.data);
272
273    const deviceVersion = msg.arg0;
274
275    if (![VERSION_WITH_CHECKSUM, VERSION_NO_CHECKSUM].includes(deviceVersion)) {
276      console.error('Version ', msg.arg0, ' not really supported!');
277    }
278    this.useChecksum = deviceVersion === VERSION_WITH_CHECKSUM;
279    this.state = AdbState.CONNECTED;
280
281    // This will resolve the promise returned by "onConnect"
282    this.onConnected();
283    this.onConnected = () => {};
284  }
285
286  shell(cmd: string): Promise<AdbStream> {
287    return this.openStream('shell:' + cmd);
288  }
289
290  socket(path: string): Promise<AdbStream> {
291    return this.openStream('localfilesystem:' + path);
292  }
293
294  openStream(svc: string): Promise<AdbStream> {
295    const stream = new AdbStreamImpl(this, ++this.lastStreamId);
296    this.streams.set(stream.localStreamId, stream);
297    this.send('OPEN', stream.localStreamId, 0, svc);
298
299    //  The stream will resolve this promise once it receives the
300    //  acknowledgement message from the device.
301    return new Promise<AdbStream>((resolve, reject) => {
302      stream.onConnect = () => {
303        stream.onClose = () => {};
304        resolve(stream);
305      };
306      stream.onClose = () =>
307          reject(new Error(`Failed to openStream svc=${svc}`));
308    });
309  }
310
311  async shellOutputAsString(cmd: string): Promise<string> {
312    const shell = await this.shell(cmd);
313
314    return new Promise<string>((resolve, _) => {
315      const output: string[] = [];
316      shell.onData = (raw) => output.push(textDecoder.decode(raw));
317      shell.onClose = () => resolve(output.join());
318    });
319  }
320
321  async send(
322      cmd: CmdType, arg0: number, arg1: number, data?: Uint8Array|string) {
323    await this.sendMsg(AdbMsgImpl.create(
324        {cmd, arg0, arg1, data, useChecksum: this.useChecksum}));
325  }
326
327  //  The header and the message data must be sent consecutively. Using 2 awaits
328  //  Another message can interleave after the first header has been sent,
329  //  resulting in something like [header1] [header2] [data1] [data2];
330  //  In this way we are waiting both promises to be resolved before continuing.
331  async sendMsg(msg: AdbMsgImpl) {
332    const sendPromises = [this.sendRaw(msg.encodeHeader())];
333    if (msg.data.length > 0) sendPromises.push(this.sendRaw(msg.data));
334    await Promise.all(sendPromises);
335  }
336
337  async recv(): Promise<AdbMsg> {
338    const res = await this.recvRaw(ADB_MSG_SIZE);
339    console.assert(res.status === 'ok');
340    const msg = AdbMsgImpl.decodeHeader(res.data!);
341
342    if (msg.dataLen > 0) {
343      const resp = await this.recvRaw(msg.dataLen);
344      msg.data = new Uint8Array(
345          resp.data!.buffer, resp.data!.byteOffset, resp.data!.byteLength);
346    }
347    if (this.useChecksum) {
348      console.assert(AdbOverWebUsb.checksum(msg.data) === msg.dataChecksum);
349    }
350    return msg;
351  }
352
353  static async initKey(): Promise<CryptoKeyPair> {
354    const KEY_SIZE = 2048;
355
356    const keySpec = {
357      name: 'RSASSA-PKCS1-v1_5',
358      modulusLength: KEY_SIZE,
359      publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
360      hash: {name: 'SHA-1'},
361    };
362
363    const key = await crypto.subtle.generateKey(
364        keySpec, /* extractable=*/ true, ['sign', 'verify']);
365    return key;
366  }
367
368  static checksum(data: Uint8Array): number {
369    let res = 0;
370    for (let i = 0; i < data.byteLength; i++) res += data[i];
371    return res & 0xFFFFFFFF;
372  }
373
374  sendRaw(buf: Uint8Array): Promise<USBOutTransferResult> {
375    console.assert(buf.length <= this.maxPayload);
376    if (!this.dev) throw Error(DEVICE_NOT_SET_ERROR);
377    return this.dev.transferOut(this.usbWriteEpEndpoint, buf.buffer);
378  }
379
380  recvRaw(dataLen: number): Promise<USBInTransferResult> {
381    if (!this.dev) throw Error(DEVICE_NOT_SET_ERROR);
382    return this.dev.transferIn(this.usbReadEndpoint, dataLen);
383  }
384}
385
386enum AdbStreamState {
387  WAITING_INITIAL_OKAY = 0,
388  CONNECTED = 1,
389  CLOSED = 2
390}
391
392
393// An AdbStream is instantiated after the creation of a shell to the device.
394// Thanks to this, we can send commands and receive their output. Messages are
395// received in the main adb class, and are forwarded to an instance of this
396// class based on a stream id match. Also streams have an initialization flow:
397//   1. WAITING_INITIAL_OKAY: waiting for first "OKAY" message. Once received,
398//      the next state will be "CONNECTED".
399//   2. CONNECTED: ready to receive or send messages.
400//   3. WRITING: this is needed because we must receive an ack after sending
401//      each message (so, before sending the next one). For this reason, many
402//      subsequent "write" calls will result in different messages in the
403//      writeQueue. After each new acknowledgement ('OKAY') a new one will be
404//      sent. When the queue is empty, the state will return to CONNECTED.
405//   4. CLOSED: entered when the device closes the stream or close() is called.
406//      For shell commands, the stream is closed after the command completed.
407export class AdbStreamImpl implements AdbStream {
408  private adb: AdbOverWebUsb;
409  localStreamId: number;
410  private remoteStreamId = -1;
411  private state: AdbStreamState = AdbStreamState.WAITING_INITIAL_OKAY;
412  private writeQueue: Uint8Array[] = [];
413
414  private sendInProgress = false;
415
416  onData: AdbStreamReadCallback = (_) => {};
417  onConnect = () => {};
418  onClose = () => {};
419
420  constructor(adb: AdbOverWebUsb, localStreamId: number) {
421    this.adb = adb;
422    this.localStreamId = localStreamId;
423  }
424
425  close() {
426    console.assert(this.state === AdbStreamState.CONNECTED);
427
428    if (this.writeQueue.length > 0) {
429      console.error(`Dropping ${
430          this.writeQueue.length} queued messages due to stream closing.`);
431      this.writeQueue = [];
432    }
433
434    this.adb.send('CLSE', this.localStreamId, this.remoteStreamId);
435  }
436
437  async write(msg: string|Uint8Array) {
438    const raw = (typeof msg === 'string') ? textEncoder.encode(msg) : msg;
439    if (this.sendInProgress ||
440        this.state === AdbStreamState.WAITING_INITIAL_OKAY) {
441      this.writeQueue.push(raw);
442      return;
443    }
444    console.assert(this.state === AdbStreamState.CONNECTED);
445    this.sendInProgress = true;
446    await this.adb.send('WRTE', this.localStreamId, this.remoteStreamId, raw);
447  }
448
449  setClosed() {
450    this.state = AdbStreamState.CLOSED;
451    this.adb.streams.delete(this.localStreamId);
452    this.onClose();
453  }
454
455  onMessage(msg: AdbMsgImpl) {
456    console.assert(msg.arg1 === this.localStreamId);
457
458    if (this.state === AdbStreamState.WAITING_INITIAL_OKAY &&
459        msg.cmd === 'OKAY') {
460      this.remoteStreamId = msg.arg0;
461      this.state = AdbStreamState.CONNECTED;
462      this.onConnect();
463      return;
464    }
465
466    if (msg.cmd === 'WRTE') {
467      this.adb.send('OKAY', this.localStreamId, this.remoteStreamId);
468      this.onData(msg.data);
469      return;
470    }
471
472    if (msg.cmd === 'OKAY') {
473      console.assert(this.sendInProgress);
474      this.sendInProgress = false;
475      const queuedMsg = this.writeQueue.shift();
476      if (queuedMsg !== undefined) this.write(queuedMsg);
477      return;
478    }
479
480    if (msg.cmd === 'CLSE') {
481      this.setClosed();
482      return;
483    }
484    console.error(
485        `Unexpected stream msg ${msg.toString()} in state ${this.state}`);
486  }
487}
488
489interface AdbStreamReadCallback {
490  (raw: Uint8Array): void;
491}
492
493const ADB_MSG_SIZE = 6 * 4;  // 6 * int32.
494
495export class AdbMsgImpl implements AdbMsg {
496  cmd: CmdType;
497  arg0: number;
498  arg1: number;
499  data: Uint8Array;
500  dataLen: number;
501  dataChecksum: number;
502
503  useChecksum: boolean;
504
505  constructor(
506      cmd: CmdType, arg0: number, arg1: number, dataLen: number,
507      dataChecksum: number, useChecksum = false) {
508    console.assert(cmd.length === 4);
509    this.cmd = cmd;
510    this.arg0 = arg0;
511    this.arg1 = arg1;
512    this.dataLen = dataLen;
513    this.data = new Uint8Array(dataLen);
514    this.dataChecksum = dataChecksum;
515    this.useChecksum = useChecksum;
516  }
517
518
519  static create({cmd, arg0, arg1, data, useChecksum = true}: {
520    cmd: CmdType; arg0: number; arg1: number;
521    data?: Uint8Array | string;
522    useChecksum?: boolean;
523  }): AdbMsgImpl {
524    const encodedData = this.encodeData(data);
525    const msg =
526        new AdbMsgImpl(cmd, arg0, arg1, encodedData.length, 0, useChecksum);
527    msg.data = encodedData;
528    return msg;
529  }
530
531  get dataStr() {
532    return textDecoder.decode(this.data);
533  }
534
535  toString() {
536    return `${this.cmd} [${this.arg0},${this.arg1}] ${this.dataStr}`;
537  }
538
539  // A brief description of the message can be found here:
540  // https://android.googlesource.com/platform/system/core/+/master/adb/protocol.txt
541  //
542  // struct amessage {
543  //     uint32_t command;    // command identifier constant
544  //     uint32_t arg0;       // first argument
545  //     uint32_t arg1;       // second argument
546  //     uint32_t data_length;// length of payload (0 is allowed)
547  //     uint32_t data_check; // checksum of data payload
548  //     uint32_t magic;      // command ^ 0xffffffff
549  // };
550  static decodeHeader(dv: DataView): AdbMsgImpl {
551    console.assert(dv.byteLength === ADB_MSG_SIZE);
552    const cmd = textDecoder.decode(dv.buffer.slice(0, 4)) as CmdType;
553    const cmdNum = dv.getUint32(0, true);
554    const arg0 = dv.getUint32(4, true);
555    const arg1 = dv.getUint32(8, true);
556    const dataLen = dv.getUint32(12, true);
557    const dataChecksum = dv.getUint32(16, true);
558    const cmdChecksum = dv.getUint32(20, true);
559    console.assert(cmdNum === (cmdChecksum ^ 0xFFFFFFFF));
560    return new AdbMsgImpl(cmd, arg0, arg1, dataLen, dataChecksum);
561  }
562
563  encodeHeader(): Uint8Array {
564    const buf = new Uint8Array(ADB_MSG_SIZE);
565    const dv = new DataView(buf.buffer);
566    const cmdBytes: Uint8Array = textEncoder.encode(this.cmd);
567    const rawMsg = AdbMsgImpl.encodeData(this.data);
568    const checksum = this.useChecksum ? AdbOverWebUsb.checksum(rawMsg) : 0;
569    for (let i = 0; i < 4; i++) dv.setUint8(i, cmdBytes[i]);
570
571    dv.setUint32(4, this.arg0, true);
572    dv.setUint32(8, this.arg1, true);
573    dv.setUint32(12, rawMsg.byteLength, true);
574    dv.setUint32(16, checksum, true);
575    dv.setUint32(20, dv.getUint32(0, true) ^ 0xFFFFFFFF, true);
576
577    return buf;
578  }
579
580  static encodeData(data?: Uint8Array|string): Uint8Array {
581    if (data === undefined) return new Uint8Array([]);
582    if (typeof data === 'string') return textEncoder.encode(data + '\0');
583    return data;
584  }
585}
586
587
588function base64StringToArray(s: string) {
589  const decoded = atob(s.replace(/-/g, '+').replace(/_/g, '/'));
590  return [...decoded].map((char) => char.charCodeAt(0));
591}
592
593const ANDROID_PUBKEY_MODULUS_SIZE = 2048;
594const MODULUS_SIZE_BYTES = ANDROID_PUBKEY_MODULUS_SIZE / 8;
595
596// RSA Public keys are encoded in a rather unique way. It's a base64 encoded
597// struct of 524 bytes in total as follows (see
598// libcrypto_utils/android_pubkey.c):
599//
600// typedef struct RSAPublicKey {
601//   // Modulus length. This must be ANDROID_PUBKEY_MODULUS_SIZE.
602//   uint32_t modulus_size_words;
603//
604//   // Precomputed montgomery parameter: -1 / n[0] mod 2^32
605//   uint32_t n0inv;
606//
607//   // RSA modulus as a little-endian array.
608//   uint8_t modulus[ANDROID_PUBKEY_MODULUS_SIZE];
609//
610//   // Montgomery parameter R^2 as a little-endian array of little-endian
611//   words. uint8_t rr[ANDROID_PUBKEY_MODULUS_SIZE];
612//
613//   // RSA modulus: 3 or 65537
614//   uint32_t exponent;
615// } RSAPublicKey;
616//
617// However, the Montgomery params (n0inv and rr) are not really used, see
618// comment in android_pubkey_decode() ("Note that we don't extract the
619// montgomery parameters...")
620async function encodePubKey(key: CryptoKey) {
621  const expPubKey = await crypto.subtle.exportKey('jwk', key);
622  const nArr = base64StringToArray(expPubKey.n as string).reverse();
623  const eArr = base64StringToArray(expPubKey.e as string).reverse();
624
625  const arr = new Uint8Array(3 * 4 + 2 * MODULUS_SIZE_BYTES);
626  const dv = new DataView(arr.buffer);
627  dv.setUint32(0, MODULUS_SIZE_BYTES / 4, true);
628
629  // The Mongomery params (n0inv and rr) are not computed.
630  dv.setUint32(4, 0 /* n0inv*/, true);
631  // Modulus
632  for (let i = 0; i < MODULUS_SIZE_BYTES; i++) dv.setUint8(8 + i, nArr[i]);
633
634  // rr:
635  for (let i = 0; i < MODULUS_SIZE_BYTES; i++) {
636    dv.setUint8(8 + MODULUS_SIZE_BYTES + i, 0 /* rr*/);
637  }
638  // Exponent
639  for (let i = 0; i < 4; i++) {
640    dv.setUint8(8 + (2 * MODULUS_SIZE_BYTES) + i, eArr[i]);
641  }
642  return btoa(String.fromCharCode(...new Uint8Array(dv.buffer))) +
643      ' perfetto@webusb';
644}
645
646// TODO(nicomazz): This token signature will be useful only when we save the
647// generated keys. So far, we are not doing so. As a consequence, a dialog is
648// displayed every time a tracing session is started.
649// The reason why it has not already been implemented is that the standard
650// crypto.subtle.sign function assumes that the input needs hashing, which is
651// not the case for ADB, where the 20 bytes token is already hashed.
652// A solution to this is implementing a custom private key signature with a js
653// implementation of big integers. Maybe, wrapping the key like in the following
654// CL can work:
655// https://android-review.googlesource.com/c/platform/external/perfetto/+/1105354/18
656async function signAdbTokenWithPrivateKey(
657    _privateKey: CryptoKey, token: Uint8Array): Promise<ArrayBuffer> {
658  // This function is not implemented.
659  return token.buffer;
660}
661