mirror of
https://github.com/yume-chan/ya-webadb.git
synced 2025-10-03 01:39:21 +02:00
refactor(adb): completely separate daemon authenticator
This commit is contained in:
parent
6db5d8eb74
commit
96824ef984
5 changed files with 277 additions and 274 deletions
|
@ -1,2 +1,2 @@
|
||||||
|
export * from "./manager.js";
|
||||||
export * from "./storage/index.js";
|
export * from "./storage/index.js";
|
||||||
export * from "./store.js";
|
|
||||||
|
|
|
@ -1,13 +1,47 @@
|
||||||
import type {
|
import type {
|
||||||
AdbCredentialStore,
|
AdbCredentialManager,
|
||||||
|
AdbDaemonDefaultAuthenticationProcessorInit,
|
||||||
AdbPrivateKey,
|
AdbPrivateKey,
|
||||||
MaybeError,
|
MaybeError,
|
||||||
} from "@yume-chan/adb";
|
} 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";
|
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 #storage: TangoKeyStorage;
|
||||||
|
|
||||||
readonly #name: string | undefined;
|
readonly #name: string | undefined;
|
|
@ -5,13 +5,16 @@ import { encodeUtf8 } from "@yume-chan/struct";
|
||||||
|
|
||||||
import { decodeBase64 } from "../utils/base64.js";
|
import { decodeBase64 } from "../utils/base64.js";
|
||||||
|
|
||||||
import type { AdbCredentialStore } from "./auth.js";
|
import type { AdbCredentialManager } from "./auth.js";
|
||||||
import { AdbAuthType, AdbDefaultAuthenticator } from "./auth.js";
|
import {
|
||||||
|
AdbAuthType,
|
||||||
|
AdbDaemonDefaultAuthenticationProcessor,
|
||||||
|
} from "./auth.js";
|
||||||
import type { SimpleRsaPrivateKey } from "./crypto.js";
|
import type { SimpleRsaPrivateKey } from "./crypto.js";
|
||||||
import { rsaParsePrivateKey } from "./crypto.js";
|
import { rsaParsePrivateKey } from "./crypto.js";
|
||||||
import { AdbCommand } from "./packet.js";
|
import { AdbCommand } from "./packet.js";
|
||||||
|
|
||||||
class MockCredentialStore implements AdbCredentialStore {
|
class MockCredentialStore implements AdbCredentialManager {
|
||||||
key: SimpleRsaPrivateKey;
|
key: SimpleRsaPrivateKey;
|
||||||
name: string | undefined;
|
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=";
|
"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("auth", () => {
|
||||||
describe("AdbDefaultAuthenticator", () => {
|
describe("AdbDaemonDefaultAuthenticationProcessor", () => {
|
||||||
it("should generate correct public key without name", async () => {
|
it("should generate correct public key without name", async () => {
|
||||||
const store = new MockCredentialStore(
|
const store = new MockCredentialStore(
|
||||||
new Uint8Array(PRIVATE_KEY),
|
new Uint8Array(PRIVATE_KEY),
|
||||||
undefined,
|
undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
const authenticator = new AdbDefaultAuthenticator(store);
|
const authenticator = new AdbDaemonDefaultAuthenticationProcessor({
|
||||||
|
credentialManager: store,
|
||||||
|
});
|
||||||
const challenge = new Uint8Array(20);
|
const challenge = new Uint8Array(20);
|
||||||
|
|
||||||
const first = await authenticator.authenticate({
|
const first = await authenticator.process({
|
||||||
command: AdbCommand.Auth,
|
command: AdbCommand.Auth,
|
||||||
arg0: AdbAuthType.Token,
|
arg0: AdbAuthType.Token,
|
||||||
arg1: 0,
|
arg1: 0,
|
||||||
|
@ -96,7 +101,7 @@ describe("auth", () => {
|
||||||
assert.strictEqual(first.command, AdbCommand.Auth);
|
assert.strictEqual(first.command, AdbCommand.Auth);
|
||||||
assert.strictEqual(first.arg0, AdbAuthType.Signature);
|
assert.strictEqual(first.arg0, AdbAuthType.Signature);
|
||||||
|
|
||||||
const result = await authenticator.authenticate({
|
const result = await authenticator.process({
|
||||||
command: AdbCommand.Auth,
|
command: AdbCommand.Auth,
|
||||||
arg0: AdbAuthType.Token,
|
arg0: AdbAuthType.Token,
|
||||||
arg1: 0,
|
arg1: 0,
|
||||||
|
@ -119,10 +124,12 @@ describe("auth", () => {
|
||||||
name,
|
name,
|
||||||
);
|
);
|
||||||
|
|
||||||
const authenticator = new AdbDefaultAuthenticator(store);
|
const authenticator = new AdbDaemonDefaultAuthenticationProcessor({
|
||||||
|
credentialManager: store,
|
||||||
|
});
|
||||||
const challenge = new Uint8Array(20);
|
const challenge = new Uint8Array(20);
|
||||||
|
|
||||||
const first = await authenticator.authenticate({
|
const first = await authenticator.process({
|
||||||
command: AdbCommand.Auth,
|
command: AdbCommand.Auth,
|
||||||
arg0: AdbAuthType.Token,
|
arg0: AdbAuthType.Token,
|
||||||
arg1: 0,
|
arg1: 0,
|
||||||
|
@ -133,7 +140,7 @@ describe("auth", () => {
|
||||||
assert.strictEqual(first.command, AdbCommand.Auth);
|
assert.strictEqual(first.command, AdbCommand.Auth);
|
||||||
assert.strictEqual(first.arg0, AdbAuthType.Signature);
|
assert.strictEqual(first.arg0, AdbAuthType.Signature);
|
||||||
|
|
||||||
const result = await authenticator.authenticate({
|
const result = await authenticator.process({
|
||||||
command: AdbCommand.Auth,
|
command: AdbCommand.Auth,
|
||||||
arg0: AdbAuthType.Token,
|
arg0: AdbAuthType.Token,
|
||||||
arg1: 0,
|
arg1: 0,
|
||||||
|
|
|
@ -1,7 +1,14 @@
|
||||||
import type { MaybePromiseLike } from "@yume-chan/async";
|
import type { MaybePromiseLike } from "@yume-chan/async";
|
||||||
import { EventEmitter } from "@yume-chan/event";
|
import { PromiseResolver } from "@yume-chan/async";
|
||||||
import { EmptyUint8Array } from "@yume-chan/struct";
|
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 {
|
import {
|
||||||
calculateBase64EncodedLength,
|
calculateBase64EncodedLength,
|
||||||
encodeBase64,
|
encodeBase64,
|
||||||
|
@ -15,8 +22,13 @@ import {
|
||||||
adbGetPublicKeySize,
|
adbGetPublicKeySize,
|
||||||
rsaSign,
|
rsaSign,
|
||||||
} from "./crypto.js";
|
} from "./crypto.js";
|
||||||
import type { AdbPacketData } from "./packet.js";
|
import type { AdbPacketData, AdbPacketInit } from "./packet.js";
|
||||||
import { AdbCommand } 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 {
|
export interface AdbPrivateKey extends SimpleRsaPrivateKey {
|
||||||
name?: string | undefined;
|
name?: string | undefined;
|
||||||
|
@ -28,7 +40,7 @@ export type AdbKeyIterable =
|
||||||
| Iterable<MaybeError<AdbPrivateKey>>
|
| Iterable<MaybeError<AdbPrivateKey>>
|
||||||
| AsyncIterable<MaybeError<AdbPrivateKey>>;
|
| AsyncIterable<MaybeError<AdbPrivateKey>>;
|
||||||
|
|
||||||
export interface AdbCredentialStore {
|
export interface AdbCredentialManager {
|
||||||
/**
|
/**
|
||||||
* Generates and stores a RSA private key with modulus length `2048` and public exponent `65537`.
|
* 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 type AdbAuthType = (typeof AdbAuthType)[keyof typeof AdbAuthType];
|
||||||
|
|
||||||
export interface AdbAuthenticator {
|
export interface AdbDaemonAuthenticationProcessor {
|
||||||
authenticate(packet: AdbPacketData): Promise<AdbPacketData>;
|
process(packet: AdbPacketData): Promise<AdbPacketData>;
|
||||||
|
|
||||||
close?(): MaybePromiseLike<undefined>;
|
close?(): MaybePromiseLike<undefined>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AdbDefaultAuthenticator implements AdbAuthenticator {
|
export interface AdbDaemonDefaultAuthenticationProcessorInit {
|
||||||
#credentialStore: AdbCredentialStore;
|
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:
|
||||||
| Iterator<MaybeError<AdbPrivateKey>, void, void>
|
| Iterator<MaybeError<AdbPrivateKey>, void, void>
|
||||||
| AsyncIterator<MaybeError<AdbPrivateKey>, void, void>
|
| AsyncIterator<MaybeError<AdbPrivateKey>, void, void>
|
||||||
|
@ -80,28 +107,12 @@ export class AdbDefaultAuthenticator implements AdbAuthenticator {
|
||||||
#prevKeyInfo: AdbKeyInfo | undefined;
|
#prevKeyInfo: AdbKeyInfo | undefined;
|
||||||
#firstKey: AdbPrivateKey | undefined;
|
#firstKey: AdbPrivateKey | undefined;
|
||||||
|
|
||||||
#onKeyLoadError = new EventEmitter<Error>();
|
constructor(init: AdbDaemonDefaultAuthenticationProcessorInit) {
|
||||||
get onKeyLoadError() {
|
this.#credentialStore = init.credentialManager;
|
||||||
return this.#onKeyLoadError.event;
|
this.#onKeyLoadError = init.onKeyLoadError;
|
||||||
}
|
this.#onSignatureAuthentication = init.onSignatureAuthentication;
|
||||||
|
this.#onSignatureRejected = init.onSignatureRejected;
|
||||||
#onSignatureAuthentication = new EventEmitter<AdbKeyInfo>();
|
this.#onPublicKeyAuthentication = init.onPublicKeyAuthentication;
|
||||||
get onSignatureAuthentication() {
|
|
||||||
return this.#onSignatureAuthentication.event;
|
|
||||||
}
|
|
||||||
|
|
||||||
#onSignatureRejected = new EventEmitter<AdbKeyInfo>();
|
|
||||||
get onSignatureRejected() {
|
|
||||||
return this.#onSignatureRejected.event;
|
|
||||||
}
|
|
||||||
|
|
||||||
#onPublicKeyAuthentication = new EventEmitter<AdbKeyInfo>();
|
|
||||||
get onPublicKeyAuthentication() {
|
|
||||||
return this.#onPublicKeyAuthentication.event;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(credentialStore: AdbCredentialStore) {
|
|
||||||
this.#credentialStore = credentialStore;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async #iterate(token: Uint8Array): Promise<AdbPacketData | undefined> {
|
async #iterate(token: Uint8Array): Promise<AdbPacketData | undefined> {
|
||||||
|
@ -122,7 +133,7 @@ export class AdbDefaultAuthenticator implements AdbAuthenticator {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result instanceof Error) {
|
if (result instanceof Error) {
|
||||||
this.#onKeyLoadError.fire(result);
|
this.#onKeyLoadError?.(result);
|
||||||
return await this.#iterate(token);
|
return await this.#iterate(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,12 +143,12 @@ export class AdbDefaultAuthenticator implements AdbAuthenticator {
|
||||||
|
|
||||||
// A new token implies the previous signature was rejected.
|
// A new token implies the previous signature was rejected.
|
||||||
if (this.#prevKeyInfo) {
|
if (this.#prevKeyInfo) {
|
||||||
this.#onSignatureRejected.fire(this.#prevKeyInfo);
|
this.#onSignatureRejected?.(this.#prevKeyInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
const fingerprint = getFingerprint(result);
|
const fingerprint = getFingerprint(result);
|
||||||
this.#prevKeyInfo = { fingerprint, name: result.name };
|
this.#prevKeyInfo = { fingerprint, name: result.name };
|
||||||
this.#onSignatureAuthentication.fire(this.#prevKeyInfo);
|
this.#onSignatureAuthentication?.(this.#prevKeyInfo);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
command: AdbCommand.Auth,
|
command: AdbCommand.Auth,
|
||||||
|
@ -147,7 +158,7 @@ export class AdbDefaultAuthenticator implements AdbAuthenticator {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async authenticate(packet: AdbPacketData): Promise<AdbPacketData> {
|
async process(packet: AdbPacketData): Promise<AdbPacketData> {
|
||||||
if (packet.arg0 !== AdbAuthType.Token) {
|
if (packet.arg0 !== AdbAuthType.Token) {
|
||||||
throw new Error("Unsupported authentication packet");
|
throw new Error("Unsupported authentication packet");
|
||||||
}
|
}
|
||||||
|
@ -186,7 +197,7 @@ export class AdbDefaultAuthenticator implements AdbAuthenticator {
|
||||||
publicKeyBuffer.set(nameBuffer, publicKeyBase64Length + 1);
|
publicKeyBuffer.set(nameBuffer, publicKeyBase64Length + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#onPublicKeyAuthentication.fire({
|
this.#onPublicKeyAuthentication?.({
|
||||||
fingerprint: getFingerprint(key),
|
fingerprint: getFingerprint(key),
|
||||||
name: key.name,
|
name: key.name,
|
||||||
});
|
});
|
||||||
|
@ -206,3 +217,169 @@ export class AdbDefaultAuthenticator implements AdbAuthenticator {
|
||||||
this.#firstKey = undefined;
|
this.#firstKey = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type AdbDaemonAuthenticateOptions = Pick<
|
||||||
|
AdbDaemonTransportInit,
|
||||||
|
| "serial"
|
||||||
|
| "connection"
|
||||||
|
| "features"
|
||||||
|
| "initialDelayedAckBytes"
|
||||||
|
| "preserveConnection"
|
||||||
|
| "readTimeLimit"
|
||||||
|
>;
|
||||||
|
|
||||||
|
export interface AdbDaemonAuthenticator {
|
||||||
|
authenticate(
|
||||||
|
options: AdbDaemonAuthenticateOptions,
|
||||||
|
): Promise<AdbDaemonTransport>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AdbDaemonDefaultAuthenticator implements AdbDaemonAuthenticator {
|
||||||
|
static authenticate(
|
||||||
|
processor: AdbDaemonDefaultAuthenticationProcessor,
|
||||||
|
options: AdbDaemonAuthenticateOptions,
|
||||||
|
): Promise<AdbDaemonTransport> {
|
||||||
|
const authenticator = new AdbDaemonDefaultAuthenticator(
|
||||||
|
() => processor,
|
||||||
|
);
|
||||||
|
return authenticator.authenticate(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
#createProcessor: () => AdbDaemonAuthenticationProcessor;
|
||||||
|
|
||||||
|
constructor(createProcessor: () => AdbDaemonAuthenticationProcessor) {
|
||||||
|
this.#createProcessor = createProcessor;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sendPacket(
|
||||||
|
writer: WritableStreamDefaultWriter<Consumable<AdbPacketInit>>,
|
||||||
|
init: AdbPacketData,
|
||||||
|
): Promise<void>;
|
||||||
|
#sendPacket(
|
||||||
|
writer: WritableStreamDefaultWriter<Consumable<AdbPacketInit>>,
|
||||||
|
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<AdbDaemonTransport> {
|
||||||
|
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<string>();
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,12 +1,5 @@
|
||||||
import type { MaybePromiseLike } from "@yume-chan/async";
|
import type { MaybePromiseLike } from "@yume-chan/async";
|
||||||
import { PromiseResolver } from "@yume-chan/async";
|
import type { Consumable, ReadableWritablePair } from "@yume-chan/stream-extra";
|
||||||
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 {
|
import type {
|
||||||
AdbIncomingSocketHandler,
|
AdbIncomingSocketHandler,
|
||||||
|
@ -17,14 +10,11 @@ import { AdbBanner } from "../banner.js";
|
||||||
import { AdbDeviceFeatures, AdbFeature } from "../features.js";
|
import { AdbDeviceFeatures, AdbFeature } from "../features.js";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
AdbAuthenticator,
|
AdbDaemonAuthenticateOptions,
|
||||||
AdbCredentialStore,
|
AdbDaemonAuthenticator,
|
||||||
AdbKeyInfo,
|
|
||||||
} from "./auth.js";
|
} from "./auth.js";
|
||||||
import { AdbDefaultAuthenticator } from "./auth.js";
|
|
||||||
import { AdbPacketDispatcher } from "./dispatcher.js";
|
import { AdbPacketDispatcher } from "./dispatcher.js";
|
||||||
import type { AdbPacketData, AdbPacketInit } from "./packet.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_VERSION_OMIT_CHECKSUM = 0x01000001;
|
||||||
export const ADB_DAEMON_DEFAULT_INITIAL_PAYLOAD_SIZE = 32 * 1024 * 1024;
|
export const ADB_DAEMON_DEFAULT_INITIAL_PAYLOAD_SIZE = 32 * 1024 * 1024;
|
||||||
|
@ -34,53 +24,7 @@ export type AdbDaemonConnection = ReadableWritablePair<
|
||||||
Consumable<AdbPacketInit>
|
Consumable<AdbPacketInit>
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export interface AdbDaemonAuthenticationOptions {
|
export interface AdbDaemonTransportInit {
|
||||||
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 {
|
|
||||||
serial: string;
|
serial: string;
|
||||||
connection: AdbDaemonConnection;
|
connection: AdbDaemonConnection;
|
||||||
version: number;
|
version: number;
|
||||||
|
@ -128,170 +72,11 @@ interface AdbDaemonTransportInit {
|
||||||
* An ADB Transport that connects to ADB Daemons directly.
|
* An ADB Transport that connects to ADB Daemons directly.
|
||||||
*/
|
*/
|
||||||
export class AdbDaemonTransport implements AdbTransport {
|
export class AdbDaemonTransport implements AdbTransport {
|
||||||
/**
|
static authenticate(
|
||||||
* Authenticate with the ADB Daemon and create a new transport.
|
authenticator: AdbDaemonAuthenticator,
|
||||||
*/
|
options: AdbDaemonAuthenticateOptions,
|
||||||
static async authenticate({
|
) {
|
||||||
serial,
|
return authenticator.authenticate(options);
|
||||||
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<AdbDaemonTransport> {
|
|
||||||
// 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<string>();
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#connection: AdbDaemonConnection;
|
#connection: AdbDaemonConnection;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue