mirror of
https://github.com/yume-chan/ya-webadb.git
synced 2025-10-03 01:39:21 +02:00
feat(credential): allow ignoring error and continue loading next key (#803)
This commit is contained in:
parent
2d75bf5e4f
commit
7df9ca2642
11 changed files with 646 additions and 122 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
|
|
@ -10,8 +10,9 @@ import {
|
||||||
writeFile,
|
writeFile,
|
||||||
} from "node:fs/promises";
|
} from "node:fs/promises";
|
||||||
import { homedir, hostname, userInfo } from "node:os";
|
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 {
|
import {
|
||||||
adbGeneratePublicKey,
|
adbGeneratePublicKey,
|
||||||
decodeBase64,
|
decodeBase64,
|
||||||
|
@ -21,12 +22,41 @@ import {
|
||||||
} from "@yume-chan/adb";
|
} from "@yume-chan/adb";
|
||||||
import type { TangoKey, TangoKeyStorage } from "@yume-chan/adb-credential-web";
|
import type { TangoKey, TangoKeyStorage } from "@yume-chan/adb-credential-web";
|
||||||
|
|
||||||
export class TangoNodeStorage implements TangoKeyStorage {
|
class KeyError extends Error {
|
||||||
#logger: ((message: string) => void) | undefined;
|
readonly path: string;
|
||||||
|
|
||||||
constructor(logger: ((message: string) => void) | undefined) {
|
constructor(message: string, path: string, options?: ErrorOptions) {
|
||||||
this.#logger = logger;
|
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() {
|
async #getAndroidDirPath() {
|
||||||
const dir = resolve(homedir(), ".android");
|
const dir = resolve(homedir(), ".android");
|
||||||
|
@ -75,11 +105,11 @@ export class TangoNodeStorage implements TangoKeyStorage {
|
||||||
pem
|
pem
|
||||||
// Parse PEM in Lax format (allows spaces/line breaks everywhere)
|
// Parse PEM in Lax format (allows spaces/line breaks everywhere)
|
||||||
// https://datatracker.ietf.org/doc/html/rfc7468
|
// 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, ""),
|
.replaceAll(/\x20|\t|\r|\n|\v|\f/g, ""),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} 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");
|
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 {
|
} catch {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
@ -108,20 +143,39 @@ export class TangoNodeStorage implements TangoKeyStorage {
|
||||||
return { privateKey, name };
|
return { privateKey, name };
|
||||||
}
|
}
|
||||||
|
|
||||||
async *#readVendorKeys(path: string) {
|
async *#readVendorKeys(
|
||||||
const stats = await stat(path);
|
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()) {
|
if (stats.isFile()) {
|
||||||
try {
|
try {
|
||||||
yield await this.#readKey(path);
|
yield await this.#readKey(path);
|
||||||
|
return;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.#logger?.(String(e));
|
yield e instanceof KeyError
|
||||||
|
? e
|
||||||
|
: new VendorKeyError(path, { cause: e });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stats.isDirectory()) {
|
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()) {
|
if (!dirent.isFile()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -130,31 +184,45 @@ export class TangoNodeStorage implements TangoKeyStorage {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const file = resolve(path, dirent.name);
|
||||||
try {
|
try {
|
||||||
yield await this.#readKey(resolve(path, dirent.name));
|
yield await this.#readKey(file);
|
||||||
} catch (e) {
|
} 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();
|
const userKeyPath = await this.#getUserKeyPath();
|
||||||
if (existsSync(userKeyPath)) {
|
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;
|
const vendorKeys = process.env.ADB_VENDOR_KEYS;
|
||||||
if (vendorKeys) {
|
if (vendorKeys) {
|
||||||
const separator = process.platform === "win32" ? ";" : ":";
|
for (const path of vendorKeys.split(delimiter).filter(Boolean)) {
|
||||||
for (const path of vendorKeys.split(separator)) {
|
|
||||||
yield* this.#readVendorKeys(path);
|
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
|
// Re-export everything except Web-only storages
|
||||||
export {
|
export {
|
||||||
AdbWebCryptoCredentialStore,
|
AdbWebCryptoCredentialStore,
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import type { MaybeError } from "@yume-chan/adb";
|
||||||
import { encodeUtf8 } from "@yume-chan/adb";
|
import { encodeUtf8 } from "@yume-chan/adb";
|
||||||
import type { MaybePromiseLike } from "@yume-chan/async";
|
import type { MaybePromiseLike } from "@yume-chan/async";
|
||||||
import {
|
import {
|
||||||
|
@ -103,22 +104,26 @@ export class TangoPasswordProtectedStorage implements TangoKeyStorage {
|
||||||
// * `data` is owned by caller and will be cleared by caller
|
// * `data` is owned by caller and will be cleared by caller
|
||||||
}
|
}
|
||||||
|
|
||||||
async *load(): AsyncGenerator<TangoKey, void, void> {
|
async *load(): AsyncGenerator<MaybeError<TangoKey>, void, void> {
|
||||||
for await (const {
|
for await (const result of this.#storage.load()) {
|
||||||
privateKey: serialized,
|
if (result instanceof Error) {
|
||||||
name,
|
yield result;
|
||||||
} of this.#storage.load()) {
|
continue;
|
||||||
const bundle = Bundle.deserialize(
|
}
|
||||||
new Uint8ArrayExactReadable(serialized),
|
|
||||||
);
|
|
||||||
|
|
||||||
const password = await this.#requestPassword("load");
|
const { privateKey: serialized, name } = result;
|
||||||
const { aesKey } = await deriveAesKey(
|
|
||||||
password,
|
|
||||||
bundle.pbkdf2Salt as Uint8Array<ArrayBuffer>,
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
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(
|
const decrypted = await crypto.subtle.decrypt(
|
||||||
{
|
{
|
||||||
name: "AES-GCM",
|
name: "AES-GCM",
|
||||||
|
@ -128,22 +133,27 @@ export class TangoPasswordProtectedStorage implements TangoKeyStorage {
|
||||||
bundle.encrypted as Uint8Array<ArrayBuffer>,
|
bundle.encrypted as Uint8Array<ArrayBuffer>,
|
||||||
);
|
);
|
||||||
|
|
||||||
yield {
|
try {
|
||||||
privateKey: new Uint8Array(decrypted),
|
yield {
|
||||||
name,
|
privateKey: new Uint8Array(decrypted),
|
||||||
};
|
name,
|
||||||
|
};
|
||||||
// Clear secret memory
|
} finally {
|
||||||
// * No way to clear `password` and `aesKey`
|
// Clear secret memory
|
||||||
// * all values in `bundle` are not secrets
|
// * No way to clear `password` and `aesKey`
|
||||||
// * Caller is not allowed to use `decrypted` after `yield` returns
|
// * all values in `bundle` are not secrets
|
||||||
new Uint8Array(decrypted).fill(0);
|
// * Caller is not allowed to use `decrypted` after `yield` returns
|
||||||
|
new Uint8Array(decrypted).fill(0);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof DOMException && e.name === "OperationError") {
|
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 {
|
import {
|
||||||
buffer,
|
buffer,
|
||||||
struct,
|
struct,
|
||||||
|
@ -136,49 +137,59 @@ export class TangoPrfStorage implements TangoKeyStorage {
|
||||||
await this.#storage.save(bundle, name);
|
await this.#storage.save(bundle, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
async *load(): AsyncGenerator<TangoKey, void, void> {
|
async *load(): AsyncGenerator<MaybeError<TangoKey>, void, void> {
|
||||||
for await (const {
|
for await (const result of this.#storage.load()) {
|
||||||
privateKey: serialized,
|
if (result instanceof Error) {
|
||||||
name,
|
yield result;
|
||||||
} of this.#storage.load()) {
|
continue;
|
||||||
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(
|
const { privateKey: serialized, name } = result;
|
||||||
{
|
|
||||||
name: "AES-GCM",
|
|
||||||
iv: bundle.aesIv as Uint8Array<ArrayBuffer>,
|
|
||||||
},
|
|
||||||
aesKey,
|
|
||||||
bundle.encrypted as Uint8Array<ArrayBuffer>,
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
yield { privateKey: new Uint8Array(decrypted), name };
|
const bundle = Bundle.deserialize(
|
||||||
} finally {
|
new Uint8ArrayExactReadable(serialized),
|
||||||
// Clear secret memory
|
);
|
||||||
// Caller is not allowed to use `decrypted` after `yield` returns
|
|
||||||
new Uint8Array(decrypted).fill(0);
|
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";
|
import type { MaybePromiseLike } from "@yume-chan/async";
|
||||||
|
|
||||||
export interface TangoKey {
|
export interface TangoKey {
|
||||||
|
@ -11,5 +12,7 @@ export interface TangoKeyStorage {
|
||||||
name: string | undefined,
|
name: string | undefined,
|
||||||
): MaybePromiseLike<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 { rsaParsePrivateKey } from "@yume-chan/adb";
|
||||||
|
|
||||||
import type { TangoKeyStorage } from "./storage/index.js";
|
import type { TangoKeyStorage } from "./storage/index.js";
|
||||||
|
@ -51,13 +55,28 @@ export class AdbWebCryptoCredentialStore implements AdbCredentialStore {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async *iterateKeys(): AsyncGenerator<AdbPrivateKey, void, void> {
|
async *iterateKeys(): AsyncGenerator<
|
||||||
for await (const key of this.#storage.load()) {
|
MaybeError<AdbPrivateKey>,
|
||||||
// `privateKey` is owned by `#storage` and will be cleared by it
|
void,
|
||||||
yield {
|
void
|
||||||
...rsaParsePrivateKey(key.privateKey),
|
> {
|
||||||
name: key.name ?? this.#name,
|
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,
|
calculateBase64EncodedLength,
|
||||||
encodeBase64,
|
encodeBase64,
|
||||||
encodeUtf8,
|
encodeUtf8,
|
||||||
|
md5Digest,
|
||||||
} from "../utils/index.js";
|
} from "../utils/index.js";
|
||||||
|
|
||||||
import type { SimpleRsaPrivateKey } from "./crypto.js";
|
import type { SimpleRsaPrivateKey } from "./crypto.js";
|
||||||
|
@ -21,9 +22,11 @@ export interface AdbPrivateKey extends SimpleRsaPrivateKey {
|
||||||
name?: string | undefined;
|
name?: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type MaybeError<T> = T | Error;
|
||||||
|
|
||||||
export type AdbKeyIterable =
|
export type AdbKeyIterable =
|
||||||
| Iterable<AdbPrivateKey>
|
| Iterable<MaybeError<AdbPrivateKey>>
|
||||||
| AsyncIterable<AdbPrivateKey>;
|
| AsyncIterable<MaybeError<AdbPrivateKey>>;
|
||||||
|
|
||||||
export interface AdbCredentialStore {
|
export interface AdbCredentialStore {
|
||||||
/**
|
/**
|
||||||
|
@ -39,6 +42,20 @@ export interface AdbCredentialStore {
|
||||||
iterateKeys(): AdbKeyIterable;
|
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 = {
|
export const AdbAuthType = {
|
||||||
Token: 1,
|
Token: 1,
|
||||||
Signature: 2,
|
Signature: 2,
|
||||||
|
@ -56,12 +73,29 @@ export interface AdbAuthenticator {
|
||||||
export class AdbDefaultAuthenticator implements AdbAuthenticator {
|
export class AdbDefaultAuthenticator implements AdbAuthenticator {
|
||||||
#credentialStore: AdbCredentialStore;
|
#credentialStore: AdbCredentialStore;
|
||||||
#iterator:
|
#iterator:
|
||||||
| Iterator<AdbPrivateKey, void, void>
|
| Iterator<MaybeError<AdbPrivateKey>, void, void>
|
||||||
| AsyncIterator<AdbPrivateKey, void, void>
|
| AsyncIterator<MaybeError<AdbPrivateKey>, void, void>
|
||||||
| undefined;
|
| undefined;
|
||||||
|
|
||||||
|
#prevKeyInfo: AdbKeyInfo | undefined;
|
||||||
#firstKey: AdbPrivateKey | 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() {
|
get onPublicKeyAuthentication() {
|
||||||
return this.#onPublicKeyAuthentication.event;
|
return this.#onPublicKeyAuthentication.event;
|
||||||
}
|
}
|
||||||
|
@ -70,11 +104,7 @@ export class AdbDefaultAuthenticator implements AdbAuthenticator {
|
||||||
this.#credentialStore = credentialStore;
|
this.#credentialStore = credentialStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
async authenticate(packet: AdbPacketData): Promise<AdbPacketData> {
|
async #iterate(token: Uint8Array): Promise<AdbPacketData | undefined> {
|
||||||
if (packet.arg0 !== AdbAuthType.Token) {
|
|
||||||
throw new Error("Unsupported authentication packet");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.#iterator) {
|
if (!this.#iterator) {
|
||||||
const iterable = this.#credentialStore.iterateKeys();
|
const iterable = this.#credentialStore.iterateKeys();
|
||||||
if (Symbol.iterator in iterable) {
|
if (Symbol.iterator in iterable) {
|
||||||
|
@ -86,21 +116,46 @@ export class AdbDefaultAuthenticator implements AdbAuthenticator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { done, value } = await this.#iterator.next();
|
const { done, value: result } = await this.#iterator.next();
|
||||||
if (!done) {
|
if (done) {
|
||||||
if (!this.#firstKey) {
|
return undefined;
|
||||||
this.#firstKey = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
command: AdbCommand.Auth,
|
|
||||||
arg0: AdbAuthType.Signature,
|
|
||||||
arg1: 0,
|
|
||||||
payload: rsaSign(value, packet.payload),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
let key = this.#firstKey;
|
||||||
if (!key) {
|
if (!key) {
|
||||||
|
@ -131,6 +186,11 @@ export class AdbDefaultAuthenticator implements AdbAuthenticator {
|
||||||
publicKeyBuffer.set(nameBuffer, publicKeyBase64Length + 1);
|
publicKeyBuffer.set(nameBuffer, publicKeyBase64Length + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.#onPublicKeyAuthentication.fire({
|
||||||
|
fingerprint: getFingerprint(key),
|
||||||
|
name: key.name,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
command: AdbCommand.Auth,
|
command: AdbCommand.Auth,
|
||||||
arg0: AdbAuthType.PublicKey,
|
arg0: AdbAuthType.PublicKey,
|
||||||
|
|
|
@ -16,7 +16,11 @@ import type {
|
||||||
import { AdbBanner } from "../banner.js";
|
import { AdbBanner } from "../banner.js";
|
||||||
import { AdbDeviceFeatures, AdbFeature } from "../features.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 { 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";
|
||||||
|
@ -138,7 +142,14 @@ export class AdbDaemonTransport implements AdbTransport {
|
||||||
| { authenticator: AdbAuthenticator }
|
| { authenticator: AdbAuthenticator }
|
||||||
| {
|
| {
|
||||||
credentialStore: AdbCredentialStore;
|
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> {
|
)): Promise<AdbDaemonTransport> {
|
||||||
// Initially, set to highest-supported version and payload size.
|
// Initially, set to highest-supported version and payload size.
|
||||||
|
@ -151,14 +162,28 @@ export class AdbDaemonTransport implements AdbTransport {
|
||||||
if ("authenticator" in options) {
|
if ("authenticator" in options) {
|
||||||
authenticator = options.authenticator;
|
authenticator = options.authenticator;
|
||||||
} else {
|
} else {
|
||||||
authenticator = new AdbDefaultAuthenticator(
|
const defaultAuthenticator = new AdbDefaultAuthenticator(
|
||||||
options.credentialStore,
|
options.credentialStore,
|
||||||
);
|
);
|
||||||
if (options.onPublicKeyAuthentication) {
|
if (options.onKeyLoadError) {
|
||||||
(
|
defaultAuthenticator.onKeyLoadError(options.onKeyLoadError);
|
||||||
authenticator as AdbDefaultAuthenticator
|
|
||||||
).onPublicKeyAuthentication(options.onPublicKeyAuthentication);
|
|
||||||
}
|
}
|
||||||
|
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`,
|
// Here is similar to `AdbPacketDispatcher`,
|
||||||
|
|
|
@ -3,6 +3,7 @@ export * from "./array-buffer.js";
|
||||||
export * from "./auto-reset-event.js";
|
export * from "./auto-reset-event.js";
|
||||||
export * from "./base64.js";
|
export * from "./base64.js";
|
||||||
export * from "./hex.js";
|
export * from "./hex.js";
|
||||||
|
export * from "./md5.js";
|
||||||
export * from "./no-op.js";
|
export * from "./no-op.js";
|
||||||
export * from "./ref.js";
|
export * from "./ref.js";
|
||||||
export * from "./sequence-equal.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