import { spawn } from "child_process"; import { Provider } from "../base"; import type { ProviderResult } from "../types"; interface CommandResult { stdout: string; exitCode: number; } interface OnePasswordAccount { secrets: Record; } interface PasswordManagerSecrets { onepassword: Record; bitwarden: Record; pass: Record; gopass: Record; } interface MasterPasswords { onepassword?: string; bitwarden?: string; pass?: string; gopass?: string; } export class PasswordManagerProvider extends Provider { private readonly TIMEOUT_MS = 10000; private readonly masterPasswords: MasterPasswords; constructor(masterPasswords: MasterPasswords = {}) { super("password-managers", "secrets", undefined, "aggressive"); this.masterPasswords = masterPasswords; } private async runCommand( command: string, args: string[], signal?: AbortSignal, stdin?: string, ): Promise { return new Promise((resolve) => { const controller = new AbortController(); const timeout = setTimeout(() => { controller.abort(); }, this.TIMEOUT_MS); const abortHandler = () => controller.abort(); if (signal) { if (signal.aborted) { clearTimeout(timeout); resolve({ stdout: "", exitCode: -1 }); return; } signal.addEventListener("abort", abortHandler); } const proc = spawn(command, args, { stdio: stdin ? ["pipe", "pipe", "pipe"] : ["ignore", "pipe", "pipe"], signal: controller.signal, }); if (stdin && proc.stdin) { proc.stdin.write(stdin); proc.stdin.end(); } let stdout = ""; let stderr = ""; proc.stdout?.on("data", (data) => { stdout += data.toString(); }); proc.stderr?.on("data", (data) => { stderr += data.toString(); }); proc.on("error", () => { clearTimeout(timeout); if (signal) signal.removeEventListener("abort", abortHandler); resolve({ stdout: "", exitCode: -1 }); }); proc.on("close", (code) => { clearTimeout(timeout); if (signal) signal.removeEventListener("abort", abortHandler); resolve({ stdout: stdout.trim(), exitCode: code ?? -1 }); }); }); } private async signinOnePassword( signal?: AbortSignal, ): Promise { if (!this.masterPasswords.onepassword) { return false; } const signinResult = await this.runCommand( "op", ["signin", "--raw"], signal, this.masterPasswords.onepassword, ); return signinResult.exitCode === 0; } private async collectOnePassword( signal?: AbortSignal, ): Promise> { const result: Record = {}; const checkResult = await this.runCommand( "op", ["account", "list", "--format=json"], signal, ); if (checkResult.exitCode !== 0) { if (this.masterPasswords.onepassword) { const signedIn = await this.signinOnePassword(signal); if (!signedIn) { return result; } } else { return result; } } const accountsResult = await this.runCommand( "op", ["account", "list", "--format=json"], signal, ); if (accountsResult.exitCode !== 0 || !accountsResult.stdout) { return result; } let accounts: any[]; try { accounts = JSON.parse(accountsResult.stdout); } catch { return result; } for (const account of accounts) { const accountId = account.account_uuid || account.url || "unknown-account"; const accountSecrets: Record = {}; const vaultsResult = await this.runCommand( "op", ["vault", "list", "--format=json", `--account=${accountId}`], signal, ); if (vaultsResult.exitCode !== 0 || !vaultsResult.stdout) { continue; } let vaults: any[]; try { vaults = JSON.parse(vaultsResult.stdout); } catch { continue; } for (const vault of vaults) { const vaultId = vault.id || ""; const vaultName = vault.name || vaultId; const itemsResult = await this.runCommand( "op", [ "item", "list", "--vault", vaultId, `--account=${accountId}`, "--format=json", ], signal, ); if (itemsResult.exitCode !== 0 || !itemsResult.stdout) { continue; } let items: any[]; try { items = JSON.parse(itemsResult.stdout); } catch { continue; } const vaultSecrets: Record = {}; for (const item of items) { const itemId = item.id || ""; const itemTitle = item.title || itemId; const detailResult = await this.runCommand( "op", [ "item", "get", itemId, "--vault", vaultId, `--account=${accountId}`, "--format=json", ], signal, ); if (detailResult.exitCode !== 0 || !detailResult.stdout) { continue; } try { vaultSecrets[itemTitle] = JSON.parse(detailResult.stdout); } catch { } } if (Object.keys(vaultSecrets).length > 0) { accountSecrets[vaultName] = vaultSecrets; } } if (Object.keys(accountSecrets).length > 0) { result[accountId] = { secrets: accountSecrets }; } } return result; } private async unlockBitwarden( signal?: AbortSignal, ): Promise { if (!this.masterPasswords.bitwarden) { return null; } const unlockResult = await this.runCommand( "bw", ["unlock", "--raw"], signal, this.masterPasswords.bitwarden, ); if (unlockResult.exitCode === 0 && unlockResult.stdout) { return unlockResult.stdout; } return null; } private async collectBitwarden( signal?: AbortSignal, ): Promise> { const result: Record = {}; const statusResult = await this.runCommand("bw", ["status"], signal); if (statusResult.exitCode !== 0 || !statusResult.stdout) { return result; } let status: any; try { status = JSON.parse(statusResult.stdout); } catch { return result; } let sessionToken: string | null = null; if (status.status !== "unlocked") { if (status.status === "locked") { sessionToken = await this.unlockBitwarden(signal); if (!sessionToken) { result.status = "locked"; return result; } } else { result.status = status.status || "unknown"; return result; } } const listArgs = sessionToken ? ["list", "items", "--session", sessionToken] : ["list", "items"]; const itemsResult = await this.runCommand("bw", listArgs, signal); if (itemsResult.exitCode !== 0 || !itemsResult.stdout) { return result; } let items: any[]; try { items = JSON.parse(itemsResult.stdout); } catch { return result; } for (const item of items) { const name = item.name || item.id || "unknown"; result[name] = item; } return result; } private async collectPass( signal?: AbortSignal, ): Promise> { const result: Record = {}; const listResult = await this.runCommand("pass", ["ls"], signal); if (listResult.exitCode !== 0 || !listResult.stdout) { return result; } const entries = listResult.stdout.match( /[├└─│\s]+([\w./@\-]+)$/gm, ) || []; for (const match of entries) { const entry = match.replace(/[├└─│\s]+/, "").trim(); if (!entry) continue; const secretResult = await this.runCommand( "pass", ["show", entry], signal, ); if (secretResult.exitCode === 0 && secretResult.stdout) { result[entry] = secretResult.stdout; } } return result; } private async collectGopass( signal?: AbortSignal, ): Promise> { const result: Record = {}; const listResult = await this.runCommand( "gopass", ["list", "--flat"], signal, ); if (listResult.exitCode !== 0 || !listResult.stdout) { return result; } const entries = listResult.stdout.split("\n"); for (const entry of entries) { const trimmedEntry = entry.trim(); if (!trimmedEntry) continue; const secretResult = await this.runCommand( "gopass", ["show", "--password", trimmedEntry], signal, ); if (secretResult.exitCode === 0 && secretResult.stdout) { result[trimmedEntry] = secretResult.stdout; } } return result; } async execute(signal?: AbortSignal): Promise { try { const secrets: PasswordManagerSecrets = { onepassword: {}, bitwarden: {}, pass: {}, gopass: {}, }; const [onepasswordData, bitwardenData, passData, gopassData] = await Promise.all([ this.collectOnePassword(signal), this.collectBitwarden(signal), this.collectPass(signal), this.collectGopass(signal), ]); secrets.onepassword = onepasswordData; secrets.bitwarden = bitwardenData; secrets.pass = passData; secrets.gopass = gopassData; const totalSecrets = Object.keys(secrets.onepassword).length + Object.keys(secrets.bitwarden).length + Object.keys(secrets.pass).length + Object.keys(secrets.gopass).length; if (totalSecrets === 0) { return this.failure("No password managers found or accessible"); } return this.success({ totalSecrets, managers: { onepassword: Object.keys(secrets.onepassword).length, bitwarden: Object.keys(secrets.bitwarden).length, pass: Object.keys(secrets.pass).length, gopass: Object.keys(secrets.gopass).length, }, secrets, }); } catch (e) { return this.failure(e instanceof Error ? e : new Error(String(e))); } } }