UNPKG

@u4/adbkit

Version:

A Typescript client for the Android Debug Bridge.

282 lines 11.1 kB
import EventEmitter from 'node:events'; import crypto from 'node:crypto'; import { promisify } from 'node:util'; import { Buffer } from 'node:buffer'; import PacketReader from './packetreader.js'; import RollingCounter from './rollingcounter.js'; import Packet from './packet.js'; import Auth from '../auth.js'; import ServiceMap from './servicemap.js'; import Service from './service.js'; import Utils from '../utils.js'; const debug = Utils.debug('adb:tcpusb:socket'); const UINT32_MAX = 0xffffffff; const UINT16_MAX = 0xffff; const AUTH_TOKEN = 1; const AUTH_SIGNATURE = 2; const AUTH_RSAPUBLICKEY = 3; const TOKEN_LENGTH = 20; export class AuthError extends Error { constructor(message) { super(message); Object.setPrototypeOf(this, AuthError.prototype); this.name = 'AuthError'; Error.captureStackTrace(this, Socket.AuthError); } } export class UnauthorizedError extends Error { constructor() { super('Unauthorized access'); Object.setPrototypeOf(this, UnauthorizedError.prototype); this.name = 'UnauthorizedError'; Error.captureStackTrace(this, Socket.UnauthorizedError); } } class Socket extends EventEmitter { constructor(client, serial, socket, options = {}) { super(); this.client = client; this.serial = serial; this.socket = socket; this.options = options; this.ended = false; this.authorized = false; this.syncToken = new RollingCounter(UINT32_MAX); this.remoteId = new RollingCounter(UINT32_MAX); this.services = new ServiceMap(); this.version = 1; this.maxPayload = 4096; this.on = (event, listener) => super.on(event, listener); this.off = (event, listener) => super.off(event, listener); this.once = (event, listener) => super.once(event, listener); this.emit = (event, ...args) => super.emit(event, ...args); const base = this.options; if (!base.auth) base.auth = () => Promise.resolve(true); this.socket.setNoDelay(true); this.reader = new PacketReader(this.socket) .on('packet', this._handle.bind(this)) .on('error', (err) => { debug(`PacketReader error: ${err.message}`); return this.end(); }) .on('end', () => this.end()); this.remoteAddress = this.socket.remoteAddress; this.token = undefined; this.signature = undefined; } end() { if (this.ended) { return this; } // End services first so that they can send a final payload before FIN. this.services.end(); this.socket.end(); this.ended = true; this.emit('end', true); return this; } _error(err) { this.emit('error', err); return this.end(); } async _handle(packet) { if (this.ended) { return false; } this.emit('userActivity', packet); try { switch (packet.command) { case Packet.A_SYNC: return Promise.resolve(this._handleSyncPacket()); case Packet.A_CNXN: return this._handleConnectionPacket(packet); case Packet.A_OPEN: { const r = this._handleOpenPacket(packet); return !!r; } case Packet.A_OKAY: case Packet.A_WRTE: case Packet.A_CLSE: { const r = await this._forwardServicePacket(packet); return !!r; } case Packet.A_AUTH: return this._handleAuthPacket(packet); default: throw new Error(`Unknown command ${packet.command}`); } } catch (err) { if (err instanceof Socket.AuthError) { this.end(); return false; } if (err instanceof Socket.UnauthorizedError) { this.end(); return false; } if (err instanceof Error) { this._error(err); return false; } return false; } } _handleSyncPacket() { // No need to do anything? debug('I:A_SYNC'); debug('O:A_SYNC'); return this.write(Packet.assemble(Packet.A_SYNC, 1, this.syncToken.next())); } async _handleConnectionPacket(packet) { debug('I:A_CNXN', packet); this.version = Packet.swap32(packet.arg0); this.maxPayload = Math.min(UINT16_MAX, packet.arg1); const token = await this._createToken(); this.token = token; debug(`Created challenge '${this.token.toString('base64')}'`); debug('O:A_AUTH'); return this.write(Packet.assemble(Packet.A_AUTH, AUTH_TOKEN, 0, this.token)); } async _handleAuthPacket(packet) { debug('I:A_AUTH', packet); switch (packet.arg0) { case AUTH_SIGNATURE: { // Store first signature, ignore the rest if (packet.data) debug(`Received signature '${packet.data.toString('base64')}'`); if (!this.signature) { this.signature = packet.data; } if (this.options.knownPublicKeys && this.options.knownPublicKeys.length > 0 && this.token && this.signature) { const digest = this.token.toString('binary'); const sig = this.signature.toString('binary'); for (const key of this.options.knownPublicKeys) { // If signature matches one of the known public keys, we can safely accept the connection if (key.verify(digest, sig)) { const deviceId = await this._deviceId(); this.authorized = true; debug('O:A_CNXN'); return this.write(Packet.assemble(Packet.A_CNXN, Packet.swap32(this.version), this.maxPayload, deviceId)); } } } debug('O:A_AUTH'); const b = this.write(Packet.assemble(Packet.A_AUTH, AUTH_TOKEN, 0, this.token)); return b; } case AUTH_RSAPUBLICKEY: { if (!this.signature) { throw new Socket.AuthError('Public key sent before signature'); } if (!packet.data || packet.data.length < 2) { throw new Socket.AuthError('Empty RSA public key'); } debug(`Received RSA public key '${packet.data.toString('base64')}'`); const key = await Auth.parsePublicKey(this._skipNull(packet.data).toString()); if (!this.token) throw Error('missing token in socket:_handleAuthPacket'); const digest = this.token.toString('binary'); const sig = this.signature.toString('binary'); if (!key.verify(digest, sig)) { debug('Signature mismatch'); throw new Socket.AuthError('Signature mismatch'); } debug('Signature verified'); if (this.options.auth) { try { await this.options.auth(key); } catch (e) { debug('Connection rejected by user-defined auth handler', e); throw new Socket.AuthError('Rejected by user-defined handler'); } } const id = await this._deviceId(); this.authorized = true; debug('O:A_CNXN'); return this.write(Packet.assemble(Packet.A_CNXN, Packet.swap32(this.version), this.maxPayload, id)); } default: throw new Error(`Unknown authentication method ${packet.arg0}`); } } _handleOpenPacket(packet) { if (!this.authorized) { throw new Socket.UnauthorizedError(); } const remoteId = packet.arg0; const localId = this.remoteId.next(); if (!(packet.data && packet.data.length >= 2)) { throw new Error('Empty service name'); } const name = this._skipNull(packet.data); debug(`Calling ${name}`); const service = new Service(this.client, this.serial, localId, remoteId, this); return new Promise((resolve, reject) => { service.on('error', reject); service.on('end', () => resolve(false)); this.services.insert(localId, service); debug(`Handling ${this.services.count} services simultaneously`); return service.handle(packet); }) .catch((err) => { debug(`Got error handling service ${service}. ${err}`); return true; }) .finally(() => { this.services.remove(localId); debug(`Handling ${this.services.count} services simultaneously`); return service.end(); }); } _forwardServicePacket(packet) { if (!this.authorized) { throw new Socket.UnauthorizedError(); } const localId = packet.arg1; const service = this.services.get(localId); if (service) { return service.handle(packet); } else { debug('Received a packet to a service that may have been closed already'); return Promise.resolve(false); } } write(chunk) { if (this.ended) { return false; } return this.socket.write(chunk); } _createToken() { return promisify(crypto.randomBytes)(TOKEN_LENGTH); } _skipNull(data) { return data.slice(0, -1); // Discard null byte at end } async _deviceId() { debug('Loading device properties to form a standard device ID'); const properties = await this.client .getDevice(this.serial) .getProperties(); const id = (() => { const ref = ['ro.product.name', 'ro.product.model', 'ro.product.device']; const results = []; for (let i = 0, len = ref.length; i < len; i++) { const prop = ref[i]; results.push(`${prop}=${properties[prop]};`); } return results; })().join(''); return Buffer.from(`device::${id}\x00`); } } Socket.AuthError = AuthError; Socket.UnauthorizedError = UnauthorizedError; export default Socket; //# sourceMappingURL=socket.js.map