* admin/gitmerge.el (gitmerge-missing):
[emacs.git] / lisp / auth-source-pass.el
blobf4f096160ef73cb8d7186b2103c4475142db83c4
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>
7 ;; Version: 2.0.0
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/>.
27 ;;; Commentary:
29 ;; Integrates password-store (http://passwordstore.org/) within
30 ;; auth-source.
32 ;;; Code:
34 (require 'seq)
35 (eval-when-compile (require 'subr-x))
36 (eval-when-compile
37 (require 'cl-lib))
38 (require 'auth-source)
39 (require 'url-parse)
41 (cl-defun auth-source-pass-search (&rest spec
42 &key backend type host user port
43 &allow-other-keys)
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")
48 (when (listp host)
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)))
56 (when entry
57 (let ((retval (list
58 :host host
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
64 retval))))
66 ;;;###autoload
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
75 (auth-source-backend
76 (format "Password store")
77 :source "." ;; not used
78 :type 'password-store
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):
99 secret
100 key1: value1
101 key2: value2"
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."
109 (with-temp-buffer
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))))
120 (and file-contents
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")))
133 (seq-remove #'null
134 (mapcar (lambda (line)
135 (let ((pair (mapcar (lambda (s) (string-trim s))
136 (split-string line ":"))))
137 (when (> (length pair) 1)
138 (cons (car pair)
139 (mapconcat #'identity (cdr pair) ":")))))
140 (cdr lines)))))
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)))
152 (cond
153 ((and user hostname) (format "%s@%s" user hostname))
154 (hostname hostname)
155 (t host))))
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))
166 (cdr 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))
171 (entry-with-user
172 (and user
173 (seq-find (lambda (entry)
174 (string-equal (auth-source-pass-get "user" entry) user))
175 entries))))
176 (auth-source-pass--do-debug "found %s matches: %s" number
177 (mapconcat #'identity entries ", "))
178 (if entry-with-user
179 (progn
180 (auth-source-pass--do-debug "return %s as it contains matching user field"
181 entry-with-user)
182 entry-with-user)
183 (auth-source-pass--do-debug "return %s as it is the first one" (car entries))
184 (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
189 often."
190 (if (auth-source-pass-parse-entry entry)
192 (auth-source-pass--do-debug "entry '%s' is not valid" entry)
193 nil))
195 ;; TODO: add tests for that when `assess-with-filesystem' is included
196 ;; in Emacs
197 (defun auth-source-pass-entries ()
198 "Return a list of all password store entries."
199 (let ((store-dir (expand-file-name "~/.password-store/")))
200 (mapcar
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)
208 (and
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
221 matching USER."
222 (auth-source-pass--do-debug "searching for '%s' in entry names (user: %s)"
223 entryname
224 user)
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")
228 nil)
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
236 found, return nil."
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>
242 (when (stringp user)
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