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