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