Loading .changeset/tangy-bottles-dream.md 0 → 100644 +6 −0 Original line number Diff line number Diff line --- "@yume-chan/adb-credential-web": major "@yume-chan/adb": major --- Refactor daemon authentication API and add support for more credential storages libraries/adb-credential-web/package.json +2 −1 Original line number Diff line number Diff line Loading @@ -31,7 +31,8 @@ }, "dependencies": { "@yume-chan/adb": "workspace:^", "@yume-chan/async": "^4.1.3" "@yume-chan/async": "^4.1.3", "@yume-chan/struct": "workspace:^" }, "devDependencies": { "@yume-chan/eslint-config": "workspace:^", Loading libraries/adb-credential-web/src/index.ts +2 −121 Original line number Diff line number Diff line // cspell: ignore RSASSA import type { AdbCredentialStore, AdbPrivateKey } from "@yume-chan/adb"; function openDatabase() { return new Promise<IDBDatabase>((resolve, reject) => { const request = indexedDB.open("Tango", 1); request.onerror = () => { reject(request.error!); }; request.onupgradeneeded = () => { const db = request.result; db.createObjectStore("Authentication", { autoIncrement: true }); }; request.onsuccess = () => { const db = request.result; resolve(db); }; }); } async function saveKey(key: Uint8Array): Promise<void> { const db = await openDatabase(); return new Promise((resolve, reject) => { const transaction = db.transaction("Authentication", "readwrite"); const store = transaction.objectStore("Authentication"); const putRequest = store.add(key); putRequest.onerror = () => { reject(putRequest.error!); }; putRequest.onsuccess = () => { resolve(); }; transaction.onerror = () => { reject(transaction.error!); }; transaction.oncomplete = () => { db.close(); }; }); } async function getAllKeys() { const db = await openDatabase(); return new Promise<Uint8Array[]>((resolve, reject) => { const transaction = db.transaction("Authentication", "readonly"); const store = transaction.objectStore("Authentication"); const getRequest = store.getAll(); getRequest.onerror = () => { reject(getRequest.error!); }; getRequest.onsuccess = () => { resolve(getRequest.result as Uint8Array[]); }; transaction.onerror = () => { reject(transaction.error!); }; transaction.oncomplete = () => { db.close(); }; }); } /** * An `AdbCredentialStore` implementation that creates RSA private keys using Web Crypto API * and stores them in IndexedDB. */ export default class AdbWebCredentialStore implements AdbCredentialStore { readonly #appName: string; constructor(appName = "Tango") { this.#appName = appName; } /** * Generates a RSA private key and store it into LocalStorage. * * Calling this method multiple times will overwrite the previous key. * * @returns The private key in PKCS #8 format. */ async generateKey(): Promise<AdbPrivateKey> { const { privateKey: cryptoKey } = await crypto.subtle.generateKey( { name: "RSASSA-PKCS1-v1_5", modulusLength: 2048, // 65537 publicExponent: new Uint8Array([0x01, 0x00, 0x01]), hash: "SHA-1", }, true, ["sign", "verify"], ); const privateKey = new Uint8Array( await crypto.subtle.exportKey("pkcs8", cryptoKey), ); await saveKey(privateKey); return { buffer: privateKey, name: `${this.#appName}@${globalThis.location.hostname}`, }; } /** * Yields the stored RSA private key. * * This method returns a generator, so `for await...of...` loop should be used to read the key. */ async *iterateKeys(): AsyncGenerator<AdbPrivateKey, void, void> { for (const key of await getAllKeys()) { yield { buffer: key, name: `${this.#appName}@${globalThis.location.hostname}`, }; } } } export * from "./storage/index.js"; export * from "./store.js"; libraries/adb-credential-web/src/storage/index.ts 0 → 100644 +5 −0 Original line number Diff line number Diff line export * from "./indexed-db.js"; export * from "./local-storage.js"; export * from "./password.js"; export * from "./prf/index.js"; export * from "./type.js"; libraries/adb-credential-web/src/storage/indexed-db.ts 0 → 100644 +89 −0 Original line number Diff line number Diff line import type { TangoDataStorage } from "./type.js"; function openDatabase() { return new Promise<IDBDatabase>((resolve, reject) => { const request = indexedDB.open("Tango", 1); request.onerror = () => { reject(request.error!); }; request.onupgradeneeded = () => { const db = request.result; db.createObjectStore("Authentication", { autoIncrement: true }); }; request.onsuccess = () => { const db = request.result; resolve(db); }; }); } function createTransaction<T>( database: IDBDatabase, callback: (transaction: IDBTransaction) => T, ): Promise<T> { return new Promise<T>((resolve, reject) => { const transaction = database.transaction("Authentication", "readwrite"); transaction.onerror = () => { reject(transaction.error!); }; transaction.oncomplete = () => { resolve(result); }; transaction.onabort = () => { reject(transaction.error ?? new Error("Transaction aborted")); }; const result = callback(transaction); }); } export class TangoIndexedDbStorage implements TangoDataStorage { async save(data: Uint8Array): Promise<undefined> { const db = await openDatabase(); try { await createTransaction(db, (tx) => { const store = tx.objectStore("Authentication"); store.add(data); }); } finally { db.close(); } } async *load(): AsyncGenerator<Uint8Array, 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[]); }; }); }); yield* keys; } finally { db.close(); } } async clear() { const db = await openDatabase(); try { await createTransaction(db, (tx) => { const store = tx.objectStore("Authentication"); store.clear(); }); } finally { db.close(); } } } Loading
.changeset/tangy-bottles-dream.md 0 → 100644 +6 −0 Original line number Diff line number Diff line --- "@yume-chan/adb-credential-web": major "@yume-chan/adb": major --- Refactor daemon authentication API and add support for more credential storages
libraries/adb-credential-web/package.json +2 −1 Original line number Diff line number Diff line Loading @@ -31,7 +31,8 @@ }, "dependencies": { "@yume-chan/adb": "workspace:^", "@yume-chan/async": "^4.1.3" "@yume-chan/async": "^4.1.3", "@yume-chan/struct": "workspace:^" }, "devDependencies": { "@yume-chan/eslint-config": "workspace:^", Loading
libraries/adb-credential-web/src/index.ts +2 −121 Original line number Diff line number Diff line // cspell: ignore RSASSA import type { AdbCredentialStore, AdbPrivateKey } from "@yume-chan/adb"; function openDatabase() { return new Promise<IDBDatabase>((resolve, reject) => { const request = indexedDB.open("Tango", 1); request.onerror = () => { reject(request.error!); }; request.onupgradeneeded = () => { const db = request.result; db.createObjectStore("Authentication", { autoIncrement: true }); }; request.onsuccess = () => { const db = request.result; resolve(db); }; }); } async function saveKey(key: Uint8Array): Promise<void> { const db = await openDatabase(); return new Promise((resolve, reject) => { const transaction = db.transaction("Authentication", "readwrite"); const store = transaction.objectStore("Authentication"); const putRequest = store.add(key); putRequest.onerror = () => { reject(putRequest.error!); }; putRequest.onsuccess = () => { resolve(); }; transaction.onerror = () => { reject(transaction.error!); }; transaction.oncomplete = () => { db.close(); }; }); } async function getAllKeys() { const db = await openDatabase(); return new Promise<Uint8Array[]>((resolve, reject) => { const transaction = db.transaction("Authentication", "readonly"); const store = transaction.objectStore("Authentication"); const getRequest = store.getAll(); getRequest.onerror = () => { reject(getRequest.error!); }; getRequest.onsuccess = () => { resolve(getRequest.result as Uint8Array[]); }; transaction.onerror = () => { reject(transaction.error!); }; transaction.oncomplete = () => { db.close(); }; }); } /** * An `AdbCredentialStore` implementation that creates RSA private keys using Web Crypto API * and stores them in IndexedDB. */ export default class AdbWebCredentialStore implements AdbCredentialStore { readonly #appName: string; constructor(appName = "Tango") { this.#appName = appName; } /** * Generates a RSA private key and store it into LocalStorage. * * Calling this method multiple times will overwrite the previous key. * * @returns The private key in PKCS #8 format. */ async generateKey(): Promise<AdbPrivateKey> { const { privateKey: cryptoKey } = await crypto.subtle.generateKey( { name: "RSASSA-PKCS1-v1_5", modulusLength: 2048, // 65537 publicExponent: new Uint8Array([0x01, 0x00, 0x01]), hash: "SHA-1", }, true, ["sign", "verify"], ); const privateKey = new Uint8Array( await crypto.subtle.exportKey("pkcs8", cryptoKey), ); await saveKey(privateKey); return { buffer: privateKey, name: `${this.#appName}@${globalThis.location.hostname}`, }; } /** * Yields the stored RSA private key. * * This method returns a generator, so `for await...of...` loop should be used to read the key. */ async *iterateKeys(): AsyncGenerator<AdbPrivateKey, void, void> { for (const key of await getAllKeys()) { yield { buffer: key, name: `${this.#appName}@${globalThis.location.hostname}`, }; } } } export * from "./storage/index.js"; export * from "./store.js";
libraries/adb-credential-web/src/storage/index.ts 0 → 100644 +5 −0 Original line number Diff line number Diff line export * from "./indexed-db.js"; export * from "./local-storage.js"; export * from "./password.js"; export * from "./prf/index.js"; export * from "./type.js";
libraries/adb-credential-web/src/storage/indexed-db.ts 0 → 100644 +89 −0 Original line number Diff line number Diff line import type { TangoDataStorage } from "./type.js"; function openDatabase() { return new Promise<IDBDatabase>((resolve, reject) => { const request = indexedDB.open("Tango", 1); request.onerror = () => { reject(request.error!); }; request.onupgradeneeded = () => { const db = request.result; db.createObjectStore("Authentication", { autoIncrement: true }); }; request.onsuccess = () => { const db = request.result; resolve(db); }; }); } function createTransaction<T>( database: IDBDatabase, callback: (transaction: IDBTransaction) => T, ): Promise<T> { return new Promise<T>((resolve, reject) => { const transaction = database.transaction("Authentication", "readwrite"); transaction.onerror = () => { reject(transaction.error!); }; transaction.oncomplete = () => { resolve(result); }; transaction.onabort = () => { reject(transaction.error ?? new Error("Transaction aborted")); }; const result = callback(transaction); }); } export class TangoIndexedDbStorage implements TangoDataStorage { async save(data: Uint8Array): Promise<undefined> { const db = await openDatabase(); try { await createTransaction(db, (tx) => { const store = tx.objectStore("Authentication"); store.add(data); }); } finally { db.close(); } } async *load(): AsyncGenerator<Uint8Array, 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[]); }; }); }); yield* keys; } finally { db.close(); } } async clear() { const db = await openDatabase(); try { await createTransaction(db, (tx) => { const store = tx.objectStore("Authentication"); store.clear(); }); } finally { db.close(); } } }