166 lines
4.6 KiB
TypeScript
166 lines
4.6 KiB
TypeScript
|
|
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
|
||
|
|
})
|
||
|
|
})
|
||
|
|
})
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|