1 ;;; auth-source-pass.el --- Integrate auth-source with password-store -*- lexical-binding: t -*-
3 ;; Copyright (C) 2015, 2017 Free Software Foundation, Inc.
5 ;; Author: Damien Cassou <damien@cassou.me>,
6 ;; Nicolas Petton <nicolas@petton.fr>
8 ;; Package-Requires: ((emacs "24.4")
9 ;; Created: 07 Jun 2015
10 ;; Keywords: pass password-store auth-source username password login
12 ;; This file is part of GNU Emacs.
14 ;; GNU Emacs is free software: you can redistribute it and/or modify
15 ;; it under the terms of the GNU General Public License as published by
16 ;; the Free Software Foundation, either version 3 of the License, or
17 ;; (at your option) any later version.
19 ;; GNU Emacs is distributed in the hope that it will be useful,
20 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
21 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 ;; GNU General Public License for more details.
24 ;; You should have received a copy of the GNU General Public License
25 ;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
29 ;; Integrates password-store (http://passwordstore.org/) within
35 (eval-when-compile (require 'subr-x
))
38 (require 'auth-source
)
41 (cl-defun auth-source-pass-search (&rest spec
42 &key backend type host user port
44 "Given a property list SPEC, return search matches from the :backend.
45 See `auth-source-search' for details on SPEC."
46 (cl-assert (or (null type
) (eq type
(oref backend type
)))
47 t
"Invalid password-store search: %s %s")
49 ;; Take the first non-nil item of the list of hosts
50 (setq host
(seq-find #'identity host
)))
51 (list (auth-source-pass--build-result host port user
)))
53 (defun auth-source-pass--build-result (host port user
)
54 "Build auth-source-pass entry matching HOST, PORT and USER."
55 (let ((entry (auth-source-pass--find-match host user
)))
59 :port
(or (auth-source-pass-get "port" entry
) port
)
60 :user
(or (auth-source-pass-get "user" entry
) user
)
61 :secret
(lambda () (auth-source-pass-get 'secret entry
)))))
62 (auth-source-pass--do-debug "return %s as final result (plus hidden password)"
63 (seq-subseq retval
0 -
2)) ;; remove password
67 (defun auth-source-pass-enable ()
68 "Enable auth-source-password-store."
69 ;; To add password-store to the list of sources, evaluate the following:
70 (add-to-list 'auth-sources
'password-store
)
71 ;; clear the cache (required after each change to #'auth-source-pass-search)
72 (auth-source-forget-all-cached))
74 (defvar auth-source-pass-backend
76 (format "Password store")
77 :source
"." ;; not used
79 :search-function
#'auth-source-pass-search
)
80 "Auth-source backend for password-store.")
82 (defun auth-source-pass-backend-parse (entry)
83 "Create a password-store auth-source backend from ENTRY."
84 (when (eq entry
'password-store
)
85 (auth-source-backend-parse-parameters entry auth-source-pass-backend
)))
87 (add-hook 'auth-source-backend-parser-functions
#'auth-source-pass-backend-parse
)
90 (defun auth-source-pass-get (key entry
)
91 "Return the value associated to KEY in the password-store entry ENTRY.
93 ENTRY is the name of a password-store entry.
94 The key used to retrieve the password is the symbol `secret'.
96 The convention used as the format for a password-store file is
97 the following (see http://www.passwordstore.org/#organization):
102 (let ((data (auth-source-pass-parse-entry entry
)))
103 (or (cdr (assoc key data
))
104 (and (string= key
"user")
105 (cdr (assoc "username" data
))))))
107 (defun auth-source-pass--read-entry (entry)
108 "Return a string with the file content of ENTRY."
110 (insert-file-contents (expand-file-name
111 (format "%s.gpg" entry
)
112 "~/.password-store"))
113 (buffer-substring-no-properties (point-min) (point-max))))
115 (defun auth-source-pass-parse-entry (entry)
116 "Return an alist of the data associated with ENTRY.
118 ENTRY is the name of a password-store entry."
119 (let ((file-contents (ignore-errors (auth-source-pass--read-entry entry
))))
121 (cons `(secret .
,(auth-source-pass--parse-secret file-contents
))
122 (auth-source-pass--parse-data file-contents
)))))
124 (defun auth-source-pass--parse-secret (contents)
125 "Parse the password-store data in the string CONTENTS and return its secret.
126 The secret is the first line of CONTENTS."
127 (car (split-string contents
"\\\n" t
)))
129 (defun auth-source-pass--parse-data (contents)
130 "Parse the password-store data in the string CONTENTS and return an alist.
131 CONTENTS is the contents of a password-store formatted file."
132 (let ((lines (split-string contents
"\\\n" t
"\\\s")))
134 (mapcar (lambda (line)
135 (let ((pair (mapcar (lambda (s) (string-trim s
))
136 (split-string line
":"))))
137 (when (> (length pair
) 1)
139 (mapconcat #'identity
(cdr pair
) ":")))))
142 (defun auth-source-pass--hostname (host)
143 "Extract hostname from HOST."
144 (let ((url (url-generic-parse-url host
)))
145 (or (url-host url
) host
)))
147 (defun auth-source-pass--hostname-with-user (host)
148 "Extract hostname and user from HOST."
149 (let* ((url (url-generic-parse-url host
))
150 (user (url-user url
))
151 (hostname (url-host url
)))
153 ((and user hostname
) (format "%s@%s" user hostname
))
157 (defun auth-source-pass--user (host)
158 "Extract user from HOST and return it.
159 Return nil if no match was found."
160 (url-user (url-generic-parse-url host
)))
162 (defun auth-source-pass--do-debug (&rest msg
)
163 "Call `auth-source-do-debug` with MSG and a prefix."
164 (apply #'auth-source-do-debug
165 (cons (concat "auth-source-password-store: " (car msg
))
168 (defun auth-source-pass--select-one-entry (entries user
)
169 "Select one entry from ENTRIES by searching for a field matching USER."
170 (let ((number (length entries
))
173 (seq-find (lambda (entry)
174 (string-equal (auth-source-pass-get "user" entry
) user
))
176 (auth-source-pass--do-debug "found %s matches: %s" number
177 (mapconcat #'identity entries
", "))
180 (auth-source-pass--do-debug "return %s as it contains matching user field"
183 (auth-source-pass--do-debug "return %s as it is the first one" (car entries
))
186 (defun auth-source-pass--entry-valid-p (entry)
187 "Return t iff ENTRY can be opened.
188 Also displays a warning if not. This function is slow, don't call it too
190 (if (auth-source-pass-parse-entry entry
)
192 (auth-source-pass--do-debug "entry '%s' is not valid" entry
)
195 ;; TODO: add tests for that when `assess-with-filesystem' is included
197 (defun auth-source-pass-entries ()
198 "Return a list of all password store entries."
199 (let ((store-dir (expand-file-name "~/.password-store/")))
201 (lambda (file) (file-name-sans-extension (file-relative-name file store-dir
)))
202 (directory-files-recursively store-dir
"\.gpg$"))))
204 (defun auth-source-pass--find-all-by-entry-name (entryname user
)
205 "Search the store for all entries either matching ENTRYNAME/USER or ENTRYNAME.
206 Only return valid entries as of `auth-source-pass--entry-valid-p'."
207 (seq-filter (lambda (entry)
210 (let ((components-host-user
211 (member entryname
(split-string entry
"/"))))
212 (and (= (length components-host-user
) 2)
213 (string-equal user
(cadr components-host-user
))))
214 (string-equal entryname
(file-name-nondirectory entry
)))
215 (auth-source-pass--entry-valid-p entry
)))
216 (auth-source-pass-entries)))
218 (defun auth-source-pass--find-one-by-entry-name (entryname user
)
219 "Search the store for an entry matching ENTRYNAME.
220 If USER is non nil, give precedence to entries containing a user field
222 (auth-source-pass--do-debug "searching for '%s' in entry names (user: %s)"
225 (let ((matching-entries (auth-source-pass--find-all-by-entry-name entryname user
)))
226 (pcase (length matching-entries
)
227 (0 (auth-source-pass--do-debug "no match found")
229 (1 (auth-source-pass--do-debug "found 1 match: %s" (car matching-entries
))
230 (car matching-entries
))
231 (_ (auth-source-pass--select-one-entry matching-entries user
)))))
233 (defun auth-source-pass--find-match (host user
)
234 "Return a password-store entry name matching HOST and USER.
235 If many matches are found, return the first one. If no match is
238 (if (auth-source-pass--user host
)
239 ;; if HOST contains a user (e.g., "user@host.com"), <HOST>
240 (auth-source-pass--find-one-by-entry-name (auth-source-pass--hostname-with-user host
) user
)
241 ;; otherwise, if USER is provided, search for <USER>@<HOST>
243 (auth-source-pass--find-one-by-entry-name (concat user
"@" (auth-source-pass--hostname host
)) user
)))
244 ;; if that didn't work, search for HOST without its user component, if any
245 (auth-source-pass--find-one-by-entry-name (auth-source-pass--hostname host
) user
)
246 ;; if that didn't work, search for HOST with user extracted from it
247 (auth-source-pass--find-one-by-entry-name
248 (auth-source-pass--hostname host
) (auth-source-pass--user host
))
249 ;; if that didn't work, remove subdomain: foo.bar.com -> bar.com
250 (let ((components (split-string host
"\\.")))
251 (when (= (length components
) 3)
252 ;; start from scratch
253 (auth-source-pass--find-match (mapconcat 'identity
(cdr components
) ".") user
)))))
255 (provide 'auth-source-pass
)
256 ;;; auth-source-pass.el ends here