import { Adb, AdbDaemonTransport } from '@yume-chan/adb' import { type AdbDaemonWebUsbDevice, AdbDaemonWebUsbDeviceManager } from '@yume-chan/adb-daemon-webusb' import { type BrowserWindow } from 'electron' import { WebUSB } from 'usb' import { type AdbCredentialStore, adbGeneratePublicKey } from '@yume-chan/adb' import { webcrypto } from 'node:crypto' import { readFile, writeFile } from 'node:fs/promises' import { homedir, hostname, userInfo } from 'node:os' import { join } from 'node:path' class AdbNodeJsCredentialStore implements AdbCredentialStore { #name: string constructor(name: string) { this.#name = name } #privateKeyPath() { return join(homedir(), '.android', 'adbkey') } #publicKeyPath() { return join(homedir(), '.android', 'adbkey.pub') } async generateKey() { const { privateKey: cryptoKey } = await webcrypto.subtle.generateKey( { name: 'RSASSA-PKCS1-v1_5', modulusLength: 2048, // 65537 publicExponent: new Uint8Array([0x01, 0x00, 0x01]), hash: 'SHA-1' }, true, ['sign', 'verify'] ) const privateKey = new Uint8Array(await crypto.subtle.exportKey('pkcs8', cryptoKey)) await writeFile(this.#privateKeyPath(), Buffer.from(privateKey).toString('utf8')) await writeFile( this.#publicKeyPath(), `${Buffer.from(adbGeneratePublicKey(privateKey)).toString('base64')} ${this.#name}\n` ) return { buffer: privateKey, name: this.#name } } async #readPubKeyName() { const content = await readFile(this.#publicKeyPath(), 'utf8') const pubKeyName = content.split(' ')[1] return pubKeyName || `${userInfo().username}@${hostname()}` } async *iterateKeys() { const content = await readFile(this.#privateKeyPath(), 'utf8') const privateKey = Buffer.from(content.split('\n').slice(1, -2).join(''), 'base64') yield { buffer: privateKey, name: await this.#readPubKeyName() } } } type ConnectedDevice = { [serial: string]: { adb: Adb metadata: { venderId?: number productId?: number manufacturerNam?: string productName?: string serialNumber?: string } } } export class AdbUsbNative { private _manager: AdbDaemonWebUsbDeviceManager private _win: BrowserWindow private _connectedDevices: ConnectedDevice = {} constructor(win: BrowserWindow) { const webUsb: WebUSB = new WebUSB({ allowedDevices: [{ serialNumber: 'd' }] }) this._manager = new AdbDaemonWebUsbDeviceManager(webUsb) this._win = win } getAvailableDevices(responseChannel: string) { this._manager.getDevices().then(devices => { this._win.webContents.send( responseChannel, devices.map(d => ({ vendorId: d.raw.vendorId, productId: d.raw.productId, manufacturerNam: d.raw.manufacturerName, productName: d.raw.productName, serialNumber: d.raw.serialNumber })) ) }) } getConnectedDevices(responseChannel: string) { this._win.webContents.send( responseChannel, Object.entries(this._connectedDevices).map(([serial, { metadata }]) => ({ serial: serial, productId: metadata.productId, venderId: metadata.venderId, manufacturerNam: metadata.manufacturerNam, productName: metadata.productName })) ) } connectDevice(responseChanel: string, serial: string) { this._manager.getDevices().then(devices => { let device: AdbDaemonWebUsbDevice | null = null for (const d of devices) { if (d.serial === serial) { device = d break } } if (!device) { this._win.webContents.send(responseChanel, { error: 'Device with serialNumber: ' + serial + ' not found' }) return } device.connect().then(connection => { const credentialStore = new AdbNodeJsCredentialStore(`${userInfo().username}@${hostname()}`) AdbDaemonTransport.authenticate({ serial: device!.serial, connection, credentialStore }).then(transport => { this._connectedDevices[device!.serial] = { adb: new Adb(transport), metadata: { venderId: device!.raw.vendorId, productId: device!.raw.productId, manufacturerNam: device!.raw.manufacturerName, productName: device!.raw.productName, serialNumber: device!.raw.serialNumber } } this._win.webContents.send(responseChanel, { serial: device!.serial }) }) }) }) } }