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