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