Subject: Re: bug#36052: 26.2.50; [PATCH] Improve auth-source-pass

From: Damien Cassou <[email-masked]>
Date: Fri, 14 Jun 2019 09:10:51 +0200

Hi everyone,

after yet another detailed review from Noam, here is my new series of
patch. I hopefully took care of every feedback I got.

Thank you so much to the reviewers, you are doing an amazing job.

-- 
Damien Cassou
http://damiencassou.seasidehosting.st

"Success is the ability to go from one failure to another without
losing enthusiasm." --Winston Churchill
>From e6948470878682577a976dcca41d7ae7af2c39c9 Mon Sep 17 00:00:00 2001
From: Magnus Henoch <[email-masked]>
Date: Fri, 2 Nov 2018 21:51:59 +0000
Subject: [PATCH 01/13] Fix auth-source-pass to return nil if no entry found

* lisp/auth-source-pass.el (auth-source-pass-search): If there is no
matching entry, auth-source-pass-search should return nil, not (nil).
This lets auth-source fall back to other backends in the auth-sources
list.
* test/lisp/auth-source-pass-tests.el: Add corresponding test.

Copyright-paperwork-exempt: yes
---
 lisp/auth-source-pass.el            | 3 ++-
 test/lisp/auth-source-pass-tests.el | 5 +++++
 2 files changed, 7 insertions(+), 1 deletion(-)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index 4283ed0392..c82c90167a 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -56,7 +56,8 @@
          ;; Do not build a result, as none will match when HOST is nil
          nil)
         (t
-         (list (auth-source-pass--build-result host port user)))))
+         (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."
diff --git a/test/lisp/auth-source-pass-tests.el b/test/lisp/auth-source-pass-tests.el
index d1e486ad6b..ab9ef92c14 100644
--- a/test/lisp/auth-source-pass-tests.el
+++ b/test/lisp/auth-source-pass-tests.el
@@ -83,6 +83,11 @@ auth-source-pass--with-store
                                   ("bar"))
     (should-not (auth-source-pass-search :host nil))))
 
+(ert-deftest auth-source-pass-not-found ()
+  (auth-source-pass--with-store '(("foo" ("port" . "foo-port") ("host" . "foo-user"))
+                                  ("bar"))
+    (should-not (auth-source-pass-search :host "baz"))))
+
 
 (ert-deftest auth-source-pass-find-match-matching-at-entry-name ()
   (auth-source-pass--with-store '(("foo"))
-- 
2.21.0

>From b4778d0e47c8b6fea2f5c805bb830dd169cb8b75 Mon Sep 17 00:00:00 2001
From: Damien Cassou <[email-masked]>
Date: Sat, 3 Nov 2018 09:06:42 +0100
Subject: [PATCH 02/13] * lisp/auth-source-pass.el: Version 4.0.2

---
 lisp/auth-source-pass.el | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index c82c90167a..d092292e51 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -4,7 +4,7 @@
 
 ;; Author: Damien Cassou <[email-masked]>,
 ;;         Nicolas Petton <[email-masked]>
-;; Version: 4.0.1
+;; Version: 4.0.2
 ;; Package-Requires: ((emacs "25"))
 ;; Url: https://github.com/DamienCassou/auth-password-store
 ;; Created: 07 Jun 2015
-- 
2.21.0

>From fe947369b636e85adf0649124c09800076698567 Mon Sep 17 00:00:00 2001
From: Damien Cassou <[email-masked]>
Date: Tue, 6 Nov 2018 14:45:20 +0100
Subject: [PATCH 03/13] * lisp/auth-source-pass.el (auth-source-pass-get): Add
 autoload

---
 lisp/auth-source-pass.el | 1 +
 1 file changed, 1 insertion(+)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index d092292e51..4fcb1015e7 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -98,6 +98,7 @@ 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.
 
-- 
2.21.0

>From 9609227fb0b1c38471835ff9bd81217e3568562c Mon Sep 17 00:00:00 2001
From: galaunay <[email-masked]>
Date: Sun, 13 Jan 2019 21:30:53 +0000
Subject: [PATCH 04/13] Add auth-source-pass-filename option

* lisp/auth-source-pass.el (auth-source-pass)
(auth-source-pass-filename): Add option to specify a customized
password-store path.
(auth-source-pass--read-entry)
(auth-source-pass-entries): Use the new option instead of hard-coded
`~/.password-store'.
---
 lisp/auth-source-pass.el | 15 +++++++++++++--
 1 file changed, 13 insertions(+), 2 deletions(-)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index 4fcb1015e7..1fda698232 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -38,6 +38,17 @@
 (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")
+
 (cl-defun auth-source-pass-search (&rest spec
                                          &key backend type host user port
                                          &allow-other-keys)
@@ -121,7 +132,7 @@ auth-source-pass--read-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)
@@ -188,7 +199,7 @@ auth-source-pass--entry-valid-p
 ;; 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$"))))
-- 
2.21.0

>From b403aad81204d92505b78aadb7fb16541247c22d Mon Sep 17 00:00:00 2001
From: Iku Iwasa <[email-masked]>
Date: Sun, 7 Apr 2019 17:59:59 +0900
Subject: [PATCH 05/13] Add auth-source-pass-port-separator option

* lisp/auth-source-pass.el (auth-source-pass-port-separator): New
option to specify separator between host and port, default to
colon (":").
(auth-source-pass--find-match-unambiguous): Adapt to make use of the
new variable.
* test/lisp/auth-source-pass-tests.el: Add corresponding tests.
---
 lisp/auth-source-pass.el            | 17 ++++++++++++++---
 test/lisp/auth-source-pass-tests.el | 11 +++++++++++
 2 files changed, 25 insertions(+), 3 deletions(-)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index 1fda698232..626dbf842c 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -49,6 +49,11 @@ auth-source-pass-filename
   :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)
@@ -254,9 +259,15 @@ auth-source-pass--find-match-unambiguous
 
 HOSTNAME should not contain any username or port number."
   (or
-   (and user port (auth-source-pass--find-one-by-entry-name (format "%s@%s:%s" user hostname port) user))
-   (and user (auth-source-pass--find-one-by-entry-name (format "%s@%s" user hostname) user))
-   (and port (auth-source-pass--find-one-by-entry-name (format "%s:%s" hostname port) nil))
+   (and user port (auth-source-pass--find-one-by-entry-name
+                   (format "%s@%s%s%s" user hostname auth-source-pass-port-separator port)
+                   user))
+   (and user (auth-source-pass--find-one-by-entry-name
+              (format "%s@%s" user hostname)
+              user))
+   (and port (auth-source-pass--find-one-by-entry-name
+              (format "%s%s%s" hostname auth-source-pass-port-separator port)
+              nil))
    (auth-source-pass--find-one-by-entry-name hostname user)
    ;; if that didn't work, remove subdomain: foo.bar.com -> bar.com
    (let ((components (split-string hostname "\\.")))
diff --git a/test/lisp/auth-source-pass-tests.el b/test/lisp/auth-source-pass-tests.el
index ab9ef92c14..ae7a696bc6 100644
--- a/test/lisp/auth-source-pass-tests.el
+++ b/test/lisp/auth-source-pass-tests.el
@@ -186,6 +186,17 @@ auth-source-pass--with-store
     (should (equal (auth-source-pass--find-match "host.com:8888" "someuser" nil)
                    "host.com"))))
 
+(ert-deftest auth-source-pass-find-host-with-port ()
+  (auth-source-pass--with-store '(("host.com:443"))
+    (should (equal (auth-source-pass--find-match "host.com" "someuser" "443")
+                   "host.com:443"))))
+
+(ert-deftest auth-source-pass-find-host-with-custom-port-separator ()
+  (let ((auth-source-pass-port-separator "#"))
+    (auth-source-pass--with-store '(("host.com#443"))
+      (should (equal (auth-source-pass--find-match "host.com" "someuser" "443")
+                     "host.com#443")))))
+
 (defmacro auth-source-pass--with-store-find-foo (store &rest body)
   "Use STORE while executing BODY.  \"foo\" is the matched entry."
   (declare (indent 1))
-- 
2.21.0

>From 2171a0c8e4e33e1791d6f194370f695452a6037c Mon Sep 17 00:00:00 2001
From: Keith Amidon <[email-masked]>
Date: Thu, 14 Feb 2019 10:27:31 -0800
Subject: [PATCH 06/13] Fix auth-source-pass to search for
 hostname:port/username

auth-source-pass supports entries with username either prefixed to the
hostname with an @ as separator or in a subdirectory under the
hostname.  This was true when there was no port or service included in
the name, but got broken with the introduction of
auth-source-pass-port-separator.

* lisp/auth-source-pass.el (auth-source-pass--find-match-unambiguous): Fix
to match hostname:port/username.
* test/lisp/auth-source-pass-tests.el: Add corresponding tests.
---
 lisp/auth-source-pass.el            |  3 +++
 test/lisp/auth-source-pass-tests.el | 11 +++++++++++
 2 files changed, 14 insertions(+)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index 626dbf842c..4aa0853be9 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -262,6 +262,9 @@ auth-source-pass--find-match-unambiguous
    (and user port (auth-source-pass--find-one-by-entry-name
                    (format "%s@%s%s%s" user hostname auth-source-pass-port-separator port)
                    user))
+   (and user port (auth-source-pass--find-one-by-entry-name
+                   (format "%s%s%s" hostname auth-source-pass-port-separator port)
+                   user))
    (and user (auth-source-pass--find-one-by-entry-name
               (format "%s@%s" user hostname)
               user))
diff --git a/test/lisp/auth-source-pass-tests.el b/test/lisp/auth-source-pass-tests.el
index ae7a696bc6..1539d9611f 100644
--- a/test/lisp/auth-source-pass-tests.el
+++ b/test/lisp/auth-source-pass-tests.el
@@ -144,6 +144,17 @@ auth-source-pass--with-store
     (should (equal (auth-source-pass--find-match "foo.bar.com" nil nil)
                    nil))))
 
+(ert-deftest auth-source-pass-find-match-matching-host-port-and-subdir-user ()
+  (auth-source-pass--with-store '(("bar.com:443/someone"))
+    (should (equal (auth-source-pass--find-match "bar.com" "someone" "443")
+                   "bar.com:443/someone"))))
+
+(ert-deftest auth-source-pass-find-match-matching-host-port-and-subdir-user-with-custom-separator ()
+  (let ((auth-source-pass-port-separator "#"))
+    (auth-source-pass--with-store '(("bar.com#443/someone"))
+      (should (equal (auth-source-pass--find-match "bar.com" "someone" "443")
+                     "bar.com#443/someone")))))
+
 (ert-deftest auth-source-pass-find-match-matching-extracting-user-from-host ()
   (auth-source-pass--with-store '(("foo.com/bar"))
     (should (equal (auth-source-pass--find-match "https://[email-masked]" nil nil)
-- 
2.21.0

>From 4263c5673e0df4231921578d371396eec80c8f04 Mon Sep 17 00:00:00 2001
From: Keith Amidon <[email-masked]>
Date: Tue, 30 Apr 2019 07:52:14 -0700
Subject: [PATCH 07/13] Split out the attribute retrieval form
 auth-source-pass-get

Eliminate the need to repeatedly retrieve and parse the data for the
entry.  This is generally a good thing since it eliminates repetitions
of the same crypto and parsing operations.  It is especially valuable
when protecting an entry with a yubikey with touch required for crypto
operations as it eliminates the need to touch the yubikey sensor for
each attribute retrieved.

* lisp/auth-source-pass.el (auth-source-pass-get): Extract some code to
`auth-source-pass--get-attr'.
(auth-source-pass--get-attr): New function to get a field value from a
parsed entry.
(auth-source-pass--build-result): Make use of
`auth-source-pass--get-attr` to avoid repeated parsing.
---
 lisp/auth-source-pass.el | 26 ++++++++++++++++++--------
 1 file changed, 18 insertions(+), 8 deletions(-)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index 4aa0853be9..a0b0841e1f 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -79,11 +79,12 @@ auth-source-pass--build-result
   "Build auth-source-pass entry matching HOST, PORT and USER."
   (let ((entry (auth-source-pass--find-match host user port)))
     (when entry
-      (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)))))
+      (let* ((entry-data (auth-source-pass-parse-entry entry))
+             (retval (list
+                      :host host
+                      :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))))
@@ -128,9 +129,18 @@ auth-source-pass-get
 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."
