Skip to main content
Version: next

Custom credential manager

To create a custom credential manager, you need to implement the AdbCredentialManager interface:

import type { MaybePromiseLike } from "@yume-chan/async";
import type { AdbPrivateKey } from "@yume-chan/adb";

export type MaybeError<T> = T | Error;

export type AdbKeyIterable =
| Iterable<MaybeError<AdbPrivateKey>>
| AsyncIterable<MaybeError<AdbPrivateKey>>;

export interface AdbCredentialManager {
/**
* 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;
}

Each private key is represented as an AdbPrivateKey object with the following structure:

interface AdbPrivateKey {
readonly buffer: Uint8Array;
readonly name?: string;
}

Where:

  • buffer: A Uint8Array containing the private key in PKCS#8 format
  • name: An optional string for the key name. On Android 11 and above, it will appear in "Settings -> Developer options -> Wireless debugging -> Paired devices"

Error handling behavior

The iterateKeys method can yield Error objects to indicate non-fatal errors when loading keys. This allows the authentication processor to report the error and continue trying other keys:

// Example error handling in iterateKeys
*iterateKeys(): Generator<MaybeError<AdbPrivateKey>, void, void> {
try {
// Attempt to load a key
const key = this.loadKey();
yield key;
} catch (error) {
// Yield the error instead of throwing it
// This allows the auth processor to continue with other keys
yield error as Error;
}
}

Authentication flow

The authentication processor follows this sequence:

  1. Calls iterateKeys() and attempts signature authentication with each key until successful or exhausted
  2. If signature authentication fails but keys were available, tries public key authentication with the first key
  3. If no keys were available or all authentication attempts failed, calls generateKey() and uses the new key for public key authentication

Example implementation

Here's an example of a credential manager using local storage (similar to TangoLocalStorage):

import type { AdbCredentialManager, AdbPrivateKey, MaybeError } from "@yume-chan/adb";
import { decodeBase64, decodeUtf8, encodeBase64 } from "@yume-chan/adb";

interface StoredKey {
privateKey: string; // base64 encoded
name?: string;
}

export class CustomLocalStorageCredentialManager implements AdbCredentialManager {
readonly #storageKey: string;

constructor(storageKey: string) {
this.#storageKey = storageKey;
}

async generateKey(): Promise<AdbPrivateKey> {
// In a real implementation, generate a new RSA key pair
// This is a placeholder implementation
const privateKey = new Uint8Array([
/* generated key bytes */
]);

// Store the key
const storedKey: StoredKey = {
privateKey: decodeUtf8(encodeBase64(privateKey)),
name: "custom-generated-key",
};

localStorage.setItem(this.#storageKey, JSON.stringify(storedKey));

return { buffer: privateKey, name: storedKey.name };
}

*iterateKeys(): Generator<MaybeError<AdbPrivateKey>, void, void> {
const json = localStorage.getItem(this.#storageKey);
if (json) {
try {
const storedKey = JSON.parse(json) as StoredKey;
const privateKey = decodeBase64(storedKey.privateKey);

yield {
buffer: privateKey,
name: storedKey.name,
};
} catch (error) {
// Yield error instead of throwing to allow continuing with other keys
yield error as Error;
}
}
}
}