|  | // Copyright (C) 2019 The Android Open Source Project | 
|  | // | 
|  | // Licensed under the Apache License, Version 2.0 (the "License"); | 
|  | // you may not use this file except in compliance with the License. | 
|  | // You may obtain a copy of the License at | 
|  | // | 
|  | //      http://www.apache.org/licenses/LICENSE-2.0 | 
|  | // | 
|  | // Unless required by applicable law or agreed to in writing, software | 
|  | // distributed under the License is distributed on an "AS IS" BASIS, | 
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
|  | // See the License for the specific language governing permissions and | 
|  | // limitations under the License. | 
|  |  | 
|  | import {_TextDecoder, _TextEncoder} from 'custom_utils'; | 
|  |  | 
|  | import {assertExists} from '../base/logging'; | 
|  |  | 
|  | import {Adb, AdbMsg, AdbStream, CmdType} from './adb_interfaces'; | 
|  |  | 
|  | const textEncoder = new _TextEncoder(); | 
|  | const textDecoder = new _TextDecoder(); | 
|  |  | 
|  | export const VERSION_WITH_CHECKSUM = 0x01000000; | 
|  | export const VERSION_NO_CHECKSUM = 0x01000001; | 
|  | export const DEFAULT_MAX_PAYLOAD_BYTES = 256 * 1024; | 
|  |  | 
|  | export enum AdbState { | 
|  | DISCONNECTED = 0, | 
|  | // Authentication steps, see AdbOverWebUsb's handleAuthentication(). | 
|  | AUTH_STEP1 = 1, | 
|  | AUTH_STEP2 = 2, | 
|  | AUTH_STEP3 = 3, | 
|  |  | 
|  | CONNECTED = 2, | 
|  | } | 
|  |  | 
|  | enum AuthCmd { | 
|  | TOKEN = 1, | 
|  | SIGNATURE = 2, | 
|  | RSAPUBLICKEY = 3, | 
|  | } | 
|  |  | 
|  | const DEVICE_NOT_SET_ERROR = 'Device not set.'; | 
|  |  | 
|  | // This class is a basic TypeScript implementation of adb that only supports | 
|  | // shell commands. It is used to send the start tracing command to the connected | 
|  | // android device, and to automatically pull the trace after the end of the | 
|  | // recording. It works through the webUSB API. A brief description of how it | 
|  | // works is the following: | 
|  | // - The connection with the device is initiated by findAndConnect, which shows | 
|  | //   a dialog with a list of connected devices. Once one is selected the | 
|  | //   authentication begins. The authentication has to pass different steps, as | 
|  | //   described in the "handeAuthentication" method. | 
|  | // - AdbOverWebUsb tracks the state of the authentication via a state machine | 
|  | //   (see AdbState). | 
|  | // - A Message handler loop is executed to keep receiving the messages. | 
|  | // - All the messages received from the device are passed to "onMessage" that is | 
|  | //   implemented as a state machine. | 
|  | // - When a new shell is established, it becomes an AdbStream, and is kept in | 
|  | //   the "streams" map. Each time a message from the device is for a specific | 
|  | //   previously opened stream, the "onMessage" function will forward it to the | 
|  | //   stream (identified by a number). | 
|  | export class AdbOverWebUsb implements Adb { | 
|  | state: AdbState = AdbState.DISCONNECTED; | 
|  | streams = new Map<number, AdbStream>(); | 
|  | devProps = ''; | 
|  | maxPayload = DEFAULT_MAX_PAYLOAD_BYTES; | 
|  | key?: CryptoKeyPair; | 
|  | onConnected = () => {}; | 
|  |  | 
|  | // Devices after Dec 2017 don't use checksum. This will be auto-detected | 
|  | // during the connection. | 
|  | useChecksum = true; | 
|  |  | 
|  | private lastStreamId = 0; | 
|  | private dev?: USBDevice; | 
|  | private usbInterfaceNumber?: number; | 
|  | private usbReadEndpoint = -1; | 
|  | private usbWriteEpEndpoint = -1; | 
|  | private filter = { | 
|  | classCode: 255,    // USB vendor specific code | 
|  | subclassCode: 66,  // Android vendor specific subclass | 
|  | protocolCode: 1,   // Adb protocol | 
|  | }; | 
|  |  | 
|  | async findDevice() { | 
|  | if (!('usb' in navigator)) { | 
|  | throw new Error('WebUSB not supported by the browser (requires HTTPS)'); | 
|  | } | 
|  | return navigator.usb.requestDevice({filters: [this.filter]}); | 
|  | } | 
|  |  | 
|  | async getPairedDevices() { | 
|  | try { | 
|  | return await navigator.usb.getDevices(); | 
|  | } catch (e) {  // WebUSB not available. | 
|  | return Promise.resolve([]); | 
|  | } | 
|  | } | 
|  |  | 
|  | async connect(device: USBDevice): Promise<void> { | 
|  | // If we are already connected, we are also already authenticated, so we can | 
|  | // skip doing the authentication again. | 
|  | if (this.state === AdbState.CONNECTED) { | 
|  | if (this.dev === device && device.opened) { | 
|  | this.onConnected(); | 
|  | this.onConnected = () => {}; | 
|  | return; | 
|  | } | 
|  | // Another device was connected. | 
|  | await this.disconnect(); | 
|  | } | 
|  |  | 
|  | this.dev = device; | 
|  | this.useChecksum = true; | 
|  | this.key = await AdbOverWebUsb.initKey(); | 
|  |  | 
|  | await this.dev.open(); | 
|  |  | 
|  | const {configValue, usbInterfaceNumber, endpoints} = | 
|  | this.findInterfaceAndEndpoint(); | 
|  | this.usbInterfaceNumber = usbInterfaceNumber; | 
|  |  | 
|  | this.usbReadEndpoint = this.findEndpointNumber(endpoints, 'in'); | 
|  | this.usbWriteEpEndpoint = this.findEndpointNumber(endpoints, 'out'); | 
|  |  | 
|  | console.assert(this.usbReadEndpoint >= 0 && this.usbWriteEpEndpoint >= 0); | 
|  |  | 
|  | await this.dev.selectConfiguration(configValue); | 
|  | await this.dev.claimInterface(usbInterfaceNumber); | 
|  |  | 
|  | await this.startAuthentication(); | 
|  |  | 
|  | // This will start a message handler loop. | 
|  | this.receiveDeviceMessages(); | 
|  | // The promise will be resolved after the handshake. | 
|  | return new Promise<void>((resolve, _) => this.onConnected = resolve); | 
|  | } | 
|  |  | 
|  | async disconnect(): Promise<void> { | 
|  | if (this.state === AdbState.DISCONNECTED) { | 
|  | return; | 
|  | } | 
|  | this.state = AdbState.DISCONNECTED; | 
|  |  | 
|  | if (!this.dev) return; | 
|  |  | 
|  | new Map(this.streams).forEach((stream, _id) => stream.setClosed()); | 
|  | console.assert(this.streams.size === 0); | 
|  |  | 
|  | await this.dev.releaseInterface(assertExists(this.usbInterfaceNumber)); | 
|  | this.dev = undefined; | 
|  | this.usbInterfaceNumber = undefined; | 
|  | } | 
|  |  | 
|  | async startAuthentication() { | 
|  | // USB connected, now let's authenticate. | 
|  | const VERSION = | 
|  | this.useChecksum ? VERSION_WITH_CHECKSUM : VERSION_NO_CHECKSUM; | 
|  | this.state = AdbState.AUTH_STEP1; | 
|  | await this.send('CNXN', VERSION, this.maxPayload, 'host:1:UsbADB'); | 
|  | } | 
|  |  | 
|  | findInterfaceAndEndpoint() { | 
|  | if (!this.dev) throw Error(DEVICE_NOT_SET_ERROR); | 
|  | for (const config of this.dev.configurations) { | 
|  | for (const interface_ of config.interfaces) { | 
|  | for (const alt of interface_.alternates) { | 
|  | if (alt.interfaceClass === this.filter.classCode && | 
|  | alt.interfaceSubclass === this.filter.subclassCode && | 
|  | alt.interfaceProtocol === this.filter.protocolCode) { | 
|  | return { | 
|  | configValue: config.configurationValue, | 
|  | usbInterfaceNumber: interface_.interfaceNumber, | 
|  | endpoints: alt.endpoints, | 
|  | }; | 
|  | }  // if (alternate) | 
|  | }    // for (interface.alternates) | 
|  | }      // for (configuration.interfaces) | 
|  | }        // for (configurations) | 
|  |  | 
|  | throw Error('Cannot find interfaces and endpoints'); | 
|  | } | 
|  |  | 
|  | findEndpointNumber( | 
|  | endpoints: USBEndpoint[], direction: 'out'|'in', type = 'bulk'): number { | 
|  | const ep = | 
|  | endpoints.find((ep) => ep.type === type && ep.direction === direction); | 
|  |  | 
|  | if (ep) return ep.endpointNumber; | 
|  |  | 
|  | throw Error(`Cannot find ${direction} endpoint`); | 
|  | } | 
|  |  | 
|  | receiveDeviceMessages() { | 
|  | this.recv() | 
|  | .then((msg) => { | 
|  | this.onMessage(msg); | 
|  | this.receiveDeviceMessages(); | 
|  | }) | 
|  | .catch((e) => { | 
|  | // Ignore error with "DEVICE_NOT_SET_ERROR" message since it is always | 
|  | // thrown after the device disconnects. | 
|  | if (e.message !== DEVICE_NOT_SET_ERROR) { | 
|  | console.error(`Exception in recv: ${e.name}. error: ${e.message}`); | 
|  | } | 
|  | this.disconnect(); | 
|  | }); | 
|  | } | 
|  |  | 
|  | async onMessage(msg: AdbMsg) { | 
|  | if (!this.key) throw Error('ADB key not initialized'); | 
|  |  | 
|  | if (msg.cmd === 'AUTH' && msg.arg0 === AuthCmd.TOKEN) { | 
|  | this.handleAuthentication(msg); | 
|  | } else if (msg.cmd === 'CNXN') { | 
|  | console.assert( | 
|  | [AdbState.AUTH_STEP2, AdbState.AUTH_STEP3].includes(this.state)); | 
|  | this.state = AdbState.CONNECTED; | 
|  | this.handleConnectedMessage(msg); | 
|  | } else if (this.state === AdbState.CONNECTED && [ | 
|  | 'OKAY', | 
|  | 'WRTE', | 
|  | 'CLSE', | 
|  | ].indexOf(msg.cmd) >= 0) { | 
|  | const stream = this.streams.get(msg.arg1); | 
|  | if (!stream) { | 
|  | console.warn(`Received message ${msg} for unknown stream ${msg.arg1}`); | 
|  | return; | 
|  | } | 
|  | stream.onMessage(msg); | 
|  | } else { | 
|  | console.error(`Unexpected message `, msg, ` in state ${this.state}`); | 
|  | } | 
|  | } | 
|  |  | 
|  | async handleAuthentication(msg: AdbMsg) { | 
|  | if (!this.key) throw Error('ADB key not initialized'); | 
|  |  | 
|  | console.assert(msg.cmd === 'AUTH' && msg.arg0 === AuthCmd.TOKEN); | 
|  | const token = msg.data; | 
|  |  | 
|  | if (this.state === AdbState.AUTH_STEP1) { | 
|  | // During this step, we send back the token received signed with our | 
|  | // private key. If the device has previously received our public key, the | 
|  | // dialog will not be displayed. Otherwise we will receive another message | 
|  | // ending up in AUTH_STEP3. | 
|  | this.state = AdbState.AUTH_STEP2; | 
|  |  | 
|  | const signedToken = | 
|  | await signAdbTokenWithPrivateKey(this.key.privateKey, token); | 
|  | this.send('AUTH', AuthCmd.SIGNATURE, 0, new Uint8Array(signedToken)); | 
|  | return; | 
|  | } | 
|  |  | 
|  | console.assert(this.state === AdbState.AUTH_STEP2); | 
|  |  | 
|  | // During this step, we send our public key. The dialog will appear, and | 
|  | // if the user chooses to remember our public key, it will be | 
|  | // saved, so that the next time we will only pass through AUTH_STEP1. | 
|  | this.state = AdbState.AUTH_STEP3; | 
|  | const encodedPubKey = await encodePubKey(this.key.publicKey); | 
|  | this.send('AUTH', AuthCmd.RSAPUBLICKEY, 0, encodedPubKey); | 
|  | } | 
|  |  | 
|  | private handleConnectedMessage(msg: AdbMsg) { | 
|  | console.assert(msg.cmd === 'CNXN'); | 
|  |  | 
|  | this.maxPayload = msg.arg1; | 
|  | this.devProps = textDecoder.decode(msg.data); | 
|  |  | 
|  | const deviceVersion = msg.arg0; | 
|  |  | 
|  | if (![VERSION_WITH_CHECKSUM, VERSION_NO_CHECKSUM].includes(deviceVersion)) { | 
|  | console.error('Version ', msg.arg0, ' not really supported!'); | 
|  | } | 
|  | this.useChecksum = deviceVersion === VERSION_WITH_CHECKSUM; | 
|  | this.state = AdbState.CONNECTED; | 
|  |  | 
|  | // This will resolve the promise returned by "onConnect" | 
|  | this.onConnected(); | 
|  | this.onConnected = () => {}; | 
|  | } | 
|  |  | 
|  | shell(cmd: string): Promise<AdbStream> { | 
|  | return this.openStream('shell:' + cmd); | 
|  | } | 
|  |  | 
|  | socket(path: string): Promise<AdbStream> { | 
|  | return this.openStream('localfilesystem:' + path); | 
|  | } | 
|  |  | 
|  | openStream(svc: string): Promise<AdbStream> { | 
|  | const stream = new AdbStreamImpl(this, ++this.lastStreamId); | 
|  | this.streams.set(stream.localStreamId, stream); | 
|  | this.send('OPEN', stream.localStreamId, 0, svc); | 
|  |  | 
|  | //  The stream will resolve this promise once it receives the | 
|  | //  acknowledgement message from the device. | 
|  | return new Promise<AdbStream>((resolve, reject) => { | 
|  | stream.onConnect = () => { | 
|  | stream.onClose = () => {}; | 
|  | resolve(stream); | 
|  | }; | 
|  | stream.onClose = () => | 
|  | reject(new Error(`Failed to openStream svc=${svc}`)); | 
|  | }); | 
|  | } | 
|  |  | 
|  | async shellOutputAsString(cmd: string): Promise<string> { | 
|  | const shell = await this.shell(cmd); | 
|  |  | 
|  | return new Promise<string>((resolve, _) => { | 
|  | const output: string[] = []; | 
|  | shell.onData = (raw) => output.push(textDecoder.decode(raw)); | 
|  | shell.onClose = () => resolve(output.join()); | 
|  | }); | 
|  | } | 
|  |  | 
|  | async send( | 
|  | cmd: CmdType, arg0: number, arg1: number, data?: Uint8Array|string) { | 
|  | await this.sendMsg(AdbMsgImpl.create( | 
|  | {cmd, arg0, arg1, data, useChecksum: this.useChecksum})); | 
|  | } | 
|  |  | 
|  | //  The header and the message data must be sent consecutively. Using 2 awaits | 
|  | //  Another message can interleave after the first header has been sent, | 
|  | //  resulting in something like [header1] [header2] [data1] [data2]; | 
|  | //  In this way we are waiting both promises to be resolved before continuing. | 
|  | async sendMsg(msg: AdbMsgImpl) { | 
|  | const sendPromises = [this.sendRaw(msg.encodeHeader())]; | 
|  | if (msg.data.length > 0) sendPromises.push(this.sendRaw(msg.data)); | 
|  | await Promise.all(sendPromises); | 
|  | } | 
|  |  | 
|  | async recv(): Promise<AdbMsg> { | 
|  | const res = await this.recvRaw(ADB_MSG_SIZE); | 
|  | console.assert(res.status === 'ok'); | 
|  | const msg = AdbMsgImpl.decodeHeader(res.data!); | 
|  |  | 
|  | if (msg.dataLen > 0) { | 
|  | const resp = await this.recvRaw(msg.dataLen); | 
|  | msg.data = new Uint8Array( | 
|  | resp.data!.buffer, resp.data!.byteOffset, resp.data!.byteLength); | 
|  | } | 
|  | if (this.useChecksum) { | 
|  | console.assert(AdbOverWebUsb.checksum(msg.data) === msg.dataChecksum); | 
|  | } | 
|  | return msg; | 
|  | } | 
|  |  | 
|  | static async initKey(): Promise<CryptoKeyPair> { | 
|  | const KEY_SIZE = 2048; | 
|  |  | 
|  | const keySpec = { | 
|  | name: 'RSASSA-PKCS1-v1_5', | 
|  | modulusLength: KEY_SIZE, | 
|  | publicExponent: new Uint8Array([0x01, 0x00, 0x01]), | 
|  | hash: {name: 'SHA-1'}, | 
|  | }; | 
|  |  | 
|  | const key = await crypto.subtle.generateKey( | 
|  | keySpec, /* extractable=*/ true, ['sign', 'verify']); | 
|  | return key; | 
|  | } | 
|  |  | 
|  | static checksum(data: Uint8Array): number { | 
|  | let res = 0; | 
|  | for (let i = 0; i < data.byteLength; i++) res += data[i]; | 
|  | return res & 0xFFFFFFFF; | 
|  | } | 
|  |  | 
|  | sendRaw(buf: Uint8Array): Promise<USBOutTransferResult> { | 
|  | console.assert(buf.length <= this.maxPayload); | 
|  | if (!this.dev) throw Error(DEVICE_NOT_SET_ERROR); | 
|  | return this.dev.transferOut(this.usbWriteEpEndpoint, buf.buffer); | 
|  | } | 
|  |  | 
|  | recvRaw(dataLen: number): Promise<USBInTransferResult> { | 
|  | if (!this.dev) throw Error(DEVICE_NOT_SET_ERROR); | 
|  | return this.dev.transferIn(this.usbReadEndpoint, dataLen); | 
|  | } | 
|  | } | 
|  |  | 
|  | enum AdbStreamState { | 
|  | WAITING_INITIAL_OKAY = 0, | 
|  | CONNECTED = 1, | 
|  | CLOSED = 2 | 
|  | } | 
|  |  | 
|  |  | 
|  | // An AdbStream is instantiated after the creation of a shell to the device. | 
|  | // Thanks to this, we can send commands and receive their output. Messages are | 
|  | // received in the main adb class, and are forwarded to an instance of this | 
|  | // class based on a stream id match. Also streams have an initialization flow: | 
|  | //   1. WAITING_INITIAL_OKAY: waiting for first "OKAY" message. Once received, | 
|  | //      the next state will be "CONNECTED". | 
|  | //   2. CONNECTED: ready to receive or send messages. | 
|  | //   3. WRITING: this is needed because we must receive an ack after sending | 
|  | //      each message (so, before sending the next one). For this reason, many | 
|  | //      subsequent "write" calls will result in different messages in the | 
|  | //      writeQueue. After each new acknowledgement ('OKAY') a new one will be | 
|  | //      sent. When the queue is empty, the state will return to CONNECTED. | 
|  | //   4. CLOSED: entered when the device closes the stream or close() is called. | 
|  | //      For shell commands, the stream is closed after the command completed. | 
|  | export class AdbStreamImpl implements AdbStream { | 
|  | private adb: AdbOverWebUsb; | 
|  | localStreamId: number; | 
|  | private remoteStreamId = -1; | 
|  | private state: AdbStreamState = AdbStreamState.WAITING_INITIAL_OKAY; | 
|  | private writeQueue: Uint8Array[] = []; | 
|  |  | 
|  | private sendInProgress = false; | 
|  |  | 
|  | onData: AdbStreamReadCallback = (_) => {}; | 
|  | onConnect = () => {}; | 
|  | onClose = () => {}; | 
|  |  | 
|  | constructor(adb: AdbOverWebUsb, localStreamId: number) { | 
|  | this.adb = adb; | 
|  | this.localStreamId = localStreamId; | 
|  | } | 
|  |  | 
|  | close() { | 
|  | console.assert(this.state === AdbStreamState.CONNECTED); | 
|  |  | 
|  | if (this.writeQueue.length > 0) { | 
|  | console.error(`Dropping ${ | 
|  | this.writeQueue.length} queued messages due to stream closing.`); | 
|  | this.writeQueue = []; | 
|  | } | 
|  |  | 
|  | this.adb.send('CLSE', this.localStreamId, this.remoteStreamId); | 
|  | } | 
|  |  | 
|  | async write(msg: string|Uint8Array) { | 
|  | const raw = (typeof msg === 'string') ? textEncoder.encode(msg) : msg; | 
|  | if (this.sendInProgress || | 
|  | this.state === AdbStreamState.WAITING_INITIAL_OKAY) { | 
|  | this.writeQueue.push(raw); | 
|  | return; | 
|  | } | 
|  | console.assert(this.state === AdbStreamState.CONNECTED); | 
|  | this.sendInProgress = true; | 
|  | await this.adb.send('WRTE', this.localStreamId, this.remoteStreamId, raw); | 
|  | } | 
|  |  | 
|  | setClosed() { | 
|  | this.state = AdbStreamState.CLOSED; | 
|  | this.adb.streams.delete(this.localStreamId); | 
|  | this.onClose(); | 
|  | } | 
|  |  | 
|  | onMessage(msg: AdbMsgImpl) { | 
|  | console.assert(msg.arg1 === this.localStreamId); | 
|  |  | 
|  | if (this.state === AdbStreamState.WAITING_INITIAL_OKAY && | 
|  | msg.cmd === 'OKAY') { | 
|  | this.remoteStreamId = msg.arg0; | 
|  | this.state = AdbStreamState.CONNECTED; | 
|  | this.onConnect(); | 
|  | return; | 
|  | } | 
|  |  | 
|  | if (msg.cmd === 'WRTE') { | 
|  | this.adb.send('OKAY', this.localStreamId, this.remoteStreamId); | 
|  | this.onData(msg.data); | 
|  | return; | 
|  | } | 
|  |  | 
|  | if (msg.cmd === 'OKAY') { | 
|  | console.assert(this.sendInProgress); | 
|  | this.sendInProgress = false; | 
|  | const queuedMsg = this.writeQueue.shift(); | 
|  | if (queuedMsg !== undefined) this.write(queuedMsg); | 
|  | return; | 
|  | } | 
|  |  | 
|  | if (msg.cmd === 'CLSE') { | 
|  | this.setClosed(); | 
|  | return; | 
|  | } | 
|  | console.error( | 
|  | `Unexpected stream msg ${msg.toString()} in state ${this.state}`); | 
|  | } | 
|  | } | 
|  |  | 
|  | interface AdbStreamReadCallback { | 
|  | (raw: Uint8Array): void; | 
|  | } | 
|  |  | 
|  | const ADB_MSG_SIZE = 6 * 4;  // 6 * int32. | 
|  |  | 
|  | export class AdbMsgImpl implements AdbMsg { | 
|  | cmd: CmdType; | 
|  | arg0: number; | 
|  | arg1: number; | 
|  | data: Uint8Array; | 
|  | dataLen: number; | 
|  | dataChecksum: number; | 
|  |  | 
|  | useChecksum: boolean; | 
|  |  | 
|  | constructor( | 
|  | cmd: CmdType, arg0: number, arg1: number, dataLen: number, | 
|  | dataChecksum: number, useChecksum = false) { | 
|  | console.assert(cmd.length === 4); | 
|  | this.cmd = cmd; | 
|  | this.arg0 = arg0; | 
|  | this.arg1 = arg1; | 
|  | this.dataLen = dataLen; | 
|  | this.data = new Uint8Array(dataLen); | 
|  | this.dataChecksum = dataChecksum; | 
|  | this.useChecksum = useChecksum; | 
|  | } | 
|  |  | 
|  |  | 
|  | static create({cmd, arg0, arg1, data, useChecksum = true}: { | 
|  | cmd: CmdType; arg0: number; arg1: number; | 
|  | data?: Uint8Array | string; | 
|  | useChecksum?: boolean; | 
|  | }): AdbMsgImpl { | 
|  | const encodedData = this.encodeData(data); | 
|  | const msg = | 
|  | new AdbMsgImpl(cmd, arg0, arg1, encodedData.length, 0, useChecksum); | 
|  | msg.data = encodedData; | 
|  | return msg; | 
|  | } | 
|  |  | 
|  | get dataStr() { | 
|  | return textDecoder.decode(this.data); | 
|  | } | 
|  |  | 
|  | toString() { | 
|  | return `${this.cmd} [${this.arg0},${this.arg1}] ${this.dataStr}`; | 
|  | } | 
|  |  | 
|  | // A brief description of the message can be found here: | 
|  | // https://android.googlesource.com/platform/system/core/+/main/adb/protocol.txt | 
|  | // | 
|  | // struct amessage { | 
|  | //     uint32_t command;    // command identifier constant | 
|  | //     uint32_t arg0;       // first argument | 
|  | //     uint32_t arg1;       // second argument | 
|  | //     uint32_t data_length;// length of payload (0 is allowed) | 
|  | //     uint32_t data_check; // checksum of data payload | 
|  | //     uint32_t magic;      // command ^ 0xffffffff | 
|  | // }; | 
|  | static decodeHeader(dv: DataView): AdbMsgImpl { | 
|  | console.assert(dv.byteLength === ADB_MSG_SIZE); | 
|  | const cmd = textDecoder.decode(dv.buffer.slice(0, 4)) as CmdType; | 
|  | const cmdNum = dv.getUint32(0, true); | 
|  | const arg0 = dv.getUint32(4, true); | 
|  | const arg1 = dv.getUint32(8, true); | 
|  | const dataLen = dv.getUint32(12, true); | 
|  | const dataChecksum = dv.getUint32(16, true); | 
|  | const cmdChecksum = dv.getUint32(20, true); | 
|  | console.assert(cmdNum === (cmdChecksum ^ 0xFFFFFFFF)); | 
|  | return new AdbMsgImpl(cmd, arg0, arg1, dataLen, dataChecksum); | 
|  | } | 
|  |  | 
|  | encodeHeader(): Uint8Array { | 
|  | const buf = new Uint8Array(ADB_MSG_SIZE); | 
|  | const dv = new DataView(buf.buffer); | 
|  | const cmdBytes: Uint8Array = textEncoder.encode(this.cmd); | 
|  | const rawMsg = AdbMsgImpl.encodeData(this.data); | 
|  | const checksum = this.useChecksum ? AdbOverWebUsb.checksum(rawMsg) : 0; | 
|  | for (let i = 0; i < 4; i++) dv.setUint8(i, cmdBytes[i]); | 
|  |  | 
|  | dv.setUint32(4, this.arg0, true); | 
|  | dv.setUint32(8, this.arg1, true); | 
|  | dv.setUint32(12, rawMsg.byteLength, true); | 
|  | dv.setUint32(16, checksum, true); | 
|  | dv.setUint32(20, dv.getUint32(0, true) ^ 0xFFFFFFFF, true); | 
|  |  | 
|  | return buf; | 
|  | } | 
|  |  | 
|  | static encodeData(data?: Uint8Array|string): Uint8Array { | 
|  | if (data === undefined) return new Uint8Array([]); | 
|  | if (typeof data === 'string') return textEncoder.encode(data + '\0'); | 
|  | return data; | 
|  | } | 
|  | } | 
|  |  | 
|  |  | 
|  | function base64StringToArray(s: string) { | 
|  | const decoded = atob(s.replaceAll('-', '+').replaceAll('_', '/')); | 
|  | return [...decoded].map((char) => char.charCodeAt(0)); | 
|  | } | 
|  |  | 
|  | const ANDROID_PUBKEY_MODULUS_SIZE = 2048; | 
|  | const MODULUS_SIZE_BYTES = ANDROID_PUBKEY_MODULUS_SIZE / 8; | 
|  |  | 
|  | // RSA Public keys are encoded in a rather unique way. It's a base64 encoded | 
|  | // struct of 524 bytes in total as follows (see | 
|  | // libcrypto_utils/android_pubkey.c): | 
|  | // | 
|  | // typedef struct RSAPublicKey { | 
|  | //   // Modulus length. This must be ANDROID_PUBKEY_MODULUS_SIZE. | 
|  | //   uint32_t modulus_size_words; | 
|  | // | 
|  | //   // Precomputed montgomery parameter: -1 / n[0] mod 2^32 | 
|  | //   uint32_t n0inv; | 
|  | // | 
|  | //   // RSA modulus as a little-endian array. | 
|  | //   uint8_t modulus[ANDROID_PUBKEY_MODULUS_SIZE]; | 
|  | // | 
|  | //   // Montgomery parameter R^2 as a little-endian array of little-endian | 
|  | //   words. uint8_t rr[ANDROID_PUBKEY_MODULUS_SIZE]; | 
|  | // | 
|  | //   // RSA modulus: 3 or 65537 | 
|  | //   uint32_t exponent; | 
|  | // } RSAPublicKey; | 
|  | // | 
|  | // However, the Montgomery params (n0inv and rr) are not really used, see | 
|  | // comment in android_pubkey_decode() ("Note that we don't extract the | 
|  | // montgomery parameters...") | 
|  | async function encodePubKey(key: CryptoKey) { | 
|  | const expPubKey = await crypto.subtle.exportKey('jwk', key); | 
|  | const nArr = base64StringToArray(expPubKey.n as string).reverse(); | 
|  | const eArr = base64StringToArray(expPubKey.e as string).reverse(); | 
|  |  | 
|  | const arr = new Uint8Array(3 * 4 + 2 * MODULUS_SIZE_BYTES); | 
|  | const dv = new DataView(arr.buffer); | 
|  | dv.setUint32(0, MODULUS_SIZE_BYTES / 4, true); | 
|  |  | 
|  | // The Mongomery params (n0inv and rr) are not computed. | 
|  | dv.setUint32(4, 0 /* n0inv*/, true); | 
|  | // Modulus | 
|  | for (let i = 0; i < MODULUS_SIZE_BYTES; i++) dv.setUint8(8 + i, nArr[i]); | 
|  |  | 
|  | // rr: | 
|  | for (let i = 0; i < MODULUS_SIZE_BYTES; i++) { | 
|  | dv.setUint8(8 + MODULUS_SIZE_BYTES + i, 0 /* rr*/); | 
|  | } | 
|  | // Exponent | 
|  | for (let i = 0; i < 4; i++) { | 
|  | dv.setUint8(8 + (2 * MODULUS_SIZE_BYTES) + i, eArr[i]); | 
|  | } | 
|  | return btoa(String.fromCharCode(...new Uint8Array(dv.buffer))) + | 
|  | ' perfetto@webusb'; | 
|  | } | 
|  |  | 
|  | // TODO(nicomazz): This token signature will be useful only when we save the | 
|  | // generated keys. So far, we are not doing so. As a consequence, a dialog is | 
|  | // displayed every time a tracing session is started. | 
|  | // The reason why it has not already been implemented is that the standard | 
|  | // crypto.subtle.sign function assumes that the input needs hashing, which is | 
|  | // not the case for ADB, where the 20 bytes token is already hashed. | 
|  | // A solution to this is implementing a custom private key signature with a js | 
|  | // implementation of big integers. Maybe, wrapping the key like in the following | 
|  | // CL can work: | 
|  | // https://android-review.googlesource.com/c/platform/external/perfetto/+/1105354/18 | 
|  | async function signAdbTokenWithPrivateKey( | 
|  | _privateKey: CryptoKey, token: Uint8Array): Promise<ArrayBuffer> { | 
|  | // This function is not implemented. | 
|  | return token.buffer; | 
|  | } |