-- 
2.21.0

>From fc79728f4d54249902175a5dd389b84d1352ad1b Mon Sep 17 00:00:00 2001
From: Keith Amidon <[email-masked]>
Date: Sun, 5 May 2019 20:21:43 -0700
Subject: [PATCH 08/13] Minimize entry parsing in auth-source-pass

Prior to this commit, while searching for the most applicable entry
password-store entries were decrypted and parsed to ensure they were
valid.  The entries were parsed in the order they were found on the
filesystem and all applicable entries would be decrypted and parsed,
which varied based on the contents of the password-store and the entry
to be found.

This is fine when the GPG key is cached and each entry can be
decrypted without user interaction.  However, for security some people
have their GPG on a hardware token like a Yubikey setup so that they
have to touch a sensor on the toke for every cryptographic operation,
in which case it becomes inconvenient as each attempt to find an entry
requires a variable number of touches of the hardware token.

The implementation already assumes that names which contain more of
the information in the search key should be preferred so there is an
ordering of preference of applicable entries.  If the decrypt and
parsing is removed from the initial identification of applicable
entries in the store then in most cases a single decrypt and parse of
the most preferred entry will suffice, improving the experience for
hardware token users that require interaction with the token.

This commit implements that strategy.  It is in spirit a refactor of
the existing code.

* lisp/auth-source-pass.el (auth-source-pass--matching-entries): New
function, generate an ordered list of regular expression matchers for
all possible names that could be in the password-store for the entry
to be found and then makes a pass over the password-store entry names
accumulating the matching entries in a list after the regexp that
matched.  This implementation ensures the password-store entry list
still only has to be scanned once.
(auth-source-pass--find-match-unambiguous): Use it to obtain candidate
entries and then parse them one by one until an entry containing the
desired information is located.  When complete, return the parsed data
of the entry instead of the entry name so that the information can be
used directly to construct the auth-source response.
(auth-source-pass--build-result): Update accordingly.
(auth-source-pass--find-match): Update docstring accordingly.
(auth-source-pass--select-one-entry)
(auth-source-pass--entry-valid-p)
(auth-source-pass--find-all-by-entry-name)
(auth-source-pass--find-one-by-entry-name): Remove.
(auth-source-pass--select-from-entries)
(auth-source-pass--accumulate-matches)
(auth-source-pass--entry-reducer)
(auth-source-pass--generate-entry-suffixes)
(auth-source-pass--domains)
(auth-source-pass--name-port-user-suffixes): New functions.

* test/lisp/auth-source-pass-tests.el: One test case was added to the
test suite to verify that only the minimal number of entries are
parsed in common cases.  The
auth-source-pass-only-return-entries-that-can-be-open test case had to
be re-implemented because the function it was used eliminated as the
functionality is provided elsewhere.  All the other fairly substantial
changes to the test suite are the result of mechanical changes that
were required to adapt to auth-source-pass--find-match returning the
data from a parsed password-store entry instead of the entry name.
---
 lisp/auth-source-pass.el            | 209 ++++++++++++----------
 test/lisp/auth-source-pass-tests.el | 264 +++++++++++++++++-----------
 2 files changed, 281 insertions(+), 192 deletions(-)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index a0b0841e1f..bcb215a6ac 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -77,14 +77,13 @@ auth-source-pass-port-separator
 
 (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 port)))
