password-store

Simple password manager using gpg and ordinary unix directories
git clone https://git.zx2c4.com/password-store
Log | Files | Refs | README | LICENSE

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