mirror of
https://github.com/yume-chan/ya-webadb.git
synced 2025-10-02 17:29:17 +02:00
feat(credential): save key name in storage (#794)
This commit is contained in:
parent
27a6614680
commit
2d75bf5e4f
22 changed files with 705 additions and 186 deletions
9
.changeset/nice-bars-stay.md
Normal file
9
.changeset/nice-bars-stay.md
Normal file
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
"@yume-chan/adb-credential-web": major
|
||||
---
|
||||
|
||||
Separate private key creation and storage.
|
||||
|
||||
- Two built-in storages for Web platform: IndexedDB, LocalStorage
|
||||
- Two encrypted storages (takes an inner storage to actually store the encrypted data): Password-protected, WebAuthn PRF extension. Encrypted storages can be chained for multi-factor authentication.
|
||||
- File-based storage for Node.js: compatible with Google ADB
|
16
libraries/adb-credential-nodejs/.npmignore
Normal file
16
libraries/adb-credential-nodejs/.npmignore
Normal file
|
@ -0,0 +1,16 @@
|
|||
.rush
|
||||
|
||||
# Test
|
||||
coverage
|
||||
**/*.spec.ts
|
||||
**/*.spec.js
|
||||
**/*.spec.js.map
|
||||
**/__helpers__
|
||||
jest.config.js
|
||||
|
||||
.eslintrc.cjs
|
||||
tsconfig.json
|
||||
tsconfig.test.json
|
||||
|
||||
# Logs
|
||||
*.log
|
21
libraries/adb-credential-nodejs/LICENSE
Normal file
21
libraries/adb-credential-nodejs/LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2020-2025 Simon Chan
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
3
libraries/adb-credential-nodejs/README.md
Normal file
3
libraries/adb-credential-nodejs/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# @yume-chan/adb-credential-nodejs
|
||||
|
||||
ADB credential store for Node.js
|
45
libraries/adb-credential-nodejs/package.json
Normal file
45
libraries/adb-credential-nodejs/package.json
Normal file
|
@ -0,0 +1,45 @@
|
|||
{
|
||||
"name": "@yume-chan/adb-credential-nodejs",
|
||||
"version": "2.0.0",
|
||||
"description": "ADB credential store for Node.js",
|
||||
"keywords": [
|
||||
"typescript"
|
||||
],
|
||||
"license": "MIT",
|
||||
"author": {
|
||||
"name": "Simon Chan",
|
||||
"email": "cnsimonchan@live.com",
|
||||
"url": "https://chensi.moe/blog"
|
||||
},
|
||||
"homepage": "https://github.com/yume-chan/ya-webadb/tree/main/libraries/adb-credential-nodejs#readme",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/yume-chan/ya-webadb.git",
|
||||
"directory": "libraries/adb-credential-nodejs"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/yume-chan/ya-webadb/issues"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "esm/index.js",
|
||||
"types": "esm/index.d.ts",
|
||||
"sideEffects": false,
|
||||
"scripts": {
|
||||
"build": "tsc -b tsconfig.build.json",
|
||||
"lint": "run-eslint && prettier src/**/*.ts --write --tab-width 4",
|
||||
"prepublishOnly": "npm run build",
|
||||
"test": "run-test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@yume-chan/adb": "workspace:^",
|
||||
"@yume-chan/adb-credential-web": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.3.0",
|
||||
"@yume-chan/eslint-config": "workspace:^",
|
||||
"@yume-chan/test-runner": "workspace:^",
|
||||
"@yume-chan/tsconfig": "workspace:^",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
168
libraries/adb-credential-nodejs/src/index.ts
Normal file
168
libraries/adb-credential-nodejs/src/index.ts
Normal file
|
@ -0,0 +1,168 @@
|
|||
// cspell: ignore adbkey
|
||||
|
||||
import { existsSync } from "node:fs";
|
||||
import {
|
||||
chmod,
|
||||
mkdir,
|
||||
opendir,
|
||||
readFile,
|
||||
stat,
|
||||
writeFile,
|
||||
} from "node:fs/promises";
|
||||
import { homedir, hostname, userInfo } from "node:os";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
import {
|
||||
adbGeneratePublicKey,
|
||||
decodeBase64,
|
||||
decodeUtf8,
|
||||
encodeBase64,
|
||||
rsaParsePrivateKey,
|
||||
} from "@yume-chan/adb";
|
||||
import type { TangoKey, TangoKeyStorage } from "@yume-chan/adb-credential-web";
|
||||
|
||||
export class TangoNodeStorage implements TangoKeyStorage {
|
||||
#logger: ((message: string) => void) | undefined;
|
||||
|
||||
constructor(logger: ((message: string) => void) | undefined) {
|
||||
this.#logger = logger;
|
||||
}
|
||||
|
||||
async #getAndroidDirPath() {
|
||||
const dir = resolve(homedir(), ".android");
|
||||
await mkdir(dir, { mode: 0o750, recursive: true });
|
||||
return dir;
|
||||
}
|
||||
|
||||
async #getUserKeyPath() {
|
||||
return resolve(await this.#getAndroidDirPath(), "adbkey");
|
||||
}
|
||||
|
||||
#getDefaultName() {
|
||||
return userInfo().username + "@" + hostname();
|
||||
}
|
||||
|
||||
async save(
|
||||
privateKey: Uint8Array,
|
||||
name: string | undefined,
|
||||
): Promise<undefined> {
|
||||
const userKeyPath = await this.#getUserKeyPath();
|
||||
|
||||
// Create PEM in Strict format
|
||||
// https://datatracker.ietf.org/doc/html/rfc7468
|
||||
let pem = "-----BEGIN PRIVATE KEY-----\n";
|
||||
const base64 = decodeUtf8(encodeBase64(privateKey));
|
||||
for (let i = 0; i < base64.length; i += 64) {
|
||||
pem += base64.substring(i, i + 64) + "\n";
|
||||
}
|
||||
pem += "-----END PRIVATE KEY-----\n";
|
||||
await writeFile(userKeyPath, pem, { encoding: "utf8", mode: 0o600 });
|
||||
await chmod(userKeyPath, 0o600);
|
||||
|
||||
name ??= this.#getDefaultName();
|
||||
const publicKey = adbGeneratePublicKey(rsaParsePrivateKey(privateKey));
|
||||
await writeFile(
|
||||
userKeyPath + ".pub",
|
||||
decodeUtf8(encodeBase64(publicKey)) + " " + name + "\n",
|
||||
{ encoding: "utf8", mode: 0o644 },
|
||||
);
|
||||
}
|
||||
|
||||
async #readPrivateKey(path: string) {
|
||||
try {
|
||||
const pem = await readFile(path, "utf8");
|
||||
return decodeBase64(
|
||||
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(/\x20|\t|\r|\n|\v|\f/g, ""),
|
||||
);
|
||||
} catch (e) {
|
||||
throw new Error("Invalid private key file: " + path, { cause: e });
|
||||
}
|
||||
}
|
||||
|
||||
async #readPublicKeyName(path: string): Promise<string | undefined> {
|
||||
// Google ADB actually never reads the `.pub` file for name,
|
||||
// it always returns the default name.
|
||||
// So we won't throw an error if the file can't be read.
|
||||
|
||||
try {
|
||||
const publicKeyPath = path + ".pub";
|
||||
if (!(await stat(publicKeyPath)).isFile()) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const publicKey = await readFile(publicKeyPath, "utf8");
|
||||
return publicKey.split(" ")[1]?.trim();
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async #readKey(path: string): Promise<TangoKey> {
|
||||
const privateKey = await this.#readPrivateKey(path);
|
||||
const name =
|
||||
(await this.#readPublicKeyName(path)) ?? this.#getDefaultName();
|
||||
return { privateKey, name };
|
||||
}
|
||||
|
||||
async *#readVendorKeys(path: string) {
|
||||
const stats = await stat(path);
|
||||
|
||||
if (stats.isFile()) {
|
||||
try {
|
||||
yield await this.#readKey(path);
|
||||
} catch (e) {
|
||||
this.#logger?.(String(e));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (stats.isDirectory()) {
|
||||
for await (const dirent of await opendir(path)) {
|
||||
if (!dirent.isFile()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!dirent.name.endsWith(".adb_key")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
yield await this.#readKey(resolve(path, dirent.name));
|
||||
} catch (e) {
|
||||
this.#logger?.(String(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async *load(): AsyncGenerator<TangoKey, void, void> {
|
||||
const userKeyPath = await this.#getUserKeyPath();
|
||||
if (existsSync(userKeyPath)) {
|
||||
yield await this.#readKey(userKeyPath);
|
||||
}
|
||||
|
||||
const vendorKeys = process.env.ADB_VENDOR_KEYS;
|
||||
if (vendorKeys) {
|
||||
const separator = process.platform === "win32" ? ";" : ":";
|
||||
for (const path of vendorKeys.split(separator)) {
|
||||
yield* this.#readVendorKeys(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Re-export everything except Web-only storages
|
||||
export {
|
||||
AdbWebCryptoCredentialStore,
|
||||
TangoPasswordProtectedStorage,
|
||||
TangoPrfStorage,
|
||||
} from "@yume-chan/adb-credential-web";
|
||||
export type {
|
||||
TangoKey,
|
||||
TangoKeyStorage,
|
||||
TangoPrfSource,
|
||||
} from "@yume-chan/adb-credential-web";
|
8
libraries/adb-credential-nodejs/tsconfig.build.json
Normal file
8
libraries/adb-credential-nodejs/tsconfig.build.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"extends": "./node_modules/@yume-chan/tsconfig/tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"types": [
|
||||
"node"
|
||||
]
|
||||
}
|
||||
}
|
16
libraries/adb-credential-nodejs/tsconfig.json
Normal file
16
libraries/adb-credential-nodejs/tsconfig.json
Normal file
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.test.json"
|
||||
},
|
||||
{
|
||||
"path": "../adb/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../adb-credential-web/tsconfig.build.json"
|
||||
},
|
||||
]
|
||||
}
|
9
libraries/adb-credential-nodejs/tsconfig.test.json
Normal file
9
libraries/adb-credential-nodejs/tsconfig.test.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"extends": "./tsconfig.build.json",
|
||||
"compilerOptions": {
|
||||
"types": [
|
||||
"node"
|
||||
],
|
||||
},
|
||||
"exclude": []
|
||||
}
|
|
@ -1,28 +1,63 @@
|
|||
import type { TangoDataStorage } from "./type.js";
|
||||
import type { TangoKey, TangoKeyStorage } from "./type.js";
|
||||
|
||||
function openDatabase() {
|
||||
return new Promise<IDBDatabase>((resolve, reject) => {
|
||||
const request = indexedDB.open("Tango", 1);
|
||||
const StoreName = "Authentication";
|
||||
|
||||
function waitRequest<T>(request: IDBRequest<T>): Promise<T> {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
request.onerror = () => {
|
||||
reject(request.error!);
|
||||
};
|
||||
request.onupgradeneeded = () => {
|
||||
const db = request.result;
|
||||
db.createObjectStore("Authentication", { autoIncrement: true });
|
||||
};
|
||||
request.onsuccess = () => {
|
||||
const db = request.result;
|
||||
resolve(db);
|
||||
resolve(request.result);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function openDatabase() {
|
||||
const request = indexedDB.open("Tango", 1);
|
||||
request.onupgradeneeded = () => {
|
||||
const db = request.result;
|
||||
db.createObjectStore(StoreName, { autoIncrement: true });
|
||||
};
|
||||
const db = await waitRequest(request);
|
||||
|
||||
// Maintain compatibility with v2 (values are pure `Uint8Array`s)
|
||||
// IndexedDB API doesn't support async upgrade transaction,
|
||||
// so have to open with old version, read the data, close the database,
|
||||
// then open with new version to trigger upgrade again
|
||||
if (db.version === 1) {
|
||||
const keys = await createTransaction(db, (tx) =>
|
||||
waitRequest(
|
||||
tx.objectStore(StoreName).getAll() as IDBRequest<Uint8Array[]>,
|
||||
),
|
||||
);
|
||||
|
||||
db.close();
|
||||
|
||||
const request = indexedDB.open("Tango", 2);
|
||||
request.onupgradeneeded = () => {
|
||||
const tx = request.transaction!;
|
||||
const store = tx.objectStore(StoreName);
|
||||
store.clear();
|
||||
for (const key of keys) {
|
||||
store.add({
|
||||
privateKey: key,
|
||||
name: undefined,
|
||||
} satisfies TangoKey);
|
||||
}
|
||||
};
|
||||
return await waitRequest(request);
|
||||
}
|
||||
|
||||
return db;
|
||||
}
|
||||
|
||||
function createTransaction<T>(
|
||||
database: IDBDatabase,
|
||||
callback: (transaction: IDBTransaction) => T,
|
||||
): Promise<T> {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
const transaction = database.transaction("Authentication", "readwrite");
|
||||
const transaction = database.transaction(StoreName, "readwrite");
|
||||
transaction.onerror = () => {
|
||||
reject(transaction.error!);
|
||||
};
|
||||
|
@ -37,35 +72,30 @@ function createTransaction<T>(
|
|||
});
|
||||
}
|
||||
|
||||
export class TangoIndexedDbStorage implements TangoDataStorage {
|
||||
async save(data: Uint8Array): Promise<undefined> {
|
||||
export class TangoIndexedDbStorage implements TangoKeyStorage {
|
||||
async save(
|
||||
privateKey: Uint8Array,
|
||||
name: string | undefined,
|
||||
): Promise<undefined> {
|
||||
const db = await openDatabase();
|
||||
|
||||
try {
|
||||
await createTransaction(db, (tx) => {
|
||||
const store = tx.objectStore("Authentication");
|
||||
store.add(data);
|
||||
const store = tx.objectStore(StoreName);
|
||||
store.add({ privateKey, name } satisfies TangoKey);
|
||||
});
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
|
||||
async *load(): AsyncGenerator<Uint8Array, void, void> {
|
||||
async *load(): AsyncGenerator<TangoKey, void, void> {
|
||||
const db = await openDatabase();
|
||||
|
||||
try {
|
||||
const keys = await createTransaction(db, (tx) => {
|
||||
return new Promise<Uint8Array[]>((resolve, reject) => {
|
||||
const store = tx.objectStore("Authentication");
|
||||
const getRequest = store.getAll();
|
||||
getRequest.onerror = () => {
|
||||
reject(getRequest.error!);
|
||||
};
|
||||
getRequest.onsuccess = () => {
|
||||
resolve(getRequest.result as Uint8Array[]);
|
||||
};
|
||||
});
|
||||
const store = tx.objectStore(StoreName);
|
||||
return waitRequest(store.getAll() as IDBRequest<TangoKey[]>);
|
||||
});
|
||||
|
||||
yield* keys;
|
||||
|
@ -79,7 +109,7 @@ export class TangoIndexedDbStorage implements TangoDataStorage {
|
|||
|
||||
try {
|
||||
await createTransaction(db, (tx) => {
|
||||
const store = tx.objectStore("Authentication");
|
||||
const store = tx.objectStore(StoreName);
|
||||
store.clear();
|
||||
});
|
||||
} finally {
|
||||
|
|
|
@ -1,22 +1,37 @@
|
|||
import { decodeBase64, decodeUtf8, encodeBase64 } from "@yume-chan/adb";
|
||||
|
||||
import type { TangoDataStorage } from "./type.js";
|
||||
import type { TangoKey, TangoKeyStorage } from "./type.js";
|
||||
|
||||
export class TangoLocalStorage implements TangoDataStorage {
|
||||
type TangoKeyJson = {
|
||||
[K in keyof TangoKey]: TangoKey[K] extends Uint8Array
|
||||
? string
|
||||
: TangoKey[K];
|
||||
};
|
||||
|
||||
export class TangoLocalStorage implements TangoKeyStorage {
|
||||
readonly #storageKey: string;
|
||||
|
||||
constructor(storageKey: string) {
|
||||
this.#storageKey = storageKey;
|
||||
}
|
||||
|
||||
save(data: Uint8Array): undefined {
|
||||
localStorage.setItem(this.#storageKey, decodeUtf8(encodeBase64(data)));
|
||||
save(privateKey: Uint8Array, name: string | undefined): undefined {
|
||||
const json = JSON.stringify({
|
||||
privateKey: decodeUtf8(encodeBase64(privateKey)),
|
||||
name,
|
||||
} satisfies TangoKeyJson);
|
||||
|
||||
localStorage.setItem(this.#storageKey, json);
|
||||
}
|
||||
|
||||
*load(): Generator<Uint8Array, void, void> {
|
||||
const data = localStorage.getItem(this.#storageKey);
|
||||
if (data) {
|
||||
yield decodeBase64(data);
|
||||
*load(): Generator<TangoKey, void, void> {
|
||||
const json = localStorage.getItem(this.#storageKey);
|
||||
if (json) {
|
||||
const { privateKey, name } = JSON.parse(json) as TangoKeyJson;
|
||||
yield {
|
||||
privateKey: decodeBase64(privateKey),
|
||||
name,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ import {
|
|||
Uint8ArrayExactReadable,
|
||||
} from "@yume-chan/struct";
|
||||
|
||||
import type { TangoDataStorage } from "./type.js";
|
||||
import type { TangoKey, TangoKeyStorage } from "./type.js";
|
||||
|
||||
const Pbkdf2SaltLength = 16;
|
||||
const Pbkdf2Iterations = 1_000_000;
|
||||
|
@ -59,21 +59,24 @@ class PasswordIncorrectError extends Error {
|
|||
}
|
||||
}
|
||||
|
||||
export class TangoPasswordProtectedStorage implements TangoDataStorage {
|
||||
export class TangoPasswordProtectedStorage implements TangoKeyStorage {
|
||||
static PasswordIncorrectError = PasswordIncorrectError;
|
||||
|
||||
readonly #storage: TangoDataStorage;
|
||||
readonly #storage: TangoKeyStorage;
|
||||
readonly #requestPassword: TangoPasswordProtectedStorage.RequestPassword;
|
||||
|
||||
constructor(
|
||||
storage: TangoDataStorage,
|
||||
storage: TangoKeyStorage,
|
||||
requestPassword: TangoPasswordProtectedStorage.RequestPassword,
|
||||
) {
|
||||
this.#storage = storage;
|
||||
this.#requestPassword = requestPassword;
|
||||
}
|
||||
|
||||
async save(data: Uint8Array<ArrayBuffer>): Promise<undefined> {
|
||||
async save(
|
||||
privateKey: Uint8Array<ArrayBuffer>,
|
||||
name: string | undefined,
|
||||
): Promise<undefined> {
|
||||
const password = await this.#requestPassword("save");
|
||||
const { salt, aesKey } = await deriveAesKey(password);
|
||||
|
||||
|
@ -83,7 +86,7 @@ export class TangoPasswordProtectedStorage implements TangoDataStorage {
|
|||
const encrypted = await crypto.subtle.encrypt(
|
||||
{ name: "AES-GCM", iv },
|
||||
aesKey,
|
||||
data,
|
||||
privateKey,
|
||||
);
|
||||
|
||||
const bundle = Bundle.serialize({
|
||||
|
@ -92,7 +95,7 @@ export class TangoPasswordProtectedStorage implements TangoDataStorage {
|
|||
encrypted: new Uint8Array(encrypted),
|
||||
});
|
||||
|
||||
await this.#storage.save(bundle);
|
||||
await this.#storage.save(bundle, name);
|
||||
|
||||
// Clear secret memory
|
||||
// * No way to clear `password` and `aesKey`
|
||||
|
@ -100,8 +103,11 @@ export class TangoPasswordProtectedStorage implements TangoDataStorage {
|
|||
// * `data` is owned by caller and will be cleared by caller
|
||||
}
|
||||
|
||||
async *load(): AsyncGenerator<Uint8Array, void, void> {
|
||||
for await (const serialized of this.#storage.load()) {
|
||||
async *load(): AsyncGenerator<TangoKey, void, void> {
|
||||
for await (const {
|
||||
privateKey: serialized,
|
||||
name,
|
||||
} of this.#storage.load()) {
|
||||
const bundle = Bundle.deserialize(
|
||||
new Uint8ArrayExactReadable(serialized),
|
||||
);
|
||||
|
@ -122,7 +128,10 @@ export class TangoPasswordProtectedStorage implements TangoDataStorage {
|
|||
bundle.encrypted as Uint8Array<ArrayBuffer>,
|
||||
);
|
||||
|
||||
yield new Uint8Array(decrypted);
|
||||
yield {
|
||||
privateKey: new Uint8Array(decrypted),
|
||||
name,
|
||||
};
|
||||
|
||||
// Clear secret memory
|
||||
// * No way to clear `password` and `aesKey`
|
||||
|
|
|
@ -1,11 +1,35 @@
|
|||
export interface TangoPrfSource {
|
||||
create(input: Uint8Array<ArrayBuffer>): Promise<{
|
||||
output: BufferSource;
|
||||
id: Uint8Array<ArrayBuffer>;
|
||||
}>;
|
||||
import type { MaybePromiseLike } from "@yume-chan/async";
|
||||
|
||||
export interface TangoPrfCreationResult {
|
||||
/**
|
||||
* The generated PRF output
|
||||
*/
|
||||
output: BufferSource;
|
||||
|
||||
/**
|
||||
* ID of the created secret key
|
||||
*/
|
||||
id: Uint8Array<ArrayBuffer>;
|
||||
}
|
||||
|
||||
export interface TangoPrfSource {
|
||||
/**
|
||||
* Creates a new secret key and generate PRF output using the key and input data.
|
||||
*
|
||||
* @param input The input data
|
||||
*/
|
||||
create(
|
||||
input: Uint8Array<ArrayBuffer>,
|
||||
): MaybePromiseLike<TangoPrfCreationResult>;
|
||||
|
||||
/**
|
||||
* Generates PRF output using the secret key and input data.
|
||||
*
|
||||
* @param id ID of the secret key
|
||||
* @param input The input data
|
||||
*/
|
||||
get(
|
||||
id: BufferSource,
|
||||
input: Uint8Array<ArrayBuffer>,
|
||||
): Promise<BufferSource>;
|
||||
): MaybePromiseLike<BufferSource>;
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import {
|
|||
Uint8ArrayExactReadable,
|
||||
} from "@yume-chan/struct";
|
||||
|
||||
import type { TangoDataStorage } from "../type.js";
|
||||
import type { TangoKey, TangoKeyStorage } from "../type.js";
|
||||
|
||||
import type { TangoPrfSource } from "./source.js";
|
||||
|
||||
|
@ -64,17 +64,29 @@ const Bundle = struct(
|
|||
{ littleEndian: true },
|
||||
);
|
||||
|
||||
export class TangoPrfStorage implements TangoDataStorage {
|
||||
readonly #storage: TangoDataStorage;
|
||||
/**
|
||||
* A `TangoDataStorage` that encrypts and decrypts data using PRF
|
||||
*/
|
||||
export class TangoPrfStorage implements TangoKeyStorage {
|
||||
readonly #storage: TangoKeyStorage;
|
||||
readonly #source: TangoPrfSource;
|
||||
#prevId: Uint8Array<ArrayBuffer> | undefined;
|
||||
|
||||
constructor(storage: TangoDataStorage, source: TangoPrfSource) {
|
||||
/**
|
||||
* Creates a new instance of `TangoPrfStorage`
|
||||
*
|
||||
* @param storage Another `TangoDataStorage` to store and retrieve the encrypted data
|
||||
* @param source The `TangoPrfSource` to generate PRF output
|
||||
*/
|
||||
constructor(storage: TangoKeyStorage, source: TangoPrfSource) {
|
||||
this.#storage = storage;
|
||||
this.#source = source;
|
||||
}
|
||||
|
||||
async save(data: Uint8Array<ArrayBuffer>): Promise<undefined> {
|
||||
async save(
|
||||
privateKey: Uint8Array<ArrayBuffer>,
|
||||
name: string | undefined,
|
||||
): Promise<undefined> {
|
||||
const prfInput = new Uint8Array(PrfInputLength);
|
||||
crypto.getRandomValues(prfInput);
|
||||
|
||||
|
@ -95,7 +107,13 @@ export class TangoPrfStorage implements TangoDataStorage {
|
|||
const salt = new Uint8Array(HkdfSaltLength);
|
||||
crypto.getRandomValues(salt);
|
||||
|
||||
const aesKey = await deriveAesKey(prfOutput, info, salt);
|
||||
let aesKey: CryptoKey;
|
||||
try {
|
||||
aesKey = await deriveAesKey(prfOutput, info, salt);
|
||||
} finally {
|
||||
// Clear secret memory
|
||||
toUint8Array(prfOutput).fill(0);
|
||||
}
|
||||
|
||||
const iv = new Uint8Array(AesIvLength);
|
||||
crypto.getRandomValues(iv);
|
||||
|
@ -103,7 +121,7 @@ export class TangoPrfStorage implements TangoDataStorage {
|
|||
const encrypted = await crypto.subtle.encrypt(
|
||||
{ name: "AES-GCM", iv },
|
||||
aesKey,
|
||||
data,
|
||||
privateKey,
|
||||
);
|
||||
|
||||
const bundle = Bundle.serialize({
|
||||
|
@ -115,18 +133,14 @@ export class TangoPrfStorage implements TangoDataStorage {
|
|||
encrypted: new Uint8Array(encrypted),
|
||||
});
|
||||
|
||||
await this.#storage.save(bundle);
|
||||
|
||||
// Clear secret memory
|
||||
// * No way to clear `aesKey`
|
||||
// * `info`, `salt`, `iv`, `encrypted` and `bundle` are not secrets
|
||||
// * `data` is owned by caller and will be cleared by caller
|
||||
// * Need to clear `prfOutput`
|
||||
toUint8Array(prfOutput).fill(0);
|
||||
await this.#storage.save(bundle, name);
|
||||
}
|
||||
|
||||
async *load(): AsyncGenerator<Uint8Array, void, void> {
|
||||
for await (const serialized of this.#storage.load()) {
|
||||
async *load(): AsyncGenerator<TangoKey, void, void> {
|
||||
for await (const {
|
||||
privateKey: serialized,
|
||||
name,
|
||||
} of this.#storage.load()) {
|
||||
const bundle = Bundle.deserialize(
|
||||
new Uint8ArrayExactReadable(serialized),
|
||||
);
|
||||
|
@ -138,11 +152,17 @@ export class TangoPrfStorage implements TangoDataStorage {
|
|||
|
||||
this.#prevId = bundle.id as Uint8Array<ArrayBuffer>;
|
||||
|
||||
const aesKey = await deriveAesKey(
|
||||
prfOutput,
|
||||
bundle.hkdfInfo as Uint8Array<ArrayBuffer>,
|
||||
bundle.hkdfSalt 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(
|
||||
{
|
||||
|
@ -153,16 +173,13 @@ export class TangoPrfStorage implements TangoDataStorage {
|
|||
bundle.encrypted as Uint8Array<ArrayBuffer>,
|
||||
);
|
||||
|
||||
yield new Uint8Array(decrypted);
|
||||
|
||||
// Clear secret memory
|
||||
// * No way to clear `aesKey`
|
||||
// * `info`, `salt`, `iv`, `encrypted` and `bundle` are not secrets
|
||||
// * `data` is owned by caller and will be cleared by caller
|
||||
// * Caller is not allowed to use `decrypted` after `yield` returns
|
||||
// * Need to clear `prfOutput`
|
||||
toUint8Array(prfOutput).fill(0);
|
||||
new Uint8Array(decrypted).fill(0);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,16 +25,24 @@ class NotSupportedError extends Error {
|
|||
}
|
||||
}
|
||||
|
||||
class AssertionFailedError extends Error {
|
||||
class OperationCancelledError extends Error {
|
||||
constructor() {
|
||||
super("Assertion failed");
|
||||
super("The operation is either cancelled by user or timed out");
|
||||
}
|
||||
}
|
||||
|
||||
export class TangoWebAuthnPrfSource implements TangoPrfSource {
|
||||
static NotSupportedError = NotSupportedError;
|
||||
static AssertionFailedError = AssertionFailedError;
|
||||
static OperationCancelledError = OperationCancelledError;
|
||||
|
||||
/**
|
||||
* Checks if the runtime supports WebAuthn PRF extension.
|
||||
*
|
||||
* Note that using the extension also requires a supported authenticator.
|
||||
* Whether an authenticator supports the extension can only be checked
|
||||
* during the `create` process.
|
||||
* @returns `true` if the runtime supports WebAuthn PRF extension
|
||||
*/
|
||||
static async isSupported(): Promise<boolean> {
|
||||
if (typeof PublicKeyCredential === "undefined") {
|
||||
return false;
|
||||
|
@ -57,7 +65,8 @@ export class TangoWebAuthnPrfSource implements TangoPrfSource {
|
|||
readonly #userName: string;
|
||||
|
||||
/**
|
||||
* Create a new instance of TangoWebAuthnPrfSource
|
||||
* Creates a new instance of `TangoWebAuthnPrfSource`
|
||||
*
|
||||
* @param appName Name of your website shows in Passkey manager
|
||||
* @param userName Display name of the credential shows in Passkey manager
|
||||
*/
|
||||
|
@ -66,6 +75,14 @@ export class TangoWebAuthnPrfSource implements TangoPrfSource {
|
|||
this.#userName = userName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new credential and generate PRF output using the credential and input data.
|
||||
*
|
||||
* @param input The input data
|
||||
* @returns The credential ID and PRF output
|
||||
* @throws `NotSupportedError` if the runtime or authenticator doesn't support PRF extension
|
||||
* @throws `OperationCancelledError` if the attestation is either cancelled by user or timed out
|
||||
*/
|
||||
async create(input: Uint8Array<ArrayBuffer>): Promise<{
|
||||
output: BufferSource;
|
||||
id: Uint8Array<ArrayBuffer>;
|
||||
|
@ -73,22 +90,27 @@ export class TangoWebAuthnPrfSource implements TangoPrfSource {
|
|||
const challenge = new Uint8Array(32);
|
||||
crypto.getRandomValues(challenge);
|
||||
|
||||
const attestation = await navigator.credentials.create({
|
||||
publicKey: {
|
||||
challenge,
|
||||
extensions: { prf: { eval: { first: input } } },
|
||||
pubKeyCredParams: [
|
||||
{ type: "public-key", alg: -7 },
|
||||
{ type: "public-key", alg: -257 },
|
||||
],
|
||||
rp: { name: this.#appName },
|
||||
user: {
|
||||
id: challenge,
|
||||
name: this.#userName,
|
||||
displayName: this.#userName,
|
||||
let attestation;
|
||||
try {
|
||||
attestation = await navigator.credentials.create({
|
||||
publicKey: {
|
||||
challenge,
|
||||
extensions: { prf: { eval: { first: input } } },
|
||||
pubKeyCredParams: [
|
||||
{ type: "public-key", alg: -7 },
|
||||
{ type: "public-key", alg: -257 },
|
||||
],
|
||||
rp: { name: this.#appName },
|
||||
user: {
|
||||
id: challenge,
|
||||
name: this.#userName,
|
||||
displayName: this.#userName,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
} catch {
|
||||
throw new OperationCancelledError();
|
||||
}
|
||||
|
||||
checkCredential(attestation);
|
||||
|
||||
|
@ -108,6 +130,14 @@ export class TangoWebAuthnPrfSource implements TangoPrfSource {
|
|||
return { output, id };
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates PRF output using a credential and input data.
|
||||
*
|
||||
* @param id ID of a previously created credential
|
||||
* @param input The input data
|
||||
* @returns PRF output
|
||||
* @throws `OperationCancelledError` if the attestation is either cancelled by user or timed out
|
||||
*/
|
||||
async get(
|
||||
id: BufferSource,
|
||||
input: Uint8Array<ArrayBuffer>,
|
||||
|
@ -125,7 +155,7 @@ export class TangoWebAuthnPrfSource implements TangoPrfSource {
|
|||
},
|
||||
});
|
||||
} catch {
|
||||
throw new AssertionFailedError();
|
||||
throw new OperationCancelledError();
|
||||
}
|
||||
|
||||
checkCredential(assertion);
|
||||
|
@ -141,5 +171,5 @@ export class TangoWebAuthnPrfSource implements TangoPrfSource {
|
|||
|
||||
export namespace TangoWebAuthnPrfSource {
|
||||
export type NotSupportedError = typeof NotSupportedError;
|
||||
export type AssertionFailedError = typeof AssertionFailedError;
|
||||
export type OperationCancelledError = typeof OperationCancelledError;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,15 @@
|
|||
import type { MaybePromiseLike } from "@yume-chan/async";
|
||||
|
||||
export interface TangoDataStorage {
|
||||
save(data: Uint8Array): MaybePromiseLike<undefined>;
|
||||
|
||||
load(): Iterable<Uint8Array> | AsyncIterable<Uint8Array>;
|
||||
export interface TangoKey {
|
||||
privateKey: Uint8Array;
|
||||
name: string | undefined;
|
||||
}
|
||||
|
||||
export interface TangoKeyStorage {
|
||||
save(
|
||||
privateKey: Uint8Array,
|
||||
name: string | undefined,
|
||||
): MaybePromiseLike<undefined>;
|
||||
|
||||
load(): Iterable<TangoKey> | AsyncIterable<TangoKey>;
|
||||
}
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
import type { AdbCredentialStore, AdbPrivateKey } from "@yume-chan/adb";
|
||||
import { rsaParsePrivateKey } from "@yume-chan/adb";
|
||||
|
||||
import type { TangoDataStorage } from "./storage/index.js";
|
||||
import type { TangoKeyStorage } from "./storage/index.js";
|
||||
|
||||
export class AdbWebCryptoCredentialStore implements AdbCredentialStore {
|
||||
readonly #storage: TangoDataStorage;
|
||||
readonly #storage: TangoKeyStorage;
|
||||
|
||||
readonly #appName: string;
|
||||
readonly #name: string | undefined;
|
||||
|
||||
constructor(storage: TangoDataStorage, appName: string = "Tango") {
|
||||
constructor(storage: TangoKeyStorage, name?: string) {
|
||||
this.#storage = storage;
|
||||
this.#appName = appName;
|
||||
this.#name = name;
|
||||
}
|
||||
|
||||
async generateKey(): Promise<AdbPrivateKey> {
|
||||
|
@ -39,7 +39,7 @@ export class AdbWebCryptoCredentialStore implements AdbCredentialStore {
|
|||
|
||||
const parsed = rsaParsePrivateKey(privateKey);
|
||||
|
||||
await this.#storage.save(privateKey);
|
||||
await this.#storage.save(privateKey, this.#name);
|
||||
|
||||
// Clear secret memory
|
||||
// * `privateKey` is not allowed to be used after `save`
|
||||
|
@ -47,16 +47,16 @@ export class AdbWebCryptoCredentialStore implements AdbCredentialStore {
|
|||
|
||||
return {
|
||||
...parsed,
|
||||
name: `${this.#appName}@${globalThis.location.hostname}`,
|
||||
name: this.#name,
|
||||
};
|
||||
}
|
||||
|
||||
async *iterateKeys(): AsyncGenerator<AdbPrivateKey, void, void> {
|
||||
for await (const privateKey of this.#storage.load()) {
|
||||
for await (const key of this.#storage.load()) {
|
||||
// `privateKey` is owned by `#storage` and will be cleared by it
|
||||
yield {
|
||||
...rsaParsePrivateKey(privateKey),
|
||||
name: `${this.#appName}@${globalThis.location.hostname}`,
|
||||
...rsaParsePrivateKey(key.privateKey),
|
||||
name: key.name ?? this.#name,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,15 +17,20 @@ export class AdbNoneProtocolSubprocessService {
|
|||
spawn = adbNoneProtocolSpawner(async (command, signal) => {
|
||||
// Android 7 added `shell,raw:${command}` service which also triggers raw mode,
|
||||
// but we want to use the most compatible one.
|
||||
//
|
||||
let service = "exec:";
|
||||
|
||||
if (!command.length) {
|
||||
throw new Error("Command cannot be empty");
|
||||
}
|
||||
|
||||
// Similar to SSH, we don't escape the `command`,
|
||||
// because the command will be invoked by `sh -c`,
|
||||
// it can contain environment variables (`KEY=value command`),
|
||||
// and shell expansions (`echo "$KEY"` vs `echo '$KEY'`),
|
||||
// which we can't know how to properly escape.
|
||||
const socket = await this.#adb.createSocket(
|
||||
`exec:${command.join(" ")}`,
|
||||
);
|
||||
service += command.join(" ");
|
||||
|
||||
const socket = await this.#adb.createSocket(service);
|
||||
|
||||
if (signal?.aborted) {
|
||||
await socket.close();
|
||||
|
@ -38,17 +43,17 @@ export class AdbNoneProtocolSubprocessService {
|
|||
async pty(
|
||||
command?: string | readonly string[],
|
||||
): Promise<AdbNoneProtocolPtyProcess> {
|
||||
if (command === undefined) {
|
||||
// Run the default shell
|
||||
command = "";
|
||||
let service = "shell:";
|
||||
|
||||
if (typeof command === "string") {
|
||||
service += command;
|
||||
} else if (Array.isArray(command)) {
|
||||
// Don't escape `command`. See `spawn` above for details
|
||||
command = command.join(" ");
|
||||
service += command.join(" ");
|
||||
}
|
||||
|
||||
return new AdbNoneProtocolPtyProcess(
|
||||
// https://github.com/microsoft/typescript/issues/17002
|
||||
await this.#adb.createSocket(`shell:${command as string}`),
|
||||
await this.#adb.createSocket(service),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,10 +20,16 @@ export class AdbShellProtocolSubprocessService {
|
|||
}
|
||||
|
||||
spawn = adbShellProtocolSpawner(async (command, signal) => {
|
||||
let service = "shell,v2,raw:";
|
||||
|
||||
if (!command.length) {
|
||||
throw new Error("Command cannot be empty");
|
||||
}
|
||||
|
||||
// Don't escape `command`. See `AdbNoneProtocolSubprocessService.prototype.spawn` for details.
|
||||
const socket = await this.#adb.createSocket(
|
||||
`shell,v2,raw:${command.join(" ")}`,
|
||||
);
|
||||
service += command.join(" ");
|
||||
|
||||
const socket = await this.#adb.createSocket(service);
|
||||
|
||||
if (signal?.aborted) {
|
||||
await socket.close();
|
||||
|
@ -37,21 +43,23 @@ export class AdbShellProtocolSubprocessService {
|
|||
command?: string | readonly string[] | undefined;
|
||||
terminalType?: string;
|
||||
}): Promise<AdbShellProtocolPtyProcess> {
|
||||
const { command, terminalType } = options ?? {};
|
||||
|
||||
let service = "shell,v2,pty";
|
||||
|
||||
if (options?.terminalType) {
|
||||
service += `,TERM=` + options.terminalType;
|
||||
if (terminalType) {
|
||||
if (terminalType.includes(",") || terminalType.includes(":")) {
|
||||
throw new Error("terminalType must not contain ',' or ':'");
|
||||
}
|
||||
service += `,TERM=` + terminalType;
|
||||
}
|
||||
|
||||
service += ":";
|
||||
|
||||
if (options) {
|
||||
if (typeof command === "string") {
|
||||
service += command;
|
||||
} else if (Array.isArray(command)) {
|
||||
// Don't escape `command`. See `AdbNoneProtocolSubprocessService.prototype.spawn` for details.
|
||||
if (typeof options.command === "string") {
|
||||
service += options.command;
|
||||
} else if (Array.isArray(options.command)) {
|
||||
service += options.command.join(" ");
|
||||
}
|
||||
service += command.join(" ");
|
||||
}
|
||||
|
||||
return new AdbShellProtocolPtyProcess(
|
||||
|
|
|
@ -295,7 +295,20 @@ function encodeBackward(
|
|||
}
|
||||
}
|
||||
|
||||
function getCharIndex(input: string, offset: number) {
|
||||
const charCode = input.charCodeAt(offset);
|
||||
const index = charToIndex[charCode];
|
||||
if (index === undefined) {
|
||||
throw new Error("Invalid Base64 character: " + input[offset]);
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
export function decodeBase64(input: string): Uint8Array<ArrayBuffer> {
|
||||
if (input.length % 4 !== 0) {
|
||||
throw new Error("Invalid Base64 length: " + input.length);
|
||||
}
|
||||
|
||||
let padding: number;
|
||||
if (input[input.length - 2] === "=") {
|
||||
padding = 2;
|
||||
|
@ -309,17 +322,18 @@ export function decodeBase64(input: string): Uint8Array<ArrayBuffer> {
|
|||
let sIndex = 0;
|
||||
let dIndex = 0;
|
||||
|
||||
while (sIndex < input.length - (padding !== 0 ? 4 : 0)) {
|
||||
const a = charToIndex[input.charCodeAt(sIndex)]!;
|
||||
const loopEnd = input.length - (padding !== 0 ? 4 : 0);
|
||||
while (sIndex < loopEnd) {
|
||||
const a = getCharIndex(input, sIndex);
|
||||
sIndex += 1;
|
||||
|
||||
const b = charToIndex[input.charCodeAt(sIndex)]!;
|
||||
const b = getCharIndex(input, sIndex);
|
||||
sIndex += 1;
|
||||
|
||||
const c = charToIndex[input.charCodeAt(sIndex)]!;
|
||||
const c = getCharIndex(input, sIndex);
|
||||
sIndex += 1;
|
||||
|
||||
const d = charToIndex[input.charCodeAt(sIndex)]!;
|
||||
const d = getCharIndex(input, sIndex);
|
||||
sIndex += 1;
|
||||
|
||||
result[dIndex] = (a << 2) | ((b & 0b11_0000) >> 4);
|
||||
|
@ -333,23 +347,23 @@ export function decodeBase64(input: string): Uint8Array<ArrayBuffer> {
|
|||
}
|
||||
|
||||
if (padding === 1) {
|
||||
const a = charToIndex[input.charCodeAt(sIndex)]!;
|
||||
const a = getCharIndex(input, sIndex);
|
||||
sIndex += 1;
|
||||
|
||||
const b = charToIndex[input.charCodeAt(sIndex)]!;
|
||||
const b = getCharIndex(input, sIndex);
|
||||
sIndex += 1;
|
||||
|
||||
const c = charToIndex[input.charCodeAt(sIndex)]!;
|
||||
const c = getCharIndex(input, sIndex);
|
||||
|
||||
result[dIndex] = (a << 2) | ((b & 0b11_0000) >> 4);
|
||||
dIndex += 1;
|
||||
|
||||
result[dIndex] = ((b & 0b1111) << 4) | ((c & 0b11_1100) >> 2);
|
||||
} else if (padding === 2) {
|
||||
const a = charToIndex[input.charCodeAt(sIndex)]!;
|
||||
const a = getCharIndex(input, sIndex);
|
||||
sIndex += 1;
|
||||
|
||||
const b = charToIndex[input.charCodeAt(sIndex)]!;
|
||||
const b = getCharIndex(input, sIndex);
|
||||
|
||||
result[dIndex] = (a << 2) | ((b & 0b11_0000) >> 4);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import type { MaybePromiseLike } from "@yume-chan/async";
|
||||
|
||||
import { TransformStream } from "./stream.js";
|
||||
|
||||
export class InspectStream<T> extends TransformStream<T, T> {
|
||||
|
|
135
pnpm-lock.yaml
generated
135
pnpm-lock.yaml
generated
|
@ -10,7 +10,7 @@ importers:
|
|||
devDependencies:
|
||||
'@changesets/cli':
|
||||
specifier: ^2.29.6
|
||||
version: 2.29.6(@types/node@24.3.1)
|
||||
version: 2.29.6(@types/node@24.4.0)
|
||||
|
||||
apps/cli:
|
||||
dependencies:
|
||||
|
@ -35,7 +35,7 @@ importers:
|
|||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^24.3.1
|
||||
version: 24.3.1
|
||||
version: 24.4.0
|
||||
'@yume-chan/eslint-config':
|
||||
specifier: workspace:^
|
||||
version: link:../../toolchain/eslint-config
|
||||
|
@ -69,7 +69,35 @@ importers:
|
|||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^24.3.1
|
||||
version: 24.3.1
|
||||
version: 24.4.0
|
||||
'@yume-chan/eslint-config':
|
||||
specifier: workspace:^
|
||||
version: link:../../toolchain/eslint-config
|
||||
'@yume-chan/test-runner':
|
||||
specifier: workspace:^
|
||||
version: link:../../toolchain/test-runner
|
||||
'@yume-chan/tsconfig':
|
||||
specifier: workspace:^
|
||||
version: link:../../toolchain/tsconfig
|
||||
prettier:
|
||||
specifier: ^3.6.2
|
||||
version: 3.6.2
|
||||
typescript:
|
||||
specifier: ^5.9.2
|
||||
version: 5.9.2
|
||||
|
||||
libraries/adb-credential-nodejs:
|
||||
dependencies:
|
||||
'@yume-chan/adb':
|
||||
specifier: workspace:^
|
||||
version: link:../adb
|
||||
'@yume-chan/adb-credential-web':
|
||||
specifier: workspace:^
|
||||
version: link:../adb-credential-web
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^24.3.0
|
||||
version: 24.3.0
|
||||
'@yume-chan/eslint-config':
|
||||
specifier: workspace:^
|
||||
version: link:../../toolchain/eslint-config
|
||||
|
@ -131,7 +159,7 @@ importers:
|
|||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^24.3.1
|
||||
version: 24.3.1
|
||||
version: 24.4.0
|
||||
'@yume-chan/eslint-config':
|
||||
specifier: workspace:^
|
||||
version: link:../../toolchain/eslint-config
|
||||
|
@ -174,7 +202,7 @@ importers:
|
|||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^24.3.1
|
||||
version: 24.3.1
|
||||
version: 24.4.0
|
||||
'@yume-chan/eslint-config':
|
||||
specifier: workspace:^
|
||||
version: link:../../toolchain/eslint-config
|
||||
|
@ -208,7 +236,7 @@ importers:
|
|||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^24.3.1
|
||||
version: 24.3.1
|
||||
version: 24.4.0
|
||||
'@yume-chan/eslint-config':
|
||||
specifier: workspace:^
|
||||
version: link:../../toolchain/eslint-config
|
||||
|
@ -236,7 +264,7 @@ importers:
|
|||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^24.3.1
|
||||
version: 24.3.1
|
||||
version: 24.4.0
|
||||
'@yume-chan/eslint-config':
|
||||
specifier: workspace:^
|
||||
version: link:../../toolchain/eslint-config
|
||||
|
@ -280,7 +308,7 @@ importers:
|
|||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^24.3.1
|
||||
version: 24.3.1
|
||||
version: 24.4.0
|
||||
'@yume-chan/eslint-config':
|
||||
specifier: workspace:^
|
||||
version: link:../../toolchain/eslint-config
|
||||
|
@ -305,7 +333,7 @@ importers:
|
|||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^24.3.1
|
||||
version: 24.3.1
|
||||
version: 24.4.0
|
||||
|
||||
libraries/media-codec:
|
||||
dependencies:
|
||||
|
@ -315,7 +343,7 @@ importers:
|
|||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^24.3.0
|
||||
version: 24.3.1
|
||||
version: 24.3.0
|
||||
'@yume-chan/eslint-config':
|
||||
specifier: workspace:^
|
||||
version: link:../../toolchain/eslint-config
|
||||
|
@ -336,7 +364,7 @@ importers:
|
|||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^24.3.1
|
||||
version: 24.3.1
|
||||
version: 24.4.0
|
||||
'@yume-chan/eslint-config':
|
||||
specifier: workspace:^
|
||||
version: link:../../toolchain/eslint-config
|
||||
|
@ -394,7 +422,7 @@ importers:
|
|||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^24.3.1
|
||||
version: 24.3.1
|
||||
version: 24.4.0
|
||||
'@yume-chan/eslint-config':
|
||||
specifier: workspace:^
|
||||
version: link:../../toolchain/eslint-config
|
||||
|
@ -499,7 +527,7 @@ importers:
|
|||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^24.3.1
|
||||
version: 24.3.1
|
||||
version: 24.4.0
|
||||
'@yume-chan/eslint-config':
|
||||
specifier: workspace:^
|
||||
version: link:../../toolchain/eslint-config
|
||||
|
@ -527,7 +555,7 @@ importers:
|
|||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^24.3.1
|
||||
version: 24.3.1
|
||||
version: 24.4.0
|
||||
'@yume-chan/eslint-config':
|
||||
specifier: workspace:^
|
||||
version: link:../../toolchain/eslint-config
|
||||
|
@ -551,7 +579,7 @@ importers:
|
|||
version: 9.35.0
|
||||
'@types/node':
|
||||
specifier: ^24.3.1
|
||||
version: 24.3.1
|
||||
version: 24.4.0
|
||||
eslint:
|
||||
specifier: ^9.35.0
|
||||
version: 9.35.0
|
||||
|
@ -573,7 +601,7 @@ importers:
|
|||
dependencies:
|
||||
'@types/node':
|
||||
specifier: ^24.3.1
|
||||
version: 24.3.1
|
||||
version: 24.4.0
|
||||
json5:
|
||||
specifier: ^2.2.3
|
||||
version: 2.2.3
|
||||
|
@ -610,7 +638,7 @@ importers:
|
|||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^24.3.1
|
||||
version: 24.3.1
|
||||
version: 24.4.0
|
||||
typescript:
|
||||
specifier: ^5.9.2
|
||||
version: 5.9.2
|
||||
|
@ -623,8 +651,8 @@ importers:
|
|||
|
||||
packages:
|
||||
|
||||
'@babel/runtime@7.28.4':
|
||||
resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==}
|
||||
'@babel/runtime@7.28.3':
|
||||
resolution: {integrity: sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@changesets/apply-release-plan@7.0.12':
|
||||
|
@ -691,6 +719,12 @@ packages:
|
|||
'@emnapi/wasi-threads@1.0.4':
|
||||
resolution: {integrity: sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==}
|
||||
|
||||
'@eslint-community/eslint-utils@4.7.0':
|
||||
resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
peerDependencies:
|
||||
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
|
||||
|
||||
'@eslint-community/eslint-utils@4.9.0':
|
||||
resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
|
@ -733,14 +767,18 @@ packages:
|
|||
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
|
||||
engines: {node: '>=18.18.0'}
|
||||
|
||||
'@humanfs/node@0.16.7':
|
||||
resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==}
|
||||
'@humanfs/node@0.16.6':
|
||||
resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==}
|
||||
engines: {node: '>=18.18.0'}
|
||||
|
||||
'@humanwhocodes/module-importer@1.0.1':
|
||||
resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
|
||||
engines: {node: '>=12.22'}
|
||||
|
||||
'@humanwhocodes/retry@0.3.1':
|
||||
resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==}
|
||||
engines: {node: '>=18.18'}
|
||||
|
||||
'@humanwhocodes/retry@0.4.3':
|
||||
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
|
||||
engines: {node: '>=18.18'}
|
||||
|
@ -978,8 +1016,11 @@ packages:
|
|||
'@types/node@12.20.55':
|
||||
resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==}
|
||||
|
||||
'@types/node@24.3.1':
|
||||
resolution: {integrity: sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g==}
|
||||
'@types/node@24.3.0':
|
||||
resolution: {integrity: sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==}
|
||||
|
||||
'@types/node@24.4.0':
|
||||
resolution: {integrity: sha512-gUuVEAK4/u6F9wRLznPUU4WGUacSEBDPoC2TrBkw3GAnOLHBL45QdfHOXp1kJ4ypBGLxTOB+t7NJLpKoC3gznQ==}
|
||||
|
||||
'@types/resolve@1.20.2':
|
||||
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
|
||||
|
@ -1025,6 +1066,10 @@ packages:
|
|||
eslint: ^8.57.0 || ^9.0.0
|
||||
typescript: '>=4.8.4 <6.0.0'
|
||||
|
||||
'@typescript-eslint/types@8.37.0':
|
||||
resolution: {integrity: sha512-ax0nv7PUF9NOVPs+lmQ7yIE7IQmAf8LGcXbMvHX5Gm+YJUYNAl340XkGnrimxZ0elXyoQJuN5sbg6C4evKA4SQ==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@typescript-eslint/types@8.43.0':
|
||||
resolution: {integrity: sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
@ -2108,6 +2153,9 @@ packages:
|
|||
undici-types@7.10.0:
|
||||
resolution: {integrity: sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==}
|
||||
|
||||
undici-types@7.11.0:
|
||||
resolution: {integrity: sha512-kt1ZriHTi7MU+Z/r9DOdAI3ONdaR3M3csEaRc6ewa4f4dTvX4cQCbJ4NkEn0ohE4hHtq85+PhPSTY+pO/1PwgA==}
|
||||
|
||||
universalify@0.1.2:
|
||||
resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==}
|
||||
engines: {node: '>= 4.0.0'}
|
||||
|
@ -2149,7 +2197,7 @@ packages:
|
|||
|
||||
snapshots:
|
||||
|
||||
'@babel/runtime@7.28.4': {}
|
||||
'@babel/runtime@7.28.3': {}
|
||||
|
||||
'@changesets/apply-release-plan@7.0.12':
|
||||
dependencies:
|
||||
|
@ -2180,7 +2228,7 @@ snapshots:
|
|||
dependencies:
|
||||
'@changesets/types': 6.1.0
|
||||
|
||||
'@changesets/cli@2.29.6(@types/node@24.3.1)':
|
||||
'@changesets/cli@2.29.6(@types/node@24.4.0)':
|
||||
dependencies:
|
||||
'@changesets/apply-release-plan': 7.0.12
|
||||
'@changesets/assemble-release-plan': 6.0.9
|
||||
|
@ -2196,7 +2244,7 @@ snapshots:
|
|||
'@changesets/should-skip-package': 0.1.2
|
||||
'@changesets/types': 6.1.0
|
||||
'@changesets/write': 0.4.0
|
||||
'@inquirer/external-editor': 1.0.1(@types/node@24.3.1)
|
||||
'@inquirer/external-editor': 1.0.1(@types/node@24.4.0)
|
||||
'@manypkg/get-packages': 1.1.3
|
||||
ansi-colors: 4.1.3
|
||||
ci-info: 3.9.0
|
||||
|
@ -2311,6 +2359,11 @@ snapshots:
|
|||
tslib: 2.8.1
|
||||
optional: true
|
||||
|
||||
'@eslint-community/eslint-utils@4.7.0(eslint@9.35.0)':
|
||||
dependencies:
|
||||
eslint: 9.35.0
|
||||
eslint-visitor-keys: 3.4.3
|
||||
|
||||
'@eslint-community/eslint-utils@4.9.0(eslint@9.35.0)':
|
||||
dependencies:
|
||||
eslint: 9.35.0
|
||||
|
@ -2357,21 +2410,23 @@ snapshots:
|
|||
|
||||
'@humanfs/core@0.19.1': {}
|
||||
|
||||
'@humanfs/node@0.16.7':
|
||||
'@humanfs/node@0.16.6':
|
||||
dependencies:
|
||||
'@humanfs/core': 0.19.1
|
||||
'@humanwhocodes/retry': 0.4.3
|
||||
'@humanwhocodes/retry': 0.3.1
|
||||
|
||||
'@humanwhocodes/module-importer@1.0.1': {}
|
||||
|
||||
'@humanwhocodes/retry@0.3.1': {}
|
||||
|
||||
'@humanwhocodes/retry@0.4.3': {}
|
||||
|
||||
'@inquirer/external-editor@1.0.1(@types/node@24.3.1)':
|
||||
'@inquirer/external-editor@1.0.1(@types/node@24.4.0)':
|
||||
dependencies:
|
||||
chardet: 2.1.0
|
||||
iconv-lite: 0.6.3
|
||||
optionalDependencies:
|
||||
'@types/node': 24.3.1
|
||||
'@types/node': 24.4.0
|
||||
|
||||
'@isaacs/balanced-match@4.0.1': {}
|
||||
|
||||
|
@ -2403,14 +2458,14 @@ snapshots:
|
|||
|
||||
'@manypkg/find-root@1.1.0':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.4
|
||||
'@babel/runtime': 7.28.3
|
||||
'@types/node': 12.20.55
|
||||
find-up: 4.1.0
|
||||
fs-extra: 8.1.0
|
||||
|
||||
'@manypkg/get-packages@1.1.3':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.4
|
||||
'@babel/runtime': 7.28.3
|
||||
'@changesets/types': 4.1.0
|
||||
'@manypkg/find-root': 1.1.0
|
||||
fs-extra: 8.1.0
|
||||
|
@ -2557,10 +2612,14 @@ snapshots:
|
|||
|
||||
'@types/node@12.20.55': {}
|
||||
|
||||
'@types/node@24.3.1':
|
||||
'@types/node@24.3.0':
|
||||
dependencies:
|
||||
undici-types: 7.10.0
|
||||
|
||||
'@types/node@24.4.0':
|
||||
dependencies:
|
||||
undici-types: 7.11.0
|
||||
|
||||
'@types/resolve@1.20.2': {}
|
||||
|
||||
'@types/w3c-web-usb@1.0.12': {}
|
||||
|
@ -2624,6 +2683,8 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/types@8.37.0': {}
|
||||
|
||||
'@typescript-eslint/types@8.43.0': {}
|
||||
|
||||
'@typescript-eslint/typescript-estree@8.43.0(typescript@5.9.2)':
|
||||
|
@ -2644,7 +2705,7 @@ snapshots:
|
|||
|
||||
'@typescript-eslint/utils@8.43.0(eslint@9.35.0)(typescript@5.9.2)':
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.9.0(eslint@9.35.0)
|
||||
'@eslint-community/eslint-utils': 4.7.0(eslint@9.35.0)
|
||||
'@typescript-eslint/scope-manager': 8.43.0
|
||||
'@typescript-eslint/types': 8.43.0
|
||||
'@typescript-eslint/typescript-estree': 8.43.0(typescript@5.9.2)
|
||||
|
@ -2937,7 +2998,7 @@ snapshots:
|
|||
|
||||
eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.43.0(eslint@9.35.0)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint@9.35.0):
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 8.43.0
|
||||
'@typescript-eslint/types': 8.37.0
|
||||
comment-parser: 1.4.1
|
||||
debug: 4.4.1
|
||||
eslint: 9.35.0
|
||||
|
@ -2972,7 +3033,7 @@ snapshots:
|
|||
'@eslint/eslintrc': 3.3.1
|
||||
'@eslint/js': 9.35.0
|
||||
'@eslint/plugin-kit': 0.3.5
|
||||
'@humanfs/node': 0.16.7
|
||||
'@humanfs/node': 0.16.6
|
||||
'@humanwhocodes/module-importer': 1.0.1
|
||||
'@humanwhocodes/retry': 0.4.3
|
||||
'@types/estree': 1.0.8
|
||||
|
@ -3653,6 +3714,8 @@ snapshots:
|
|||
|
||||
undici-types@7.10.0: {}
|
||||
|
||||
undici-types@7.11.0: {}
|
||||
|
||||
universalify@0.1.2: {}
|
||||
|
||||
unrs-resolver@1.11.1:
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue