bitwarden.el/bitwarden.el

367 lines
11 KiB
EmacsLisp
Raw Normal View History

2022-03-02 17:52:39 +00:00
;;; bitwarden.el --- Interact with Bitwarden -*- lexical-binding: t -*-
2022-02-13 13:12:29 +00:00
;; Author: Lucien Cartier-Tilet <lucien@phundrak.com>
;; Maintainer: Lucien Cartier-Tilet <lucien@phundrak.com>
;; Version: 0.1.0
;; Package-Requires: ((emacs "27.1") (transient "0.3.7"))
;; Homepage: https://labs.phundrak.com/phundrak/bitwarden.el
2022-03-02 17:52:39 +00:00
;; Keywords: convenience
2022-02-13 13:12:29 +00:00
;; This file is not part of GNU Emacs
;; This program is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;; # Introduction
;; bitwarden.el is a Bitwarden porcelain for Emacs. It aims to be a
;; complete text-based interface for the Bitwarden CLI
;; (https://github.com/bitwarden/cli).
;;
;; Most of its public function are transient functions from the
;; transient library to provide the user an easy-to-use interface with
;; most of its options exposed.
;;
;; # Notes
;; ## Login
;;
;; Loging in with the --apikey option is not supported due to its
;; interactive nature.
;;
;; Bitwarden allows three different sources for your password:
;; - a plain password as an argument following the username
;; - an environment variable containing the password
;; - a file containing the password
;; Bitwarden.el allows a fourth option: the authinfo file on computer.
;; To use this option, simply add the following line in your .authinfo
;; or .authinfo.gpg file:
;;
;; machine bitwarden.example.com login yourusername password yourpassword
;;
;; Of course, you will have to replace `bitwarden.example.com` with
;; the actual server, `yourusername` with your actual username, and
;; `yourpassword` with your actual password. If you do not set your
;; username or your password in bitwarden.el, the package will look
;; for them in your auth source file on login. Bitwarden.el retrieves
;; the server name from the command
;;
;; bw config server
;;
;; and it strips the result from any http:// or https:// prefix. For
;; instance, if the command returns "https://example.com/bitwarden",
;; bitwarden.el will look for "example.com/bitwarden" in your authinfo
;; file.
;;; Code:
;;; Requires
(require 'cl-lib)
(require 'eshell)
(require 'auth-source)
(require 'transient)
;;; Constants
(defconst bitwarden-version "0.1.0")
(defconst bitwarden-login-methods '(("Authentificator" . "0") ("Email" . "1") ("YubiKey" . "3"))
"Bitwarden login methods.")
;;; Group and Custom Variables
(defgroup bitwarden ()
"Interact with Bitwarden from Emacs."
:group 'applications
:prefix "bitwarden-"
:link '(url-link :tag "Gitea" "https://labs.phundrak.com/phundrak/bitwarden.el"))
(defcustom bitwarden-cli-executable "bw"
"Path to the Bitwarden CLI executable."
:type 'string
:group 'bitwarden
:safe #'stringp
:version "0.1.0")
(defcustom bitwarden-default-cli-arguments '("--nointeraction")
"Default arguments for the Bitwarden CLI."
:group 'bitwarden
:type '(repeat string)
:version "0.1.0")
2022-03-02 17:34:05 +00:00
(defvar bitwarden--shell-status nil
"Exit status of the last `bitwarden--run-cli' call.")
(defvar bitwarden--session)
2022-02-13 13:12:29 +00:00
;;; Internal Functions
;;; FIXME: undefined when compiling the package with straight <>
2022-02-13 13:12:29 +00:00
(defun bitwarden--run-cli (&rest args)
"Run the Bitwarden cli with ARGS.
The default arguments specified by
`bitwarden-default-cli-arguments' immediately follow the clis
name defined in `bitwarden-cli-executable'.
The exit status of the command is stored in
`bitwarden--shell-status'."
2022-02-13 13:12:29 +00:00
(eshell-command-result
(mapconcat #'identity
(flatten-tree `(,bitwarden-cli-executable ,bitwarden-default-cli-arguments ,args))
" ")
'bitwarden--shell-status))
2022-03-02 16:43:10 +00:00
(defun bitwarden-dummy ()
2022-02-13 13:12:29 +00:00
"Dummy function."
(interactive)
2022-03-02 16:43:10 +00:00
(message "Hello from bitwarden.el!"))
2022-02-13 13:12:29 +00:00
;;; Transient Infixes
(eval-when-compile
(defmacro bitwarden--define-infix (key name description type version
default &rest reader)
"Define infix and its corresponding variable at once.
The variable is named bitwarden-NAME is of type TYPE, has a
DESCRIPTION and a specified VERSION.
The KEY and READER are for the infix declaration.
This macro is largely copied from Tecosaurs screenshot.el"
(let ((var-name (concat "bitwarden--" name)))
2022-02-13 13:12:29 +00:00
`(progn
(defcustom ,(intern var-name) ,default
,description
:type ,type
:group 'bitwarden
:version ,version)
(transient-define-infix ,(intern (concat "bitwarden--set-" name)) ()
"Set Bitwarden options."
:class 'transient-lisp-variable
:variable ',(intern var-name)
:key ,key
:description ,description
:argument ,(concat "--" name)
:reader (lambda (&rest _) ,@reader)))))
;; Config
(bitwarden--define-infix
"-s" "config-server" "On-premises hosted installation URL"
2022-02-13 13:12:29 +00:00
'string "0.1.0"
(let ((output-str (bitwarden--run-cli "config" "server")))
(if (eq 0 bitwarden--shell-status)
(string-trim output-str "https?://")
""))
(read-string "Server URL: "))
;; Login
(bitwarden--define-infix
"-u" "login-username" "Email of the user."
2022-02-13 13:12:29 +00:00
'string "0.1.0"
nil
2022-02-13 13:12:29 +00:00
(read-string "Email address: "))
(bitwarden--define-infix
"-p" "login-password" "Use a direct password input."
'boolean "0.1.0"
nil
(not bitwarden--login-password))
2022-02-13 13:12:29 +00:00
(bitwarden--define-infix
"-s" "login-sso" "Log in with Single-Sign On"
'boolean "0.1.0"
nil
2022-03-02 16:47:42 +00:00
(not bitwarden--login-sso))
2022-02-13 13:12:29 +00:00
(bitwarden--define-infix
"-e" "login-passwordenv" "Environment variable storing your password"
2022-02-13 13:12:29 +00:00
'string "0.1.0"
nil
2022-02-13 13:12:29 +00:00
(read-string "Environment variable: "))
(bitwarden--define-infix
"-f" "login-passwordfile"
2022-02-13 13:12:29 +00:00
"Path to a file containing your password as its first line"
'string "0.1.0"
nil
2022-02-13 13:12:29 +00:00
(read-file-name "Password file: "))
(bitwarden--define-infix
"-c" "login-code"
2022-02-13 13:12:29 +00:00
"Two-step login code"
'string "0.1.0"
nil
2022-02-13 13:12:29 +00:00
(read-string "Two-step code: "))
(bitwarden--define-infix
"-m" "login-method"
2022-02-13 13:12:29 +00:00
"Two-step login method"
'string "0.1.0"
"0"
2022-02-13 13:12:29 +00:00
(cdr (assoc (completing-read "Two-step login method: "
bitwarden-login-methods)
bitwarden-login-methods))))
(defsubst bitwarden--empty-or-nil (var)
"Return non-nil if VAR is an empty string or nil."
(or (not var)
(string= "" var)))
(defsubst bitwarden--quote-string (str)
"Escape STR and surround it by single quotes."
(format "\'%s\'"
(replace-regexp-in-string "'"
(regexp-quote "\\'")
str)))
(defun bitwarden--get-username ()
"Get username.
2022-03-02 16:47:42 +00:00
If the `bitwarden--login-username' variable is empty, retrieve it
from the authinfo source."
(if (bitwarden--empty-or-nil bitwarden--login-username)
(plist-get (car (auth-source-search :max 1
2022-03-02 16:47:42 +00:00
:host bitwarden--config-server))
:user)
2022-03-02 16:47:42 +00:00
bitwarden--login-username))
(defun bitwarden--get-password ()
"Get password or password source.
By order of preference, retrieve the password from the user if
`bitwarden--login-password' is non-nil, from the environment file
designated by the variable `bitwarden--login-passwordenv', by the
file designated by the variable `bitwarden--login-passwordfile',
or from the authinfo source."
(bitwarden--quote-string
(cond
(bitwarden--login-password
(read-passwd "Bitwarden Password: "))
((not (bitwarden--empty-or-nil bitwarden--login-passwordenv))
`("--passwordenv" ,bitwarden--login-passwordenv))
((not (bitwarden--empty-or-nil bitwarden--login-passwordfile))
`("--passwordfile" ,bitwarden--login-passwordfile))
(t (funcall
(plist-get
(car (auth-source-search :max 1
:host bitwarden--config-server))
:secret))))))
2022-02-13 13:12:29 +00:00
;;; Transient Actions
(eval-when-compile
(defmacro bitwarden--def-action (name description transient &rest body)
"Create a function called from TRANSIENT.
DESCRIPTION is the docstring of the function named
bitwarden--NAME, and it gets BODY as its body."
`(defun ,(intern (concat "bitwarden--action-" name)) (&optional _args)
,(concat description "
This function is meant to be called by a transient.")
(interactive
(list (transient-args ,transient)))
,@body))
;; Config
(bitwarden--def-action
"set-server"
"Set the Bitwarden server."
'bitwarden-config
2022-03-02 16:47:42 +00:00
(bitwarden--run-cli "config" "server" bitwarden--config-server))
(bitwarden--def-action
"get-server"
"Get the URL of the on-premises hosted installation."
'bitwarden-config
(bitwarden--run-cli "config" "server"))
2022-02-13 13:12:29 +00:00
;; Login
(bitwarden--def-action
"login"
"Log in Bitwarden."
'bitwarden-login
2022-03-02 16:47:42 +00:00
(if bitwarden--login-sso
2022-02-13 13:12:29 +00:00
(bitwarden--run-cli "login" "--raw" "--sso")
(bitwarden--run-cli "login" "--raw"
(format "'%s'" (bitwarden--get-username))
(bitwarden--get-password))))
2022-02-13 13:12:29 +00:00
(bitwarden--def-action
"logout"
"Log out of Bitwarden."
'bitwarden-login
2022-02-13 13:12:29 +00:00
(shell-command (concat bitwarden-cli-executable " logout"))))
;;; Transient Prefixes
(transient-define-prefix bitwarden-config ()
["Options"
(bitwarden--set-config-server)]
2022-02-13 13:12:29 +00:00
["Actions"
("g" "Get current server" bitwarden--action-get-server)
("s" "Set current server" bitwarden--action-set-server)]
(interactive)
(transient-setup 'bitwarden-config))
2022-02-13 13:12:29 +00:00
(transient-define-prefix bitwarden-login ()
["Options"
(bitwarden--set-login-username)
(bitwarden--set-login-password)
(bitwarden--set-login-passwordenv)
(bitwarden--set-login-passwordfile)
(bitwarden--set-login-method)
(bitwarden--set-login-code)]
2022-02-13 13:12:29 +00:00
["Actions"
("l" "Login" bitwarden--action-login)
("L" "Logout" bitwarden--action-logout)]
(interactive)
(transient-setup 'bitwarden-login))
2022-02-13 13:12:29 +00:00
(transient-define-prefix bitwarden-transient ()
["Actions"
("c" "Config" bitwarden-config)
("l" "Login" bitwarden-login)
("L" "Lock" bitwarden-lock)
("s" "sync" bitwarden-dummy)]
(interactive)
(transient-setup 'bitwarden-transient))
;;; Public functions
;;;###autoload
(defun bitwarden-lock ()
"Lock the vault and destroy active session keys.
The command may fail in the user is not logged in."
(interactive)
(progn
2022-03-02 16:47:42 +00:00
(setq bitwarden--session nil
bitwarden--login-password nil
bitwarden--login-passwordenv nil
bitwarden--login-passwordfile nil
bitwarden--login-code nil)
(bitwarden--run-cli "lock")
(if (= 0 bitwarden--shell-status)
(message "Bitwarden: successfuly locked")
(message "Bitwarden: something went wrong"))))
;;;###autoload
(defun bitwarden ()
"Call the main transient for Bitwarden."
(interactive)
(call-interactively #'bitwarden-transient))
2022-02-13 13:12:29 +00:00
(provide 'bitwarden)
;;; bitwarden.el ends here