mirror of
https://github.com/yume-chan/ya-webadb.git
synced 2025-10-03 01:39:21 +02:00
Compare commits
2 commits
f6cd87a8e0
...
befd887c39
Author | SHA1 | Date | |
---|---|---|---|
![]() |
befd887c39 | ||
![]() |
7df9ca2642 |
12 changed files with 647 additions and 123 deletions
7
.changeset/wet-trams-retire.md
Normal file
7
.changeset/wet-trams-retire.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
"@yume-chan/adb-credential-nodejs": major
|
||||
"@yume-chan/adb-credential-web": major
|
||||
"@yume-chan/adb": major
|
||||
---
|
||||
|
||||
Allow credential stores to report loading error but still trying to load next key
|
2
.github/workflows/publish.yml
vendored
2
.github/workflows/publish.yml
vendored
|
@ -17,7 +17,7 @@ jobs:
|
|||
- name: Setup node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 22
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
|
|
|
@ -10,8 +10,9 @@ import {
|
|||
writeFile,
|
||||
} from "node:fs/promises";
|
||||
import { homedir, hostname, userInfo } from "node:os";
|
||||
import { resolve } from "node:path";
|
||||
import { delimiter, resolve } from "node:path";
|
||||
|
||||
import type { MaybeError } from "@yume-chan/adb";
|
||||
import {
|
||||
adbGeneratePublicKey,
|
||||
decodeBase64,
|
||||
|
@ -21,12 +22,41 @@ import {
|
|||
} from "@yume-chan/adb";
|
||||
import type { TangoKey, TangoKeyStorage } from "@yume-chan/adb-credential-web";
|
||||
|
||||
export class TangoNodeStorage implements TangoKeyStorage {
|
||||
#logger: ((message: string) => void) | undefined;
|
||||
class KeyError extends Error {
|
||||
readonly path: string;
|
||||
|
||||
constructor(logger: ((message: string) => void) | undefined) {
|
||||
this.#logger = logger;
|
||||
constructor(message: string, path: string, options?: ErrorOptions) {
|
||||
super(message, options);
|
||||
this.path = path;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Can't read or parse a private key file.
|
||||
*
|
||||
* Check `path` for file path, and `cause` for the error.
|
||||
*/
|
||||
class InvalidKeyError extends KeyError {
|
||||
constructor(path: string, options?: ErrorOptions) {
|
||||
super(`Can't read private key file at "${path}"`, path, options);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Can't read or parse a vendor key.
|
||||
*
|
||||
* Check `path` for file path, and `cause` for the error.
|
||||
*/
|
||||
class VendorKeyError extends KeyError {
|
||||
constructor(path: string, options?: ErrorOptions) {
|
||||
super(`Can't read vendor key file at "${path}"`, path, options);
|
||||
}
|
||||
}
|
||||
|
||||
export class TangoNodeStorage implements TangoKeyStorage {
|
||||
static readonly KeyError = KeyError;
|
||||
static readonly InvalidKeyError = InvalidKeyError;
|
||||
static readonly VendorKeyError = VendorKeyError;
|
||||
|
||||
async #getAndroidDirPath() {
|
||||
const dir = resolve(homedir(), ".android");
|
||||
|
@ -75,11 +105,11 @@ export class TangoNodeStorage implements TangoKeyStorage {
|
|||
pem
|
||||
// Parse PEM in Lax format (allows spaces/line breaks everywhere)
|
||||
// https://datatracker.ietf.org/doc/html/rfc7468
|
||||
.replaceAll(/-----(BEGIN|END) PRIVATE KEY-----/g, "")
|
||||
.replaceAll(/-----(BEGIN|END)( RSA)? PRIVATE KEY-----/g, "")
|
||||
.replaceAll(/\x20|\t|\r|\n|\v|\f/g, ""),
|
||||
);
|
||||
} catch (e) {
|
||||
throw new Error("Invalid private key file: " + path, { cause: e });
|
||||
throw new InvalidKeyError(path, { cause: e });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -95,7 +125,12 @@ export class TangoNodeStorage implements TangoKeyStorage {
|
|||
}
|
||||
|
||||
const publicKey = await readFile(publicKeyPath, "utf8");
|
||||
return publicKey.split(" ")[1]?.trim();
|
||||
const spaceIndex = publicKey.indexOf(" ");
|
||||
if (spaceIndex === -1) {
|
||||
return undefined;
|
||||
}
|
||||
const name = publicKey.substring(spaceIndex + 1).trim();
|
||||
return name ? name : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
|
@ -108,20 +143,39 @@ export class TangoNodeStorage implements TangoKeyStorage {
|
|||
return { privateKey, name };
|
||||
}
|
||||
|
||||
async *#readVendorKeys(path: string) {
|
||||
const stats = await stat(path);
|
||||
async *#readVendorKeys(
|
||||
path: string,
|
||||
): AsyncGenerator<MaybeError<TangoKey>, void, void> {
|
||||
let stats;
|
||||
try {
|
||||
stats = await stat(path);
|
||||
} catch (e) {
|
||||
yield new VendorKeyError(path, { cause: e });
|
||||
return;
|
||||
}
|
||||
|
||||
if (stats.isFile()) {
|
||||
try {
|
||||
yield await this.#readKey(path);
|
||||
return;
|
||||
} catch (e) {
|
||||
this.#logger?.(String(e));
|
||||
yield e instanceof KeyError
|
||||
? e
|
||||
: new VendorKeyError(path, { cause: e });
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (stats.isDirectory()) {
|
||||
for await (const dirent of await opendir(path)) {
|
||||
let dir;
|
||||
try {
|
||||
dir = await opendir(path);
|
||||
} catch (e) {
|
||||
yield new VendorKeyError(path, { cause: e });
|
||||
return;
|
||||
}
|
||||
|
||||
for await (const dirent of dir) {
|
||||
if (!dirent.isFile()) {
|
||||
continue;
|
||||
}
|
||||
|
@ -130,31 +184,45 @@ export class TangoNodeStorage implements TangoKeyStorage {
|
|||
continue;
|
||||
}
|
||||
|
||||
const file = resolve(path, dirent.name);
|
||||
try {
|
||||
yield await this.#readKey(resolve(path, dirent.name));
|
||||
yield await this.#readKey(file);
|
||||
} catch (e) {
|
||||
this.#logger?.(String(e));
|
||||
yield e instanceof KeyError
|
||||
? e
|
||||
: new VendorKeyError(file, { cause: e });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async *load(): AsyncGenerator<TangoKey, void, void> {
|
||||
async *load(): AsyncGenerator<MaybeError<TangoKey>, void, void> {
|
||||
const userKeyPath = await this.#getUserKeyPath();
|
||||
if (existsSync(userKeyPath)) {
|
||||
yield await this.#readKey(userKeyPath);
|
||||
try {
|
||||
yield await this.#readKey(userKeyPath);
|
||||
} catch (e) {
|
||||
yield e instanceof KeyError
|
||||
? e
|
||||
: new InvalidKeyError(userKeyPath, { cause: e });
|
||||
}
|
||||
}
|
||||
|
||||
const vendorKeys = process.env.ADB_VENDOR_KEYS;
|
||||
if (vendorKeys) {
|
||||
const separator = process.platform === "win32" ? ";" : ":";
|
||||
for (const path of vendorKeys.split(separator)) {
|
||||
for (const path of vendorKeys.split(delimiter).filter(Boolean)) {
|
||||
yield* this.#readVendorKeys(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export namespace TangoNodeStorage {
|
||||
export type KeyError = typeof KeyError;
|
||||
export type InvalidKeyError = typeof InvalidKeyError;
|
||||
export type VendorKeyError = typeof VendorKeyError;
|
||||
}
|
||||
|
||||
// Re-export everything except Web-only storages
|
||||
export {
|
||||
AdbWebCryptoCredentialStore,
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import type { MaybeError } from "@yume-chan/adb";
|
||||
import { encodeUtf8 } from "@yume-chan/adb";
|
||||
import type { MaybePromiseLike } from "@yume-chan/async";
|
||||
import {
|
||||
|
@ -103,22 +104,26 @@ export class TangoPasswordProtectedStorage implements TangoKeyStorage {
|
|||
// * `data` is owned by caller and will be cleared by caller
|
||||
}
|
||||
|
||||
async *load(): AsyncGenerator<TangoKey, void, void> {
|
||||
for await (const {
|
||||
privateKey: serialized,
|
||||
name,
|
||||
} of this.#storage.load()) {
|
||||
const bundle = Bundle.deserialize(
|
||||
new Uint8ArrayExactReadable(serialized),
|
||||
);
|
||||
async *load(): AsyncGenerator<MaybeError<TangoKey>, void, void> {
|
||||
for await (const result of this.#storage.load()) {
|
||||
if (result instanceof Error) {
|
||||
yield result;
|
||||
continue;
|
||||
}
|
||||
|
||||
const password = await this.#requestPassword("load");
|
||||
const { aesKey } = await deriveAesKey(
|
||||
password,
|
||||
bundle.pbkdf2Salt as Uint8Array<ArrayBuffer>,
|
||||
);
|
||||
const { privateKey: serialized, name } = result;
|
||||
|
||||
try {
|
||||
const bundle = Bundle.deserialize(
|
||||
new Uint8ArrayExactReadable(serialized),
|
||||
);
|
||||
|
||||
const password = await this.#requestPassword("load");
|
||||
const { aesKey } = await deriveAesKey(
|
||||
password,
|
||||
bundle.pbkdf2Salt as Uint8Array<ArrayBuffer>,
|
||||
);
|
||||
|
||||
const decrypted = await crypto.subtle.decrypt(
|
||||
{
|
||||
name: "AES-GCM",
|
||||
|
@ -128,22 +133,27 @@ export class TangoPasswordProtectedStorage implements TangoKeyStorage {
|
|||
bundle.encrypted as Uint8Array<ArrayBuffer>,
|
||||
);
|
||||
|
||||
yield {
|
||||
privateKey: new Uint8Array(decrypted),
|
||||
name,
|
||||
};
|
||||
|
||||
// Clear secret memory
|
||||
// * No way to clear `password` and `aesKey`
|
||||
// * all values in `bundle` are not secrets
|
||||
// * Caller is not allowed to use `decrypted` after `yield` returns
|
||||
new Uint8Array(decrypted).fill(0);
|
||||
try {
|
||||
yield {
|
||||
privateKey: new Uint8Array(decrypted),
|
||||
name,
|
||||
};
|
||||
} finally {
|
||||
// Clear secret memory
|
||||
// * No way to clear `password` and `aesKey`
|
||||
// * all values in `bundle` are not secrets
|
||||
// * Caller is not allowed to use `decrypted` after `yield` returns
|
||||
new Uint8Array(decrypted).fill(0);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof DOMException && e.name === "OperationError") {
|
||||
throw new PasswordIncorrectError();
|
||||
yield new PasswordIncorrectError();
|
||||
continue;
|
||||
}
|
||||
|
||||
throw e;
|
||||
yield e instanceof Error
|
||||
? e
|
||||
: new Error(String(e), { cause: e });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import type { MaybeError } from "@yume-chan/adb";
|
||||
import {
|
||||
buffer,
|
||||
struct,
|
||||
|
@ -136,49 +137,59 @@ export class TangoPrfStorage implements TangoKeyStorage {
|
|||
await this.#storage.save(bundle, name);
|
||||
}
|
||||
|
||||
async *load(): AsyncGenerator<TangoKey, void, void> {
|
||||
for await (const {
|
||||
privateKey: serialized,
|
||||
name,
|
||||
} of this.#storage.load()) {
|
||||
const bundle = Bundle.deserialize(
|
||||
new Uint8ArrayExactReadable(serialized),
|
||||
);
|
||||
|
||||
const prfOutput = await this.#source.get(
|
||||
bundle.id as Uint8Array<ArrayBuffer>,
|
||||
bundle.prfInput as Uint8Array<ArrayBuffer>,
|
||||
);
|
||||
|
||||
this.#prevId = bundle.id as Uint8Array<ArrayBuffer>;
|
||||
|
||||
let aesKey: CryptoKey;
|
||||
try {
|
||||
aesKey = await deriveAesKey(
|
||||
prfOutput,
|
||||
bundle.hkdfInfo as Uint8Array<ArrayBuffer>,
|
||||
bundle.hkdfSalt as Uint8Array<ArrayBuffer>,
|
||||
);
|
||||
} finally {
|
||||
// Clear secret memory
|
||||
toUint8Array(prfOutput).fill(0);
|
||||
async *load(): AsyncGenerator<MaybeError<TangoKey>, void, void> {
|
||||
for await (const result of this.#storage.load()) {
|
||||
if (result instanceof Error) {
|
||||
yield result;
|
||||
continue;
|
||||
}
|
||||
|
||||
const decrypted = await crypto.subtle.decrypt(
|
||||
{
|
||||
name: "AES-GCM",
|
||||
iv: bundle.aesIv as Uint8Array<ArrayBuffer>,
|
||||
},
|
||||
aesKey,
|
||||
bundle.encrypted as Uint8Array<ArrayBuffer>,
|
||||
);
|
||||
const { privateKey: serialized, name } = result;
|
||||
|
||||
try {
|
||||
yield { privateKey: new Uint8Array(decrypted), name };
|
||||
} finally {
|
||||
// Clear secret memory
|
||||
// Caller is not allowed to use `decrypted` after `yield` returns
|
||||
new Uint8Array(decrypted).fill(0);
|
||||
const bundle = Bundle.deserialize(
|
||||
new Uint8ArrayExactReadable(serialized),
|
||||
);
|
||||
|
||||
const prfOutput = await this.#source.get(
|
||||
bundle.id as Uint8Array<ArrayBuffer>,
|
||||
bundle.prfInput as Uint8Array<ArrayBuffer>,
|
||||
);
|
||||
|
||||
this.#prevId = bundle.id as Uint8Array<ArrayBuffer>;
|
||||
|
||||
let aesKey: CryptoKey;
|
||||
try {
|
||||
aesKey = await deriveAesKey(
|
||||
prfOutput,
|
||||
bundle.hkdfInfo as Uint8Array<ArrayBuffer>,
|
||||
bundle.hkdfSalt as Uint8Array<ArrayBuffer>,
|
||||
);
|
||||
} finally {
|
||||
// Clear secret memory
|
||||
toUint8Array(prfOutput).fill(0);
|
||||
}
|
||||
|
||||
const decrypted = await crypto.subtle.decrypt(
|
||||
{
|
||||
name: "AES-GCM",
|
||||
iv: bundle.aesIv as Uint8Array<ArrayBuffer>,
|
||||
},
|
||||
aesKey,
|
||||
bundle.encrypted as Uint8Array<ArrayBuffer>,
|
||||
);
|
||||
|
||||
try {
|
||||
yield { privateKey: new Uint8Array(decrypted), name };
|
||||
} finally {
|
||||
// Clear secret memory
|
||||
// Caller is not allowed to use `decrypted` after `yield` returns
|
||||
new Uint8Array(decrypted).fill(0);
|
||||
}
|
||||
} catch (e) {
|
||||
yield e instanceof Error
|
||||
? e
|
||||
: new Error(String(e), { cause: e });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import type { MaybeError } from "@yume-chan/adb";
|
||||
import type { MaybePromiseLike } from "@yume-chan/async";
|
||||
|
||||
export interface TangoKey {
|
||||
|
@ -11,5 +12,7 @@ export interface TangoKeyStorage {
|
|||
name: string | undefined,
|
||||
): MaybePromiseLike<undefined>;
|
||||
|
||||
load(): Iterable<TangoKey> | AsyncIterable<TangoKey>;
|
||||
load():
|
||||
| Iterable<MaybeError<TangoKey>>
|
||||
| AsyncIterable<MaybeError<TangoKey>>;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
import type { AdbCredentialStore, AdbPrivateKey } from "@yume-chan/adb";
|
||||
import type {
|
||||
AdbCredentialStore,
|
||||
AdbPrivateKey,
|
||||
MaybeError,
|
||||
} from "@yume-chan/adb";
|
||||
import { rsaParsePrivateKey } from "@yume-chan/adb";
|
||||
|
||||
import type { TangoKeyStorage } from "./storage/index.js";
|
||||
|
@ -51,13 +55,28 @@ export class AdbWebCryptoCredentialStore implements AdbCredentialStore {
|
|||
};
|
||||
}
|
||||
|
||||
async *iterateKeys(): AsyncGenerator<AdbPrivateKey, void, void> {
|
||||
for await (const key of this.#storage.load()) {
|
||||
// `privateKey` is owned by `#storage` and will be cleared by it
|
||||
yield {
|
||||
...rsaParsePrivateKey(key.privateKey),
|
||||
name: key.name ?? this.#name,
|
||||
};
|
||||
async *iterateKeys(): AsyncGenerator<
|
||||
MaybeError<AdbPrivateKey>,
|
||||
void,
|
||||
void
|
||||
> {
|
||||
for await (const result of this.#storage.load()) {
|
||||
if (result instanceof Error) {
|
||||
yield result;
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
// `privateKey` is owned by `#storage` and will be cleared by it
|
||||
yield {
|
||||
...rsaParsePrivateKey(result.privateKey),
|
||||
name: result.name ?? this.#name,
|
||||
};
|
||||
} catch (e) {
|
||||
yield e instanceof Error
|
||||
? e
|
||||
: new Error(String(e), { cause: e });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
calculateBase64EncodedLength,
|
||||
encodeBase64,
|
||||
encodeUtf8,
|
||||
md5Digest,
|
||||
} from "../utils/index.js";
|
||||
|
||||
import type { SimpleRsaPrivateKey } from "./crypto.js";
|
||||
|
@ -21,9 +22,11 @@ export interface AdbPrivateKey extends SimpleRsaPrivateKey {
|
|||
name?: string | undefined;
|
||||
}
|
||||
|
||||
export type MaybeError<T> = T | Error;
|
||||
|
||||
export type AdbKeyIterable =
|
||||
| Iterable<AdbPrivateKey>
|
||||
| AsyncIterable<AdbPrivateKey>;
|
||||
| Iterable<MaybeError<AdbPrivateKey>>
|
||||
| AsyncIterable<MaybeError<AdbPrivateKey>>;
|
||||
|
||||
export interface AdbCredentialStore {
|
||||
/**
|
||||
|
@ -39,6 +42,20 @@ export interface AdbCredentialStore {
|
|||
iterateKeys(): AdbKeyIterable;
|
||||
}
|
||||
|
||||
// https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/services/core/java/com/android/server/adb/AdbDebuggingManager.java;l=1419;drc=61197364367c9e404c7da6900658f1b16c42d0da
|
||||
function getFingerprint(key: AdbPrivateKey) {
|
||||
const publicKey = adbGeneratePublicKey(key);
|
||||
const md5 = md5Digest(publicKey);
|
||||
return Array.from(md5, (byte) => byte.toString(16).padStart(2, "0")).join(
|
||||
":",
|
||||
);
|
||||
}
|
||||
|
||||
export interface AdbKeyInfo {
|
||||
fingerprint: string;
|
||||
name: string | undefined;
|
||||
}
|
||||
|
||||
export const AdbAuthType = {
|
||||
Token: 1,
|
||||
Signature: 2,
|
||||
|
@ -56,12 +73,29 @@ export interface AdbAuthenticator {
|
|||
export class AdbDefaultAuthenticator implements AdbAuthenticator {
|
||||
#credentialStore: AdbCredentialStore;
|
||||
#iterator:
|
||||
| Iterator<AdbPrivateKey, void, void>
|
||||
| AsyncIterator<AdbPrivateKey, void, void>
|
||||
| Iterator<MaybeError<AdbPrivateKey>, void, void>
|
||||
| AsyncIterator<MaybeError<AdbPrivateKey>, void, void>
|
||||
| undefined;
|
||||
|
||||
#prevKeyInfo: AdbKeyInfo | undefined;
|
||||
#firstKey: AdbPrivateKey | undefined;
|
||||
|
||||
#onPublicKeyAuthentication = new EventEmitter<void>();
|
||||
#onKeyLoadError = new EventEmitter<Error>();
|
||||
get onKeyLoadError() {
|
||||
return this.#onKeyLoadError.event;
|
||||
}
|
||||
|
||||
#onSignatureAuthentication = new EventEmitter<AdbKeyInfo>();
|
||||
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;
|
||||
}
|
||||
|
@ -70,11 +104,7 @@ export class AdbDefaultAuthenticator implements AdbAuthenticator {
|
|||
this.#credentialStore = credentialStore;
|
||||
}
|
||||
|
||||
async authenticate(packet: AdbPacketData): Promise<AdbPacketData> {
|
||||
if (packet.arg0 !== AdbAuthType.Token) {
|
||||
throw new Error("Unsupported authentication packet");
|
||||
}
|
||||
|
||||
async #iterate(token: Uint8Array): Promise<AdbPacketData | undefined> {
|
||||
if (!this.#iterator) {
|
||||
const iterable = this.#credentialStore.iterateKeys();
|
||||
if (Symbol.iterator in iterable) {
|
||||
|
@ -86,21 +116,46 @@ export class AdbDefaultAuthenticator implements AdbAuthenticator {
|
|||
}
|
||||
}
|
||||
|
||||
const { done, value } = await this.#iterator.next();
|
||||
if (!done) {
|
||||
if (!this.#firstKey) {
|
||||
this.#firstKey = value;
|
||||
}
|
||||
|
||||
return {
|
||||
command: AdbCommand.Auth,
|
||||
arg0: AdbAuthType.Signature,
|
||||
arg1: 0,
|
||||
payload: rsaSign(value, packet.payload),
|
||||
};
|
||||
const { done, value: result } = await this.#iterator.next();
|
||||
if (done) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
this.#onPublicKeyAuthentication.fire();
|
||||
if (result instanceof Error) {
|
||||
this.#onKeyLoadError.fire(result);
|
||||
return await this.#iterate(token);
|
||||
}
|
||||
|
||||
if (!this.#firstKey) {
|
||||
this.#firstKey = result;
|
||||
}
|
||||
|
||||
// A new token implies the previous signature was rejected.
|
||||
if (this.#prevKeyInfo) {
|
||||
this.#onSignatureRejected.fire(this.#prevKeyInfo);
|
||||
}
|
||||
|
||||
const fingerprint = getFingerprint(result);
|
||||
this.#prevKeyInfo = { fingerprint, name: result.name };
|
||||
this.#onSignatureAuthentication.fire(this.#prevKeyInfo);
|
||||
|
||||
return {
|
||||
command: AdbCommand.Auth,
|
||||
arg0: AdbAuthType.Signature,
|
||||
arg1: 0,
|
||||
payload: rsaSign(result, token),
|
||||
};
|
||||
}
|
||||
|
||||
async authenticate(packet: AdbPacketData): Promise<AdbPacketData> {
|
||||
if (packet.arg0 !== AdbAuthType.Token) {
|
||||
throw new Error("Unsupported authentication packet");
|
||||
}
|
||||
|
||||
const signature = await this.#iterate(packet.payload);
|
||||
if (signature) {
|
||||
return signature;
|
||||
}
|
||||
|
||||
let key = this.#firstKey;
|
||||
if (!key) {
|
||||
|
@ -131,6 +186,11 @@ export class AdbDefaultAuthenticator implements AdbAuthenticator {
|
|||
publicKeyBuffer.set(nameBuffer, publicKeyBase64Length + 1);
|
||||
}
|
||||
|
||||
this.#onPublicKeyAuthentication.fire({
|
||||
fingerprint: getFingerprint(key),
|
||||
name: key.name,
|
||||
});
|
||||
|
||||
return {
|
||||
command: AdbCommand.Auth,
|
||||
arg0: AdbAuthType.PublicKey,
|
||||
|
|
|
@ -16,7 +16,11 @@ import type {
|
|||
import { AdbBanner } from "../banner.js";
|
||||
import { AdbDeviceFeatures, AdbFeature } from "../features.js";
|
||||
|
||||
import type { AdbAuthenticator, AdbCredentialStore } from "./auth.js";
|
||||
import type {
|
||||
AdbAuthenticator,
|
||||
AdbCredentialStore,
|
||||
AdbKeyInfo,
|
||||
} from "./auth.js";
|
||||
import { AdbDefaultAuthenticator } from "./auth.js";
|
||||
import { AdbPacketDispatcher } from "./dispatcher.js";
|
||||
import type { AdbPacketData, AdbPacketInit } from "./packet.js";
|
||||
|
@ -138,7 +142,14 @@ export class AdbDaemonTransport implements AdbTransport {
|
|||
| { authenticator: AdbAuthenticator }
|
||||
| {
|
||||
credentialStore: AdbCredentialStore;
|
||||
onPublicKeyAuthentication?: (() => void) | undefined;
|
||||
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.
|
||||
|
@ -151,14 +162,28 @@ export class AdbDaemonTransport implements AdbTransport {
|
|||
if ("authenticator" in options) {
|
||||
authenticator = options.authenticator;
|
||||
} else {
|
||||
authenticator = new AdbDefaultAuthenticator(
|
||||
const defaultAuthenticator = new AdbDefaultAuthenticator(
|
||||
options.credentialStore,
|
||||
);
|
||||
if (options.onPublicKeyAuthentication) {
|
||||
(
|
||||
authenticator as AdbDefaultAuthenticator
|
||||
).onPublicKeyAuthentication(options.onPublicKeyAuthentication);
|
||||
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`,
|
||||
|
|
|
@ -3,6 +3,7 @@ export * from "./array-buffer.js";
|
|||
export * from "./auto-reset-event.js";
|
||||
export * from "./base64.js";
|
||||
export * from "./hex.js";
|
||||
export * from "./md5.js";
|
||||
export * from "./no-op.js";
|
||||
export * from "./ref.js";
|
||||
export * from "./sequence-equal.js";
|
||||
|
|
135
libraries/adb/src/utils/md5.spec.ts
Normal file
135
libraries/adb/src/utils/md5.spec.ts
Normal file
|
@ -0,0 +1,135 @@
|
|||
import assert from "assert";
|
||||
import { describe, it } from "node:test";
|
||||
|
||||
import { encodeUtf8 } from "@yume-chan/struct";
|
||||
|
||||
import { Md5, md5Digest } from "./md5.js";
|
||||
|
||||
describe("md5", () => {
|
||||
it("should digest empty string", () => {
|
||||
const expected = "d41d8cd98f00b204e9800998ecf8427e";
|
||||
const actual = md5Digest(encodeUtf8(""));
|
||||
assert.deepStrictEqual(
|
||||
Buffer.from(actual),
|
||||
Buffer.from(expected, "hex"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should digest 'abc'", () => {
|
||||
const expected = "900150983cd24fb0d6963f7d28e17f72";
|
||||
const actual = md5Digest(encodeUtf8("abc"));
|
||||
assert.deepStrictEqual(
|
||||
Buffer.from(actual),
|
||||
Buffer.from(expected, "hex"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should digest 'The quick brown fox jumps over the lazy dog'", () => {
|
||||
const expected = "9e107d9d372bb6826bd81d3542a419d6";
|
||||
const actual = md5Digest(
|
||||
encodeUtf8("The quick brown fox jumps over the lazy dog"),
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
Buffer.from(actual),
|
||||
Buffer.from(expected, "hex"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should digest 'c'\u00e8'", () => {
|
||||
const expected = "8ef7c2941d78fe89f31e614437c9db59";
|
||||
const actual = md5Digest(encodeUtf8("c'\u00e8"));
|
||||
assert.deepStrictEqual(
|
||||
Buffer.from(actual),
|
||||
Buffer.from(expected, "hex"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should digest 'THIS IS A MESSAGE'", () => {
|
||||
const expected = "78eebfd9d42958e3f31244f116ab7bbe";
|
||||
const md5 = new Md5();
|
||||
md5.update(encodeUtf8("THIS IS "));
|
||||
md5.update(encodeUtf8("A MESSAGE"));
|
||||
const actual = md5.digest();
|
||||
assert.deepStrictEqual(
|
||||
Buffer.from(actual),
|
||||
Buffer.from(expected, "hex"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should digest a long message", () => {
|
||||
const input = Buffer.from(
|
||||
"0100002903018d32e9c6dc423774c4c39a5a1b78f44cc2cab5f676d39" +
|
||||
"f703d29bfa27dfeb870000002002f01000200004603014c2c1e835d39" +
|
||||
"da71bc0857eb04c2b50fe90dbb2a8477fe7364598d6f0575999c20a6c" +
|
||||
"7248c5174da6d03ac711888f762fc4ed54f7254b32273690de849c843" +
|
||||
"073d002f000b0003d20003cf0003cc308203c8308202b0a0030201020" +
|
||||
"20100300d06092a864886f70d0101050500308186310b300906035504" +
|
||||
"0613025553311d301b060355040a13144469676974616c2042617a616" +
|
||||
"1722c20496e632e31443042060355040b133b4269746d756e6b206c6f" +
|
||||
"63616c686f73742d6f6e6c7920436572746966696361746573202d204" +
|
||||
"17574686f72697a6174696f6e20766961204254503112301006035504" +
|
||||
"0313096c6f63616c686f7374301e170d3130303231343137303931395" +
|
||||
"a170d3230303231333137303931395a308186310b3009060355040613" +
|
||||
"025553311d301b060355040a13144469676974616c2042617a6161722" +
|
||||
"c20496e632e31443042060355040b133b4269746d756e6b206c6f6361" +
|
||||
"6c686f73742d6f6e6c7920436572746966696361746573202d2041757" +
|
||||
"4686f72697a6174696f6e207669612042545031123010060355040313" +
|
||||
"096c6f63616c686f737430820122300d06092a864886f70d010101050" +
|
||||
"00382010f003082010a0282010100dc436f17d6909d8a9d6186ea218e" +
|
||||
"b5c86b848bae02219bd56a71203daf07e81bc19e7e98134136bcb0128" +
|
||||
"81864bf03b3774652ad5eab85dba411a5114ffeac09babce75f313143" +
|
||||
"45512cd87c91318b2e77433270a52185fc16f428c3ca412ad6e9484bc" +
|
||||
"2fb87abb4e8fb71bf0f619e31a42340b35967f06c24a741a31c979c0b" +
|
||||
"b8921a90a47025fbeb8adca576979e70a56830c61170c9647c18c0794" +
|
||||
"d68c0df38f3aac5fc3b530e016ea5659715339f3f3c209cdee9dbe794" +
|
||||
"b5af92530c5754c1d874b78974bfad994e0dfc582275e79feb522f6e4" +
|
||||
"bcc2b2945baedfb0dbdaebb605f9483ff0bea29ecd5f4d6f2769965d1" +
|
||||
"b3e04f8422716042680011ff676f0203010001a33f303d300c0603551" +
|
||||
"d130101ff04023000300e0603551d0f0101ff0404030204f0301d0603" +
|
||||
"551d250416301406082b0601050507030106082b06010505070302300" +
|
||||
"d06092a864886f70d010105050003820101009c4562be3f2d8d8e3880" +
|
||||
"85a697f2f106eaeff4992a43f198fe3dcf15c8229cf1043f061a38204" +
|
||||
"f73d86f4fb6348048cc5279ed719873aa10e3773d92b629c2c3fcce04" +
|
||||
"012c81ba3b4ec451e9644ec5191078402d845e05d02c7b4d974b45882" +
|
||||
"76e5037aba7ef26a8bddeb21e10698c82f425e767dc401adf722fa73a" +
|
||||
"b78cfa069bd69052d7ca6a75cc9225550e315d71c5f8764362ea4dbc6" +
|
||||
"ecb837a8471043c5a7f826a71af145a053090bd4bccca6a2c552841cd" +
|
||||
"b1908a8352f49283d2e641acdef667c7543af441a16f8294251e2ac37" +
|
||||
"6fa507b53ae418dd038cd20cef1e7bfbf5ae03a7c88d93d843abaabbd" +
|
||||
"c5f3431132f3e559d2dd414c3eda38a210b80e0000001000010201002" +
|
||||
"6a220b7be857402819b78d81080d01a682599bbd00902985cc64edf8e" +
|
||||
"520e4111eb0e1729a14ffa3498ca259cc9ad6fc78fa130d968ebdb78d" +
|
||||
"c0b950c0aa44355f13ba678419185d7e4608fe178ca6b2cef33e41937" +
|
||||
"78d1a70fe4d0dfcb110be4bbb4dbaa712177655728f914ab4c0f6c4ae" +
|
||||
"f79a46b3d996c82b2ebe9ed1748eb5cace7dc44fb67e73f452a047f2e" +
|
||||
"d199b3d50d5db960acf03244dc8efa4fc129faf8b65f9e52e62b55447" +
|
||||
"22bd17d2358e817a777618a4265a3db277fc04851a82a91fe6cdcb812" +
|
||||
"7f156e0b4a5d1f54ce2742eb70c895f5f8b85f5febe69bc73e891f928" +
|
||||
"0826860a0c2ef94c7935e6215c3c4cd6b0e43e80cca396d913d36be",
|
||||
"hex",
|
||||
);
|
||||
const expected = "d15a2da0e92c3da55dc573f885b6e653";
|
||||
|
||||
const md5 = new Md5();
|
||||
md5.update(input);
|
||||
const actual = md5.digest();
|
||||
assert.deepStrictEqual(
|
||||
Buffer.from(actual),
|
||||
Buffer.from(expected, "hex"),
|
||||
);
|
||||
});
|
||||
it("should digest multiple long messages", () => {
|
||||
// Note: might be too slow on old browsers
|
||||
// done multiple times to check hot loop optimizations
|
||||
for (let loop = 0; loop < 3; loop += 1) {
|
||||
const md5 = new Md5();
|
||||
for (let i = 0; i < 10000; i += 1) {
|
||||
md5.update(encodeUtf8("abc"));
|
||||
}
|
||||
assert.deepStrictEqual(
|
||||
Buffer.from(md5.digest()),
|
||||
Buffer.from("b3e98306e7367f93cd7cb870af64f7b7", "hex"),
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
185
libraries/adb/src/utils/md5.ts
Normal file
185
libraries/adb/src/utils/md5.ts
Normal file
|
@ -0,0 +1,185 @@
|
|||
import {
|
||||
getUint32LittleEndian,
|
||||
setUint32LittleEndian,
|
||||
setUint64LittleEndian,
|
||||
} from "@yume-chan/no-data-view";
|
||||
|
||||
// Taken from https://github.com/digitalbazaar/forge/blob/e3c68e9695607702587583cda291d74e5369f21c/tests/unit/md5.js#L103
|
||||
// LICENSE: https://github.com/digitalbazaar/forge/blob/2c37d0bd2864199409edbb520f674d1c93652b23/LICENSE
|
||||
|
||||
// g values
|
||||
// prettier-ignore
|
||||
const gs = [
|
||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
|
||||
1, 6, 11, 0, 5, 10, 15, 4, 9, 14, 3, 8, 13, 2, 7, 12,
|
||||
5, 8, 11, 14, 1, 4, 7, 10, 13, 0, 3, 6, 9, 12, 15, 2,
|
||||
0, 7, 14, 5, 12, 3, 10, 1, 8, 15, 6, 13, 4, 11, 2, 9];
|
||||
|
||||
// rounds table
|
||||
// prettier-ignore
|
||||
const rs = [
|
||||
7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22,
|
||||
5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20,
|
||||
4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23,
|
||||
6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21];
|
||||
|
||||
export class Md5 {
|
||||
static #k = new Uint32Array(64);
|
||||
|
||||
static {
|
||||
for (let i = 0; i < 64; i += 1) {
|
||||
// get the result of abs(sin(i + 1)) as a 32-bit integer
|
||||
Md5.#k[i] = Math.floor(Math.abs(Math.sin(i + 1)) * 0x100000000);
|
||||
}
|
||||
}
|
||||
|
||||
#state = new Uint32Array([0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476]);
|
||||
#length = 0n;
|
||||
|
||||
#buffer = new Uint8Array(64);
|
||||
#bufferLength = 0;
|
||||
|
||||
#w = new Uint32Array(16);
|
||||
|
||||
update(input: Uint8Array) {
|
||||
this.#length += BigInt(input.length);
|
||||
|
||||
let offset = 0;
|
||||
|
||||
if (this.#bufferLength) {
|
||||
const remaining = 64 - this.#bufferLength;
|
||||
this.#buffer.set(input.subarray(0, remaining), this.#bufferLength);
|
||||
|
||||
if (input.length < remaining) {
|
||||
this.#bufferLength += input.length;
|
||||
return this;
|
||||
}
|
||||
|
||||
this.#update(this.#buffer);
|
||||
this.#bufferLength = 0;
|
||||
offset = remaining;
|
||||
}
|
||||
|
||||
const end = input.length - 64;
|
||||
for (; offset <= end; offset += 64) {
|
||||
this.#update(input, offset);
|
||||
}
|
||||
|
||||
if (offset < input.length) {
|
||||
this.#buffer.set(input.subarray(offset));
|
||||
this.#bufferLength = input.length - offset;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
#update(input: Uint8Array, offset = 0) {
|
||||
let a = this.#state[0]!;
|
||||
let b = this.#state[1]!;
|
||||
let c = this.#state[2]!;
|
||||
let d = this.#state[3]!;
|
||||
|
||||
let t = 0;
|
||||
let f = 0;
|
||||
let r = 0;
|
||||
let i = 0;
|
||||
|
||||
// round 1
|
||||
for (; i < 16; i += 1) {
|
||||
this.#w[i] = getUint32LittleEndian(input, offset + i * 4);
|
||||
f = d ^ (b & (c ^ d));
|
||||
t = a + f + Md5.#k[i]! + this.#w[i]!;
|
||||
r = rs[i]!;
|
||||
a = d;
|
||||
d = c;
|
||||
c = b;
|
||||
b += (t << r) | (t >>> (32 - r));
|
||||
}
|
||||
|
||||
// round 2
|
||||
for (; i < 32; i += 1) {
|
||||
f = c ^ (d & (b ^ c));
|
||||
t = a + f + Md5.#k[i]! + this.#w[gs[i]!]!;
|
||||
r = rs[i]!;
|
||||
a = d;
|
||||
d = c;
|
||||
c = b;
|
||||
b += (t << r) | (t >>> (32 - r));
|
||||
}
|
||||
|
||||
// round 3
|
||||
for (; i < 48; i += 1) {
|
||||
f = b ^ c ^ d;
|
||||
t = a + f + Md5.#k[i]! + this.#w[gs[i]!]!;
|
||||
r = rs[i]!;
|
||||
a = d;
|
||||
d = c;
|
||||
c = b;
|
||||
b += (t << r) | (t >>> (32 - r));
|
||||
}
|
||||
|
||||
// round 4
|
||||
for (; i < 64; i += 1) {
|
||||
f = c ^ (b | ~d);
|
||||
t = a + f + Md5.#k[i]! + this.#w[gs[i]!]!;
|
||||
r = rs[i]!;
|
||||
a = d;
|
||||
d = c;
|
||||
c = b;
|
||||
b += (t << r) | (t >>> (32 - r));
|
||||
}
|
||||
|
||||
this.#state[0]! += a;
|
||||
this.#state[1]! += b;
|
||||
this.#state[2]! += c;
|
||||
this.#state[3]! += d;
|
||||
}
|
||||
|
||||
digest() {
|
||||
this.#buffer[this.#bufferLength] = 0x80;
|
||||
this.#buffer.subarray(this.#bufferLength + 1).fill(0);
|
||||
|
||||
if (64 - this.#bufferLength < 8) {
|
||||
this.#update(this.#buffer);
|
||||
|
||||
this.#buffer.fill(0);
|
||||
this.#bufferLength = 0;
|
||||
}
|
||||
|
||||
setUint64LittleEndian(
|
||||
this.#buffer,
|
||||
this.#buffer.length - 8,
|
||||
this.#length << 3n,
|
||||
);
|
||||
this.#update(this.#buffer);
|
||||
|
||||
const result = new Uint8Array(16);
|
||||
setUint32LittleEndian(result, 0, this.#state[0]!);
|
||||
setUint32LittleEndian(result, 4, this.#state[1]!);
|
||||
setUint32LittleEndian(result, 8, this.#state[2]!);
|
||||
setUint32LittleEndian(result, 12, this.#state[3]!);
|
||||
return result;
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.#state[0] = 0x67452301;
|
||||
this.#state[1] = 0xefcdab89;
|
||||
this.#state[2] = 0x98badcfe;
|
||||
this.#state[3] = 0x10325476;
|
||||
this.#bufferLength = 0;
|
||||
this.#length = 0n;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
let instance: Md5 | undefined;
|
||||
|
||||
export function md5Digest(input: Uint8Array) {
|
||||
if (!instance) {
|
||||
instance = new Md5();
|
||||
}
|
||||
|
||||
const result = instance.update(input).digest();
|
||||
instance.reset();
|
||||
return result;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue