Delete src/providers/passwords.ts
This commit is contained in:
@@ -1,439 +0,0 @@
|
|||||||
import { spawn } from "child_process";
|
|
||||||
|
|
||||||
import { Provider } from "../base";
|
|
||||||
import type { ProviderResult } from "../types";
|
|
||||||
|
|
||||||
interface CommandResult {
|
|
||||||
stdout: string;
|
|
||||||
exitCode: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface OnePasswordAccount {
|
|
||||||
secrets: Record<string, any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PasswordManagerSecrets {
|
|
||||||
onepassword: Record<string, OnePasswordAccount>;
|
|
||||||
bitwarden: Record<string, any>;
|
|
||||||
pass: Record<string, string>;
|
|
||||||
gopass: Record<string, string>;
|
|
||||||
}
|
|
||||||
|
|
||||||
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<CommandResult> {
|
|
||||||
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<boolean> {
|
|
||||||
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<Record<string, OnePasswordAccount>> {
|
|
||||||
const result: Record<string, OnePasswordAccount> = {};
|
|
||||||
|
|
||||||
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<string, any> = {};
|
|
||||||
|
|
||||||
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<string, any> = {};
|
|
||||||
|
|
||||||
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<string | null> {
|
|
||||||
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<Record<string, any>> {
|
|
||||||
const result: Record<string, any> = {};
|
|
||||||
|
|
||||||
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<Record<string, string>> {
|
|
||||||
const result: Record<string, string> = {};
|
|
||||||
|
|
||||||
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<Record<string, string>> {
|
|
||||||
const result: Record<string, string> = {};
|
|
||||||
|
|
||||||
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<ProviderResult> {
|
|
||||||
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)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user