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