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