bitwarden.el/bitwarden.el
Lucien Cartier-Tilet f1a5d7f125
Add remaining server settings, simplify config infix declaration
Add support for custom URLs available through `bw config server`

Simplify reading and setting an option through a DWIM function.
2022-03-16 20:20:26 +01:00

448 lines
14 KiB
EmacsLisp
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

;;; bitwarden.el --- Interact with Bitwarden -*- lexical-binding: t -*-
;; 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
;; Keywords: convenience
;; 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")
(defvar bitwarden--shell-status nil
"Exit status of the last `bitwarden--run-cli' call.")
(defvar bitwarden--session)
;;; Internal Functions
;;; FIXME: undefined when compiling the package with straight <>
(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'."
(eshell-command-result
(mapconcat #'identity
(flatten-tree `(,bitwarden-cli-executable ,bitwarden-default-cli-arguments ,args))
" ")
'bitwarden--shell-status))
(cl-defun bitwarden--dwim-config-url (&key option (value nil))
"Get the servers url for OPTION or set it to VALUE.
If OPTION is nil, then return the base URL for the server. OPTION
values can be one of the accepted options by the command `bw
config server' minus the two preceding dashes (for instance
\"api\" instead of \"--api\")."
(let ((output-str (cond
((and value
option)
(bitwarden--run-cli "config"
"server"
(concat "--" option)
(bitwarden--quote-string value)))
((and value
(null option))
(bitwarden--run-cli "config"
"server"
(bitwarden--quote-string value)))
((and option
(null value))
(bitwarden--run-cli "config"
"server"
(concat "--" option)))
(t
(bitwarden--run-cli "config" "server")))))
(if (eq 0 bitwarden--shell-status)
(string-trim output-str "https?://")
"")))
(defun bitwarden-dummy ()
"Dummy function."
(interactive)
(message "Hello from bitwarden.el!"))
;;; 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)))
`(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"
'string "0.1.0"
(bitwarden--dwim-config-url)
(let ((server (read-string "Server URL: ")))
(bitwarden--dwim-config-url :value server)
server))
(bitwarden--define-infix
"-a" "config-api" "Custom API URL."
'string "0.1.0"
(bitwarden--dwim-config-url :option "api")
(let ((api-url (read-string "Server API URL: ")))
(bitwarden--dwim-config-url :option "api" :value server)
server))
(bitwarden--define-infix
"-w" "config-web-vault" "Custom web vault URL."
'string "0.1.0"
(bitwarden--dwim-config-url :option "web-vault")
(let ((webvault-url (read-string "Server web-vault URL: ")))
(bitwarden--dwim-config-url :option "web-vault" :value webvault-url)))
(bitwarden--define-infix
"-I" "config-identify" "Custom identity URL."
'string "0.1.0"
(bitwarden--dwim-config-url :option "identity")
(let ((identity-url (read-string "Server identity URL: ")))
(bitwarden--dwim-config-url :option "identity" :value identity-url)
identity-url))
(bitwarden--define-infix
"-i" "config-icons" "Custom icons service URL."
'string "0.1.0"
(bitwarden--dwim-config-url :option "icons")
(let ((icons-url (read-string "Icons service URL: ")))
(bitwarden--dwim-config-url :option "icons" :value icons-url)
icons-url))
(bitwarden--define-infix
"-n" "config-notifications" "Custom notifications URL."
'string "0.1.0"
(bitwarden--dwim-config-url :option "notifications")
(let ((notifications-url (read-string "Notifications URL: ")))
(bitwarden--dwim-config-url :option "notifications" :value notifications-url)
notifications-url))
(bitwarden--define-infix
"-e" "config-events" "Custom events URL."
'string "0.1.0"
(bitwarden--dwim-config-url :option "events")
(let ((events-url (read-string "Events URL: ")))
(bitwarden--dwim-config-url :option "events" :value events-url)
events-url))
(bitwarden--define-infix
"-k" "config-key-connector" "Custom URLfor your Key Connector server."
'string "0.1.0"
(bitwarden--dwim-config-url :option "key-connector")
(let ((key-connector-url (read-string "Key Connector URL: ")))
(bitwarden--dwim-config-url :option "key-connector" :value key-connector-url)
key-connector-url))
;; Login
(bitwarden--define-infix
"-u" "login-username" "Email of the user."
'string "0.1.0"
nil
(read-string "Email address: "))
(bitwarden--define-infix
"-p" "login-password" "Use a direct password input."
'boolean "0.1.0"
nil
(not bitwarden--login-password))
(bitwarden--define-infix
"-s" "login-sso" "Log in with Single-Sign On"
'boolean "0.1.0"
nil
(not bitwarden--login-sso))
(bitwarden--define-infix
"-e" "login-passwordenv" "Environment variable storing your password"
'string "0.1.0"
nil
(read-string "Environment variable: "))
(bitwarden--define-infix
"-f" "login-passwordfile"
"Path to a file containing your password as its first line"
'string "0.1.0"
nil
(read-file-name "Password file: "))
(bitwarden--define-infix
"-c" "login-code"
"Two-step login code"
'string "0.1.0"
nil
(read-string "Two-step code: "))
(bitwarden--define-infix
"-m" "login-method"
"Two-step login method"
'string "0.1.0"
"0"
(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.
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
:host bitwarden--config-server))
:user)
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))))))
;;; 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
"config-quit"
"Exit Bitwarden config."
'bitwarden-config
#'nil)
;; Login
(bitwarden--def-action
"login"
"Log in Bitwarden."
'bitwarden-login
(if bitwarden--login-sso
(bitwarden--run-cli "login" "--raw" "--sso")
(bitwarden--run-cli "login" "--raw"
(format "'%s'" (bitwarden--get-username))
(bitwarden--get-password))))
(bitwarden--def-action
"logout"
"Log out of Bitwarden."
'bitwarden-login
(shell-command (concat bitwarden-cli-executable " logout"))))
;;; Transient Prefixes
(transient-define-prefix bitwarden-config ()
["Options"
(bitwarden--set-config-server)
(bitwarden--set-config-api)
(bitwarden--set-config-web-vault)
(bitwarden--set-config-identify)
(bitwarden--set-config-icons)
(bitwarden--set-config-notifications)
(bitwarden--set-config-events)
(bitwarden--set-config-key-connector)]
["Actions"
("q" "Quit" bitwarden--action-config-quit)]
(interactive)
(transient-setup 'bitwarden-config))
(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)]
["Actions"
("l" "Login" bitwarden--action-login)
("L" "Logout" bitwarden--action-logout)]
(interactive)
(transient-setup 'bitwarden-login))
(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
(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))
(provide 'bitwarden)
;;; bitwarden.el ends here