bitwarden.el 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. ;;; bitwarden.el --- Bitwarden command wrapper -*- lexical-binding: t -*-
  2. ;; Copyright (C) 2018 Sean Farley
  3. ;; Author: Sean Farley
  4. ;; URL: https://github.com/seanfarley/emacs-bitwarden
  5. ;; Version: 0.1.0
  6. ;; Created: 2018-09-04
  7. ;; Package-Requires: ((emacs "24.4"))
  8. ;; Keywords: extensions processes bw bitwarden
  9. ;;; License
  10. ;; This program is free software: you can redistribute it and/or modify
  11. ;; it under the terms of the GNU General Public License as published by
  12. ;; the Free Software Foundation, either version 3 of the License, or
  13. ;; (at your option) any later version.
  14. ;; This program is distributed in the hope that it will be useful,
  15. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. ;; GNU General Public License for more details.
  18. ;; You should have received a copy of the GNU General Public License
  19. ;; along with this program. If not, see <http://www.gnu.org/licenses/>.
  20. ;;; Commentary:
  21. ;; This package wraps the bitwarden command-line program.
  22. ;;; Code:
  23. (require 'json)
  24. (defcustom bitwarden-bw-executable (executable-find "bw")
  25. "The bw cli executable used by Bitwarden."
  26. :group 'bitwarden
  27. :type 'string)
  28. (defcustom bitwarden-data-file
  29. (expand-file-name "~/Library/Application Support/Bitwarden CLI/data.json")
  30. "The bw cli executable used by Bitwarden."
  31. :group 'bitwarden
  32. :type 'string)
  33. (defcustom bitwarden-user nil
  34. "Bitwarden user e-mail."
  35. :group 'bitwarden
  36. :type 'string)
  37. (defcustom bitwarden-automatic-unlock nil
  38. "Optional function to be called to attempt to unlock the vault.
  39. Set this to a lamdba that will evaluate to a password. For
  40. example, this can be the :secret plist from
  41. `auth-source-search'."
  42. :group 'bitwarden
  43. :type 'function)
  44. (defun bitwarden-logged-in-p ()
  45. "Check if `bitwarden-user' is logged in.
  46. Returns nil if not logged in."
  47. (let* ((json-object-type 'hash-table)
  48. (json-key-type 'string)
  49. (json (json-read-file bitwarden-data-file)))
  50. (gethash "__PROTECTED__key" json)))
  51. (defun bitwarden-unlocked-p ()
  52. "Check if we have already set the 'BW_SESSION' environment variable."
  53. (and (bitwarden-logged-in-p) (getenv "BW_SESSION")))
  54. (defun bitwarden--raw-runcmd (cmd &rest args)
  55. "Run bw command CMD with ARGS.
  56. Returns a list with the first element being the exit code and the
  57. second element being the output."
  58. (with-temp-buffer
  59. (list (apply 'call-process
  60. bitwarden-bw-executable
  61. nil (current-buffer) nil
  62. (cons cmd args))
  63. (replace-regexp-in-string "\n$" ""
  64. (buffer-string)))))
  65. (defun bitwarden-runcmd (cmd &rest args)
  66. "Run bw command CMD with ARGS.
  67. This is a wrapper for `bitwarden--raw-runcmd' that also checks
  68. for common errors."
  69. (if (bitwarden-logged-in-p)
  70. (if (bitwarden-unlocked-p)
  71. (let* ((ret (apply #'bitwarden--raw-runcmd cmd args))
  72. (exit-code (nth 0 ret))
  73. (output (nth 1 ret)))
  74. (if (eq exit-code 0)
  75. output
  76. (cond ((string-match "^More than one result was found." output)
  77. (message "Bitwarden: more than one result found")
  78. nil)
  79. (t
  80. (message "Bitwarden: unknown error: %s" output)
  81. nil))))
  82. (message "Bitwarden: vault is locked")
  83. nil)
  84. (message "Bitwarden: you are not logged in")
  85. nil))
  86. (defun bitwarden--login-proc-filter (proc string)
  87. "Interacts with PROC by sending line-by-line STRING."
  88. ;; read username if not defined
  89. (when (string-match "^? Email address:" string)
  90. (let ((user (read-string "Bitwarden email: ")))
  91. ;; if we are here then the user forgot to fill in this field so let's do
  92. ;; that now
  93. (setq bitwarden-user user)
  94. (process-send-string proc (concat bitwarden-user "\n"))))
  95. ;; read master password
  96. (when (string-match "^? Master password:" string)
  97. (process-send-string
  98. proc (concat (read-passwd "Bitwarden master password: ") "\n")))
  99. ;; check for bad password
  100. (when (string-match "^Username or password is incorrect" string)
  101. (message "Bitwarden: incorrect master password"))
  102. ;; if trying to unlock, check if logged in
  103. (when (string-match "^You are not logged in" string)
  104. (message "Bitwarden: cannot unlock: not logged in"))
  105. ;; read the 2fa code
  106. (when (string-match "^? Two-step login code:" string)
  107. (process-send-string
  108. proc (concat (read-passwd "Bitwarden two-step login code: ") "\n")))
  109. ;; check for bad code
  110. (when (string-match "^Login failed" string)
  111. (message "Bitwarden: incorrect two-step code"))
  112. ;; check for already logged in
  113. (when (string-match "^You are already logged in" string)
  114. (string-match "You are already logged in as \\(.*\\)\\." string)
  115. (message "Bitwarden: already logged in as %s" (match-string 1 string)))
  116. ;; success! now save the BW_SESSION into the environment so spawned processes
  117. ;; inherit it
  118. (when (string-match "^\\(You are logged in\\|Your vault is now unlocked\\)"
  119. string)
  120. ;; set the session env variable so spawned processes inherit
  121. (string-match "export BW_SESSION=\"\\(.*\\)\"" string)
  122. (setenv "BW_SESSION" (match-string 1 string))
  123. (message
  124. (concat "Bitwarden: successfully logged in as " bitwarden-user))))
  125. (defun bitwarden--raw-unlock (cmd)
  126. "Raw CMD to either unlock a vault or login.
  127. The only difference between unlock and login is just the name of
  128. the command and whether to pass the user."
  129. (when (get-process "bitwarden")
  130. (delete-process "bitwarden"))
  131. (let ((process (start-process-shell-command
  132. "bitwarden"
  133. nil ; don't use a buffer
  134. (concat bitwarden-bw-executable " " cmd))))
  135. (set-process-filter process #'bitwarden--login-proc-filter)))
  136. (defun bitwarden-unlock ()
  137. "Unlock bitwarden vault.
  138. It is not sufficient to check the env variable for BW_SESSION
  139. since that could be set yet could be expired or incorrect."
  140. (interactive "M")
  141. (let ((pass (when bitwarden-automatic-unlock
  142. (concat " " (funcall bitwarden-automatic-unlock)))))
  143. (bitwarden--raw-unlock (concat "unlock" pass))))
  144. (defun bitwarden-login ()
  145. "Prompts user for password if not logged in."
  146. (interactive "M")
  147. (unless bitwarden-user
  148. (setq bitwarden-user (read-string "Bitwarden email: ")))
  149. (let ((pass (when bitwarden-automatic-unlock
  150. (concat " " (funcall bitwarden-automatic-unlock)))))
  151. (bitwarden--raw-unlock (concat "login " bitwarden-user pass))))
  152. (defun bitwarden-lock ()
  153. "Lock the bw vault. Does not ask for confirmation."
  154. (interactive)
  155. (when (bitwarden-unlocked-p)
  156. (setenv "BW_SESSION" nil)))
  157. ;;;###autoload
  158. (defun bitwarden-logout ()
  159. "Log out bw. Does not ask for confirmation."
  160. (interactive)
  161. (when (bitwarden-logged-in-p)
  162. (bitwarden-runcmd "logout")
  163. (bitwarden-lock)))
  164. ;;;###autoload
  165. (defun bitwarden-getpass (account &optional print-message)
  166. "Get password associated with ACCOUNT.
  167. If run interactively PRINT-MESSAGE gets set and password is
  168. printed to minibuffer."
  169. (interactive "MBitwarden account name: \np")
  170. (let ((pass (bitwarden-runcmd "get" "password" account)))
  171. (if (not pass)
  172. ;; try to unlock automatically, if possible
  173. (if bitwarden-automatic-unlock
  174. (progn
  175. ;; because I don't understand how emacs is asyncronous here nor
  176. ;; how to tell it to wait until the process is done, we do so here
  177. ;; manually
  178. (bitwarden-unlock)
  179. (while (get-process "bitwarden")
  180. (sleep-for 0.1))
  181. (setq pass (bitwarden-runcmd "get" "password" account))
  182. (if (not pass)
  183. (message "Bitwarden: could not get password for %s" account)
  184. (when print-message
  185. (message "Bitwarden: password for account %s is: %s" account pass))
  186. pass))
  187. (message "Bitwarden: could not get password for %s" account))
  188. (when print-message
  189. (message "Bitwarden: password for account %s is: %s" account pass))
  190. pass)))
  191. (provide 'bitwarden)
  192. ;;; bitwarden.el ends here