;;; 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) ;;; 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 output status of the command is stored in RESULT, which should be a variable’s symbol." (eshell-command-result (mapconcat #'identity (flatten-tree `(,bitwarden-cli-executable ,bitwarden-default-cli-arguments ,args)) " ") 'bitwarden--shell-status)) (defun bitwarden-hello () "Dummy function." (interactive) (message "hello")) ;;; 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" "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" "username" "Email of the user." 'string "0.1.0" "" (read-string "Email address: ")) (bitwarden--define-infix "-p" "password" "Password of the user" 'string "0.1.0" "" (read-string "Password: ")) (bitwarden--define-infix "-s" "sso" "Log in with Single-Sign On" 'boolean "0.1.0" nil (not bitwarden-sso)) (bitwarden--define-infix "-e" "passwordenv" "Environment variable storing your password" 'string "0.1.0" "" (read-string "Environment variable: ")) (bitwarden--define-infix "-f" "passwordfile" "Path to a file containing your password as its first line" 'string "0.1.0" "" (read-file-name "Password file: ")) (bitwarden--define-infix "-c" "code" "Two-step login code" 'string "0.1.0" "" (read-string "Two-step code: ")) (bitwarden--define-infix "-m" "method" "Two-step login method" 'string "0.1.0" "0" (cdr (assoc (completing-read "Two-step login method: " bitwarden-login-methods) bitwarden-login-methods)))) ;;; 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 (message "%s" (format "%S" _args))) ;; Login (bitwarden--def-action "login" "Log in Bitwarden." 'bitwarden--login (if bitwarden-sso (bitwarden--run-cli "login" "--raw" "--sso") (bitwarden--run-cli "login" "--raw" ;; bitwarden-username (format "\"%s\"" (if (string= "" bitwarden-username)) (plist-get (car (auth-source-search :max 1 :host bitwarden-server)) :user) bitwarden-username) (format "\"%s\"" (cond ((not (string= "" bitwarden-password)) bitwarden-password) ((! (string= "" bitwarden-passwordenv)) bitwarden-passwordenv) ((! (string= "" bitwarden-passwordfile)) bitwarden-passwordfile) (t (funcall (plist-get (car (auth-source-search :max 1 :host bitwarden-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-server)] ["Actions" ("g" "Get current server" bitwarden-hello) ("s" "Set current server" bitwarden-hello)]) (transient-define-prefix bitwarden-login () ["Options" (bitwarden--set-username) (bitwarden--set-password) (bitwarden--set-passwordenv) (bitwarden--set-passwordfile) (bitwarden--set-method) (bitwarden--set-code)] ["Actions" ("l" "Login" bitwarden-login) ("L" "Logout" bitwarden-logout)]) (transient-define-prefix bitwarden-transient () ["Actions" ("c" "Config" bitwarden--config) ("l" "Login" bitwarden--login) ("L" "Lock" bitwarden-hello) ("s" "sync" bitwarden-hello)]) ;; (defun bitwarden () ;; (interactive) ;; (call-interactively #'bitwarden-transient)) (provide 'bitwarden) ;;; bitwarden.el ends here