;;; bitwarden.el --- Interact with Bitwarden from Emacs -*- lexical-binding: t -*- ;; Author: Lucien Cartier-Tilet ;; Maintainer: Lucien Cartier-Tilet ;; Version: 0.1.0 ;; Package-Requires: ((emacs "27.1") (transient "0.3.7")) ;; Homepage: https://labs.phundrak.com/phundrak/bitwarden.el ;; Keywords: ;; 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 . ;;; 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) (defvar bitwarden--session) ;;; Internal Functions (defun bitwarden--run-cli (&rest args) "Run the Bitwarden cli with ARGS. The default arguments specified by `bitwarden-default-cli-arguments' immediately follow the cli’s 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)) (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 Tecosaur’s 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" (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." '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-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 t if VAR is an empty string or nil. Return nil otherwise." (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--username' variable is empty, retrieve it from the authinfo source." (if (bitwarden--empty-or-nil bitwarden--username) (plist-get (car (auth-source-search :max 1 :host bitwarden--server)) :user) bitwarden--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." (cond ((! (bitwarden--empty-or-nil bitwarden--passwordenv)) `("--passwordenv" ,bitwarden--passwordenv)) ((! (bitwarden--set-passwordfile bitwarden--passwordfile)) `("--passwordfile" ,bitwarden--passwordfile)) (bitwarden--login-password (read-passwd "Bitwarden Password: ")) (t (funcall (plist-get (car (auth-source-search :max 1 :host bitwarden--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 "set-server" "Set the Bitwarden server." 'bitwarden--server (bitwarden--run-cli "config" "server" bitwarden--server)) (bitwarden--def-action "get-server" "Get the URL of the on-premises hosted installation." 'bitwarden--server (bitwarden--run-cli "config" "server")) ;; Login (bitwarden--def-action "login" "Log in Bitwarden." 'bitwarden--login (if bitwarden--sso (bitwarden--run-cli "login" "--raw" "--sso") (bitwarden--run-cli "login" "--raw" (format "\"%s\"" (bitwarden--get-username)) (cond ((not (bitwarden--empty-or-nil bitwarden--login-password)) (bitwarden--quote-string bitwarden--login-password)) ((not (bitwarden--empty-or-nil bitwarden--login-passwordenv)) `("--passwordenv" ,(bitwarden--quote-string bitwarden--login-passwordenv))) ((not (bitwarden--empty-or-nil bitwarden--login-passwordfile)) `("--paswordfile" ,(bitwarden--quote-string bitwarden--login-passwordfile))) (t (funcall (plist-get (car (auth-source-search :max 1 :host bitwarden--config-server)) :secret))))))) (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)] ["Actions" ("g" "Get current server" bitwarden--action-get-server) ("s" "Set current server" bitwarden--action-set-server)]) (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)]) (transient-define-prefix bitwarden-transient () ["Actions" ("c" "Config" bitwarden--config) ("l" "Login" bitwarden--login) ("L" "Lock" bitwarden-lock) ("s" "sync" bitwarden-dummy)]) ;;; Public functions (defun bitwarden-lock () "Lock the vault and destroy active session keys." (progn (setq bitwarden--session nil bitwarden--password nil bitwarden--passwordenv nil bitwarden--passwordfile nil bitwarden--code nil) (bitwarden--run-cli "lock"))) ;; (defun bitwarden () ;; (interactive) ;; (call-interactively #'bitwarden-transient)) (provide 'bitwarden) ;;; bitwarden.el ends here