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