From 96824ef9845486ebe04843732c164926792f4df1 Mon Sep 17 00:00:00 2001 From: Simon Chan <1330321+yume-chan@users.noreply.github.com> Date: Tue, 30 Sep 2025 21:11:54 +0800 Subject: [PATCH] refactor(adb): completely separate daemon authenticator --- libraries/adb-credential-web/src/index.ts | 2 +- .../src/{store.ts => manager.ts} | 40 ++- libraries/adb/src/daemon/auth.spec.ts | 27 +- libraries/adb/src/daemon/auth.ts | 249 +++++++++++++++--- libraries/adb/src/daemon/transport.ts | 233 +--------------- 5 files changed, 277 insertions(+), 274 deletions(-) rename libraries/adb-credential-web/src/{store.ts => manager.ts} (69%) diff --git a/libraries/adb-credential-web/src/index.ts b/libraries/adb-credential-web/src/index.ts index 6528b220..c3d67a92 100644 --- a/libraries/adb-credential-web/src/index.ts +++ b/libraries/adb-credential-web/src/index.ts @@ -1,2 +1,2 @@ +export * from "./manager.js"; export * from "./storage/index.js"; -export * from "./store.js"; diff --git a/libraries/adb-credential-web/src/store.ts b/libraries/adb-credential-web/src/manager.ts similarity index 69% rename from libraries/adb-credential-web/src/store.ts rename to libraries/adb-credential-web/src/manager.ts index e16be2a5..4fc0e0c1 100644 --- a/libraries/adb-credential-web/src/store.ts +++ b/libraries/adb-credential-web/src/manager.ts @@ -1,13 +1,47 @@ import type { - AdbCredentialStore, + AdbCredentialManager, + AdbDaemonDefaultAuthenticationProcessorInit, AdbPrivateKey, MaybeError, } from "@yume-chan/adb"; -import { rsaParsePrivateKey } from "@yume-chan/adb"; +import { + AdbDaemonDefaultAuthenticationProcessor, + AdbDaemonDefaultAuthenticator, + rsaParsePrivateKey, +} from "@yume-chan/adb"; import type { TangoKeyStorage } from "./storage/index.js"; -export class AdbWebCryptoCredentialManager implements AdbCredentialStore { +export class AdbWebCryptoCredentialManager implements AdbCredentialManager { + static createDefaultAuthenticationProcessor( + storage: TangoKeyStorage, + init?: Omit< + AdbDaemonDefaultAuthenticationProcessorInit, + "credentialStore" + > & { + name?: string; + }, + ) { + return new AdbDaemonDefaultAuthenticationProcessor({ + ...init, + credentialManager: new this(storage, init?.name), + }); + } + + static createDefaultAuthenticator( + storage: TangoKeyStorage, + init?: Omit< + AdbDaemonDefaultAuthenticationProcessorInit, + "credentialStore" + > & { + name?: string; + }, + ) { + return new AdbDaemonDefaultAuthenticator(() => + this.createDefaultAuthenticationProcessor(storage, init), + ); + } + readonly #storage: TangoKeyStorage; readonly #name: string | undefined; diff --git a/libraries/adb/src/daemon/auth.spec.ts b/libraries/adb/src/daemon/auth.spec.ts index 806ac67c..452cc346 100644 --- a/libraries/adb/src/daemon/auth.spec.ts +++ b/libraries/adb/src/daemon/auth.spec.ts @@ -5,13 +5,16 @@ import { encodeUtf8 } from "@yume-chan/struct"; import { decodeBase64 } from "../utils/base64.js"; -import type { AdbCredentialStore } from "./auth.js"; -import { AdbAuthType, AdbDefaultAuthenticator } from "./auth.js"; +import type { AdbCredentialManager } from "./auth.js"; +import { + AdbAuthType, + AdbDaemonDefaultAuthenticationProcessor, +} from "./auth.js"; import type { SimpleRsaPrivateKey } from "./crypto.js"; import { rsaParsePrivateKey } from "./crypto.js"; import { AdbCommand } from "./packet.js"; -class MockCredentialStore implements AdbCredentialStore { +class MockCredentialStore implements AdbCredentialManager { key: SimpleRsaPrivateKey; name: string | undefined; @@ -75,17 +78,19 @@ const PUBLIC_KEY = "QAAAANVsDNqDk46/2Qg74n5POy5nK/XA8glCLkvXMks9p885+GQ2WiVUctG8LP/W5cII11Pk1KsZ+90ccZV2fdjv+tnW/8li9iEWTC+G1udFMxsIQ+HRPvJF0Xl9JXDsC6pvdo9ic4d6r5BC9BGiijd0enoG/tHkJhMhbPf/j7+MWXDrF+BeJeyj0mWArbqS599IO2qUCZiNjRakAa/iESG6Om4xCJWTT8wGhSTs81cHcEeSmQ2ixRwS+uaa/8iK/mv6BvCep5qgFrJW1G9LD2WciVgTpOSc6B1N/OA92hwJYp2lHLPWZl6bJIYHqrzdHCxc4EEVVYHkSBdFy1w2vhg2YgRTlpbP00NVrZb6Car8BTqPnwTRIkHBC6nnrg6cWMQ0xusMtxChKBoYGhCLHY4iKK6ra3P1Ou1UXu0WySau3s+Av9FFXxtAuMAJUA+5GSMQGGECRhwLX910OfnHHN+VxqJkHQye4vNhIH5C1dJ39HJoxAdwH2tF7v7GF2fwsy2lUa3Vj6bBssWivCB9cKyJR0GVPZJZ1uah24ecvspwtAqbtxvj7ZD9l7AD92geEJdLrsbfhNaDyAioQ2grI32gdp80su/7BrdAsPaSomxCYBB8opmS+oJq6qTYxNZ0doT9EEyT5D9rl9UXXxq+rQbDpKV1rOQo5zJJ2GkELhUrslFm6n4+JQEAAQA="; describe("auth", () => { - describe("AdbDefaultAuthenticator", () => { + describe("AdbDaemonDefaultAuthenticationProcessor", () => { it("should generate correct public key without name", async () => { const store = new MockCredentialStore( new Uint8Array(PRIVATE_KEY), undefined, ); - const authenticator = new AdbDefaultAuthenticator(store); + const authenticator = new AdbDaemonDefaultAuthenticationProcessor({ + credentialManager: store, + }); const challenge = new Uint8Array(20); - const first = await authenticator.authenticate({ + const first = await authenticator.process({ command: AdbCommand.Auth, arg0: AdbAuthType.Token, arg1: 0, @@ -96,7 +101,7 @@ describe("auth", () => { assert.strictEqual(first.command, AdbCommand.Auth); assert.strictEqual(first.arg0, AdbAuthType.Signature); - const result = await authenticator.authenticate({ + const result = await authenticator.process({ command: AdbCommand.Auth, arg0: AdbAuthType.Token, arg1: 0, @@ -119,10 +124,12 @@ describe("auth", () => { name, ); - const authenticator = new AdbDefaultAuthenticator(store); + const authenticator = new AdbDaemonDefaultAuthenticationProcessor({ + credentialManager: store, + }); const challenge = new Uint8Array(20); - const first = await authenticator.authenticate({ + const first = await authenticator.process({ command: AdbCommand.Auth, arg0: AdbAuthType.Token, arg1: 0, @@ -133,7 +140,7 @@ describe("auth", () => { assert.strictEqual(first.command, AdbCommand.Auth); assert.strictEqual(first.arg0, AdbAuthType.Signature); - const result = await authenticator.authenticate({ + const result = await authenticator.process({ command: AdbCommand.Auth, arg0: AdbAuthType.Token, arg1: 0, diff --git a/libraries/adb/src/daemon/auth.ts b/libraries/adb/src/daemon/auth.ts index b8651224..68f75a54 100644 --- a/libraries/adb/src/daemon/auth.ts +++ b/libraries/adb/src/daemon/auth.ts @@ -1,7 +1,14 @@ import type { MaybePromiseLike } from "@yume-chan/async"; -import { EventEmitter } from "@yume-chan/event"; -import { EmptyUint8Array } from "@yume-chan/struct"; +import { PromiseResolver } from "@yume-chan/async"; +import type { WritableStreamDefaultWriter } from "@yume-chan/stream-extra"; +import { + AbortController, + Consumable, + WritableStream, +} from "@yume-chan/stream-extra"; +import { decodeUtf8, EmptyUint8Array } from "@yume-chan/struct"; +import { AdbDeviceFeatures, AdbFeature } from "../features.js"; import { calculateBase64EncodedLength, encodeBase64, @@ -15,8 +22,13 @@ import { adbGetPublicKeySize, rsaSign, } from "./crypto.js"; -import type { AdbPacketData } from "./packet.js"; -import { AdbCommand } from "./packet.js"; +import type { AdbPacketData, AdbPacketInit } from "./packet.js"; +import { AdbCommand, calculateChecksum } from "./packet.js"; +import type { AdbDaemonTransportInit } from "./transport.js"; +import { + ADB_DAEMON_DEFAULT_INITIAL_PAYLOAD_SIZE, + AdbDaemonTransport, +} from "./transport.js"; export interface AdbPrivateKey extends SimpleRsaPrivateKey { name?: string | undefined; @@ -28,7 +40,7 @@ export type AdbKeyIterable = | Iterable> | AsyncIterable>; -export interface AdbCredentialStore { +export interface AdbCredentialManager { /** * Generates and stores a RSA private key with modulus length `2048` and public exponent `65537`. */ @@ -64,14 +76,29 @@ export const AdbAuthType = { export type AdbAuthType = (typeof AdbAuthType)[keyof typeof AdbAuthType]; -export interface AdbAuthenticator { - authenticate(packet: AdbPacketData): Promise; +export interface AdbDaemonAuthenticationProcessor { + process(packet: AdbPacketData): Promise; close?(): MaybePromiseLike; } -export class AdbDefaultAuthenticator implements AdbAuthenticator { - #credentialStore: AdbCredentialStore; +export interface AdbDaemonDefaultAuthenticationProcessorInit { + credentialManager: AdbCredentialManager; + onKeyLoadError?: ((error: Error) => void) | undefined; + onSignatureAuthentication?: ((key: AdbKeyInfo) => void) | undefined; + onSignatureRejected?: ((key: AdbKeyInfo) => void) | undefined; + onPublicKeyAuthentication?: ((key: AdbKeyInfo) => void) | undefined; +} + +export class AdbDaemonDefaultAuthenticationProcessor + implements AdbDaemonAuthenticationProcessor +{ + #credentialStore: AdbCredentialManager; + #onKeyLoadError: ((error: Error) => void) | undefined; + #onSignatureAuthentication: ((key: AdbKeyInfo) => void) | undefined; + #onSignatureRejected: ((key: AdbKeyInfo) => void) | undefined; + #onPublicKeyAuthentication: ((key: AdbKeyInfo) => void) | undefined; + #iterator: | Iterator, void, void> | AsyncIterator, void, void> @@ -80,28 +107,12 @@ export class AdbDefaultAuthenticator implements AdbAuthenticator { #prevKeyInfo: AdbKeyInfo | undefined; #firstKey: AdbPrivateKey | undefined; - #onKeyLoadError = new EventEmitter(); - get onKeyLoadError() { - return this.#onKeyLoadError.event; - } - - #onSignatureAuthentication = new EventEmitter(); - get onSignatureAuthentication() { - return this.#onSignatureAuthentication.event; - } - - #onSignatureRejected = new EventEmitter(); - get onSignatureRejected() { - return this.#onSignatureRejected.event; - } - - #onPublicKeyAuthentication = new EventEmitter(); - get onPublicKeyAuthentication() { - return this.#onPublicKeyAuthentication.event; - } - - constructor(credentialStore: AdbCredentialStore) { - this.#credentialStore = credentialStore; + constructor(init: AdbDaemonDefaultAuthenticationProcessorInit) { + this.#credentialStore = init.credentialManager; + this.#onKeyLoadError = init.onKeyLoadError; + this.#onSignatureAuthentication = init.onSignatureAuthentication; + this.#onSignatureRejected = init.onSignatureRejected; + this.#onPublicKeyAuthentication = init.onPublicKeyAuthentication; } async #iterate(token: Uint8Array): Promise { @@ -122,7 +133,7 @@ export class AdbDefaultAuthenticator implements AdbAuthenticator { } if (result instanceof Error) { - this.#onKeyLoadError.fire(result); + this.#onKeyLoadError?.(result); return await this.#iterate(token); } @@ -132,12 +143,12 @@ export class AdbDefaultAuthenticator implements AdbAuthenticator { // A new token implies the previous signature was rejected. if (this.#prevKeyInfo) { - this.#onSignatureRejected.fire(this.#prevKeyInfo); + this.#onSignatureRejected?.(this.#prevKeyInfo); } const fingerprint = getFingerprint(result); this.#prevKeyInfo = { fingerprint, name: result.name }; - this.#onSignatureAuthentication.fire(this.#prevKeyInfo); + this.#onSignatureAuthentication?.(this.#prevKeyInfo); return { command: AdbCommand.Auth, @@ -147,7 +158,7 @@ export class AdbDefaultAuthenticator implements AdbAuthenticator { }; } - async authenticate(packet: AdbPacketData): Promise { + async process(packet: AdbPacketData): Promise { if (packet.arg0 !== AdbAuthType.Token) { throw new Error("Unsupported authentication packet"); } @@ -186,7 +197,7 @@ export class AdbDefaultAuthenticator implements AdbAuthenticator { publicKeyBuffer.set(nameBuffer, publicKeyBase64Length + 1); } - this.#onPublicKeyAuthentication.fire({ + this.#onPublicKeyAuthentication?.({ fingerprint: getFingerprint(key), name: key.name, }); @@ -206,3 +217,169 @@ export class AdbDefaultAuthenticator implements AdbAuthenticator { this.#firstKey = undefined; } } + +export type AdbDaemonAuthenticateOptions = Pick< + AdbDaemonTransportInit, + | "serial" + | "connection" + | "features" + | "initialDelayedAckBytes" + | "preserveConnection" + | "readTimeLimit" +>; + +export interface AdbDaemonAuthenticator { + authenticate( + options: AdbDaemonAuthenticateOptions, + ): Promise; +} + +export class AdbDaemonDefaultAuthenticator implements AdbDaemonAuthenticator { + static authenticate( + processor: AdbDaemonDefaultAuthenticationProcessor, + options: AdbDaemonAuthenticateOptions, + ): Promise { + const authenticator = new AdbDaemonDefaultAuthenticator( + () => processor, + ); + return authenticator.authenticate(options); + } + + #createProcessor: () => AdbDaemonAuthenticationProcessor; + + constructor(createProcessor: () => AdbDaemonAuthenticationProcessor) { + this.#createProcessor = createProcessor; + } + + #sendPacket( + writer: WritableStreamDefaultWriter>, + init: AdbPacketData, + ): Promise; + #sendPacket( + writer: WritableStreamDefaultWriter>, + init: AdbPacketInit, + ) { + // Always send checksum in auth steps + // Because we don't know if the device needs it or not. + init.checksum = calculateChecksum(init.payload); + init.magic = init.command ^ 0xffffffff; + return Consumable.WritableStream.write(writer, init); + } + + async authenticate({ + serial, + connection, + features = AdbDeviceFeatures, + initialDelayedAckBytes = ADB_DAEMON_DEFAULT_INITIAL_PAYLOAD_SIZE, + preserveConnection, + readTimeLimit, + }: AdbDaemonAuthenticateOptions): Promise { + const processor = this.#createProcessor(); + + // Initially, set to highest-supported version and payload size. + let version = 0x01000001; + // Android 4: 4K, Android 7: 256K, Android 9: 1M + let maxPayloadSize = 1024 * 1024; + + const resolver = new PromiseResolver(); + + // Here is similar to `AdbPacketDispatcher`, + // But the received packet types and send packet processing are different. + const abortController = new AbortController(); + + const writer = connection.writable.getWriter(); + + const pipe = connection.readable + .pipeTo( + new WritableStream({ + write: async (packet) => { + switch (packet.command) { + case AdbCommand.Connect: + version = Math.min(version, packet.arg0); + maxPayloadSize = Math.min( + maxPayloadSize, + packet.arg1, + ); + resolver.resolve(decodeUtf8(packet.payload)); + break; + case AdbCommand.Auth: { + await this.#sendPacket( + writer, + await processor.process(packet), + ); + break; + } + default: + // Maybe the previous ADB client exited without reading all packets, + // so they are still waiting in OS internal buffer. + // Just ignore them. + // Because a `Connect` packet will reset the device, + // Eventually there will be `Connect` and `Auth` response packets. + break; + } + }, + }), + { + // Don't cancel the source ReadableStream on AbortSignal abort. + preventCancel: true, + signal: abortController.signal, + }, + ) + .then( + async () => { + await processor.close?.(); + + // If `resolver` is already settled, call `reject` won't do anything. + resolver.reject( + new Error("Connection closed unexpectedly"), + ); + }, + async (e) => { + await processor.close?.(); + + resolver.reject(e); + }, + ); + + if (initialDelayedAckBytes <= 0) { + const index = features.indexOf(AdbFeature.DelayedAck); + if (index !== -1) { + features = features.toSpliced(index, 1); + } + } + + let banner: string; + try { + await this.#sendPacket(writer, { + command: AdbCommand.Connect, + arg0: version, + arg1: maxPayloadSize, + // The terminating `;` is required in formal definition + // But ADB daemon (all versions) can still work without it + payload: encodeUtf8(`host::features=${features.join(",")}`), + }); + + banner = await resolver.promise; + } finally { + // When failed, release locks on `connection` so the caller can try again. + // When success, also release locks so `AdbPacketDispatcher` can use them. + abortController.abort(); + writer.releaseLock(); + + // Wait until pipe stops (`ReadableStream` lock released) + await pipe; + } + + return new AdbDaemonTransport({ + serial, + connection, + version, + maxPayloadSize, + banner, + features, + initialDelayedAckBytes, + preserveConnection: preserveConnection, + readTimeLimit: readTimeLimit, + }); + } +} diff --git a/libraries/adb/src/daemon/transport.ts b/libraries/adb/src/daemon/transport.ts index 504393ba..371e1005 100644 --- a/libraries/adb/src/daemon/transport.ts +++ b/libraries/adb/src/daemon/transport.ts @@ -1,12 +1,5 @@ import type { MaybePromiseLike } from "@yume-chan/async"; -import { PromiseResolver } from "@yume-chan/async"; -import type { ReadableWritablePair } from "@yume-chan/stream-extra"; -import { - AbortController, - Consumable, - WritableStream, -} from "@yume-chan/stream-extra"; -import { decodeUtf8, encodeUtf8 } from "@yume-chan/struct"; +import type { Consumable, ReadableWritablePair } from "@yume-chan/stream-extra"; import type { AdbIncomingSocketHandler, @@ -17,14 +10,11 @@ import { AdbBanner } from "../banner.js"; import { AdbDeviceFeatures, AdbFeature } from "../features.js"; import type { - AdbAuthenticator, - AdbCredentialStore, - AdbKeyInfo, + AdbDaemonAuthenticateOptions, + AdbDaemonAuthenticator, } from "./auth.js"; -import { AdbDefaultAuthenticator } from "./auth.js"; import { AdbPacketDispatcher } from "./dispatcher.js"; import type { AdbPacketData, AdbPacketInit } from "./packet.js"; -import { AdbCommand, calculateChecksum } from "./packet.js"; export const ADB_DAEMON_VERSION_OMIT_CHECKSUM = 0x01000001; export const ADB_DAEMON_DEFAULT_INITIAL_PAYLOAD_SIZE = 32 * 1024 * 1024; @@ -34,53 +24,7 @@ export type AdbDaemonConnection = ReadableWritablePair< Consumable >; -export interface AdbDaemonAuthenticationOptions { - serial: string; - connection: AdbDaemonConnection; - features?: readonly AdbFeature[]; - - /** - * The number of bytes the device can send before receiving an ack packet. - * Using delayed ack can improve the throughput, - * especially when the device is connected over Wi-Fi (so the latency is higher). - * - * Set to 0 or any negative value to disable delayed ack in handshake. - * Otherwise the value must be in the range of unsigned 32-bit integer. - * - * Delayed ack was added in Android 14, - * this option will be ignored when the device doesn't support it. - * - * @default ADB_DAEMON_DEFAULT_INITIAL_PAYLOAD_SIZE - */ - initialDelayedAckBytes?: number; - - /** - * Whether to keep the `connection` open (don't call `writable.close` and `readable.cancel`) - * when `AdbDaemonTransport.close` is called. - * - * Note that when `authenticate` fails, - * no matter which value this option has, - * the `connection` is always kept open, so it can be used in another `authenticate` call. - * - * @default false - */ - preserveConnection?: boolean | undefined; - - /** - * When set, the transport will throw an error when - * one of the socket readable stalls for this amount of milliseconds. - * - * Because ADB is a multiplexed protocol, blocking one socket will also block all other sockets. - * It's important to always read from all sockets to prevent stalling. - * - * This option is helpful to detect bugs in the client code. - * - * @default undefined - */ - readTimeLimit?: number | undefined; -} - -interface AdbDaemonTransportInit { +export interface AdbDaemonTransportInit { serial: string; connection: AdbDaemonConnection; version: number; @@ -128,170 +72,11 @@ interface AdbDaemonTransportInit { * An ADB Transport that connects to ADB Daemons directly. */ export class AdbDaemonTransport implements AdbTransport { - /** - * Authenticate with the ADB Daemon and create a new transport. - */ - static async authenticate({ - serial, - connection, - features = AdbDeviceFeatures, - initialDelayedAckBytes = ADB_DAEMON_DEFAULT_INITIAL_PAYLOAD_SIZE, - ...options - }: AdbDaemonAuthenticationOptions & - ( - | { authenticator: AdbAuthenticator } - | { - credentialStore: AdbCredentialStore; - onKeyLoadError?: ((error: Error) => void) | undefined; - onSignatureAuthentication?: - | ((key: AdbKeyInfo) => void) - | undefined; - onSignatureRejected?: ((key: AdbKeyInfo) => void) | undefined; - onPublicKeyAuthentication?: - | ((key: AdbKeyInfo) => void) - | undefined; - } - )): Promise { - // Initially, set to highest-supported version and payload size. - let version = 0x01000001; - // Android 4: 4K, Android 7: 256K, Android 9: 1M - let maxPayloadSize = 1024 * 1024; - - const resolver = new PromiseResolver(); - let authenticator: AdbAuthenticator; - if ("authenticator" in options) { - authenticator = options.authenticator; - } else { - const defaultAuthenticator = new AdbDefaultAuthenticator( - options.credentialStore, - ); - if (options.onKeyLoadError) { - defaultAuthenticator.onKeyLoadError(options.onKeyLoadError); - } - if (options.onSignatureAuthentication) { - defaultAuthenticator.onSignatureAuthentication( - options.onSignatureAuthentication, - ); - } - if (options.onSignatureRejected) { - defaultAuthenticator.onSignatureRejected( - options.onSignatureRejected, - ); - } - if (options.onPublicKeyAuthentication) { - defaultAuthenticator.onPublicKeyAuthentication( - options.onPublicKeyAuthentication, - ); - } - authenticator = defaultAuthenticator; - } - - // Here is similar to `AdbPacketDispatcher`, - // But the received packet types and send packet processing are different. - const abortController = new AbortController(); - const pipe = connection.readable - .pipeTo( - new WritableStream({ - async write(packet) { - switch (packet.command) { - case AdbCommand.Connect: - version = Math.min(version, packet.arg0); - maxPayloadSize = Math.min( - maxPayloadSize, - packet.arg1, - ); - resolver.resolve(decodeUtf8(packet.payload)); - break; - case AdbCommand.Auth: { - await sendPacket( - await authenticator.authenticate(packet), - ); - break; - } - default: - // Maybe the previous ADB client exited without reading all packets, - // so they are still waiting in OS internal buffer. - // Just ignore them. - // Because a `Connect` packet will reset the device, - // Eventually there will be `Connect` and `Auth` response packets. - break; - } - }, - }), - { - // Don't cancel the source ReadableStream on AbortSignal abort. - preventCancel: true, - signal: abortController.signal, - }, - ) - .then( - async () => { - await authenticator.close?.(); - - // If `resolver` is already settled, call `reject` won't do anything. - resolver.reject( - new Error("Connection closed unexpectedly"), - ); - }, - async (e) => { - await authenticator.close?.(); - - resolver.reject(e); - }, - ); - - const writer = connection.writable.getWriter(); - async function sendPacket(init: AdbPacketData) { - // Always send checksum in auth steps - // Because we don't know if the device needs it or not. - (init as AdbPacketInit).checksum = calculateChecksum(init.payload); - (init as AdbPacketInit).magic = init.command ^ 0xffffffff; - await Consumable.WritableStream.write( - writer, - init as AdbPacketInit, - ); - } - - if (initialDelayedAckBytes <= 0) { - const index = features.indexOf(AdbFeature.DelayedAck); - if (index !== -1) { - features = features.toSpliced(index, 1); - } - } - - let banner: string; - try { - await sendPacket({ - command: AdbCommand.Connect, - arg0: version, - arg1: maxPayloadSize, - // The terminating `;` is required in formal definition - // But ADB daemon (all versions) can still work without it - payload: encodeUtf8(`host::features=${features.join(",")}`), - }); - - banner = await resolver.promise; - } finally { - // When failed, release locks on `connection` so the caller can try again. - // When success, also release locks so `AdbPacketDispatcher` can use them. - abortController.abort(); - writer.releaseLock(); - - // Wait until pipe stops (`ReadableStream` lock released) - await pipe; - } - - return new AdbDaemonTransport({ - serial, - connection, - version, - maxPayloadSize, - banner, - features, - initialDelayedAckBytes, - preserveConnection: options.preserveConnection, - readTimeLimit: options.readTimeLimit, - }); + static authenticate( + authenticator: AdbDaemonAuthenticator, + options: AdbDaemonAuthenticateOptions, + ) { + return authenticator.authenticate(options); } #connection: AdbDaemonConnection;