Browse Source

add login flow

master
Andrew Kaiser 6 years ago
parent
commit
ca6a60dafe
7 changed files with 76 additions and 37 deletions
  1. +2
    -3
      README.md
  2. +15
    -13
      bin/cli.js
  3. +7
    -4
      src/exec-bitwarden-cli.js
  4. +10
    -11
      src/exec-dmenu.js
  5. +29
    -6
      src/index.js
  6. +12
    -0
      src/util/error.js
  7. +1
    -0
      src/util/obfuscate/bitwarden-cli.js

+ 2
- 3
README.md View File

@ -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
```


+ 15
- 13
bin/cli.js View File

@ -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()
}
})
})

+ 7
- 4
src/exec-bitwarden-cli.js View File

@ -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$/, '')
}

+ 10
- 11
src/exec-dmenu.js View File

@ -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())
})
})

+ 29
- 6
src/index.js View File

@ -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)


+ 12
- 0
src/util/error.js View File

@ -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
}

+ 1
- 0
src/util/obfuscate/bitwarden-cli.js View File

@ -5,4 +5,5 @@ module.exports = command =>
? command
.replace(/unlock\s.*--raw$/, `unlock ****** --raw`)
.replace(/session=.*/, 'session=******')
.replace(/login\s.*--raw/, 'login ****** ****** --raw')
: command

Loading…
Cancel
Save