UNPKG

@yume-chan/adb

Version:

TypeScript implementation of Android Debug Bridge (ADB) protocol.

205 lines (169 loc) 6.14 kB
import type { MaybePromiseLike } from "@yume-chan/async"; import { PromiseResolver } from "@yume-chan/async"; import type { Disposable } from "@yume-chan/event"; import { EmptyUint8Array } from "@yume-chan/struct"; import { calculateBase64EncodedLength, encodeBase64, encodeUtf8, } from "../utils/index.js"; import { adbGeneratePublicKey, adbGetPublicKeySize, rsaSign, } from "./crypto.js"; import type { AdbPacketData } from "./packet.js"; import { AdbCommand } from "./packet.js"; export interface AdbPrivateKey { /** * The private key in PKCS #8 format. */ buffer: Uint8Array; name?: string | undefined; } export type AdbKeyIterable = | Iterable<AdbPrivateKey> | AsyncIterable<AdbPrivateKey>; export interface AdbCredentialStore { /** * Generates and stores a RSA private key with modulus length `2048` and public exponent `65537`. */ generateKey(): MaybePromiseLike<AdbPrivateKey>; /** * Synchronously or asynchronously iterates through all stored RSA private keys. * * Each call to `iterateKeys` must return a different iterator that iterate through all stored keys. */ iterateKeys(): AdbKeyIterable; } export const AdbAuthType = { Token: 1, Signature: 2, PublicKey: 3, } as const; export type AdbAuthType = (typeof AdbAuthType)[keyof typeof AdbAuthType]; export interface AdbAuthenticator { /** * @param getNextRequest * * Call this function to get the next authentication request packet from device. * * After calling `getNextRequest`, authenticator can `yield` a packet as response, or `return` to indicate its incapability of handling the request. * * After `return`, the `AdbAuthenticatorHandler` will move on to next authenticator and never go back. * * Calling `getNextRequest` multiple times without `yield` or `return` will always return the same request. */ ( credentialStore: AdbCredentialStore, getNextRequest: () => Promise<AdbPacketData>, ): AsyncIterable<AdbPacketData>; } export const AdbSignatureAuthenticator: AdbAuthenticator = async function* ( credentialStore: AdbCredentialStore, getNextRequest: () => Promise<AdbPacketData>, ): AsyncIterable<AdbPacketData> { for await (const key of credentialStore.iterateKeys()) { const packet = await getNextRequest(); if (packet.arg0 !== AdbAuthType.Token) { return; } const signature = rsaSign(key.buffer, packet.payload); yield { command: AdbCommand.Auth, arg0: AdbAuthType.Signature, arg1: 0, payload: signature, }; } }; export const AdbPublicKeyAuthenticator: AdbAuthenticator = async function* ( credentialStore: AdbCredentialStore, getNextRequest: () => Promise<AdbPacketData>, ): AsyncIterable<AdbPacketData> { const packet = await getNextRequest(); if (packet.arg0 !== AdbAuthType.Token) { return; } let privateKey: AdbPrivateKey | undefined; for await (const key of credentialStore.iterateKeys()) { privateKey = key; break; } if (!privateKey) { privateKey = await credentialStore.generateKey(); } const publicKeyLength = adbGetPublicKeySize(); const [publicKeyBase64Length] = calculateBase64EncodedLength(publicKeyLength); const nameBuffer = privateKey.name?.length ? encodeUtf8(privateKey.name) : EmptyUint8Array; const publicKeyBuffer = new Uint8Array( publicKeyBase64Length + (nameBuffer.length ? nameBuffer.length + 1 : 0) + // Space character + name 1, // Null character ); adbGeneratePublicKey(privateKey.buffer, publicKeyBuffer); encodeBase64(publicKeyBuffer.subarray(0, publicKeyLength), publicKeyBuffer); if (nameBuffer.length) { publicKeyBuffer[publicKeyBase64Length] = 0x20; publicKeyBuffer.set(nameBuffer, publicKeyBase64Length + 1); } yield { command: AdbCommand.Auth, arg0: AdbAuthType.PublicKey, arg1: 0, payload: publicKeyBuffer, }; }; export const ADB_DEFAULT_AUTHENTICATORS: AdbAuthenticator[] = [ AdbSignatureAuthenticator, AdbPublicKeyAuthenticator, ]; export class AdbAuthenticationProcessor implements Disposable { readonly authenticators: readonly AdbAuthenticator[]; readonly #credentialStore: AdbCredentialStore; #pendingRequest = new PromiseResolver<AdbPacketData>(); #iterator: AsyncIterator<AdbPacketData, void, void> | undefined; constructor( authenticators: readonly AdbAuthenticator[], credentialStore: AdbCredentialStore, ) { this.authenticators = authenticators; this.#credentialStore = credentialStore; } #getNextRequest = (): Promise<AdbPacketData> => { return this.#pendingRequest.promise; }; async *#invokeAuthenticator(): AsyncGenerator<AdbPacketData, void, void> { for (const authenticator of this.authenticators) { for await (const packet of authenticator( this.#credentialStore, this.#getNextRequest, )) { // If the authenticator yielded a response // Prepare `nextRequest` for next authentication request this.#pendingRequest = new PromiseResolver(); // Yield the response to outer layer yield packet; } // If the authenticator returned, // Next authenticator will be given the same `pendingRequest` } } async process(packet: AdbPacketData): Promise<AdbPacketData> { if (!this.#iterator) { this.#iterator = this.#invokeAuthenticator(); } this.#pendingRequest.resolve(packet); const result = await this.#iterator.next(); if (result.done) { throw new Error("No authenticator can handle the request"); } return result.value; } dispose() { void this.#iterator?.return?.(); } }