-    (when entry
-      (let* ((entry-data (auth-source-pass-parse-entry entry))
-             (retval (list
-                      :host host
-                      :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)))))
+  (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-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))))
@@ -183,33 +182,6 @@ auth-source-pass--do-debug
          (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 ()
@@ -219,37 +191,8 @@ auth-source-pass-entries
      (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.
-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 port)
-  "Return a password-store entry name matching HOST, USER and PORT.
+  "Return password-store entry data matching HOST, USER and PORT.
 
 Disambiguate between user provided inside HOST (e.g., [email-masked]) and
 inside USER by giving priority to USER.  Same for PORT."
@@ -263,33 +206,123 @@ auth-source-pass--find-match
      (or port (number-to-string (url-port url))))))
 
 (defun auth-source-pass--find-match-unambiguous (hostname user port)
-  "Return a password-store entry name matching HOSTNAME, USER and 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."
-  (or
-   (and user port (auth-source-pass--find-one-by-entry-name
-                   (format "%s@%s%s%s" user hostname auth-source-pass-port-separator port)
-                   user))
-   (and user port (auth-source-pass--find-one-by-entry-name
-                   (format "%s%s%s" hostname auth-source-pass-port-separator port)
-                   user))
-   (and user (auth-source-pass--find-one-by-entry-name
-              (format "%s@%s" user hostname)
-              user))
-   (and port (auth-source-pass--find-one-by-entry-name
-              (format "%s%s%s" hostname auth-source-pass-port-separator port)
-              nil))
-   (auth-source-pass--find-one-by-entry-name hostname user)
-   ;; if that didn't work, remove subdomain: foo.bar.com -> bar.com
-   (let ((components (split-string hostname "\\.")))
-     (when (= (length components) 3)
-       ;; start from scratch
-       (auth-source-pass--find-match-unambiguous
-        (mapconcat 'identity (cdr components) ".")
-        user
-        port)))))
+  (cl-reduce
+   (lambda (result entries)
+     (or result
+         (pcase (length entries)
+           (0 nil)
+           (1 (auth-source-pass-parse-entry (car entries)))
+           (_ (auth-source-pass--select-from-entries entries user)))))
+   (auth-source-pass--matching-entries hostname user port)
+   :initial-value nil))
+
+(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."
+  (cl-reduce
+   (lambda (result entry)
+     (let ((entry-data (auth-source-pass-parse-entry entry)))
+       (cond ((equal (auth-source-pass--get-attr "user" result) user)
+              result)
+             ((equal (auth-source-pass--get-attr "user" entry-data) user)
+              entry-data)
+             (t
+              result))))
+   entries
+   :initial-value (auth-source-pass-parse-entry (car entries))))
+
+(defun auth-source-pass--matching-entries (hostname user port)
+  "Return all matching password-store entries for HOSTNAME, USER, & PORT.
+
+The result is a list of lists of password-store entries, where
+each sublist contains entries that actually exist in the
+password-store matching one of the entry name formats that
+auth-source-pass expects, most specific to least specific."
+  (let* ((entries-lists (mapcar
+                         #'cdr
+                         (auth-source-pass--accumulate-matches hostname user port)))
+         (entries (apply #'cl-concatenate (cons 'list entries-lists))))
+    (if entries
+        (auth-source-pass--do-debug (format "found: %S" entries))
+      (auth-source-pass--do-debug "no matches found"))
+    entries-lists))
+
+(defun auth-source-pass--accumulate-matches (hostname user port)
+  "Accumulate matching password-store entries into sublists.
+
+Entries matching supported formats that combine HOSTNAME, USER, &
+PORT are accumulated into sublists where the car of each sublist
+is a regular expression for matching paths in the password-store
+and the remainder is the list of matching entries."
+  (let ((suffix-match-lists
+         (mapcar (lambda (suffix) (list (format "\\(^\\|/\\)%s$" suffix)))
+                 (auth-source-pass--generate-entry-suffixes hostname user port))))
+    (cl-reduce #'auth-source-pass--entry-reducer
+               (auth-source-pass-entries)
+               :initial-value suffix-match-lists)))
+
+(defun auth-source-pass--entry-reducer (match-lists entry)
+  "Match MATCH-LISTS sublists against ENTRY.
+
+The result is a copy of match-lists with the entry added to the
+end of any sublists for which the regular expression at the head
+of the list matches the entry name."
+  (mapcar (lambda (match-list)
+            (if (string-match (car match-list) entry)
+                (append match-list (list entry))
+              match-list))
+          match-lists))
+
+(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
diff --git a/test/lisp/auth-source-pass-tests.el b/test/lisp/auth-source-pass-tests.el
index 1539d9611f..2c28f79945 100644
--- a/test/lisp/auth-source-pass-tests.el
+++ b/test/lisp/auth-source-pass-tests.el
@@ -63,14 +63,19 @@ auth-source-pass--debug
 This function is intended to be set to `auth-source-debug`."
   (add-to-list 'auth-source-pass--debug-log (apply #'format msg) t))
 
+(defvar auth-source-pass--parse-log nil)
+
 (defmacro auth-source-pass--with-store (store &rest body)
   "Use STORE as password-store while executing BODY."
   (declare (indent 1))
-  `(cl-letf (((symbol-function 'auth-source-pass-parse-entry) (lambda (entry) (cdr (cl-find entry ,store :key #'car :test #'string=))) )
-             ((symbol-function 'auth-source-pass-entries) (lambda () (mapcar #'car ,store)))
-             ((symbol-function 'auth-source-pass--entry-valid-p) (lambda (_entry) t)))
+  `(cl-letf (((symbol-function 'auth-source-pass-parse-entry)
+              (lambda (entry)
+                (add-to-list 'auth-source-pass--parse-log entry)
+                (cdr (cl-find entry ,store :key #'car :test #'string=))))
+             ((symbol-function 'auth-source-pass-entries) (lambda () (mapcar #'car ,store))))
      (let ((auth-source-debug #'auth-source-pass--debug)
-           (auth-source-pass--debug-log nil))
+           (auth-source-pass--debug-log nil)
+           (auth-source-pass--parse-log nil))
        ,@body)))
 
 (ert-deftest auth-source-pass-any-host ()
@@ -88,125 +93,184 @@ auth-source-pass--with-store
                                   ("bar"))
     (should-not (auth-source-pass-search :host "baz"))))
 
+(ert-deftest auth-source-pass-find-match-minimal-parsing ()
+  (let ((store-contents
+         '(("baz" ("secret" . "baz password"))
+           ("baz:123" ("secret" . "baz:123 password"))
+           ("baz/foo" ("secret" . "baz/foo password"))
+           ("foo@baz" ("secret" . "foo@baz password"))
+           ("baz:123/foo" ("secret" . "baz:123/foo password"))
+           ("foo@baz:123" ("secret" . "foo@baz:123 password"))
+           ("bar.baz" ("secret" . "bar.baz password"))
+           ("bar.baz:123" ("secret" . "bar.baz:123 password"))
+           ("bar.baz/foo" ("secret" . "bar.baz/foo password"))
+           ("[email-masked]" ("secret" . "[email-masked] password"))
+           ("bar.baz:123/foo" ("secret" . "bar.baz:123/foo password"))
+           ("[email-masked]:123" ("secret" . "[email-masked]:123 password")))))
+    (auth-source-pass--with-store store-contents
+      (auth-source-pass--find-match "bar.baz" "foo" "123")
+      (should (equal auth-source-pass--parse-log '("[email-masked]:123"))))
+    (auth-source-pass--with-store store-contents
+      (auth-source-pass--find-match "bar.baz" "foo" nil)
+      (should (equal auth-source-pass--parse-log '("[email-masked]"))))
+    (auth-source-pass--with-store store-contents
+      (auth-source-pass--find-match "bar.baz" nil "123")
+      (should (equal auth-source-pass--parse-log '("bar.baz:123"))))
+    (auth-source-pass--with-store store-contents
+      (auth-source-pass--find-match "bar.baz" nil nil)
+      (should (equal auth-source-pass--parse-log '("bar.baz"))))
+    (auth-source-pass--with-store store-contents
+      (auth-source-pass--find-match "baz" nil nil)
+      (should (equal auth-source-pass--parse-log '("baz"))))))
 
 (ert-deftest auth-source-pass-find-match-matching-at-entry-name ()
-  (auth-source-pass--with-store '(("foo"))
-    (should (equal (auth-source-pass--find-match "foo" nil nil)
-                   "foo"))))
+  (auth-source-pass--with-store
+      '(("foo" ("secret" . "foo password")))
+    (let ((result (auth-source-pass--find-match "foo" nil nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "foo password")))))
 
 (ert-deftest auth-source-pass-find-match-matching-at-entry-name-part ()
-  (auth-source-pass--with-store '(("foo"))
-    (should (equal (auth-source-pass--find-match "https://foo" nil nil)
-                   "foo"))))
+  (auth-source-pass--with-store
+      '(("foo" ("secret" . "foo password")))
+    (let ((result (auth-source-pass--find-match "https://foo" nil nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "foo password")))))
 
 (ert-deftest auth-source-pass-find-match-matching-at-entry-name-ignoring-user ()
-  (auth-source-pass--with-store '(("foo"))
-    (should (equal (auth-source-pass--find-match "https://SomeUser@foo" nil nil)
-                   "foo"))))
+  (auth-source-pass--with-store
+      '(("foo" ("secret" . "foo password")))
+    (let ((result (auth-source-pass--find-match "https://SomeUser@foo" nil nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "foo password")))))
 
 (ert-deftest auth-source-pass-find-match-matching-at-entry-name-with-user ()
-  (auth-source-pass--with-store '(("SomeUser@foo"))
-    (should (equal (auth-source-pass--find-match "https://SomeUser@foo" nil nil)
-                   "SomeUser@foo"))))
+  (auth-source-pass--with-store
+      '(("SomeUser@foo" ("secret" . "SomeUser@foo password")))
+    (let ((result (auth-source-pass--find-match "https://SomeUser@foo" nil nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "SomeUser@foo password")))))
 
 (ert-deftest auth-source-pass-find-match-matching-at-entry-name-prefer-full ()
-  (auth-source-pass--with-store '(("SomeUser@foo") ("foo"))
-    (should (equal (auth-source-pass--find-match "https://SomeUser@foo" nil nil)
-                   "SomeUser@foo"))))
+  (auth-source-pass--with-store
+      '(("SomeUser@foo" ("secret" . "SomeUser@foo password"))
+        ("foo" ("secret" . "foo password")))
+    (let ((result (auth-source-pass--find-match "https://SomeUser@foo" nil nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "SomeUser@foo password")))))
 
 (ert-deftest auth-source-pass-find-match-matching-at-entry-name-prefer-full-reversed ()
-  (auth-source-pass--with-store '(("foo") ("SomeUser@foo"))
-    (should (equal (auth-source-pass--find-match "https://SomeUser@foo" nil nil)
-                   "SomeUser@foo"))))
-
-(ert-deftest auth-source-pass-find-match-matching-at-entry-name-without-subdomain ()
+  (auth-source-pass--with-store
+      '(("foo" ("secret" . "foo password"))
+        ("SomeUser@foo" ("secret" . "SomeUser@foo password")))
+    (let ((result (auth-source-pass--find-match "https://SomeUser@foo" nil nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "SomeUser@foo password")))))
+
+(ert-deftest auth-source-pass-matching-entries-name-without-subdomain ()
   (auth-source-pass--with-store '(("bar.com"))
-    (should (equal (auth-source-pass--find-match "foo.bar.com" nil nil)
-                   "bar.com"))))
+    (should (equal (auth-source-pass--matching-entries "foo.bar.com" nil nil)
+                   '(nil ("bar.com") nil)))))
 
-(ert-deftest auth-source-pass-find-match-matching-at-entry-name-without-subdomain-with-user ()
+(ert-deftest auth-source-pass-matching-entries-name-without-subdomain-with-user ()
   (auth-source-pass--with-store '(("[email-masked]"))
-    (should (equal (auth-source-pass--find-match "foo.bar.com" "someone" nil)
-                   "[email-masked]"))))
+    (should (equal (auth-source-pass--matching-entries "foo.bar.com" "someone" nil)
+                   '(nil nil nil ("[email-masked]") nil nil nil nil nil)))))
 
-(ert-deftest auth-source-pass-find-match-matching-at-entry-name-without-subdomain-with-bad-user ()
+(ert-deftest auth-source-pass-matching-entries-name-without-subdomain-with-bad-user ()
   (auth-source-pass--with-store '(("[email-masked]"))
-    (should (equal (auth-source-pass--find-match "foo.bar.com" "someone" nil)
-                   nil))))
+    (should (equal (auth-source-pass--matching-entries "foo.bar.com" "someone" nil)
+                   '(nil nil nil nil nil nil nil nil nil)))))
 
-(ert-deftest auth-source-pass-find-match-matching-at-entry-name-without-subdomain-prefer-full ()
+(ert-deftest auth-source-pass-matching-entries-name-without-subdomain-prefer-full ()
   (auth-source-pass--with-store '(("bar.com") ("foo.bar.com"))
-    (should (equal (auth-source-pass--find-match "foo.bar.com" nil nil)
-                   "foo.bar.com"))))
+    (should (equal (auth-source-pass--matching-entries "foo.bar.com" nil nil)
+                   '(("foo.bar.com") ("bar.com") nil)))))
 
 (ert-deftest auth-source-pass-dont-match-at-folder-name ()
   (auth-source-pass--with-store '(("foo.bar.com/foo"))
-    (should (equal (auth-source-pass--find-match "foo.bar.com" nil nil)
-                   nil))))
+    (should (equal (auth-source-pass--matching-entries "foo.bar.com" nil nil)
+                   '(nil nil nil)))))
 
-(ert-deftest auth-source-pass-find-match-matching-host-port-and-subdir-user ()
+(ert-deftest auth-source-pass-matching-entries-host-port-and-subdir-user ()
   (auth-source-pass--with-store '(("bar.com:443/someone"))
-    (should (equal (auth-source-pass--find-match "bar.com" "someone" "443")
-                   "bar.com:443/someone"))))
+    (should (equal (auth-source-pass--matching-entries "bar.com" "someone" "443")
+                   '(nil ("bar.com:443/someone") nil nil nil nil
+                         nil nil nil nil nil nil)))))
 
-(ert-deftest auth-source-pass-find-match-matching-host-port-and-subdir-user-with-custom-separator ()
+(ert-deftest auth-source-pass-matching-entries-host-port-and-subdir-user-with-custom-separator ()
   (let ((auth-source-pass-port-separator "#"))
     (auth-source-pass--with-store '(("bar.com#443/someone"))
-      (should (equal (auth-source-pass--find-match "bar.com" "someone" "443")
-                     "bar.com#443/someone")))))
-
-(ert-deftest auth-source-pass-find-match-matching-extracting-user-from-host ()
-  (auth-source-pass--with-store '(("foo.com/bar"))
-    (should (equal (auth-source-pass--find-match "https://[email-masked]" nil nil)
-                   "foo.com/bar"))))
-
-(ert-deftest auth-source-pass-search-with-user-first ()
+      (should (equal (auth-source-pass--matching-entries "bar.com" "someone" "443")
+                     '(nil ("bar.com#443/someone") nil nil nil nil
+                           nil nil nil nil nil nil))))))
+
+(ert-deftest auth-source-pass-matching-entries-extracting-user-from-host ()
+  (auth-source-pass--with-store
+      '(("foo.com/bar" ("secret" . "foo.com/bar password")))
+    (let ((result (auth-source-pass--find-match "https://[email-masked]" nil nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "foo.com/bar password")))))
+
+(ert-deftest auth-source-pass-matching-entries-with-user-first ()
   (auth-source-pass--with-store '(("foo") ("user@foo"))
-    (should (equal (auth-source-pass--find-match "foo" "user" nil)
-                   "user@foo"))
-    (auth-source-pass--should-have-message-containing "Found 1 match")))
+    (should (equal (auth-source-pass--matching-entries "foo" "user" nil)
+                   '(("user@foo") nil ("foo"))))
+    (auth-source-pass--should-have-message-containing "found: (\"user@foo\" \"foo\"")))
 
 (ert-deftest auth-source-pass-give-priority-to-desired-user ()
-  (auth-source-pass--with-store '(("foo") ("subdir/foo" ("user" . "someone")))
-    (should (equal (auth-source-pass--find-match "foo" "someone" nil)
-                   "subdir/foo"))
-    (auth-source-pass--should-have-message-containing "Found 2 matches")
-    (auth-source-pass--should-have-message-containing "matching user field")))
+  (auth-source-pass--with-store
+      '(("foo" ("secret" . "foo password"))
+        ("subdir/foo" ("secret" . "subdir/foo password") ("user" . "someone")))
+    (let ((result (auth-source-pass--find-match "foo" "someone" nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "subdir/foo password"))
+      (should (equal (auth-source-pass--get-attr "user" result)
+                     "someone")))
+    (auth-source-pass--should-have-message-containing "found: (\"foo\" \"subdir/foo\"")))
 
 (ert-deftest auth-source-pass-give-priority-to-desired-user-reversed ()
-  (auth-source-pass--with-store '(("foo" ("user" . "someone")) ("subdir/foo"))
-    (should (equal (auth-source-pass--find-match "foo" "someone" nil)
-                   "foo"))
-    (auth-source-pass--should-have-message-containing "Found 2 matches")
-    (auth-source-pass--should-have-message-containing "matching user field")))
+  (auth-source-pass--with-store
+      '(("foo" ("secret" . "foo password") ("user" . "someone"))
+        ("subdir/foo" ("secret" . "subdir/foo password")))
+    (let ((result (auth-source-pass--find-match "foo" "someone" nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "foo password")))
+    (auth-source-pass--should-have-message-containing "found: (\"foo\" \"subdir/foo\"")))
 
 (ert-deftest auth-source-pass-return-first-when-several-matches ()
-  (auth-source-pass--with-store '(("foo") ("subdir/foo"))
-    (should (equal (auth-source-pass--find-match "foo" nil nil)
-                   "foo"))
-    (auth-source-pass--should-have-message-containing "Found 2 matches")
-    (auth-source-pass--should-have-message-containing "the first one")))
-
-(ert-deftest auth-source-pass-make-divansantana-happy ()
+  (auth-source-pass--with-store
+      '(("foo" ("secret" . "foo password"))
+        ("subdir/foo" ("secret" . "subdir/foo password")))
+    (let ((result (auth-source-pass--find-match "foo" nil nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "foo password")))
+    (auth-source-pass--should-have-message-containing "found: (\"foo\" \"subdir/foo\"")))
+
+(ert-deftest auth-source-pass-matching-entries-make-divansantana-happy ()
   (auth-source-pass--with-store '(("host.com"))
-    (should (equal (auth-source-pass--find-match "smtp.host.com" "[email-masked]" nil)
-                   "host.com"))))
+    (should (equal (auth-source-pass--matching-entries "smtp.host.com" "[email-masked]" nil)
+                   '(nil nil nil nil nil ("host.com") nil nil nil)))))
 
 (ert-deftest auth-source-pass-find-host-without-port ()
-  (auth-source-pass--with-store '(("host.com"))
-    (should (equal (auth-source-pass--find-match "host.com:8888" "someuser" nil)
-                   "host.com"))))
+  (auth-source-pass--with-store
+      '(("host.com" ("secret" . "host.com password")))
+    (let ((result (auth-source-pass--find-match "host.com:8888" "someuser" nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "host.com password")))))
 
-(ert-deftest auth-source-pass-find-host-with-port ()
+(ert-deftest auth-source-pass-matching-entries-host-with-port ()
   (auth-source-pass--with-store '(("host.com:443"))
-    (should (equal (auth-source-pass--find-match "host.com" "someuser" "443")
-                   "host.com:443"))))
+    (should (equal (auth-source-pass--matching-entries "host.com" "someuser" "443")
+                   '(nil nil nil nil ("host.com:443") nil
+                         nil nil nil nil nil nil)))))
 
-(ert-deftest auth-source-pass-find-host-with-custom-port-separator ()
+(ert-deftest auth-source-pass-matching-entries-with-custom-port-separator ()
   (let ((auth-source-pass-port-separator "#"))
     (auth-source-pass--with-store '(("host.com#443"))
-      (should (equal (auth-source-pass--find-match "host.com" "someuser" "443")
-                     "host.com#443")))))
+      (should (equal (auth-source-pass--matching-entries "host.com" "someuser" "443")
+                     '(nil nil nil nil ("host.com#443") nil
+                           nil nil nil nil nil nil))))))
 
 (defmacro auth-source-pass--with-store-find-foo (store &rest body)
   "Use STORE while executing BODY.  \"foo\" is the matched entry."
@@ -218,7 +282,8 @@ auth-source-pass--with-store-find-foo
        ,@body)))
 
 (ert-deftest auth-source-pass-build-result-return-parameters ()
-  (auth-source-pass--with-store-find-foo '(("foo"))
+  (auth-source-pass--with-store-find-foo
+      '(("foo" ("secret" . "foo password")))
     (let ((result (auth-source-pass--build-result "foo" 512 "user")))
       (should (equal (plist-get result :port) 512))
       (should (equal (plist-get result :user) "user")))))
@@ -238,7 +303,9 @@ auth-source-pass--with-store-find-foo
 (ert-deftest auth-source-pass-build-result-passes-full-host-to-find-match ()
   (let (passed-host)
     (cl-letf (((symbol-function 'auth-source-pass--find-match)
-               (lambda (host _user _port) (setq passed-host host))))
+               (lambda (host _user _port)
+                 (setq passed-host host)
+                 nil)))
       (auth-source-pass--build-result "https://[email-masked]:123" nil nil)
       (should (equal passed-host "https://[email-masked]:123"))
       (auth-source-pass--build-result "https://[email-masked]" nil nil)
@@ -249,27 +316,16 @@ auth-source-pass--with-store-find-foo
       (should (equal passed-host "[email-masked]:443")))))
 
 (ert-deftest auth-source-pass-only-return-entries-that-can-be-open ()
-  (cl-letf (((symbol-function 'auth-source-pass-entries)
-             (lambda () '("foo.site.com" "bar.site.com" "mail/baz.site.com/scott")))
-            ((symbol-function 'auth-source-pass--entry-valid-p)
-             ;; only foo.site.com and "mail/baz.site.com/scott" are valid
-             (lambda (entry) (member entry '("foo.site.com" "mail/baz.site.com/scott")))))
-    (should (equal (auth-source-pass--find-all-by-entry-name "foo.site.com" "someuser")
-                   '("foo.site.com")))
-    (should (equal (auth-source-pass--find-all-by-entry-name "bar.site.com" "someuser")
-                   '()))
-    (should (equal (auth-source-pass--find-all-by-entry-name "baz.site.com" "scott")
-                   '("mail/baz.site.com/scott")))))
-
-(ert-deftest auth-source-pass-entry-is-not-valid-when-unreadable ()
-  (cl-letf (((symbol-function 'auth-source-pass--read-entry)
-             (lambda (entry)
-               ;; only foo is a valid entry
-               (if (string-equal entry "foo")
-                   "password"
-                 nil))))
-    (should (auth-source-pass--entry-valid-p "foo"))
-    (should-not (auth-source-pass--entry-valid-p "bar"))))
+  (auth-source-pass--with-store
+      '(("foo.site.com" ("secret" . "foo.site.com password"))
+        ("bar.site.com") ; An entry name with no data is invalid
+        ("mail/baz.site.com/scott" ("secret" . "mail/baz.site.com/scott password")))
+    (should (equal (auth-source-pass--find-match "foo.site.com" "someuser" nil)
+                   '(("secret" . "foo.site.com password"))))
+    (should (equal (auth-source-pass--find-match "bar.site.com" "someuser" nil)
+                   nil))
+    (should (equal (auth-source-pass--find-match "baz.site.com" "scott" nil)
+                   '(("secret" . "mail/baz.site.com/scott password"))))))
 
 (ert-deftest auth-source-pass-can-start-from-auth-source-search ()
   (auth-source-pass--with-store '(("gitlab.com" ("user" . "someone")))
-- 
2.21.0

>From 61ca0e36e192ebaa389900366fb03432ed342d7a Mon Sep 17 00:00:00 2001
From: Keith Amidon <[email-masked]>
Date: Sat, 11 May 2019 08:22:56 -0700
Subject: [PATCH 09/13] * lisp/auth-source-pass.el: Add Keith Amidon to authors

---
 lisp/auth-source-pass.el | 1 +
 1 file changed, 1 insertion(+)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index bcb215a6ac..af421645cb 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -4,6 +4,7 @@
 
 ;; Author: Damien Cassou <[email-masked]>,
 ;;         Nicolas Petton <[email-masked]>
+;;         Keith Amidon <[email-masked]>
 ;; Version: 4.0.2
 ;; Package-Requires: ((emacs "25"))
 ;; Url: https://github.com/DamienCassou/auth-password-store
-- 
2.21.0

>From 380780bdccecee625c61f27e46754c14058b0e2b Mon Sep 17 00:00:00 2001
From: Damien Cassou <[email-masked]>
Date: Tue, 14 May 2019 05:50:59 +0200
Subject: [PATCH 10/13] Refactoring of auth-source-pass

* lisp/auth-source-pass.el (auth-source-pass--find-match): Refactor by
moving some code to auth-source-pass--disambiguate.
(auth-source-pass--disambiguate)
(auth-source-pass--entries-matching-suffix): New function.
(auth-source-pass--find-match-unambiguous)
(auth-source-pass--select-from-entries)
(auth-source-pass--entry-reducer): Refactor to simplify and improve
logging.
(auth-source-pass--matching-entries)
(auth-source-pass--accumulate-matches): Remove.
* test/lisp/auth-source-pass-tests.el: Complete rewrite to facilitate
maintenance.
(auth-source-pass--have-message-containing): Remove.
(auth-source-pass--have-message-matching)
(auth-source-pass--explain--have-message-matching)
(auth-source-pass--explain-match-entry-p)
(auth-source-pass--includes-sorted-entries)
(auth-source-pass--explain-includes-sorted-entries)
(auth-source-pass--explain-match-any-entry-p)
(auth-source-pass--matching-entries)
(auth-source-pass-match-entry-p)
(auth-source-pass-match-any-entry-p): New function.
---
 lisp/auth-source-pass.el            | 108 +++----
 test/lisp/auth-source-pass-tests.el | 482 ++++++++++++++++++----------
 2 files changed, 349 insertions(+), 241 deletions(-)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index af421645cb..295bda507b 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -197,10 +197,17 @@ auth-source-pass--find-match
 
 Disambiguate between user provided inside HOST (e.g., [email-masked]) 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.,
[email-masked]) 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)))))
-    (auth-source-pass--find-match-unambiguous
+    (list
      (or (url-host url) host)
      (or user (url-user url))
      ;; url-port returns 443 (because of the https:// above) by default
@@ -212,74 +219,49 @@ auth-source-pass--find-match-unambiguous
 return nil.
 
 HOSTNAME should not contain any username or port number."
-  (cl-reduce
-   (lambda (result entries)
-     (or result
-         (pcase (length entries)
-           (0 nil)
-           (1 (auth-source-pass-parse-entry (car entries)))
-           (_ (auth-source-pass--select-from-entries entries user)))))
-   (auth-source-pass--matching-entries hostname user port)
-   :initial-value nil))
+  (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."
-  (cl-reduce
-   (lambda (result entry)
-     (let ((entry-data (auth-source-pass-parse-entry entry)))
-       (cond ((equal (auth-source-pass--get-attr "user" result) user)
-              result)
-             ((equal (auth-source-pass--get-attr "user" entry-data) user)
-              entry-data)
-             (t
-              result))))
-   entries
-   :initial-value (auth-source-pass-parse-entry (car entries))))
-
-(defun auth-source-pass--matching-entries (hostname user port)
-  "Return all matching password-store entries for HOSTNAME, USER, & PORT.
-
-The result is a list of lists of password-store entries, where
-each sublist contains entries that actually exist in the
-password-store matching one of the entry name formats that
-auth-source-pass expects, most specific to least specific."
-  (let* ((entries-lists (mapcar
-                         #'cdr
-                         (auth-source-pass--accumulate-matches hostname user port)))
-         (entries (apply #'cl-concatenate (cons 'list entries-lists))))
-    (if entries
-        (auth-source-pass--do-debug (format "found: %S" entries))
-      (auth-source-pass--do-debug "no matches found"))
-    entries-lists))
-
-(defun auth-source-pass--accumulate-matches (hostname user port)
-  "Accumulate matching password-store entries into sublists.
-
-Entries matching supported formats that combine HOSTNAME, USER, &
-PORT are accumulated into sublists where the car of each sublist
-is a regular expression for matching paths in the password-store
-and the remainder is the list of matching entries."
-  (let ((suffix-match-lists
-         (mapcar (lambda (suffix) (list (format "\\(^\\|/\\)%s$" suffix)))
-                 (auth-source-pass--generate-entry-suffixes hostname user port))))
-    (cl-reduce #'auth-source-pass--entry-reducer
-               (auth-source-pass-entries)
-               :initial-value suffix-match-lists)))
-
-(defun auth-source-pass--entry-reducer (match-lists entry)
-  "Match MATCH-LISTS sublists against ENTRY.
-
-The result is a copy of match-lists with the entry added to the
-end of any sublists for which the regular expression at the head
-of the list matches the entry name."
-  (mapcar (lambda (match-list)
-            (if (string-match (car match-list) entry)
-                (append match-list (list entry))
-              match-list))
-          match-lists))
+  (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.
diff --git a/test/lisp/auth-source-pass-tests.el b/test/lisp/auth-source-pass-tests.el
index 2c28f79945..6f0d308ceb 100644
--- a/test/lisp/auth-source-pass-tests.el
+++ b/test/lisp/auth-source-pass-tests.el
@@ -52,11 +52,21 @@
 (defvar auth-source-pass--debug-log nil
   "Contains a list of all messages passed to `auth-source-do-debug`.")
 
-(defun auth-source-pass--should-have-message-containing (regexp)
-  "Assert that at least one `auth-source-do-debug` matched REGEXP."
-  (should (seq-find (lambda (message)
-                      (string-match regexp message))
-                    auth-source-pass--debug-log)))
+(defun auth-source-pass--have-message-matching (regexp)
+  "Return non-nil iff at least one `auth-source-do-debug` match REGEXP."
+  (seq-find (lambda (message)
+              (string-match regexp message))
+            auth-source-pass--debug-log))
+
+(defun auth-source-pass--explain--have-message-matching (regexp)
+  "Explainer function for `auth-source-pass--have-message-matching'.
+REGEXP is the same as in `auth-source-pass--have-message-matching'."
+  `(regexp
+    ,regexp
+    messages
+    ,(mapconcat #'identity auth-source-pass--debug-log "\n- ")))
+
+(put #'auth-source-pass--have-message-matching 'ert-explainer #'auth-source-pass--explain--have-message-matching)
 
 (defun auth-source-pass--debug (&rest msg)
   "Format MSG and add that to `auth-source-pass--debug-log`.
@@ -78,6 +88,82 @@ auth-source-pass--with-store
            (auth-source-pass--parse-log nil))
        ,@body)))
 
+(defun auth-source-pass--explain-match-entry-p (entry hostname &optional user port)
+  "Explainer function for `auth-source-pass-match-entry-p'.
+
+ENTRY, HOSTNAME, USER and PORT are the same as in `auth-source-pass-match-entry-p'."
+  `(entry
+    ,entry
+    store
+    ,(auth-source-pass-entries)
+    matching-entries
+    ,(auth-source-pass--matching-entries hostname user port)))
+
+(put 'auth-source-pass-match-entry-p 'ert-explainer #'auth-source-pass--explain-match-entry-p)
+
+(defun auth-source-pass--includes-sorted-entries (entries hostname &optional user port)
+  "Return non-nil iff ENTRIES matching the parameters are found in store.
+ENTRIES should be sorted from most specific to least specific.
+
+HOSTNAME, USER and PORT are passed unchanged to
+`auth-source-pass--matching-entries'."
+  (if (seq-empty-p entries)
+      t
+    (and
+     (auth-source-pass-match-entry-p (car entries) hostname user port)
+     (auth-source-pass--includes-sorted-entries (cdr entries) hostname user port))))
+
+(defun auth-source-pass--explain-includes-sorted-entries (entries hostname &optional user port)
+  "Explainer function for `auth-source-pass--includes-sorted-entries'.
+
+ENTRIES, HOSTNAME, USER and PORT are the same as in `auth-source-pass--includes-sorted-entries'."
+  `(store
+    ,(auth-source-pass-entries)
+    matching-entries
+    ,(auth-source-pass--matching-entries hostname user port)
+    entries
+    ,entries))
+
+(put 'auth-source-pass--includes-sorted-entries 'ert-explainer #'auth-source-pass--explain-includes-sorted-entries)
+
+(defun auth-source-pass--explain-match-any-entry-p (hostname &optional user port)
+  "Explainer function for `auth-source-pass-match-any-entry-p'.
+
+HOSTNAME, USER and PORT are the same as in `auth-source-pass-match-any-entry-p'."
+  `(store
+    ,(auth-source-pass-entries)
+    matching-entries
+    ,(auth-source-pass--matching-entries hostname user port)))
+
+(put 'auth-source-pass-match-any-entry-p 'ert-explainer #'auth-source-pass--explain-match-any-entry-p)
+
+(defun auth-source-pass--matching-entries (hostname &optional user port)
+  "Return password-store entries matching HOSTNAME, USER, PORT.
+
+The result is a list of lists of password-store entries.  Each
+sublist contains the password-store entries whose names match a
+suffix in `auth-source-pass--generate-entry-suffixes'.  The
+result is ordered the same way as the suffixes."
+  (let ((entries (auth-source-pass-entries)))
+    (mapcar (lambda (suffix) (auth-source-pass--entries-matching-suffix suffix entries))
+            (auth-source-pass--generate-entry-suffixes hostname user port))))
+
+(defun auth-source-pass-match-entry-p (entry hostname &optional user port)
+  "Return non-nil iff an ENTRY matching the parameters is found in store.
+
+HOSTNAME, USER and PORT are passed unchanged to
+`auth-source-pass--matching-entries'."
+  (cl-find-if
+   (lambda (entries) (cl-find entry entries :test #'string=))
+   (auth-source-pass--matching-entries hostname user port)))
+
+(defun auth-source-pass-match-any-entry-p (hostname &optional user port)
+  "Return non-nil iff there is at least one entry matching the parameters.
+
+HOSTNAME, USER and PORT are passed unchanged to
+`auth-source-pass--matching-entries'."
+  (cl-find-if #'identity (auth-source-pass--matching-entries hostname user port)))
+
 (ert-deftest auth-source-pass-any-host ()
   (auth-source-pass--with-store '(("foo" ("port" . "foo-port") ("host" . "foo-user"))
                                   ("bar"))
@@ -93,6 +179,101 @@ auth-source-pass--with-store
                                   ("bar"))
     (should-not (auth-source-pass-search :host "baz"))))
 
+(ert-deftest auth-source-pass--disambiguate-extract-host-from-hostname ()
+  ;; no user or port
+  (should (equal (cl-first (auth-source-pass--disambiguate "foo")) "foo"))
+  ;; only user
+  (should (equal (cl-first (auth-source-pass--disambiguate "user@foo")) "foo"))
+  ;; only port
+  (should (equal (cl-first (auth-source-pass--disambiguate "https://foo")) "foo"))
+  (should (equal (cl-first (auth-source-pass--disambiguate "foo:80")) "foo"))
+  ;; both user and port
+  (should (equal (cl-first (auth-source-pass--disambiguate "https://user@foo")) "foo"))
+  (should (equal (cl-first (auth-source-pass--disambiguate "user@foo:80")) "foo"))
+  ;; all of the above with a trailing path
+  (should (equal (cl-first (auth-source-pass--disambiguate "foo/path")) "foo"))
+  (should (equal (cl-first (auth-source-pass--disambiguate "user@foo/path")) "foo"))
+  (should (equal (cl-first (auth-source-pass--disambiguate "https://foo/path")) "foo"))
+  (should (equal (cl-first (auth-source-pass--disambiguate "foo:80/path")) "foo"))
+  (should (equal (cl-first (auth-source-pass--disambiguate "https://user@foo/path")) "foo"))
+  (should (equal (cl-first (auth-source-pass--disambiguate "user@foo:80/path")) "foo")))
+
+(ert-deftest auth-source-pass--disambiguate-extract-user-from-hostname ()
+  ;; no user or port
+  (should (equal (cl-second (auth-source-pass--disambiguate "foo")) nil))
+  ;; only user
+  (should (equal (cl-second (auth-source-pass--disambiguate "user@foo")) "user"))
+  ;; only port
+  (should (equal (cl-second (auth-source-pass--disambiguate "https://foo")) nil))
+  (should (equal (cl-second (auth-source-pass--disambiguate "foo:80")) nil))
+  ;; both user and port
+  (should (equal (cl-second (auth-source-pass--disambiguate "https://user@foo")) "user"))
+  (should (equal (cl-second (auth-source-pass--disambiguate "user@foo:80")) "user"))
+  ;; all of the above with a trailing path
+  (should (equal (cl-second (auth-source-pass--disambiguate "foo/path")) nil))
+  (should (equal (cl-second (auth-source-pass--disambiguate "user@foo/path")) "user"))
+  (should (equal (cl-second (auth-source-pass--disambiguate "https://foo/path")) nil))
+  (should (equal (cl-second (auth-source-pass--disambiguate "foo:80/path")) nil))
+  (should (equal (cl-second (auth-source-pass--disambiguate "https://user@foo/path")) "user"))
+  (should (equal (cl-second (auth-source-pass--disambiguate "user@foo:80/path")) "user")))
+
+(ert-deftest auth-source-pass--disambiguate-prefer-user-parameter ()
+  ;; no user or port
+  (should (equal (cl-second (auth-source-pass--disambiguate "foo" "user2")) "user2"))
+  ;; only user
+  (should (equal (cl-second (auth-source-pass--disambiguate "user@foo" "user2")) "user2"))
+  ;; only port
+  (should (equal (cl-second (auth-source-pass--disambiguate "https://foo" "user2")) "user2"))
+  (should (equal (cl-second (auth-source-pass--disambiguate "foo:80" "user2")) "user2"))
+  ;; both user and port
+  (should (equal (cl-second (auth-source-pass--disambiguate "https://user@foo" "user2")) "user2"))
+  (should (equal (cl-second (auth-source-pass--disambiguate "user@foo:80" "user2")) "user2"))
+  ;; all of the above with a trailing path
+  (should (equal (cl-second (auth-source-pass--disambiguate "foo/path" "user2")) "user2"))
+  (should (equal (cl-second (auth-source-pass--disambiguate "user@foo/path" "user2")) "user2"))
+  (should (equal (cl-second (auth-source-pass--disambiguate "https://foo/path" "user2")) "user2"))
+  (should (equal (cl-second (auth-source-pass--disambiguate "foo:80/path" "user2")) "user2"))
+  (should (equal (cl-second (auth-source-pass--disambiguate "https://user@foo/path" "user2")) "user2"))
+  (should (equal (cl-second (auth-source-pass--disambiguate "user@foo:80/path" "user2")) "user2")))
+
+(ert-deftest auth-source-pass--disambiguate-extract-port-from-hostname ()
+  ;; no user or port
+  (should (equal (cl-third (auth-source-pass--disambiguate "foo")) "443"))
+  ;; only user
+  (should (equal (cl-third (auth-source-pass--disambiguate "user@foo")) "443"))
+  ;; only port
+  (should (equal (cl-third (auth-source-pass--disambiguate "https://foo")) "443"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "foo:80")) "80"))
+  ;; both user and port
+  (should (equal (cl-third (auth-source-pass--disambiguate "https://user@foo")) "443"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "user@foo:80")) "80"))
+  ;; all of the above with a trailing path
+  (should (equal (cl-third (auth-source-pass--disambiguate "foo/path")) "443"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "user@foo/path")) "443"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "https://foo/path")) "443"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "foo:80/path")) "80"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "https://user@foo/path")) "443"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "user@foo:80/path")) "80")))
+
+(ert-deftest auth-source-pass--disambiguate-prefer-port-parameter ()
+  ;; no user or port
+  (should (equal (cl-third (auth-source-pass--disambiguate "foo" "user2" "8080")) "8080"))
+  ;; only user
+  (should (equal (cl-third (auth-source-pass--disambiguate "user@foo" "user2" "8080")) "8080"))
+  ;; only port
+  (should (equal (cl-third (auth-source-pass--disambiguate "https://foo" "user2" "8080")) "8080"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "foo:80" "user2" "8080")) "8080"))
+  ;; both user and port
+  (should (equal (cl-third (auth-source-pass--disambiguate "https://user@foo" "user2" "8080")) "8080"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "user@foo:80" "user2" "8080")) "8080"))
+  ;; all of the above with a trailing path
+  (should (equal (cl-third (auth-source-pass--disambiguate "foo/path" "user2" "8080")) "8080"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "user@foo/path" "user2" "8080")) "8080"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "https://foo/path" "user2" "8080")) "8080"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "foo:80/path" "user2" "8080")) "8080"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "https://user@foo/path" "user2" "8080")) "8080"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "user@foo:80/path" "user2" "8080")) "8080")))
+
 (ert-deftest auth-source-pass-find-match-minimal-parsing ()
   (let ((store-contents
          '(("baz" ("secret" . "baz password"))
@@ -121,156 +302,110 @@ auth-source-pass--with-store
       (should (equal auth-source-pass--parse-log '("bar.baz"))))
     (auth-source-pass--with-store store-contents
       (auth-source-pass--find-match "baz" nil nil)
-      (should (equal auth-source-pass--parse-log '("baz"))))))
-
-(ert-deftest auth-source-pass-find-match-matching-at-entry-name ()
-  (auth-source-pass--with-store
-      '(("foo" ("secret" . "foo password")))
-    (let ((result (auth-source-pass--find-match "foo" nil nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "foo password")))))
-
-(ert-deftest auth-source-pass-find-match-matching-at-entry-name-part ()
-  (auth-source-pass--with-store
-      '(("foo" ("secret" . "foo password")))
-    (let ((result (auth-source-pass--find-match "https://foo" nil nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "foo password")))))
-
-(ert-deftest auth-source-pass-find-match-matching-at-entry-name-ignoring-user ()
-  (auth-source-pass--with-store
-      '(("foo" ("secret" . "foo password")))
-    (let ((result (auth-source-pass--find-match "https://SomeUser@foo" nil nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "foo password")))))
-
-(ert-deftest auth-source-pass-find-match-matching-at-entry-name-with-user ()
-  (auth-source-pass--with-store
-      '(("SomeUser@foo" ("secret" . "SomeUser@foo password")))
-    (let ((result (auth-source-pass--find-match "https://SomeUser@foo" nil nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "SomeUser@foo password")))))
-
-(ert-deftest auth-source-pass-find-match-matching-at-entry-name-prefer-full ()
-  (auth-source-pass--with-store
-      '(("SomeUser@foo" ("secret" . "SomeUser@foo password"))
-        ("foo" ("secret" . "foo password")))
-    (let ((result (auth-source-pass--find-match "https://SomeUser@foo" nil nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "SomeUser@foo password")))))
-
-(ert-deftest auth-source-pass-find-match-matching-at-entry-name-prefer-full-reversed ()
-  (auth-source-pass--with-store
-      '(("foo" ("secret" . "foo password"))
-        ("SomeUser@foo" ("secret" . "SomeUser@foo password")))
-    (let ((result (auth-source-pass--find-match "https://SomeUser@foo" nil nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "SomeUser@foo password")))))
-
-(ert-deftest auth-source-pass-matching-entries-name-without-subdomain ()
+      (should (equal auth-source-pass--parse-log '("baz"))))
+    (auth-source-pass--with-store
+        '(("dir1/bar.com" ("key" . "val"))
+          ("dir2/bar.com" ("key" . "val"))
+          ("dir3/bar.com" ("key" . "val")))
+      (auth-source-pass--find-match "bar.com" nil nil)
+      (should (= (length auth-source-pass--parse-log) 1)))))
+
+(ert-deftest auth-source-pass--find-match-return-parsed-data ()
+  (auth-source-pass--with-store '(("bar.com" ("key" . "val")))
+    (should (consp (auth-source-pass--find-match "bar.com" nil nil))))
+  (auth-source-pass--with-store '(("dir1/bar.com" ("key1" . "val1")) ("dir2/bar.com" ("key2" . "val2")))
+    (should (consp (auth-source-pass--find-match "bar.com" nil nil)))))
+
+(ert-deftest auth-source-pass--matching-entries ()
   (auth-source-pass--with-store '(("bar.com"))
-    (should (equal (auth-source-pass--matching-entries "foo.bar.com" nil nil)
-                   '(nil ("bar.com") nil)))))
-
-(ert-deftest auth-source-pass-matching-entries-name-without-subdomain-with-user ()
-  (auth-source-pass--with-store '(("[email-masked]"))
-    (should (equal (auth-source-pass--matching-entries "foo.bar.com" "someone" nil)
-                   '(nil nil nil ("[email-masked]") nil nil nil nil nil)))))
-
-(ert-deftest auth-source-pass-matching-entries-name-without-subdomain-with-bad-user ()
-  (auth-source-pass--with-store '(("[email-masked]"))
-    (should (equal (auth-source-pass--matching-entries "foo.bar.com" "someone" nil)
-                   '(nil nil nil nil nil nil nil nil nil)))))
-
-(ert-deftest auth-source-pass-matching-entries-name-without-subdomain-prefer-full ()
-  (auth-source-pass--with-store '(("bar.com") ("foo.bar.com"))
-    (should (equal (auth-source-pass--matching-entries "foo.bar.com" nil nil)
-                   '(("foo.bar.com") ("bar.com") nil)))))
-
-(ert-deftest auth-source-pass-dont-match-at-folder-name ()
-  (auth-source-pass--with-store '(("foo.bar.com/foo"))
-    (should (equal (auth-source-pass--matching-entries "foo.bar.com" nil nil)
-                   '(nil nil nil)))))
-
-(ert-deftest auth-source-pass-matching-entries-host-port-and-subdir-user ()
-  (auth-source-pass--with-store '(("bar.com:443/someone"))
-    (should (equal (auth-source-pass--matching-entries "bar.com" "someone" "443")
-                   '(nil ("bar.com:443/someone") nil nil nil nil
-                         nil nil nil nil nil nil)))))
-
-(ert-deftest auth-source-pass-matching-entries-host-port-and-subdir-user-with-custom-separator ()
+    (should (auth-source-pass-match-entry-p "bar.com" "bar.com"))
+    ;; match even if sub-domain is asked for
+    (should (auth-source-pass-match-entry-p "bar.com" "foo.bar.com"))
+    ;; match even if a user is asked for
+    (should (auth-source-pass-match-entry-p "bar.com" "bar.com" "user"))
+    ;; match even if user as an @ sign
+    (should (auth-source-pass-match-entry-p "bar.com" "bar.com" "user@someplace"))
+    ;; match even if a port is asked for
+    (should (auth-source-pass-match-entry-p "bar.com" "bar.com" nil "8080"))
+    ;; match even if a user and a port are asked for
+    (should (auth-source-pass-match-entry-p "bar.com" "bar.com" "user" "8080"))
+    ;; don't match if a '.' is replaced with another character
+    (auth-source-pass--with-store '(("barXcom"))
+      (should-not (auth-source-pass-match-any-entry-p "bar.com" nil nil)))))
+
+(ert-deftest auth-source-pass--matching-entries-find-entries-with-a-username ()
+  (auth-source-pass--with-store '(("user@foo"))
+    (should (auth-source-pass-match-entry-p "user@foo" "foo" "user")))
+  ;; match even if sub-domain is asked for
+  (auth-source-pass--with-store '(("[email-masked]"))
+    (should (auth-source-pass-match-entry-p "[email-masked]" "foo.bar.com" "user")))
+  ;; don't match if no user is asked for
+  (auth-source-pass--with-store '(("user@foo"))
+    (should-not (auth-source-pass-match-any-entry-p "foo")))
+  ;; don't match if user is different
+  (auth-source-pass--with-store '(("user1@foo"))
+    (should-not (auth-source-pass-match-any-entry-p "foo" "user2")))
+  ;; don't match if sub-domain is asked for but user is different
+  (auth-source-pass--with-store '(("[email-masked]"))
+    (should-not (auth-source-pass-match-any-entry-p "foo.bar.com" "user2"))))
+
+(ert-deftest auth-source-pass--matching-entries-find-entries-with-a-port ()
+  (auth-source-pass--with-store '(("bar.com:8080"))
+    (should (auth-source-pass-match-entry-p "bar.com:8080" "bar.com" nil "8080"))))
+
+(ert-deftest auth-source-pass--matching-entries-find-entries-with-slash ()
+  ;; match if entry filename matches user
+  (auth-source-pass--with-store '(("foo.com/user"))
+    (should (auth-source-pass-match-entry-p "foo.com/user" "foo.com" "user")))
+  ;; match with port if entry filename matches user
+  (auth-source-pass--with-store '(("foo.com:8080/user"))
+    (should (auth-source-pass-match-entry-p "foo.com:8080/user" "foo.com" "user" "8080")))
+  ;; don't match if entry filename doesn't match user
+  (auth-source-pass--with-store '(("foo.com/baz"))
+    (should-not (auth-source-pass-match-any-entry-p "foo.com" "user"))))
+
+(ert-deftest auth-source-pass-matching-entries-with-custom-separator ()
   (let ((auth-source-pass-port-separator "#"))
     (auth-source-pass--with-store '(("bar.com#443/someone"))
-      (should (equal (auth-source-pass--matching-entries "bar.com" "someone" "443")
-                     '(nil ("bar.com#443/someone") nil nil nil nil
-                           nil nil nil nil nil nil))))))
-
-(ert-deftest auth-source-pass-matching-entries-extracting-user-from-host ()
-  (auth-source-pass--with-store
-      '(("foo.com/bar" ("secret" . "foo.com/bar password")))
-    (let ((result (auth-source-pass--find-match "https://[email-masked]" nil nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "foo.com/bar password")))))
-
-(ert-deftest auth-source-pass-matching-entries-with-user-first ()
+      (should (auth-source-pass-match-entry-p "bar.com#443/someone" "bar.com" "someone" "443")))))
+
+(ert-deftest auth-source-pass--matching-entries-sort-results ()
+  (auth-source-pass--with-store '(("user@foo") ("foo"))
+    (should (auth-source-pass--includes-sorted-entries '("user@foo" "foo") "foo" "user")))
+  ;; same, but store is reversed
   (auth-source-pass--with-store '(("foo") ("user@foo"))
-    (should (equal (auth-source-pass--matching-entries "foo" "user" nil)
-                   '(("user@foo") nil ("foo"))))
-    (auth-source-pass--should-have-message-containing "found: (\"user@foo\" \"foo\"")))
-
-(ert-deftest auth-source-pass-give-priority-to-desired-user ()
-  (auth-source-pass--with-store
-      '(("foo" ("secret" . "foo password"))
-        ("subdir/foo" ("secret" . "subdir/foo password") ("user" . "someone")))
-    (let ((result (auth-source-pass--find-match "foo" "someone" nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "subdir/foo password"))
-      (should (equal (auth-source-pass--get-attr "user" result)
-                     "someone")))
-    (auth-source-pass--should-have-message-containing "found: (\"foo\" \"subdir/foo\"")))
-
-(ert-deftest auth-source-pass-give-priority-to-desired-user-reversed ()
-  (auth-source-pass--with-store
-      '(("foo" ("secret" . "foo password") ("user" . "someone"))
-        ("subdir/foo" ("secret" . "subdir/foo password")))
-    (let ((result (auth-source-pass--find-match "foo" "someone" nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "foo password")))
-    (auth-source-pass--should-have-message-containing "found: (\"foo\" \"subdir/foo\"")))
-
-(ert-deftest auth-source-pass-return-first-when-several-matches ()
-  (auth-source-pass--with-store
-      '(("foo" ("secret" . "foo password"))
-        ("subdir/foo" ("secret" . "subdir/foo password")))
-    (let ((result (auth-source-pass--find-match "foo" nil nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "foo password")))
-    (auth-source-pass--should-have-message-containing "found: (\"foo\" \"subdir/foo\"")))
-
-(ert-deftest auth-source-pass-matching-entries-make-divansantana-happy ()
-  (auth-source-pass--with-store '(("host.com"))
-    (should (equal (auth-source-pass--matching-entries "smtp.host.com" "[email-masked]" nil)
-                   '(nil nil nil nil nil ("host.com") nil nil nil)))))
-
-(ert-deftest auth-source-pass-find-host-without-port ()
-  (auth-source-pass--with-store
-      '(("host.com" ("secret" . "host.com password")))
-    (let ((result (auth-source-pass--find-match "host.com:8888" "someuser" nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "host.com password")))))
-
-(ert-deftest auth-source-pass-matching-entries-host-with-port ()
-  (auth-source-pass--with-store '(("host.com:443"))
-    (should (equal (auth-source-pass--matching-entries "host.com" "someuser" "443")
-                   '(nil nil nil nil ("host.com:443") nil
-                         nil nil nil nil nil nil)))))
-
-(ert-deftest auth-source-pass-matching-entries-with-custom-port-separator ()
-  (let ((auth-source-pass-port-separator "#"))
-    (auth-source-pass--with-store '(("host.com#443"))
-      (should (equal (auth-source-pass--matching-entries "host.com" "someuser" "443")
-                     '(nil nil nil nil ("host.com#443") nil
-                           nil nil nil nil nil nil))))))
+    (should (auth-source-pass--includes-sorted-entries '("user@foo" "foo") "foo" "user")))
+  ;; with sub-domain
+  (auth-source-pass--with-store '(("bar.com") ("foo.bar.com"))
+    (should (auth-source-pass--includes-sorted-entries '("foo.bar.com" "bar.com") "foo.bar.com")))
+  ;; matching user in the entry data takes priority
+  (auth-source-pass--with-store '(("dir1/bar.com") ("dir2/bar.com" ("user" . "user")))
+    (should (auth-source-pass--includes-sorted-entries
+             '("dir2/bar.com" "dir1/bar.com")
+             "bar.com" "user")))
+  ;; same, but store is reversed
+  (auth-source-pass--with-store '(("dir2/bar.com" ("user" . "user")) ("dir1/bar.com"))
+    (should (auth-source-pass--includes-sorted-entries
+             '("dir2/bar.com" "dir1/bar.com")
+             "bar.com" "user"))))
+
+(ert-deftest auth-source-pass-all-supported-organizations ()
+  ;; test every possible entry to store this data: user=rms host=gnu.org port=22
+  (dolist (entry '(;; only host name
+                   "gnu.org"
+                   ;; hostname + user
+                   "gnu.org/rms" "[email-masked]"
+                   ;; hostname + port
+                   "gnu.org:22"
+                   ;; hostname + user + port
+                   "gnu.org:22/rms" "[email-masked]:22"
+                   ;; all of the above in a random folder
+                   "a/b/gnu.org"
+                   "a/b/gnu.org/rms" "a/b/[email-masked]"
+                   "a/b/gnu.org:22"
+                   "a/b/gnu.org:22/rms" "a/b/[email-masked]:22"))
+    (auth-source-pass--with-store `((,entry))
+      (should (auth-source-pass-match-entry-p entry "gnu.org" "rms" "22")))))
 
 (defmacro auth-source-pass--with-store-find-foo (store &rest body)
   "Use STORE while executing BODY.  \"foo\" is the matched entry."
@@ -300,33 +435,6 @@ auth-source-pass--with-store-find-foo
       (should (equal (plist-get result :port) 512))
       (should (equal (plist-get result :user) "anuser")))))
 
-(ert-deftest auth-source-pass-build-result-passes-full-host-to-find-match ()
-  (let (passed-host)
-    (cl-letf (((symbol-function 'auth-source-pass--find-match)
-               (lambda (host _user _port)
-                 (setq passed-host host)
-                 nil)))
-      (auth-source-pass--build-result "https://[email-masked]:123" nil nil)
-      (should (equal passed-host "https://[email-masked]:123"))
-      (auth-source-pass--build-result "https://[email-masked]" nil nil)
-      (should (equal passed-host "https://[email-masked]"))
-      (auth-source-pass--build-result "[email-masked]" nil nil)
-      (should (equal passed-host "[email-masked]"))
-      (auth-source-pass--build-result "[email-masked]:443" nil nil)
-      (should (equal passed-host "[email-masked]:443")))))
-
-(ert-deftest auth-source-pass-only-return-entries-that-can-be-open ()
-  (auth-source-pass--with-store
-      '(("foo.site.com" ("secret" . "foo.site.com password"))
-        ("bar.site.com") ; An entry name with no data is invalid
-        ("mail/baz.site.com/scott" ("secret" . "mail/baz.site.com/scott password")))
-    (should (equal (auth-source-pass--find-match "foo.site.com" "someuser" nil)
-                   '(("secret" . "foo.site.com password"))))
-    (should (equal (auth-source-pass--find-match "bar.site.com" "someuser" nil)
-                   nil))
-    (should (equal (auth-source-pass--find-match "baz.site.com" "scott" nil)
-                   '(("secret" . "mail/baz.site.com/scott password"))))))
-
 (ert-deftest auth-source-pass-can-start-from-auth-source-search ()
   (auth-source-pass--with-store '(("gitlab.com" ("user" . "someone")))
     (auth-source-pass-enable)
@@ -334,6 +442,24 @@ auth-source-pass--with-store-find-foo
       (should (equal (plist-get result :user) "someone"))
       (should (equal (plist-get result :host) "gitlab.com")))))
 
+(ert-deftest auth-source-pass-prints-meaningful-debug-log ()
+  (auth-source-pass--with-store '()
+    (auth-source-pass--find-match "gitlab.com" nil nil)
+    (should (auth-source-pass--have-message-matching
+             "entries matching hostname=\"gitlab.com\""))
+    (should (auth-source-pass--have-message-matching
+             "corresponding suffixes to search for: .*\"gitlab.com\""))
+    (should (auth-source-pass--have-message-matching
+             "found no entries matching \"gitlab.com\"")))
+  (auth-source-pass--with-store '(("gitlab.com"))
+    (auth-source-pass--find-match "gitlab.com" nil nil)
+    (should (auth-source-pass--have-message-matching
+             "found 1 entry matching \"gitlab.com\": \"gitlab.com\"")))
+  (auth-source-pass--with-store '(("a/gitlab.com") ("b/gitlab.com"))
+    (auth-source-pass--find-match "gitlab.com" nil nil)
+    (should (auth-source-pass--have-message-matching
+             "found 2 entries matching \"gitlab.com\": (\"a/gitlab.com\" \"b/gitlab.com\")"))))
+
 (provide 'auth-source-pass-tests)
 
 ;;; auth-source-pass-tests.el ends here
-- 
2.21.0

>From fe04bfbb0bdeb58b1396578d6ece2bfcbb624b39 Mon Sep 17 00:00:00 2001
From: Damien Cassou <[email-masked]>
Date: Tue, 28 May 2019 08:46:41 +0200
Subject: [PATCH 11/13] * lisp/auth-source-pass.el: Version 5.0.0

---
 lisp/auth-source-pass.el | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index 295bda507b..f16d910890 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -5,7 +5,7 @@
 ;; Author: Damien Cassou <[email-masked]>,
 ;;         Nicolas Petton <[email-masked]>
 ;;         Keith Amidon <[email-masked]>
-;; Version: 4.0.2
+;; Version: 5.0.0
 ;; Package-Requires: ((emacs "25"))
 ;; Url: https://github.com/DamienCassou/auth-password-store
 ;; Created: 07 Jun 2015
-- 
2.21.0

>From 3cff601ee272dad609a5fb7099f562b470a6ac7a Mon Sep 17 00:00:00 2001
From: Damien Cassou <[email-masked]>
Date: Sun, 2 Jun 2019 11:08:40 +0200
Subject: [PATCH 12/13] * etc/NEWS: Describe changes to auth-source-pass

---
 etc/NEWS | 19 +++++++++++++++++++
 1 file changed, 19 insertions(+)

diff --git a/etc/NEWS b/etc/NEWS
index 95d7e08074..747e195b1b 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -1512,6 +1512,25 @@ the new variable 'buffer-auto-revert-by-notification' to a non-nil
 value.  Auto Revert mode can use this information to avoid polling the
 buffer periodically when 'auto-revert-avoid-polling' is non-nil.
 
+** auth-source-pass
+
++++
+*** New customizable variable 'auth-source-pass-filename'.
+Allows setting the path to the password-store, defaults to
+~/.password-store.
+
++++
+*** New customizable variable 'auth-source-pass-port-separator'.
+Specifies separator between host and port, defaults to colon ":".
+
+---
+*** Minimize the number of decryptions during password lookup.
+This makes the package usable with physical tokens requiring touching
+a sensor for every decryption.
+
+---
+*** 'auth-source-pass-get' is now autoloaded.
+
 ** Bookmarks
 
 ---
-- 
2.21.0

>From 1b51160e1e7d9cee8d89b652c9495c6af8bcc215 Mon Sep 17 00:00:00 2001
From: Damien Cassou <[email-masked]>
Date: Thu, 13 Jun 2019 21:54:21 +0200
Subject: [PATCH 13/13] * doc/misc/auth.texi (The Unix password store):
 Complete rewrite

---
 doc/misc/auth.texi | 50 ++++++++++++++++++++++++++++++++++++----------
 1 file changed, 40 insertions(+), 10 deletions(-)

diff --git a/doc/misc/auth.texi b/doc/misc/auth.texi
index a46e3d73fc..5b4914e8f5 100644
--- a/doc/misc/auth.texi
+++ b/doc/misc/auth.texi
@@ -445,19 +445,39 @@ The Unix password store
 
 @uref{http://www.passwordstore.org,,The standard unix password
 manager} (or just @code{pass}) stores your passwords in
-@code{gpg}-protected files following the Unix philosophy.
+@code{gpg}-protected files following the Unix philosophy.  The store
+location (any directory) must be specified in the
+@code{auth-source-pass-filename} variable which defaults to
+@code{"~/.password-store"}.
 
-Emacs integration of @code{pass} follows the first approach suggested
-by the pass project itself for data organization to find data. This
-means that the filename of the file containing the password for a user
-on a particular host must contain the host name.  The file itself must
-contain the password on the first line, as well as a @code{username}
-field containing the username on a subsequent line. A @code{port}
-field can be used to differentiate the authentication data for several
-services with the same username on the same host.
+Emacs integration of @code{pass} follows the approach suggested by the
+pass project itself for data organization to find data.  In particular,
+to store a password for the user @code{rms} on the host @code{gnu.org}
+on port @code{22}, you should use one of the following filenames.
+
+@itemize
+@item @code{"gnu.org.gpg"} : No username or port in the filename means that any username and port will match.
+@item @code{"gnu.org/rms.gpg"} : The username to match can be expressed as filename inside a directory whose name matches the host.  This is useful if the store has passwords for several users on the same host.
+@item @code{"rms@@gnu.org.gpg"} : The username can also be expressed as a prefix, separated from the host with an at-sign (@code{@@}).
+@item @code{"gnu.org:22.gpg"} : The port (aka. service) to match can only be expressed after the host and separated with a colon (@code{:}).  The separator can be changed through the @code{auth-source-pass-port-separator} variable.
+@item @code{"gnu.org:22/rms.gpg"}
+@item @code{"rms@@gnu.org:22.gpg"}
+@item @code{"a/b/gnu.org.gpg"} : Entries can be stored in arbitrary directories.
+@item @code{"a/b/gnu.org/rms.gpg"}
+@item @code{"a/b/rms@@gnu.org.gpg"}
+@item @code{"a/b/gnu.org:22.gpg"}
+@item @code{"a/b/gnu.org:22/rms.gpg"}
+@item @code{"a/b/rms@@gnu.org:22.gpg"}
+@end itemize
+
+If several entries match, the one matching the most items (where an
+``item'' is one of username, port or host) is preferred.  For example,
+while searching for an entry matching the @code{rms} user on host
+@code{gnu.org} for port @code{22}, then the entry
+@code{"gnu.org:22/rms.gpg"} is preferred over @code{"gnu.org.gpg"}.
 
 Users of @code{pass} may also be interested in functionality provided
-by other Emacs packages dealing with pass:
+by other Emacs packages:
 
 @itemize
 @item
@@ -468,6 +488,16 @@ The Unix password store
 @uref{https://github.com/jabranham/helm-pass,,helm-pass}: helm interface for pass.
 @end itemize
 
+@defvar auth-source-pass-filename
+Set this variable to a string locating the password store on the disk.
+Defaults to @code{"~/.password-store"}.
+@end defvar
+
+@defvar auth-source-pass-port-separator
+Set this variable to a string that should separate an host name from a
+port in an entry.  Defaults to @code{":"}.
+@end defvar
+
 @node Help for developers
 @chapter Help for developers
 
-- 
2.21.0