summaryrefslogtreecommitdiff
path: root/lisp/auth-source-pass.el
diff options
context:
space:
mode:
Diffstat (limited to 'lisp/auth-source-pass.el')
-rw-r--r--lisp/auth-source-pass.el286
1 files changed, 172 insertions, 114 deletions
diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index bd90045b38d..f16d9108903 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -4,10 +4,11 @@
;; Author: Damien Cassou <damien@cassou.me>,
;; Nicolas Petton <nicolas@petton.fr>
-;; Version: 2.0.0
-;; Package-Requires: ((emacs "24.4")
+;; Keith Amidon <camalot@picnicpark.org>
+;; Version: 5.0.0
+;; Package-Requires: ((emacs "25"))
+;; Url: https://github.com/DamienCassou/auth-password-store
;; Created: 07 Jun 2015
-;; Keywords: pass password-store auth-source username password login
;; This file is part of GNU Emacs.
@@ -38,6 +39,22 @@
(require 'auth-source)
(require 'url-parse)
+(defgroup auth-source-pass nil
+ "password-store integration within auth-source."
+ :prefix "auth-source-pass-"
+ :group 'auth-source
+ :version "27.1")
+
+(defcustom auth-source-pass-filename "~/.password-store"
+ "Filename of the password-store folder."
+ :type 'directory
+ :version "27.1")
+
+(defcustom auth-source-pass-port-separator ":"
+ "Separator string between host and port in entry filename."
+ :type 'string
+ :version "27.1")
+
(cl-defun auth-source-pass-search (&rest spec
&key backend type host user port
&allow-other-keys)
@@ -45,20 +62,29 @@
See `auth-source-search' for details on SPEC."
(cl-assert (or (null type) (eq type (oref backend type)))
t "Invalid password-store search: %s %s")
- (when (listp host)
+ (when (consp host)
+ (warn "auth-source-pass ignores all but first host in spec.")
;; Take the first non-nil item of the list of hosts
(setq host (seq-find #'identity host)))
- (list (auth-source-pass--build-result host port user)))
+ (cond ((eq host t)
+ (warn "auth-source-pass does not handle host wildcards.")
+ nil)
+ ((null host)
+ ;; Do not build a result, as none will match when HOST is nil
+ nil)
+ (t
+ (when-let ((result (auth-source-pass--build-result host port user)))
+ (list result)))))
(defun auth-source-pass--build-result (host port user)
"Build auth-source-pass entry matching HOST, PORT and USER."
- (let ((entry (auth-source-pass--find-match host user)))
- (when entry
+ (let ((entry-data (auth-source-pass--find-match host user port)))
+ (when entry-data
(let ((retval (list
:host host
- :port (or (auth-source-pass-get "port" entry) port)
- :user (or (auth-source-pass-get "user" entry) user)
- :secret (lambda () (auth-source-pass-get 'secret entry)))))
+ :port (or (auth-source-pass--get-attr "port" entry-data) port)
+ :user (or (auth-source-pass--get-attr "user" entry-data) user)
+ :secret (lambda () (auth-source-pass--get-attr 'secret entry-data)))))
(auth-source-pass--do-debug "return %s as final result (plus hidden password)"
(seq-subseq retval 0 -2)) ;; remove password
retval))))
@@ -73,7 +99,7 @@ See `auth-source-search' for details on SPEC."
(defvar auth-source-pass-backend
(auth-source-backend
- (format "Password store")
+ (when (<= emacs-major-version 25) "password-store")
:source "." ;; not used
:type 'password-store
:search-function #'auth-source-pass-search)
@@ -84,9 +110,12 @@ See `auth-source-search' for details on SPEC."
(when (eq entry 'password-store)
(auth-source-backend-parse-parameters entry auth-source-pass-backend)))
-(add-hook 'auth-source-backend-parser-functions #'auth-source-pass-backend-parse)
+(if (boundp 'auth-source-backend-parser-functions)
+ (add-hook 'auth-source-backend-parser-functions #'auth-source-pass-backend-parse)
+ (advice-add 'auth-source-backend-parse :before-until #'auth-source-pass-backend-parse))
+;;;###autoload
(defun auth-source-pass-get (key entry)
"Return the value associated to KEY in the password-store entry ENTRY.
@@ -100,16 +129,25 @@ secret
key1: value1
key2: value2"
(let ((data (auth-source-pass-parse-entry entry)))
- (or (cdr (assoc key data))
- (and (string= key "user")
- (cdr (assoc "username" data))))))
+ (auth-source-pass--get-attr key data)))
+
+(defun auth-source-pass--get-attr (key entry-data)
+ "Return value associated with KEY in an ENTRY-DATA.
+
+ENTRY-DATA is the data from a parsed password-store entry.
+The key used to retrieve the password is the symbol `secret'.
+
+See `auth-source-pass-get'."
+ (or (cdr (assoc key entry-data))
+ (and (string= key "user")
+ (cdr (assoc "username" entry-data)))))
(defun auth-source-pass--read-entry (entry)
"Return a string with the file content of ENTRY."
(with-temp-buffer
(insert-file-contents (expand-file-name
(format "%s.gpg" entry)
- "~/.password-store"))
+ auth-source-pass-filename))
(buffer-substring-no-properties (point-min) (point-max))))
(defun auth-source-pass-parse-entry (entry)
@@ -124,12 +162,12 @@ ENTRY is the name of a password-store entry."
(defun auth-source-pass--parse-secret (contents)
"Parse the password-store data in the string CONTENTS and return its secret.
The secret is the first line of CONTENTS."
- (car (split-string contents "\\\n" t)))
+ (car (split-string contents "\n" t)))
(defun auth-source-pass--parse-data (contents)
"Parse the password-store data in the string CONTENTS and return an alist.
CONTENTS is the contents of a password-store formatted file."
- (let ((lines (split-string contents "\\\n" t "\\\s")))
+ (let ((lines (split-string contents "\n" t "[ \t]+")))
(seq-remove #'null
(mapcar (lambda (line)
(let ((pair (mapcar (lambda (s) (string-trim s))
@@ -139,115 +177,135 @@ CONTENTS is the contents of a password-store formatted file."
(mapconcat #'identity (cdr pair) ":")))))
(cdr lines)))))
-(defun auth-source-pass--user-match-p (entry user)
- "Return true iff ENTRY match USER."
- (or (null user)
- (string= user (auth-source-pass-get "user" entry))))
-
-(defun auth-source-pass--hostname (host)
- "Extract hostname from HOST."
- (let ((url (url-generic-parse-url host)))
- (or (url-host url) host)))
-
-(defun auth-source-pass--hostname-with-user (host)
- "Extract hostname and user from HOST."
- (let* ((url (url-generic-parse-url host))
- (user (url-user url))
- (hostname (url-host url)))
- (cond
- ((and user hostname) (format "%s@%s" user hostname))
- (hostname hostname)
- (t host))))
-
(defun auth-source-pass--do-debug (&rest msg)
"Call `auth-source-do-debug` with MSG and a prefix."
(apply #'auth-source-do-debug
- (cons (concat "auth-source-password-store: " (car msg))
+ (cons (concat "auth-source-pass: " (car msg))
(cdr msg))))
-(defun auth-source-pass--select-one-entry (entries user)
- "Select one entry from ENTRIES by searching for a field matching USER."
- (let ((number (length entries))
- (entry-with-user
- (and user
- (seq-find (lambda (entry)
- (string-equal (auth-source-pass-get "user" entry) user))
- entries))))
- (auth-source-pass--do-debug "found %s matches: %s" number
- (mapconcat #'identity entries ", "))
- (if entry-with-user
- (progn
- (auth-source-pass--do-debug "return %s as it contains matching user field"
- entry-with-user)
- entry-with-user)
- (auth-source-pass--do-debug "return %s as it is the first one" (car entries))
- (car entries))))
-
-(defun auth-source-pass--entry-valid-p (entry)
- "Return t iff ENTRY can be opened.
-Also displays a warning if not. This function is slow, don't call it too
-often."
- (if (auth-source-pass-parse-entry entry)
- t
- (auth-source-pass--do-debug "entry '%s' is not valid" entry)
- nil))
-
;; TODO: add tests for that when `assess-with-filesystem' is included
;; in Emacs
(defun auth-source-pass-entries ()
"Return a list of all password store entries."
- (let ((store-dir (expand-file-name "~/.password-store/")))
+ (let ((store-dir (expand-file-name auth-source-pass-filename)))
(mapcar
(lambda (file) (file-name-sans-extension (file-relative-name file store-dir)))
- (directory-files-recursively store-dir "\.gpg$"))))
-
-(defun auth-source-pass--find-all-by-entry-name (entryname user)
- "Search the store for all entries either matching ENTRYNAME/USER or ENTRYNAME.
-Only return valid entries as of `auth-source-pass--entry-valid-p'."
- (seq-filter (lambda (entry)
- (and
- (or
- (let ((components-host-user
- (member entryname (split-string entry "/"))))
- (and (= (length components-host-user) 2)
- (string-equal user (cadr components-host-user))))
- (string-equal entryname (file-name-nondirectory entry)))
- (auth-source-pass--entry-valid-p entry)))
- (auth-source-pass-entries)))
-
-(defun auth-source-pass--find-one-by-entry-name (entryname user)
- "Search the store for an entry matching ENTRYNAME.
+ (directory-files-recursively store-dir "\\.gpg$"))))
+
+(defun auth-source-pass--find-match (host user port)
+ "Return password-store entry data matching HOST, USER and PORT.
+
+Disambiguate between user provided inside HOST (e.g., user@server.com) and
+inside USER by giving priority to USER. Same for PORT."
+ (apply #'auth-source-pass--find-match-unambiguous (auth-source-pass--disambiguate host user port)))
+
+(defun auth-source-pass--disambiguate (host &optional user port)
+ "Return (HOST USER PORT) after disambiguation.
+Disambiguate between having user provided inside HOST (e.g.,
+user@server.com) and inside USER by giving priority to USER.
+Same for PORT."
+ (let* ((url (url-generic-parse-url (if (string-match-p ".*://" host)
+ host
+ (format "https://%s" host)))))
+ (list
+ (or (url-host url) host)
+ (or user (url-user url))
+ ;; url-port returns 443 (because of the https:// above) by default
+ (or port (number-to-string (url-port url))))))
+
+(defun auth-source-pass--find-match-unambiguous (hostname user port)
+ "Return password-store entry data matching HOSTNAME, USER and PORT.
+If many matches are found, return the first one. If no match is found,
+return nil.
+
+HOSTNAME should not contain any username or port number."
+ (let ((all-entries (auth-source-pass-entries))
+ (suffixes (auth-source-pass--generate-entry-suffixes hostname user port)))
+ (auth-source-pass--do-debug "searching for entries matching hostname=%S, user=%S, port=%S"
+ hostname (or user "") (or port ""))
+ (auth-source-pass--do-debug "corresponding suffixes to search for: %S" suffixes)
+ (catch 'auth-source-pass-break
+ (dolist (suffix suffixes)
+ (let* ((matching-entries (auth-source-pass--entries-matching-suffix suffix all-entries))
+ (best-entry-data (auth-source-pass--select-from-entries matching-entries user)))
+ (pcase (length matching-entries)
+ (0 (auth-source-pass--do-debug "found no entries matching %S" suffix))
+ (1 (auth-source-pass--do-debug "found 1 entry matching %S: %S"
+ suffix
+ (car matching-entries)))
+ (_ (auth-source-pass--do-debug "found %s entries matching %S: %S"
+ (length matching-entries)
+ suffix
+ matching-entries)))
+ (when best-entry-data
+ (throw 'auth-source-pass-break best-entry-data)))))))
+
+(defun auth-source-pass--select-from-entries (entries user)
+ "Return best matching password-store entry data from ENTRIES.
+
If USER is non nil, give precedence to entries containing a user field
matching USER."
- (auth-source-pass--do-debug "searching for '%s' in entry names (user: %s)"
- entryname
- user)
- (let ((matching-entries (auth-source-pass--find-all-by-entry-name entryname user)))
- (pcase (length matching-entries)
- (0 (auth-source-pass--do-debug "no match found")
- nil)
- (1 (auth-source-pass--do-debug "found 1 match: %s" (car matching-entries))
- (car matching-entries))
- (_ (auth-source-pass--select-one-entry matching-entries user)))))
-
-(defun auth-source-pass--find-match (host user)
- "Return a password-store entry name matching HOST and USER.
-If many matches are found, return the first one. If no match is
-found, return nil."
- (or
- (if (url-user (url-generic-parse-url host))
- ;; if HOST contains a user (e.g., "user@host.com"), <HOST>
- (auth-source-pass--find-one-by-entry-name (auth-source-pass--hostname-with-user host) user)
- ;; otherwise, if USER is provided, search for <USER>@<HOST>
- (when (stringp user)
- (auth-source-pass--find-one-by-entry-name (concat user "@" (auth-source-pass--hostname host)) user)))
- ;; if that didn't work, search for HOST without it's user component if any
- (auth-source-pass--find-one-by-entry-name (auth-source-pass--hostname host) user)
- ;; if that didn't work, remove subdomain: foo.bar.com -> bar.com
- (let ((components (split-string host "\\.")))
- (when (= (length components) 3)
- ;; start from scratch
- (auth-source-pass--find-match (mapconcat 'identity (cdr components) ".") user)))))
+ (let (fallback)
+ (catch 'auth-source-pass-break
+ (dolist (entry entries fallback)
+ (let ((entry-data (auth-source-pass-parse-entry entry)))
+ (when (and entry-data (not fallback))
+ (setq fallback entry-data)
+ (when (or (not user) (equal (auth-source-pass--get-attr "user" entry-data) user))
+ (throw 'auth-source-pass-break entry-data))))))))
+
+(defun auth-source-pass--entries-matching-suffix (suffix entries)
+ "Return entries matching SUFFIX.
+If ENTRIES is nil, use the result of calling `auth-source-pass-entries' instead."
+ (cl-remove-if-not
+ (lambda (entry) (string-match-p
+ (format "\\(^\\|/\\)%s$" (regexp-quote suffix))
+ entry))
+ (or entries (auth-source-pass-entries))))
+
+(defun auth-source-pass--generate-entry-suffixes (hostname user port)
+ "Return a list of possible entry path suffixes in the password-store.
+
+Based on the supported pathname patterns for HOSTNAME, USER, &
+PORT, return a list of possible suffixes for matching entries in
+the password-store."
+ (let ((domains (auth-source-pass--domains (split-string hostname "\\."))))
+ (seq-mapcat (lambda (n)
+ (auth-source-pass--name-port-user-suffixes n user port))
+ domains)))
+
+(defun auth-source-pass--domains (name-components)
+ "Return a list of possible domain names matching the hostname.
+
+This function takes a list of NAME-COMPONENTS, the strings
+separated by periods in the hostname, and returns a list of full
+domain names containing the trailing sequences of those
+components, from longest to shortest."
+ (cl-maplist (lambda (components) (mapconcat #'identity components "."))
+ name-components))
+
+(defun auth-source-pass--name-port-user-suffixes (name user port)
+ "Return a list of possible path suffixes for NAME, USER, & PORT.
+
+The resulting list is ordered from most specifc to least
+specific, with paths matching all of NAME, USER, & PORT first,
+then NAME & USER, then NAME & PORT, then just NAME."
+ (seq-mapcat
+ #'identity
+ (list
+ (when (and user port)
+ (list
+ (format "%s@%s%s%s" user name auth-source-pass-port-separator port)
+ (format "%s%s%s/%s" name auth-source-pass-port-separator port user)))
+ (when user
+ (list
+ (format "%s@%s" user name)
+ (format "%s/%s" name user)))
+ (when port
+ (list
+ (format "%s%s%s" name auth-source-pass-port-separator port)))
+ (list
+ (format "%s" name)))))
(provide 'auth-source-pass)
;;; auth-source-pass.el ends here