/* eslint-disable no-console */ import { cyanBright, red } from 'chalk'; import { execSync } from 'child_process'; /** A single item as returned by `bw list items`. */ interface BitwardenItem { name: string; login?: { username: string; password: string; }; fields?: [ { name: string; value: string; }, ]; } /** Interface for a vault which provides values. */ export interface Vault { error: string | undefined; getValue(name: string, fieldName: string): string | undefined; } /** Represents a Bitwarden vault accessed through Bitwarden CLI commands. */ export class BitwardenVault implements Vault { constructor(private serverUrl: string | undefined) { this.error = this.sync(); if (!this.error) { this.items = this.fetchItems(); } } /** If there is an error getting items from the vault this will not be undefined. */ public error: string | undefined; private readonly items: BitwardenItem[] | undefined; /** Get a value from the vault. If the value is not found returns undefined. */ public getValue(name: string, fieldName: string): string | undefined { for (const item of this.items ?? []) { if (item.name === name) { if (item.login && fieldName === 'username') { return item.login.username; } if (item.login && fieldName === 'password') { return item.login.password; } if (item.fields) { for (const field of item.fields) { if (field.name === fieldName) { return field.value; } } } } } return undefined; } /** * Syncs the Bitwarden vault. Note: this only downloads data. * * We could make this more interactive: * - Get status ('yarn bw status') and parse it * - Set server url automatically if its not set. Fail with error if it is set but different * - Prompt for username & password if not logged in. Login and store session id in memory * - Prompt for password if locked. Unlock and store session id in memory * - Session id can then be passed to `sync` & `list items` commands with --session * Reasons to not do this: * - The script becomes more complex * - Additional security concern of handling password entry * - Password would need to be entered every time the script is run (rather than just once per terminal session) * - Blocker: unlocked status is not reported correctly: https://github.com/bitwarden/clients/issues/2729. This * makes it really hard to determine the correct course of action. */ private sync(): string | undefined { try { execSync(`yarn bw --nointeraction sync`); console.log(`Bitwarden vault synced.`); return undefined; } catch (e) { console.log(); // Try to determine what is the issue and provide some direction if (e instanceof Error && e.message.includes('getaddrinfo ENOTFOUND')) { return 'The Bitwarden server could not be found. Do you need to connect to the VPN?'; } if (e instanceof Error && e.message.includes('You are not logged in')) { return this.serverUrl !== undefined ? 'You are not logged in. Run command `' + cyanBright(`yarn bw config server ${this.serverUrl}`) + '` to set the server url, then command `' + cyanBright('yarn bw login') + '` and follow the instructions to unlock your vault.' : 'You are not logged in. Run command `' + cyanBright('yarn bw login') + '` and follow the instructions to unlock your vault.'; } if (e instanceof Error && e.message.includes('Vault is locked')) { return ( 'Your vault is locked. Do you need to connect to the VPN? Run command `' + cyanBright('yarn bw unlock') + '` and follow the instructions to unlock your vault.' ); } return ( 'Unexpected error syncing Bitwarden vault. You may need to run command `' + cyanBright('yarn install') + '` to install the Bitwarden CLI otherwise use command `' + cyanBright('yarn bw status') + '` to show the full status.' ); } } /** * Gets Bitwarden collection items from the vault. */ private fetchItems(): BitwardenItem[] { try { const json = execSync( 'yarn run --silent bw --nointeraction list items', ).toString(); return JSON.parse(json) as BitwardenItem[]; } catch (_e) { console.log(); console.error(red('ERROR: Could not list items in the vault.')); process.exit(1); } } }