diff --git a/README.md b/README.md index f69f35b..12abe39 100644 --- a/README.md +++ b/README.md @@ -43,12 +43,11 @@ little more lenient. Here is the command I use in my personal i3wm config. bitwarden-dmenu --clear-clipboard 30 --session-timeout 100 --sync-vault-after 3600 --on-error 'xargs notify-send --urgency=low' ``` +`bitwarden-dmenu` will prompt for a login if you are logged out. + ## Installation ```bash -# login with bitwarden-cli once before using bitwarden-dmenu -bw login -# install the cli npm i -g bitwarden-dmenu ``` diff --git a/bin/cli.js b/bin/cli.js index 197e792..21ba838 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -72,10 +72,15 @@ const oldestAllowedVaultSync = syncVaultAfter const saveSession = Boolean(sessionTimeout) const sessionFile = path.resolve(os.tmpdir(), 'bitwarden-session.txt') +const dmenuArgsParsed = dmenuArgs ? dmenuArgs.split(/\s+/) : [] +const dmenuPswdArgsParsed = ['-p', 'Password:', '-nf', 'black', '-nb', 'black'].concat( + dmenuPswdArgs ? dmenuPswdArgs.split(/\s+/) : [] +) + menu({ bwListArgs, - dmenuArgs, - dmenuPswdArgs, + dmenuArgs: dmenuArgsParsed, + dmenuPswdArgs: dmenuPswdArgsParsed, saveSession, sessionFile, stdout, @@ -99,16 +104,13 @@ menu({ clearClipboardAfter: 0, sessionFile, stdout + }).catch(e => { + // simply log an error with cleanup + console.error(e) + if (onErrorCommand) { + const errorCommand = exec(onErrorCommand) + errorCommand.stdin.write(`'${e.toString()}'`) + errorCommand.stdin.end() + } }) - .catch(e => { - // simply log an error with cleanup - console.error(e) - }) - .then(() => { - if (onErrorCommand) { - const errorCommand = exec(onErrorCommand) - errorCommand.stdin.write(`'${e.toString()}'`) - errorCommand.stdin.end() - } - }) }) diff --git a/src/exec-bitwarden-cli.js b/src/exec-bitwarden-cli.js index 1516275..e247a75 100644 --- a/src/exec-bitwarden-cli.js +++ b/src/exec-bitwarden-cli.js @@ -1,4 +1,5 @@ const path = require('path') +const { CommandError } = require('./util/error') const { spawnSync } = require('child_process') const obfuscate = require('./util/obfuscate/bitwarden-cli') @@ -6,9 +7,11 @@ const bwExecutable = path.resolve(__dirname, '../node_modules/.bin/bw') module.exports = (...args) => { const execCommand = `${bwExecutable} ${args.join(' ')}` console.debug('$', obfuscate(execCommand)) - const { stdout, status } = spawnSync(bwExecutable, args) - if (status !== 0) { - throw new Error(`bw: "${stdout.toString().trim()}"`) + const commandProcess = spawnSync(bwExecutable, args) + + if (commandProcess.status !== 0) { + throw new CommandError('bw command failed.', commandProcess) + } else { + return commandProcess.stdout.toString().trim() } - return stdout.toString().replace(/\n$/, '') } diff --git a/src/exec-dmenu.js b/src/exec-dmenu.js index c1faa7e..94bee74 100644 --- a/src/exec-dmenu.js +++ b/src/exec-dmenu.js @@ -1,27 +1,26 @@ -const { exec } = require('child_process') +const { spawn } = require('child_process') const dmenuPath = process.env.DMENU_PATH || 'dmenu' module.exports = (...args) => choices => new Promise((resolve, reject) => { - let choice = '' - const error = [] - - // Use a default of 'dmenu' if not specified in process.env - const execCommand = `${dmenuPath} ${args}` + const execCommand = `${dmenuPath} ${args.join(' ')}` console.debug('$', execCommand) - const dmenu = exec(execCommand) + + const dmenu = spawn(dmenuPath, args) dmenu.stdin.write(choices) dmenu.stdin.end() + let choice = '' + let stderr = '' dmenu.stdout.on('data', data => { choice += data }) dmenu.stderr.on('data', data => { - error.push(data) + stderr += data }) - dmenu.on('close', code => { - if (code !== 0) reject(Buffer.concat(error).toString()) - else resolve(choice.replace(/\n$/, '')) + dmenu.on('close', status => { + if (status !== 0) reject(new Error(stderr.trim())) + else resolve(choice.trim()) }) }) diff --git a/src/index.js b/src/index.js index 2e46096..9c85cf3 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,6 @@ const { existsSync, writeFileSync, readFileSync } = require('fs') const clipboardy = require('clipboardy') +const { CommandError } = require('./util/error') const dmenuRun = require('./exec-dmenu') const bwRun = require('./exec-bitwarden-cli') const obfuscate = require('./util/obfuscate/object') @@ -11,7 +12,29 @@ class BitwardenDmenu { } } -const loginIfNecessary = () => {} +const isLoggedIn = async () => { + try { + bwRun('login', '--check') + } catch (e) { + if (e instanceof CommandError && e.stdout === 'You are not logged in.') { + return false + } else { + throw e + } + } + return true +} +const login = async ({ dmenuArgs, dmenuPswdArgs }) => { + const email = await dmenuRun( + '-p', + 'You are logged out. Please provide your email:', + ...dmenuArgs + )('\n') + const password = await dmenuRun(...dmenuPswdArgs)('\n') + const session = bwRun('login', email, password, '--raw') + return session +} + // get a session token, either from existing sessionFile or by `bw unlock [password]` const getSessionVar = async ({ dmenuPswdArgs, saveSession, sessionFile }) => { if (saveSession) { @@ -27,7 +50,7 @@ const getSessionVar = async ({ dmenuPswdArgs, saveSession, sessionFile }) => { } else { console.debug('no session file found.') // prompt for password in dmenu - const password = await dmenuRun(`-p Password: -nf black -nb black ${dmenuPswdArgs}`)('\n') + const password = await dmenuRun(...dmenuPswdArgs)('\n') if (!password.length) throw new Error('no password given!') const session = bwRun('unlock', password, '--raw') writeFileSync(sessionFile, session) @@ -36,7 +59,7 @@ const getSessionVar = async ({ dmenuPswdArgs, saveSession, sessionFile }) => { } } else { // Why doesn't dmenuRun('...', dmenuPswdArgs)('\n') work here? - const password = await dmenuRun(`-p Password: -nf black -nb black ${dmenuPswdArgs}`)('\n') + const password = await dmenuRun(...dmenuPswdArgs)('\n') if (!password.length) throw new Error('no password given!') const session = bwRun('unlock', password, '--raw') return session @@ -68,7 +91,7 @@ const chooseAccount = async ({ dmenuArgs }, list) => { const loginList = list.filter(a => a.type === LOGIN_TYPE) const accountNames = loginList.map(a => `${a.name}: ${a.login.username}`) - const selected = await dmenuRun(dmenuArgs)(accountNames.join('\n')) + const selected = await dmenuRun(...dmenuArgs)(accountNames.join('\n')) const index = accountNames.indexOf(selected) // accountNames indexes match loginList indexes const selectedAccount = loginList[index] @@ -91,7 +114,7 @@ const chooseField = async ({ dmenuArgs }, selectedAccount) => { {} ) } - const field = await dmenuRun(dmenuArgs)(Object.keys(copyable).join('\n')) + const field = await dmenuRun(...dmenuArgs)(Object.keys(copyable).join('\n')) console.debug(`selected field '${field}'`) const valueToCopy = copyable[field] return valueToCopy @@ -100,7 +123,7 @@ const chooseField = async ({ dmenuArgs }, selectedAccount) => { module.exports = async args => { console.debug(`bitwarden-dmenu v${packageJson.version}`) - const session = await getSessionVar(args) + const session = (await isLoggedIn()) ? await getSessionVar(args) : await login(args) // bw sync if necessary syncIfNecessary(args, session) diff --git a/src/util/error.js b/src/util/error.js new file mode 100644 index 0000000..64a661f --- /dev/null +++ b/src/util/error.js @@ -0,0 +1,12 @@ +class CommandError extends Error { + constructor(message, { status, stderr, stdout }) { + super(message) + this.status = status + this.stdout = stdout.toString().trim() + this.stderr = stderr.toString().trim() + } +} + +module.exports = { + CommandError +} diff --git a/src/util/obfuscate/bitwarden-cli.js b/src/util/obfuscate/bitwarden-cli.js index f2e655f..0b50aaf 100644 --- a/src/util/obfuscate/bitwarden-cli.js +++ b/src/util/obfuscate/bitwarden-cli.js @@ -5,4 +5,5 @@ module.exports = command => ? command .replace(/unlock\s.*--raw$/, `unlock ****** --raw`) .replace(/session=.*/, 'session=******') + .replace(/login\s.*--raw/, 'login ****** ****** --raw') : command