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