bitwarden.el 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  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. (defconst bitwarden--err-logged-in "you are not logged in")
  45. (defconst bitwarden--err-multiple "more than one result found")
  46. (defconst bitwarden--err-locked "vault is locked")
  47. (defun bitwarden-logged-in-p ()
  48. "Check if `bitwarden-user' is logged in.
  49. Returns nil if not logged in."
  50. (let* ((json-object-type 'hash-table)
  51. (json-key-type 'string)
  52. (json (json-read-file bitwarden-data-file)))
  53. (gethash "__PROTECTED__key" json)))
  54. (defun bitwarden-unlocked-p ()
  55. "Check if we have already set the 'BW_SESSION' environment variable."
  56. (and (bitwarden-logged-in-p) (getenv "BW_SESSION")))
  57. (defun bitwarden--raw-runcmd (cmd &rest args)
  58. "Run bw command CMD with ARGS.
  59. Returns a list with the first element being the exit code and the
  60. second element being the output."
  61. (with-temp-buffer
  62. (list (apply 'call-process
  63. bitwarden-bw-executable
  64. nil (current-buffer) nil
  65. (cons cmd args))
  66. (replace-regexp-in-string "\n$" ""
  67. (buffer-string)))))
  68. (defun bitwarden-runcmd (cmd &rest args)
  69. "Run bw command CMD with ARGS.
  70. This is a wrapper for `bitwarden--raw-runcmd' that also checks
  71. for common errors."
  72. (if (bitwarden-logged-in-p)
  73. (if (bitwarden-unlocked-p)
  74. (let* ((ret (apply #'bitwarden--raw-runcmd cmd args))
  75. (exit-code (nth 0 ret))
  76. (output (nth 1 ret)))
  77. (if (eq exit-code 0)
  78. output
  79. (cond ((string-match "^More than one result was found." output)
  80. bitwarden--err-multiple)
  81. (t nil))))
  82. bitwarden--err-locked)
  83. bitwarden--err-logged-in))
  84. (defun bitwarden--login-proc-filter (proc string)
  85. "Interacts with PROC by sending line-by-line STRING."
  86. ;; read username if not defined
  87. (when (string-match "^? Email address:" string)
  88. (let ((user (read-string "Bitwarden email: ")))
  89. ;; if we are here then the user forgot to fill in this field so let's do
  90. ;; that now
  91. (setq bitwarden-user user)
  92. (process-send-string proc (concat bitwarden-user "\n"))))
  93. ;; read master password
  94. (when (string-match "^? Master password:" string)
  95. (process-send-string
  96. proc (concat (read-passwd "Bitwarden master password: ") "\n")))
  97. ;; check for bad password
  98. (when (string-match "^Username or password is incorrect" string)
  99. (message "Bitwarden: incorrect master password"))
  100. ;; if trying to unlock, check if logged in
  101. (when (string-match "^You are not logged in" string)
  102. (message "Bitwarden: cannot unlock: not logged in"))
  103. ;; read the 2fa code
  104. (when (string-match "^? Two-step login code:" string)
  105. (process-send-string
  106. proc (concat (read-passwd "Bitwarden two-step login code: ") "\n")))
  107. ;; check for bad code
  108. (when (string-match "^Login failed" string)
  109. (message "Bitwarden: incorrect two-step code"))
  110. ;; check for already logged in
  111. (when (string-match "^You are already logged in" string)
  112. (string-match "You are already logged in as \\(.*\\)\\." string)
  113. (message "Bitwarden: already logged in as %s" (match-string 1 string)))
  114. ;; success! now save the BW_SESSION into the environment so spawned processes
  115. ;; inherit it
  116. (when (string-match "^\\(You are logged in\\|Your vault is now unlocked\\)"
  117. string)
  118. ;; set the session env variable so spawned processes inherit
  119. (string-match "export BW_SESSION=\"\\(.*\\)\"" string)
  120. (setenv "BW_SESSION" (match-string 1 string))
  121. (message
  122. (concat "Bitwarden: successfully logged in as " bitwarden-user))))
  123. (defun bitwarden--raw-unlock (cmd)
  124. "Raw CMD to either unlock a vault or login.
  125. The only difference between unlock and login is just the name of
  126. the command and whether to pass the user."
  127. (when (get-process "bitwarden")
  128. (delete-process "bitwarden"))
  129. (let ((process (start-process-shell-command
  130. "bitwarden"
  131. nil ; don't use a buffer
  132. (concat bitwarden-bw-executable " " cmd))))
  133. (set-process-filter process #'bitwarden--login-proc-filter)))
  134. (defun bitwarden-unlock ()
  135. "Unlock bitwarden vault.
  136. It is not sufficient to check the env variable for BW_SESSION
  137. since that could be set yet could be expired or incorrect."
  138. (interactive "M")
  139. (let ((pass (when bitwarden-automatic-unlock
  140. (concat " " (funcall bitwarden-automatic-unlock)))))
  141. (bitwarden--raw-unlock (concat "unlock" pass))))
  142. (defun bitwarden-login ()
  143. "Prompts user for password if not logged in."
  144. (interactive "M")
  145. (unless bitwarden-user
  146. (setq bitwarden-user (read-string "Bitwarden email: ")))
  147. (let ((pass (when bitwarden-automatic-unlock
  148. (concat " " (funcall bitwarden-automatic-unlock)))))
  149. (bitwarden--raw-unlock (concat "login " bitwarden-user pass))))
  150. (defun bitwarden-lock ()
  151. "Lock the bw vault. Does not ask for confirmation."
  152. (interactive)
  153. (when (bitwarden-unlocked-p)
  154. (setenv "BW_SESSION" nil)))
  155. ;;;###autoload
  156. (defun bitwarden-logout ()
  157. "Log out bw. Does not ask for confirmation."
  158. (interactive)
  159. (when (bitwarden-logged-in-p)
  160. (bitwarden-runcmd "logout")
  161. (bitwarden-lock)))
  162. ;;;###autoload
  163. (defun bitwarden-getpass (account &optional print-message recursive-pass)
  164. "Get password associated with ACCOUNT.
  165. If run interactively PRINT-MESSAGE gets set and password is
  166. printed to minibuffer.
  167. If RECURSIVE-PASS is set, then treat this call as an attempt to auto-unlock."
  168. (interactive "MBitwarden account name: \np")
  169. (let* ((pass (or recursive-pass
  170. (bitwarden-runcmd "get" "password" account))))
  171. (cond
  172. ((string-match bitwarden--err-locked pass)
  173. ;; try to unlock automatically, if possible
  174. (if (not bitwarden-automatic-unlock)
  175. (progn
  176. (when print-message
  177. (message "Bitwarden: error: %s" pass))
  178. nil)
  179. ;; only attempt a retry once; to prevent infinite recursion
  180. (when (not recursive-pass)
  181. ;; because I don't understand how emacs is asyncronous here nor
  182. ;; how to tell it to wait until the process is done, we do so here
  183. ;; manually
  184. (bitwarden-unlock)
  185. (while (get-process "bitwarden")
  186. (sleep-for 0.1))
  187. (bitwarden-getpass account print-message
  188. (bitwarden-runcmd "get" "password" account)))))
  189. ((string-match bitwarden--err-logged-in pass)
  190. (when print-message
  191. (message "Bitwarden: error: %s" pass))
  192. nil)
  193. ((string-match bitwarden--err-multiple pass)
  194. (when print-message
  195. (message "Bitwarden: error: %s" pass))
  196. nil)
  197. (t pass))))
  198. (provide 'bitwarden)
  199. ;;; bitwarden.el ends here