password-store.el (14926B)
1 ;;; password-store.el --- Password store (pass) support -*- lexical-binding: t; -*- 2 3 ;; Copyright (C) 2014-2019 Svend Sorensen <svend@svends.net> 4 5 ;; Author: Svend Sorensen <svend@svends.net> 6 ;; Maintainer: Tino Calancha <tino.calancha@gmail.com> 7 ;; Version: 2.3.2 8 ;; URL: https://www.passwordstore.org/ 9 ;; Package-Requires: ((emacs "26.1") (with-editor "2.5.11")) 10 ;; SPDX-License-Identifier: GPL-3.0-or-later 11 ;; Keywords: tools pass password password-store gpg 12 13 ;; This file is not part of GNU Emacs. 14 15 ;; This program is free software: you can redistribute it and/or 16 ;; modify it under the terms of the GNU General Public License as 17 ;; published by the Free Software Foundation, either version 3 of 18 ;; the License, or (at your option) any later version. 19 20 ;; This program is distributed in the hope that it will be 21 ;; useful, but WITHOUT ANY WARRANTY; without even the implied 22 ;; warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR 23 ;; PURPOSE. See the GNU General Public License for more details. 24 25 ;; You should have received a copy of the GNU General Public 26 ;; License along with this program. If not, see 27 ;; <http://www.gnu.org/licenses/>. 28 29 ;;; Commentary: 30 31 ;; This package provides and Emacs interface for working with 32 ;; pass ("the standard Unix password manager"). 33 34 ;; https://www.passwordstore.org/ 35 36 ;;; Code: 37 38 (require 'with-editor) 39 (require 'auth-source-pass) 40 41 (defgroup password-store '() 42 "Emacs mode for password-store. 43 The standard Unix password manager" 44 :prefix "password-store-" 45 :group 'password-store 46 :link '(url-link :tag "Description" "https://www.passwordstore.org/") 47 :link '(url-link :tag "Download" "https://melpa.org/#/password-store") 48 :link `(url-link :tag "Send Bug Report" 49 ,(concat "mailto:" "password-store" "@" "lists.zx2c4" ".com?subject= 50 password-store.el bug: \ 51 &body=Describe bug here, starting with `emacs -q'. \ 52 Don't forget to mention your Emacs and library versions."))) 53 54 (defcustom password-store-password-length 25 55 "Default password length." 56 :group 'password-store 57 :type 'number) 58 59 (defcustom password-store-time-before-clipboard-restore 60 (if (getenv "PASSWORD_STORE_CLIP_TIME") 61 (string-to-number (getenv "PASSWORD_STORE_CLIP_TIME")) 62 45) 63 "Number of seconds to wait before restoring the clipboard." 64 :group 'password-store 65 :type 'number) 66 67 (defcustom password-store-url-field "url" 68 "Field name used in the files to indicate a URL." 69 :group 'password-store 70 :type 'string) 71 72 (defvar password-store-executable 73 (executable-find "pass") 74 "Pass executable.") 75 76 (defvar password-store-timeout-timer nil 77 "Timer for clearing clipboard.") 78 79 (defun password-store-timeout () 80 "Number of seconds to wait before restoring the clipboard. 81 82 This function just returns 83 `password-store-time-before-clipboard-restore'. Kept for 84 backward compatibility with other libraries." 85 password-store-time-before-clipboard-restore) 86 87 (make-obsolete 'password-store-timeout 'password-store-time-before-clipboard-restore "2.0.4") 88 89 (defun password-store--run-1 (callback &rest args) 90 "Run pass with ARGS. 91 92 Nil arguments are ignored. Calls CALLBACK with the output on 93 success, or outputs error message on failure." 94 (let ((output "")) 95 (make-process 96 :name "password-store-gpg" 97 :command (cons password-store-executable (delq nil args)) 98 :connection-type 'pipe 99 :noquery t 100 :filter (lambda (process text) 101 (setq output (concat output text))) 102 :sentinel (lambda (process state) 103 (cond 104 ((and (eq (process-status process) 'exit) 105 (zerop (process-exit-status process))) 106 (funcall callback output)) 107 ((eq (process-status process) 'run) (accept-process-output process)) 108 (t (error (concat "password-store: " state)))))))) 109 110 (defun password-store--run (&rest args) 111 "Run pass with ARGS. 112 113 Nil arguments are ignored. Returns the output on success, or 114 outputs error message on failure." 115 (let ((output nil) 116 (slept-for 0)) 117 (apply #'password-store--run-1 (lambda (password) 118 (setq output password)) 119 (delq nil args)) 120 (while (not output) 121 (sleep-for .1)) 122 output)) 123 124 (defun password-store--run-async (&rest args) 125 "Run pass asynchronously with ARGS. 126 127 Nil arguments are ignored. Output is discarded." 128 (let ((args (mapcar #'shell-quote-argument args))) 129 (with-editor-async-shell-command 130 (mapconcat 'identity 131 (cons password-store-executable 132 (delq nil args)) " ")))) 133 134 (defun password-store--run-init (gpg-ids &optional subdir) 135 (apply 'password-store--run "init" 136 (if subdir (format "--path=%s" subdir)) 137 gpg-ids)) 138 139 (defun password-store--run-list (&optional subdir) 140 (error "Not implemented")) 141 142 (defun password-store--run-grep (&optional string) 143 (error "Not implemented")) 144 145 (defun password-store--run-find (&optional string) 146 (error "Not implemented")) 147 148 (defun password-store--run-show (entry &optional callback) 149 (if callback 150 (password-store--run-1 callback "show" entry) 151 (password-store--run "show" entry))) 152 153 (defun password-store--run-insert (entry password &optional force) 154 (error "Not implemented")) 155 156 (defun password-store--run-edit (entry) 157 (password-store--run-async "edit" 158 entry)) 159 160 (defun password-store--run-generate (entry password-length &optional force no-symbols) 161 (password-store--run "generate" 162 (if force "--force") 163 (if no-symbols "--no-symbols") 164 entry 165 (number-to-string password-length))) 166 167 (defun password-store--run-remove (entry &optional recursive) 168 (password-store--run "remove" 169 "--force" 170 (if recursive "--recursive") 171 entry)) 172 173 (defun password-store--run-rename (entry new-entry &optional force) 174 (password-store--run "rename" 175 (if force "--force") 176 entry 177 new-entry)) 178 179 (defun password-store--run-copy (entry new-entry &optional force) 180 (password-store--run "copy" 181 (if force "--force") 182 entry 183 new-entry)) 184 185 (defun password-store--run-git (&rest args) 186 (apply 'password-store--run "git" 187 args)) 188 189 (defun password-store--run-version () 190 (password-store--run "version")) 191 192 (defvar password-store-kill-ring-pointer nil 193 "The tail of of the kill ring ring whose car is the password.") 194 195 (defun password-store-dir () 196 "Return password store directory." 197 (or (bound-and-true-p auth-source-pass-filename) 198 (getenv "PASSWORD_STORE_DIR") 199 "~/.password-store")) 200 201 (defun password-store--entry-to-file (entry) 202 "Return file name corresponding to ENTRY." 203 (concat (expand-file-name entry (password-store-dir)) ".gpg")) 204 205 (defun password-store--file-to-entry (file) 206 "Return entry name corresponding to FILE." 207 (file-name-sans-extension (file-relative-name file (password-store-dir)))) 208 209 (defun password-store--completing-read (&optional require-match) 210 "Read a password entry in the minibuffer, with completion. 211 212 Require a matching password if `REQUIRE-MATCH' is 't'." 213 (completing-read "Password entry: " (password-store-list) nil require-match)) 214 215 (defun password-store-parse-entry (entry) 216 "Return an alist of the data associated with ENTRY. 217 218 ENTRY is the name of a password-store entry." 219 (auth-source-pass-parse-entry entry)) 220 221 (defun password-store-read-field (entry) 222 "Read a field in the minibuffer, with completion for ENTRY." 223 (let* ((inhibit-message t) 224 (valid-fields (mapcar #'car (password-store-parse-entry entry)))) 225 (completing-read "Field: " valid-fields nil 'match))) 226 227 (defun password-store-list (&optional subdir) 228 "List password entries under SUBDIR." 229 (unless subdir (setq subdir "")) 230 (let ((dir (expand-file-name subdir (password-store-dir)))) 231 (if (file-directory-p dir) 232 (delete-dups 233 (mapcar 'password-store--file-to-entry 234 (directory-files-recursively dir ".+\\.gpg\\'")))))) 235 236 ;;;###autoload 237 (defun password-store-edit (entry) 238 "Edit password for ENTRY." 239 (interactive (list (password-store--completing-read t))) 240 (password-store--run-edit entry)) 241 242 ;;;###autoload 243 (defun password-store-get (entry &optional callback) 244 "Return password for ENTRY. 245 246 Returns the first line of the password data. When CALLBACK is 247 non-`NIL', call CALLBACK with the first line instead." 248 (let* ((inhibit-message t) 249 (secret (auth-source-pass-get 'secret entry))) 250 (if (not callback) secret 251 (password-store--run-show 252 entry 253 (lambda (_) (funcall callback secret)))))) 254 255 ;;;###autoload 256 (defun password-store-get-field (entry field &optional callback) 257 "Return FIELD for ENTRY. 258 FIELD is a string, for instance \"url\". When CALLBACK is 259 non-`NIL', call it with the line associated to FIELD instead. If 260 FIELD equals to symbol secret, then this function reduces to 261 `password-store-get'." 262 (let* ((inhibit-message t) 263 (secret (auth-source-pass-get field entry))) 264 (if (not callback) secret 265 (password-store--run-show 266 entry 267 (lambda (_) (and secret (funcall callback secret))))))) 268 269 270 ;;;###autoload 271 (defun password-store-clear (&optional field) 272 "Clear secret in the kill ring. 273 274 Optional argument FIELD, a symbol or a string, describes the 275 stored secret to clear; if nil, then set it to 'secret. Note, 276 FIELD does not affect the function logic; it is only used to 277 display the message: 278 279 \(message \"Field %s cleared from kill ring and system clipboard.\" field)." 280 (interactive "i") 281 (unless field (setq field 'secret)) 282 (when password-store-timeout-timer 283 (cancel-timer password-store-timeout-timer) 284 (setq password-store-timeout-timer nil)) 285 (when password-store-kill-ring-pointer 286 (setcar password-store-kill-ring-pointer "") 287 (kill-new "") 288 (setq password-store-kill-ring-pointer nil) 289 (message "Field %s cleared from kill ring and system clipboard." field))) 290 291 (defun password-store--save-field-in-kill-ring (entry secret field) 292 (password-store-clear field) 293 (kill-new secret) 294 (setq password-store-kill-ring-pointer kill-ring-yank-pointer) 295 (message "Copied %s for %s to the kill ring and system clipboard. Will clear in %s seconds." 296 field entry password-store-time-before-clipboard-restore) 297 (setq password-store-timeout-timer 298 (run-at-time password-store-time-before-clipboard-restore nil 299 (lambda () (funcall #'password-store-clear field))))) 300 301 ;;;###autoload 302 (defun password-store-copy (entry) 303 "Add password for ENTRY into the kill ring. 304 305 Clear previous password from the kill ring. Pointer to the kill 306 ring is stored in `password-store-kill-ring-pointer'. Password 307 is cleared after `password-store-time-before-clipboard-restore' 308 seconds." 309 (interactive (list (password-store--completing-read t))) 310 (password-store-get 311 entry 312 (lambda (password) 313 (password-store--save-field-in-kill-ring entry password 'secret)))) 314 315 ;;;###autoload 316 (defun password-store-copy-field (entry field) 317 "Add FIELD for ENTRY into the kill ring. 318 319 Clear previous secret from the kill ring. Pointer to the kill 320 ring is stored in `password-store-kill-ring-pointer'. Secret 321 field is cleared after 322 `password-store-time-before-clipboard-restore' seconds. If FIELD 323 equals to symbol secret, then this function reduces to 324 `password-store-copy'." 325 (interactive 326 (let ((entry (password-store--completing-read))) 327 (list entry (password-store-read-field entry)))) 328 (password-store-get-field 329 entry 330 field 331 (lambda (secret-value) 332 (password-store--save-field-in-kill-ring entry secret-value field)))) 333 334 ;;;###autoload 335 (defun password-store-init (gpg-id) 336 "Initialize new password store and use GPG-ID for encryption. 337 338 Separate multiple IDs with spaces." 339 (interactive (list (read-string "GPG ID: "))) 340 (message "%s" (password-store--run-init (split-string gpg-id)))) 341 342 ;;;###autoload 343 (defun password-store-insert (entry password) 344 "Insert a new ENTRY containing PASSWORD." 345 (interactive (list (password-store--completing-read) 346 (read-passwd "Password: " t))) 347 (let* ((command (format "echo %s | %s insert -m -f %s" 348 (shell-quote-argument password) 349 password-store-executable 350 (shell-quote-argument entry))) 351 (ret (process-file-shell-command command))) 352 (if (zerop ret) 353 (message "Successfully inserted entry for %s" entry) 354 (message "Cannot insert entry for %s" entry)) 355 nil)) 356 357 ;;;###autoload 358 (defun password-store-generate (entry &optional password-length) 359 "Generate a new password for ENTRY with PASSWORD-LENGTH. 360 361 Default PASSWORD-LENGTH is `password-store-password-length'." 362 (interactive (list (password-store--completing-read) 363 (and current-prefix-arg 364 (abs (prefix-numeric-value current-prefix-arg))))) 365 ;; A message with the output of the command is not printed 366 ;; because the output contains the password. 367 (password-store--run-generate 368 entry 369 (or password-length password-store-password-length) 370 'force) 371 nil) 372 373 ;;;###autoload 374 (defun password-store-generate-no-symbols (entry &optional password-length) 375 "Generate a new password without symbols for ENTRY with PASSWORD-LENGTH. 376 377 Default PASSWORD-LENGTH is `password-store-password-length'." 378 (interactive (list (password-store--completing-read) 379 (and current-prefix-arg 380 (abs (prefix-numeric-value current-prefix-arg))))) 381 382 ;; A message with the output of the command is not printed 383 ;; because the output contains the password. 384 (password-store--run-generate 385 entry 386 (or password-length password-store-password-length) 387 'force 'no-symbols) 388 nil) 389 390 ;;;###autoload 391 (defun password-store-remove (entry) 392 "Remove ENTRY." 393 (interactive (list (password-store--completing-read t))) 394 (message "%s" (password-store--run-remove entry t))) 395 396 ;;;###autoload 397 (defun password-store-rename (entry new-entry) 398 "Rename ENTRY to NEW-ENTRY." 399 (interactive (list (password-store--completing-read t) 400 (read-string "Rename entry to: "))) 401 (message "%s" (password-store--run-rename entry new-entry t))) 402 403 ;;;###autoload 404 (defun password-store-version () 405 "Show version of `password-store-executable'." 406 (interactive) 407 (message "%s" (password-store--run-version))) 408 409 ;;;###autoload 410 (defun password-store-url (entry) 411 "Load URL for ENTRY." 412 (interactive (list (password-store--completing-read t))) 413 (let ((url (password-store-get-field entry password-store-url-field))) 414 (if url (browse-url url) 415 (error "Field `%s' not found" password-store-url-field)))) 416 417 418 (provide 'password-store) 419 420 ;;; password-store.el ends here