From f4967e7ecd5c3e5598eb8285c0389567bdf321c7 Mon Sep 17 00:00:00 2001 From: Andrew Kaiser Date: Wed, 12 Sep 2018 16:19:20 -0400 Subject: [PATCH] initial commit --- .gitignore | 1 + .prettierrc | 4 + .tern-project | 12 + LICENSE | 21 ++ README.md | 31 +++ bin/cli.js | 63 +++++ package-lock.json | 472 ++++++++++++++++++++++++++++++++++++++ package.json | 27 +++ src/exec-bitwarden-cli.js | 12 + src/exec-dmenu.js | 22 ++ src/index.js | 75 ++++++ src/schedule-cleanup.js | 29 +++ 12 files changed, 769 insertions(+) create mode 100644 .gitignore create mode 100644 .prettierrc create mode 100755 .tern-project create mode 100644 LICENSE create mode 100644 README.md create mode 100755 bin/cli.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/exec-bitwarden-cli.js create mode 100644 src/exec-dmenu.js create mode 100644 src/index.js create mode 100644 src/schedule-cleanup.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..b2095be --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "semi": false, + "singleQuote": true +} diff --git a/.tern-project b/.tern-project new file mode 100755 index 0000000..3c3fd94 --- /dev/null +++ b/.tern-project @@ -0,0 +1,12 @@ +{ + "libs": [ + "ecma5", + "ecma6", + "browser" + ], + "loadEagerly": [ + ], + "plugins": { + "node": {} + } +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bd49d13 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Andrew Kaiser + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..62786d0 --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +# bitwarden-dmenu + +[bitwarden](https://bitwarden.com/) client using dmenu + +## Depends on +- [dmenu](https://tools.suckless.org/dmenu/) +- [bitwarden-cli](https://help.bitwarden.com/article/cli/) + +## Installation +```bash +# login with bitwarden-cli once before using bitwarden-dmenu +bw login +# install the cli +npm i -g bitwarden-dmenu +``` + +## Usage +``` +$ bitwarden-dmenu --help +Usage: bitwarden-dmenu [options] + +Options: + --clear-clipboard Number of seconds to keep selected field in the clipboard. + Defaults to 15s. + --session-timeout Number of seconds after an unlock that the menu can be accessed + without providing a password again. Defaults to 0s. + --sync-vault-after Number of seconds allowable between last bitwarden sync and + current time. Defaults to 0s. + --on-error Arbitrary command to run if the program fails. Defaults to none. + +``` diff --git a/bin/cli.js b/bin/cli.js new file mode 100755 index 0000000..d32bb76 --- /dev/null +++ b/bin/cli.js @@ -0,0 +1,63 @@ +#!/usr/bin/env node + +const os = require('os') +const path = require('path') +const { exec } = require('child_process') +const minimist = require('minimist') +const menu = require('../src/index') +const scheduleCleanup = require('../src/schedule-cleanup') + +const cachePasswordDefault = 15 +const sessionTimeoutDefault = 0 +const syncVaultAfterDefault = 0 +const args = minimist(process.argv.slice(2)) +if (args.help) { + console.log( + `Usage: bitwarden-dmenu [options] + +Options: + --clear-clipboard Number of seconds to keep selected field in the clipboard. + Defaults to ${cachePasswordDefault}s. + --session-timeout Number of seconds after an unlock that the menu can be accessed + without providing a password again. Defaults to ${sessionTimeoutDefault}s. + --sync-vault-after Number of seconds allowable between last bitwarden sync and + current time. Defaults to ${syncVaultAfterDefault}s. + --on-error Arbitrary command to run if the program fails. Defaults to none. +` + ) + process.exit() +} + +const clearClipboardAfter = args['clear-clipboard'] || cachePasswordDefault +const sessionTimeout = args['session-timeout'] || sessionTimeoutDefault +const syncVaultAfter = args['sync-vault-after'] || syncVaultAfterDefault +const onErrorCommand = args['on-error'] + +const oldestAllowedVaultSync = syncVaultAfter +const saveSession = Boolean(sessionTimeout) +const sessionFile = path.resolve(os.tmpdir(), 'bitwarden-session.txt') + +menu({ saveSession, sessionFile, oldestAllowedVaultSync }) + .then(() => + scheduleCleanup({ + lockBitwardenAfter: sessionTimeout, + clearClipboardAfter, + sessionFile + }) + ) + .catch(e => { + console.log(e) + // if something goes wrong, immediately clear the clipboard & lock bitwarden, + // then run error command + scheduleCleanup({ + lockBitwardenAfter: 0, + clearClipboardAfter: 0, + sessionFile + }).then(() => { + if (onErrorCommand) { + const errorCommand = exec(onErrorCommand) + errorCommand.stdin.write(`'${e.toString()}'`) + errorCommand.stdin.end() + } + }) + }) diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..3ddab62 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,472 @@ +{ + "name": "bitwarden-dmenu", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@bitwarden/cli": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@bitwarden/cli/-/cli-1.3.0.tgz", + "integrity": "sha512-zXJfkSGsvk0MH+Ik150htYqmW4vEPrvuEaV4NGsHzYhoqyu4Hfk7Vf4w2soKmT3LF8Cj7eMiGnDo/G852edCBw==", + "requires": { + "chalk": "2.4.1", + "commander": "2.15.1", + "form-data": "2.3.2", + "inquirer": "5.2.0", + "lowdb": "1.0.0", + "lunr": "2.3.1", + "node-fetch": "2.1.2", + "node-forge": "0.7.1", + "papaparse": "4.3.5" + } + }, + "ansi-escapes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.1.0.tgz", + "integrity": "sha512-UgAb8H9D41AQnu/PbWlCofQVcnV4Gs2bBJi9eZPxfU/hgglFh3SMDMENRIqdr7H6XFnXdoknctFByVsCOotTVw==" + }, + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "arch": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/arch/-/arch-2.1.1.tgz", + "integrity": "sha512-BLM56aPo9vLLFVa8+/+pJLnrZ7QGGTVHWsCwieAWT9o9K8UeGaQbzZbGoabWLOo2ksBCztoXdqBZBplqLDDCSg==" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "chalk": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "chardet": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz", + "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=" + }, + "cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "requires": { + "restore-cursor": "^2.0.0" + } + }, + "cli-width": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", + "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=" + }, + "clipboardy": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-1.2.3.tgz", + "integrity": "sha512-2WNImOvCRe6r63Gk9pShfkwXsVtKCroMAevIbiae021mS850UkWPbevxsBz3tnvjZIEGvlwaqCPsw+4ulzNgJA==", + "requires": { + "arch": "^2.1.0", + "execa": "^0.8.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "combined-stream": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", + "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "2.15.1", + "resolved": "http://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==" + }, + "cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "requires": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "execa": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.8.0.tgz", + "integrity": "sha1-2NdrvBtVIX7RkP1t1J08d07PyNo=", + "requires": { + "cross-spawn": "^5.0.1", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "external-editor": { + "version": "2.2.0", + "resolved": "http://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz", + "integrity": "sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==", + "requires": { + "chardet": "^0.4.0", + "iconv-lite": "^0.4.17", + "tmp": "^0.0.33" + } + }, + "figures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", + "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "form-data": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", + "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "1.0.6", + "mime-types": "^2.1.12" + } + }, + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" + }, + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "inquirer": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-5.2.0.tgz", + "integrity": "sha512-E9BmnJbAKLPGonz0HeWHtbKf+EeSP93paWO3ZYoUpq/aowXvYGjjCSuashhXPpzbArIjBbji39THkxTz9ZeEUQ==", + "requires": { + "ansi-escapes": "^3.0.0", + "chalk": "^2.0.0", + "cli-cursor": "^2.1.0", + "cli-width": "^2.0.0", + "external-editor": "^2.1.0", + "figures": "^2.0.0", + "lodash": "^4.3.0", + "mute-stream": "0.0.7", + "run-async": "^2.2.0", + "rxjs": "^5.5.2", + "string-width": "^2.1.0", + "strip-ansi": "^4.0.0", + "through": "^2.3.6" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + }, + "is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", + "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, + "lodash": { + "version": "4.17.10", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", + "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==" + }, + "lowdb": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lowdb/-/lowdb-1.0.0.tgz", + "integrity": "sha512-2+x8esE/Wb9SQ1F9IHaYWfsC9FIecLOPrK4g17FGEayjUWH172H6nwicRovGvSE2CPZouc2MCIqCI7h9d+GftQ==", + "requires": { + "graceful-fs": "^4.1.3", + "is-promise": "^2.1.0", + "lodash": "4", + "pify": "^3.0.0", + "steno": "^0.4.1" + } + }, + "lru-cache": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.3.tgz", + "integrity": "sha512-fFEhvcgzuIoJVUF8fYr5KR0YqxD238zgObTps31YdADwPPAp82a4M8TrckkWyx7ekNlf9aBcVn81cFwwXngrJA==", + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "lunr": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.1.tgz", + "integrity": "sha1-ETYWorYC3cEJMqe/ik5uV+v+zfI=" + }, + "mime-db": { + "version": "1.36.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.36.0.tgz", + "integrity": "sha512-L+xvyD9MkoYMXb1jAmzI/lWYAxAMCPvIBSWur0PZ5nOf5euahRLVqH//FKW9mWp2lkqUgYiXPgkzfMUFi4zVDw==" + }, + "mime-types": { + "version": "2.1.20", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.20.tgz", + "integrity": "sha512-HrkrPaP9vGuWbLK1B1FfgAkbqNjIuy4eHlIYnFi7kamZyLLrGlo2mpcx0bBmNpKqBtYtAfGbodDddIgddSJC2A==", + "requires": { + "mime-db": "~1.36.0" + } + }, + "mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==" + }, + "minimist": { + "version": "1.2.0", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + }, + "mute-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", + "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=" + }, + "node-fetch": { + "version": "2.1.2", + "resolved": "http://registry.npmjs.org/node-fetch/-/node-fetch-2.1.2.tgz", + "integrity": "sha1-q4hOjn5X44qUR1POxwb3iNF2i7U=" + }, + "node-forge": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.1.tgz", + "integrity": "sha1-naYR6giYL0uUIGs760zJZl8gwwA=" + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "requires": { + "path-key": "^2.0.0" + } + }, + "onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "requires": { + "mimic-fn": "^1.0.0" + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" + }, + "papaparse": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-4.3.5.tgz", + "integrity": "sha1-ts31yub+nsYDsb5m8RSmOsZFoDY=" + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" + }, + "restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "requires": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + } + }, + "run-async": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", + "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", + "requires": { + "is-promise": "^2.1.0" + } + }, + "rxjs": { + "version": "5.5.12", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.12.tgz", + "integrity": "sha512-xx2itnL5sBbqeeiVgNPVuQQ1nC8Jp2WfNJhXWHmElW9YmrpS9UVnNzhP3EH3HFqexO5Tlp8GhYY+WEcqcVMvGw==", + "requires": { + "symbol-observable": "1.0.1" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + }, + "steno": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/steno/-/steno-0.4.4.tgz", + "integrity": "sha1-BxEFvfwobmYVwEA8J+nXtdy4Vcs=", + "requires": { + "graceful-fs": "^4.1.3" + } + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "^3.0.0" + } + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + }, + "symbol-observable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.1.tgz", + "integrity": "sha1-g0D8RwLDEi310iKI+IKD9RPT/dQ=" + }, + "through": { + "version": "2.3.8", + "resolved": "http://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" + }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "requires": { + "os-tmpdir": "~1.0.2" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "requires": { + "isexe": "^2.0.0" + } + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..a1fd472 --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "name": "bitwarden-dmenu", + "version": "1.0.0", + "description": "", + "keywords": [ + "bitwarden", + "cli", + "dmenu" + ], + "license": "MIT", + "author": "Andrew Kaiser", + "main": "index.js", + "bin": { + "bitwarden-dmenu": "bin/cli.js" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "dependencies": { + "@bitwarden/cli": "^1.3.0", + "clipboardy": "^1.2.3", + "minimist": "^1.2.0" + }, + "engines": { + "node": ">=8" + } +} diff --git a/src/exec-bitwarden-cli.js b/src/exec-bitwarden-cli.js new file mode 100644 index 0000000..6afa35f --- /dev/null +++ b/src/exec-bitwarden-cli.js @@ -0,0 +1,12 @@ +const path = require('path') +const { execSync } = require('child_process') + +const bwExecutable = path.resolve(__dirname, '../node_modules/.bin/bw') +module.exports = args => { + try { + const stdout = execSync(`${bwExecutable} ${args}`) + return stdout.toString().replace(/\n$/, '') + } catch (e) { + throw new Error(e.stdout.toString().trim()) + } +} diff --git a/src/exec-dmenu.js b/src/exec-dmenu.js new file mode 100644 index 0000000..85602cc --- /dev/null +++ b/src/exec-dmenu.js @@ -0,0 +1,22 @@ +const { exec, execSync, spawn } = require('child_process') + +module.exports = (choices = '\n', args = '') => + new Promise((resolve, reject) => { + let choice = '' + let error = [] + + const dmenu = exec(`dmenu ${args}`) + dmenu.stdin.write(choices) + dmenu.stdin.end() + + dmenu.stdout.on('data', data => { + choice += data + }) + dmenu.stderr.on('data', data => { + error.push(data) + }) + dmenu.on('close', code => { + if (code !== 0) reject(Buffer.concat(error).toString()) + else resolve(choice.replace(/\n$/, '')) + }) + }) diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..fe0a8ac --- /dev/null +++ b/src/index.js @@ -0,0 +1,75 @@ +const { existsSync, writeFileSync, readFileSync } = require('fs') +const dmenuRun = require('./exec-dmenu') +const bwRun = require('./exec-bitwarden-cli') +const clipboardy = require('clipboardy') + +const getSessionVar = async ({ saveSession, sessionFile }) => { + if (saveSession) { + const sessionFileExists = existsSync(sessionFile) + + if (sessionFileExists) { + const session = readFileSync(sessionFile) + .toString() + .replace(/\n$/, '') + return session + } else { + // 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) + return session + } + } else { + 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`) + return session + } +} + +module.exports = async ({ + saveSession, + sessionFile, + oldestAllowedVaultSync +}) => { + const session = await getSessionVar({ saveSession, sessionFile }) + console.log({ session }) + + // bw sync if necessary + const last = bwRun(`sync --last --session=${session}`) + const timeSinceSync = (new Date().getTime() - new Date(last).getTime()) / 1000 + if (timeSinceSync > oldestAllowedVaultSync) { + bwRun(`sync --session=${session}`) + } + console.log('synced') + + // bw list + const listStr = bwRun(`list items --session=${session}`) + const list = JSON.parse(listStr) + const accountNames = list.map(a => `${a.name}: ${a.login.username}`) + + // choose account in dmenu + const selected = await dmenuRun(accountNames.join('\n')) + const index = accountNames.indexOf(selected) + const selectedAccount = list[index] + + // choose field to copy in dmenu + const copyable = { + password: selectedAccount.login.password, + username: selectedAccount.login.username, + notes: selectedAccount.notes, + ...(selectedAccount.fields || []).reduce( + (acc, f) => ({ + ...acc, + ['custom.' + f.name]: f.value + }), + {} + ) + } + const field = await dmenuRun(Object.keys(copyable).join('\n')) + const valueToCopy = copyable[field] + + // copy to clipboard + clipboardy.writeSync(valueToCopy) +} diff --git a/src/schedule-cleanup.js b/src/schedule-cleanup.js new file mode 100644 index 0000000..b9316c7 --- /dev/null +++ b/src/schedule-cleanup.js @@ -0,0 +1,29 @@ +const fs = require('fs') +const bwRun = require('./exec-bitwarden-cli') +const clipboardy = require('clipboardy') + +const timeout = n => new Promise(resolve => setTimeout(resolve, n)) + +/* + * timeouts in seconds + * + */ + +module.exports = ({ lockBitwardenAfter, clearClipboardAfter, sessionFile }) => + Promise.all([ + timeout(lockBitwardenAfter * 1000).then(() => { + try { + fs.unlinkSync(sessionFile) + console.log(`${sessionFile} removed.`) + } catch (e) { + if (e.code !== 'ENOENT') throw e + console.log(`${sessionFile} already removed.`) + } + bwRun('lock') + console.log('bitwarden is locked.') + }), + timeout(clearClipboardAfter * 1000).then(() => { + clipboardy.writeSync('') + console.log('clipboard is cleared.') + }) + ])