mirror of
https://github.com/yume-chan/ya-webadb.git
synced 2025-10-06 03:50:18 +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
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": []
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue