Browse Source

add verbose flag, distinguish debug logs from info logs.

- 'obfuscate' sensitive information away from debug logs
- bump version number
master
Andrew Kaiser 6 years ago
parent
commit
0bbf19c550
9 changed files with 115 additions and 28 deletions
  1. +7
    -2
      README.md
  2. +5
    -0
      bin/cli.js
  3. +2
    -2
      package.json
  4. +4
    -1
      src/exec-bitwarden-cli.js
  5. +4
    -2
      src/exec-dmenu.js
  6. +46
    -15
      src/index.js
  7. +8
    -6
      src/schedule-cleanup.js
  8. +4
    -0
      src/util/obfuscate/bitwarden-cli.js
  9. +35
    -0
      src/util/obfuscate/object.js

+ 7
- 2
README.md View File

@ -1,14 +1,14 @@
# bitwarden-dmenu
[![npm](https://img.shields.io/npm/v/bitwarden-dmenu.svg)](https://www.npmjs.com/package/bitwarden-dmenu)
[![node](https://img.shields.io/node/v/bitwarden-dmenu.svg)](http://npmjs.com/package/bitwarden-dmenu)
[![GitHub](https://img.shields.io/github/license/andykais/bitwarden-dmenu.svg)](https://github.com/andykais/bitwarden-dmenu/blob/master/LICENSE)
dmenu for [bitwarden](https://bitwarden.com/) which can copy usernames, passwords, and various fields to your
clipboard.
## Usage
```
$ bitwarden-dmenu --help
Usage: bitwarden-dmenu [options]
@ -22,7 +22,10 @@ Options:
current time. Defaults to 0s.
--on-error Arbitrary command to run if the program fails. The thrown error
is piped to the given command. Defaults to none.
--verbose Show extra logs useful for debugging.
```
By default, this program runs at its most secure. No session is stored for any time period, the vault is updated
every time it is used, and the clipboard is cleared every 15 seconds. In reality, you may want something a
little more lenient. Here is the command I use in my personal i3wm config.
@ -32,6 +35,7 @@ bitwarden-dmenu --clear-clipboard 30 --session-timeout 100 --sync-vault-after 36
```
## Installation
```bash
# login with bitwarden-cli once before using bitwarden-dmenu
bw login
@ -40,6 +44,7 @@ npm i -g bitwarden-dmenu
```
## Depends on
- [dmenu](https://tools.suckless.org/dmenu/)
- [bitwarden-cli](https://help.bitwarden.com/article/cli/)


+ 5
- 0
bin/cli.js View File

@ -24,6 +24,8 @@ Options:
current time. Defaults to ${syncVaultAfterDefault}s.
--on-error Arbitrary command to run if the program fails. The thrown error
is piped to the given command. Defaults to none.
--verbose Show extra logs useful for debugging.
`
)
process.exit()
@ -33,6 +35,9 @@ const clearClipboardAfter = args['clear-clipboard'] || cachePasswordDefault
const sessionTimeout = args['session-timeout'] || sessionTimeoutDefault
const syncVaultAfter = args['sync-vault-after'] || syncVaultAfterDefault
const onErrorCommand = args['on-error']
console.debug = args['verbose']
? (...msgs) => console.log(...msgs, '\n')
: () => {}
const oldestAllowedVaultSync = syncVaultAfter
const saveSession = Boolean(sessionTimeout)


+ 2
- 2
package.json View File

@ -1,6 +1,6 @@
{
"name": "bitwarden-dmenu",
"version": "1.0.1",
"version": "1.1.0",
"description": "",
"keywords": [
"bitwarden",
@ -13,7 +13,7 @@
},
"license": "MIT",
"author": "Andrew Kaiser",
"main": "index.js",
"main": "src/index.js",
"bin": {
"bitwarden-dmenu": "bin/cli.js"
},


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

@ -1,10 +1,13 @@
const path = require('path')
const { execSync } = require('child_process')
const obfuscate = require('./util/obfuscate/bitwarden-cli')
const bwExecutable = path.resolve(__dirname, '../node_modules/.bin/bw')
module.exports = args => {
const execCommand = `${bwExecutable} ${args}`
console.debug('$', obfuscate(execCommand))
try {
const stdout = execSync(`${bwExecutable} ${args}`)
const stdout = execSync(execCommand)
return stdout.toString().replace(/\n$/, '')
} catch (e) {
throw new Error(e.stdout.toString().trim())


+ 4
- 2
src/exec-dmenu.js View File

@ -3,9 +3,11 @@ const { exec, execSync, spawn } = require('child_process')
module.exports = (choices = '\n', args = '') =>
new Promise((resolve, reject) => {
let choice = ''
let error = []
const error = []
const dmenu = exec(`dmenu ${args}`)
const execCommand = `dmenu ${args}`
console.debug('$', execCommand)
const dmenu = exec(execCommand)
dmenu.stdin.write(choices)
dmenu.stdin.end()


+ 46
- 15
src/index.js View File

@ -1,23 +1,29 @@
const { existsSync, writeFileSync, readFileSync } = require('fs')
const clipboardy = require('clipboardy')
const dmenuRun = require('./exec-dmenu')
const bwRun = require('./exec-bitwarden-cli')
const clipboardy = require('clipboardy')
const obfuscate = require('./util/obfuscate/object')
// get a session token, either from existing sessionFile or by `bw unlock [password]`
const getSessionVar = async ({ saveSession, sessionFile }) => {
if (saveSession) {
console.debug(`checking for session file at ${sessionFile}`)
const sessionFileExists = existsSync(sessionFile)
if (sessionFileExists) {
const session = readFileSync(sessionFile)
.toString()
.replace(/\n$/, '')
console.debug('read existing session file.')
return session
} else {
console.debug('no session file found.')
// prompt for password in dmenu
const password = await dmenuRun('\n', '-p Password: -nf black -nb black')
if (!password) throw new Error('no password given!')
const session = bwRun(`unlock '${password}' --raw`)
writeFileSync(sessionFile, session)
console.debug('saved new session file.')
return session
}
} else {
@ -28,33 +34,37 @@ const getSessionVar = async ({ saveSession, sessionFile }) => {
}
}
module.exports = async ({
saveSession,
sessionFile,
oldestAllowedVaultSync
}) => {
const session = await getSessionVar({ saveSession, sessionFile })
console.log({ session })
// bw sync if necessary
// sync the password accounts with the remote server
// if --sync-vault-after < time since the last sync
const syncIfNecessary = ({ session, oldestAllowedVaultSync }) => {
const last = bwRun(`sync --last --session=${session}`)
const timeSinceSync = (new Date().getTime() - new Date(last).getTime()) / 1000
if (timeSinceSync > oldestAllowedVaultSync) {
console.debug('syncing vault...')
bwRun(`sync --session=${session}`)
console.debug(`sync complete, last sync was ${last}`)
}
console.log('synced')
}
// bw list
// get the list all password accounts in the vault
const getAccounts = ({ session }) => {
const listStr = bwRun(`list items --session=${session}`)
const list = JSON.parse(listStr)
const accountNames = list.map(a => `${a.name}: ${a.login.username}`)
return list
}
// choose account in dmenu
// choose one account with dmenu
const chooseAccount = async ({ list }) => {
const accountNames = list.map(a => `${a.name}: ${a.login.username}`)
const selected = await dmenuRun(accountNames.join('\n'))
const index = accountNames.indexOf(selected)
const selectedAccount = list[index]
console.debug('selected account:\n', obfuscate(selectedAccount))
return selectedAccount
}
// choose field to copy in dmenu
// choose one field with dmenu
const chooseField = async ({ selectedAccount }) => {
const copyable = {
password: selectedAccount.login.password,
username: selectedAccount.login.username,
@ -69,6 +79,27 @@ module.exports = async ({
}
const field = await dmenuRun(Object.keys(copyable).join('\n'))
const valueToCopy = copyable[field]
return valueToCopy
}
module.exports = async ({
saveSession,
sessionFile,
oldestAllowedVaultSync
}) => {
const session = await getSessionVar({ saveSession, sessionFile })
// bw sync if necessary
syncIfNecessary({ session, oldestAllowedVaultSync })
// bw list
const list = getAccounts({ session })
// choose account in dmenu
const selectedAccount = await chooseAccount({ list })
// choose field to copy in dmenu
const valueToCopy = await chooseField({ selectedAccount })
// copy to clipboard
clipboardy.writeSync(valueToCopy)


+ 8
- 6
src/schedule-cleanup.js View File

@ -9,21 +9,23 @@ const timeout = n => new Promise(resolve => setTimeout(resolve, n))
*
*/
module.exports = ({ lockBitwardenAfter, clearClipboardAfter, sessionFile }) =>
Promise.all([
module.exports = ({ lockBitwardenAfter, clearClipboardAfter, sessionFile }) => {
console.debug('begin cleanup')
return Promise.all([
timeout(lockBitwardenAfter * 1000).then(() => {
try {
fs.unlinkSync(sessionFile)
console.log(`${sessionFile} removed.`)
console.debug(`${sessionFile} removed.`)
} catch (e) {
if (e.code !== 'ENOENT') throw e
console.log(`${sessionFile} already removed.`)
console.debug(`${sessionFile} already removed.`)
}
bwRun('lock')
console.log('bitwarden is locked.')
console.info('bitwarden is locked.')
}),
timeout(clearClipboardAfter * 1000).then(() => {
clipboardy.writeSync('')
console.log('clipboard is cleared.')
console.info('clipboard is cleared.')
})
])
}

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

@ -0,0 +1,4 @@
module.exports = command =>
command
.replace(/unlock\s'.*'/, `unlock '******'`)
.replace(/session=.*/, 'session=****** ')

+ 35
- 0
src/util/obfuscate/object.js View File

@ -0,0 +1,35 @@
const isArray = val => Array.isArray(val)
const isObject = val => val !== null && typeof val === 'object'
const traverseObject = applyFunc => any => {
if (isObject(any)) {
for (const key in any) {
const val = any[key]
any[key] = traverseObject(applyFunc)(val)
}
return any
} else if (isArray(any)) {
for (const i of any.keys()) {
const val = any[i]
any[i] = traverseObject(applyFunc)(val)
}
return any
} else {
return applyFunc(any)
}
}
const replaceNonNullValues = traverseObject(v => {
if (v === null || v === undefined) {
return v
} else {
return '******'
}
})
module.exports = object => {
const copied = JSON.parse(JSON.stringify(object))
const obfuscatedObject = replaceNonNullValues(copied)
return JSON.stringify(obfuscatedObject, null, 2)
}

Loading…
Cancel
Save