dmenu for bitwarden-cli
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

133 lines
4.7 KiB

6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
  1. #!/usr/bin/env node
  2. const os = require('os')
  3. const path = require('path')
  4. const { exec } = require('child_process')
  5. const minimist = require('minimist')
  6. const packageJson = require('../package.json')
  7. const menu = require('../src')
  8. const obfuscateState = require('../src/util/obfuscate')
  9. const { scheduleCleanup } = require('../src/schedule-cleanup')
  10. const { CancelError, CommandError } = require('../src/util/error')
  11. const BW_LIST_ARGS_DEFAULT = ''
  12. const CACHE_PASSWORD_DEFAULT = 15
  13. const DMENU_ARGS_DEFAULT = ''
  14. const DMENU_PSWD_ARGS_DEFAULT = ''
  15. const SESSION_TIMEOUT_DEFAULT = 0
  16. const SYNC_VAULT_AFTER_DEFAULT = 0
  17. const STDOUT_DEFAULT = false
  18. const args = minimist(process.argv.slice(2))
  19. if (args.help) {
  20. console.log(
  21. `bitwarden-dmenu v${packageJson.version}
  22. Usage: bitwarden-dmenu [options]
  23. The DMENU_PATH environment variable can be used to point to an alternative dmenu implementation. Defaults to 'dmenu'.
  24. Options:
  25. --bw-list-args Arbitrary arguments to pass to bitwarden's 'list' command
  26. Defaults to nothing.
  27. --clear-clipboard Number of seconds to keep selected field in the clipboard.
  28. Defaults to ${CACHE_PASSWORD_DEFAULT}s.
  29. --dmenu-args Sets arbitrary arguments to pass to dmenu
  30. Defaults to nothing.
  31. --dmenu-pswd-args Sets arbitrary arguments to pass to the dmenu password prompt
  32. Defaults to nothing.
  33. --session-timeout Number of seconds after an unlock that the menu can be accessed
  34. without providing a password again. Defaults to ${SESSION_TIMEOUT_DEFAULT}s.
  35. --stdout Prints the password and username to stdout
  36. --sync-vault-after Number of seconds allowable between last bitwarden sync and
  37. current time. Defaults to ${SYNC_VAULT_AFTER_DEFAULT}s.
  38. --on-error Arbitrary command to run if the program fails. The thrown error
  39. is piped to the given command. Defaults to none.
  40. --stdout Prints the password and username to stdout
  41. --debug Show extra logs useful for debugging.
  42. --debug-unsafe Show debug logs WITHOUT obfuscating your sensitive info. Do not share!
  43. `
  44. )
  45. process.exit()
  46. }
  47. const bwListArgs = args['bw-list-args'] || BW_LIST_ARGS_DEFAULT
  48. const dmenuArgs = args['dmenu-args'] || DMENU_ARGS_DEFAULT
  49. const dmenuPswdArgs = args['dmenu-pswd-args'] || DMENU_PSWD_ARGS_DEFAULT
  50. const unsafeDebug = args['debug-unsafe']
  51. const sessionTimeout = args['session-timeout'] || SESSION_TIMEOUT_DEFAULT
  52. const syncVaultAfter = args['sync-vault-after'] || SYNC_VAULT_AFTER_DEFAULT
  53. const onErrorCommand = args['on-error']
  54. const stdout = args['stdout'] || STDOUT_DEFAULT
  55. // prevent clipboard clearing from locking up process when printing to stdout
  56. const clearClipboardAfter = stdout ? 0 : args['clear-clipboard'] || CACHE_PASSWORD_DEFAULT
  57. console.debug =
  58. args['debug'] || args['debug-unsafe'] ? (...msgs) => console.log(...msgs, '\n') : () => {}
  59. console.info = args['stdout'] ? () => {} : console.info
  60. if (args['debug-unsafe']) obfuscateState.turnOff()
  61. const oldestAllowedVaultSync = syncVaultAfter
  62. const saveSession = Boolean(sessionTimeout)
  63. const sessionFile = path.resolve(os.tmpdir(), 'bitwarden-session.txt')
  64. const dmenuArgsParsed = dmenuArgs ? dmenuArgs.split(/\s+/) : []
  65. const dmenuPswdArgsParsed = ['-p', 'Password:', '-nf', 'black', '-nb', 'black'].concat(
  66. dmenuPswdArgs ? dmenuPswdArgs.split(/\s+/) : []
  67. )
  68. const pipeErrorToCommand = (command, e) =>
  69. new Promise((resolve, reject) => {
  70. const errorCommand = exec(onErrorCommand)
  71. console.log(e)
  72. if (e instanceof CommandError) errorCommand.stdin.write(`'${e.errorMessage}'`)
  73. else errorCommand.stdin.write(`'${e.toString()}'`)
  74. errorCommand.stdin.end()
  75. errorCommand.on('close', status => {
  76. if (status === 0) resolve()
  77. else reject()
  78. })
  79. })
  80. async function runBitwardenDmenu() {
  81. try {
  82. await menu({
  83. bwListArgs,
  84. dmenuArgs: dmenuArgsParsed,
  85. dmenuPswdArgs: dmenuPswdArgsParsed,
  86. saveSession,
  87. sessionFile,
  88. stdout,
  89. oldestAllowedVaultSync,
  90. unsafeDebug
  91. })
  92. return { lockBitwardenAfter: sessionTimeout, clearClipboardAfter }
  93. } catch (e) {
  94. if (e instanceof CancelError) {
  95. console.debug('cancelled bitwarden-dmenu early.')
  96. return { lockBitwardenAfter: sessionTimeout, clearClipboardAfter: null }
  97. } else if (onErrorCommand) await pipeErrorToCommand(onErrorCommand, e)
  98. else {
  99. console.error('an error occurred:')
  100. console.error(e)
  101. }
  102. return { lockBitwardenAfter: 0, clearClipboardAfter: 0 }
  103. }
  104. }
  105. ;(async () => {
  106. const { lockBitwardenAfter, clearClipboardAfter } = await runBitwardenDmenu()
  107. await scheduleCleanup({
  108. lockBitwardenAfter,
  109. clearClipboardAfter,
  110. sessionFile,
  111. stdout
  112. })
  113. })()