Enable `auth-source-debug' by default. Add and use `auth-source-do-warn'.
[gnus] / lisp / auth-source.el
1 ;;; auth-source.el --- authentication sources for Gnus and Emacs
2
3 ;; Copyright (C) 2008-2011 Free Software Foundation, Inc.
4
5 ;; Author: Ted Zlatanov <tzz@lifelogs.com>
6 ;; Keywords: news
7
8 ;; This file is part of GNU Emacs.
9
10 ;; GNU Emacs is free software: you can redistribute it and/or modify
11 ;; it under the terms of the GNU General Public License as published by
12 ;; the Free Software Foundation, either version 3 of the License, or
13 ;; (at your option) any later version.
14
15 ;; GNU Emacs is distributed in the hope that it will be useful,
16 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
17 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18 ;; GNU General Public License for more details.
19
20 ;; You should have received a copy of the GNU General Public License
21 ;; along with GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.
22
23 ;;; Commentary:
24
25 ;; This is the auth-source.el package.  It lets users tell Gnus how to
26 ;; authenticate in a single place.  Simplicity is the goal.  Instead
27 ;; of providing 5000 options, we'll stick to simple, easy to
28 ;; understand options.
29
30 ;; See the auth.info Info documentation for details.
31
32 ;; TODO:
33
34 ;; - never decode the backend file unless it's necessary
35 ;; - a more generic way to match backends and search backend contents
36 ;; - absorb netrc.el and simplify it
37 ;; - protect passwords better
38 ;; - allow creating and changing netrc lines (not files) e.g. change a password
39
40 ;;; Code:
41
42 (require 'password-cache)
43 (require 'gnus-util)
44 (require 'netrc)
45 (require 'assoc)
46 (eval-when-compile (require 'cl))
47 (require 'eieio)
48
49 (autoload 'secrets-create-item "secrets")
50 (autoload 'secrets-delete-item "secrets")
51 (autoload 'secrets-get-alias "secrets")
52 (autoload 'secrets-get-attributes "secrets")
53 (autoload 'secrets-get-secret "secrets")
54 (autoload 'secrets-list-collections "secrets")
55 (autoload 'secrets-search-items "secrets")
56
57 (defvar secrets-enabled)
58
59 (defgroup auth-source nil
60   "Authentication sources."
61   :version "23.1" ;; No Gnus
62   :group 'gnus)
63
64 (defclass auth-source-backend ()
65   ((type :initarg :type
66          :initform 'netrc
67          :type symbol
68          :custom symbol
69          :documentation "The backend type.")
70    (source :initarg :source
71            :type string
72            :custom string
73            :documentation "The backend source.")
74    (host :initarg :host
75          :initform t
76          :type t
77          :custom string
78          :documentation "The backend host.")
79    (user :initarg :user
80          :initform t
81          :type t
82          :custom string
83          :documentation "The backend user.")
84    (protocol :initarg :protocol
85              :initform t
86              :type t
87              :custom string
88              :documentation "The backend protocol.")
89    (create-function :initarg :create-function
90                     :initform ignore
91                     :type function
92                     :custom function
93                     :documentation "The create function.")
94    (search-function :initarg :search-function
95                     :initform ignore
96                     :type function
97                     :custom function
98                     :documentation "The search function.")))
99
100 (defcustom auth-source-protocols '((imap "imap" "imaps" "143" "993")
101                                    (pop3 "pop3" "pop" "pop3s" "110" "995")
102                                    (ssh  "ssh" "22")
103                                    (sftp "sftp" "115")
104                                    (smtp "smtp" "25"))
105   "List of authentication protocols and their names"
106
107   :group 'auth-source
108   :version "23.2" ;; No Gnus
109   :type '(repeat :tag "Authentication Protocols"
110                  (cons :tag "Protocol Entry"
111                        (symbol :tag "Protocol")
112                        (repeat :tag "Names"
113                                (string :tag "Name")))))
114
115 ;;; generate all the protocols in a format Customize can use
116 ;;; TODO: generate on the fly from auth-source-protocols
117 (defconst auth-source-protocols-customize
118   (mapcar (lambda (a)
119             (let ((p (car-safe a)))
120               (list 'const
121                     :tag (upcase (symbol-name p))
122                     p)))
123           auth-source-protocols))
124
125 (defvar auth-source-creation-defaults nil
126   "Defaults for creating token values.  Usually let-bound.")
127
128 (make-obsolete 'auth-source-hide-passwords nil "Emacs 24.1")
129
130 (defvar auth-source-magic "auth-source-magic ")
131
132 (defcustom auth-source-do-cache t
133   "Whether auth-source should cache information with `password-cache'."
134   :group 'auth-source
135   :version "23.2" ;; No Gnus
136   :type `boolean)
137
138 (defcustom auth-source-debug t
139   "Whether auth-source should log debug messages.
140
141 If the value is nil, debug messages are not logged.
142
143 If the value is t, debug messages are logged with `message'.  In
144 that case, your authentication data will be in the clear (except
145 for passwords).
146
147 If the value is a function, debug messages are logged by calling
148  that function using the same arguments as `message'."
149   :group 'auth-source
150   :version "23.2" ;; No Gnus
151   :type `(choice
152           :tag "auth-source debugging mode"
153           (const :tag "Log using `message' to the *Messages* buffer" t)
154           (function :tag "Function that takes arguments like `message'")
155           (const :tag "Don't log anything" nil)))
156
157 (defcustom auth-sources '("~/.authinfo.gpg" "~/.authinfo")
158   "List of authentication sources.
159
160 The default will get login and password information from
161 \"~/.authinfo.gpg\", which you should set up with the EPA/EPG
162 packages to be encrypted.  If that file doesn't exist, it will
163 try the unencrypted version \"~/.authinfo\".
164
165 See the auth.info manual for details.
166
167 Each entry is the authentication type with optional properties.
168
169 It's best to customize this with `M-x customize-variable' because the choices
170 can get pretty complex."
171   :group 'auth-source
172   :version "24.1" ;; No Gnus
173   :type `(repeat :tag "Authentication Sources"
174                  (choice
175                   (string :tag "Just a file")
176                   (const :tag "Default Secrets API Collection" 'default)
177                   (const :tag "Login Secrets API Collection" "secrets:Login")
178                   (const :tag "Temp Secrets API Collection" "secrets:session")
179                   (list :tag "Source definition"
180                         (const :format "" :value :source)
181                         (choice :tag "Authentication backend choice"
182                                 (string :tag "Authentication Source (file)")
183                                 (list
184                                  :tag "Secret Service API/KWallet/GNOME Keyring"
185                                  (const :format "" :value :secrets)
186                                  (choice :tag "Collection to use"
187                                          (string :tag "Collection name")
188                                          (const :tag "Default" 'default)
189                                          (const :tag "Login" "Login")
190                                          (const
191                                           :tag "Temporary" "session"))))
192                         (repeat :tag "Extra Parameters" :inline t
193                                 (choice :tag "Extra parameter"
194                                         (list
195                                          :tag "Host"
196                                          (const :format "" :value :host)
197                                          (choice :tag "Host (machine) choice"
198                                                  (const :tag "Any" t)
199                                                  (regexp
200                                                   :tag "Regular expression")))
201                                         (list
202                                          :tag "Protocol"
203                                          (const :format "" :value :protocol)
204                                          (choice
205                                           :tag "Protocol"
206                                           (const :tag "Any" t)
207                                           ,@auth-source-protocols-customize))
208                                         (list :tag "User" :inline t
209                                               (const :format "" :value :user)
210                                               (choice :tag "Personality/Username"
211                                                       (const :tag "Any" t)
212                                                       (string :tag "Name")))))))))
213
214 (defcustom auth-source-gpg-encrypt-to t
215   "List of recipient keys that `authinfo.gpg' encrypted to.
216 If the value is not a list, symmetric encryption will be used."
217   :group 'auth-source
218   :version "24.1" ;; No Gnus
219   :type '(choice (const :tag "Symmetric encryption" t)
220                  (repeat :tag "Recipient public keys"
221                          (string :tag "Recipient public key"))))
222
223 ;; temp for debugging
224 ;; (unintern 'auth-source-protocols)
225 ;; (unintern 'auth-sources)
226 ;; (customize-variable 'auth-sources)
227 ;; (setq auth-sources nil)
228 ;; (format "%S" auth-sources)
229 ;; (customize-variable 'auth-source-protocols)
230 ;; (setq auth-source-protocols nil)
231 ;; (format "%S" auth-source-protocols)
232 ;; (auth-source-pick nil :host "a" :port 'imap)
233 ;; (auth-source-user-or-password "login" "imap.myhost.com" 'imap)
234 ;; (auth-source-user-or-password "password" "imap.myhost.com" 'imap)
235 ;; (auth-source-user-or-password-imap "login" "imap.myhost.com")
236 ;; (auth-source-user-or-password-imap "password" "imap.myhost.com")
237 ;; (auth-source-protocol-defaults 'imap)
238
239 ;; (let ((auth-source-debug 'debug)) (auth-source-do-debug "hello"))
240 ;; (let ((auth-source-debug t)) (auth-source-do-debug "hello"))
241 ;; (let ((auth-source-debug nil)) (auth-source-do-debug "hello"))
242 (defun auth-source-do-debug (&rest msg)
243   (when auth-source-debug
244     (apply 'auth-source-do-warn msg)))
245
246 (defun auth-source-do-warn (&rest msg)
247   (apply
248     ;; set logger to either the function in auth-source-debug or 'message
249     ;; note that it will be 'message if auth-source-debug is nil
250    (if (functionp auth-source-debug)
251        auth-source-debug
252      'message)
253    msg))
254
255
256 ;; (auth-source-pick nil :host "any" :protocol 'imap :user "joe")
257 ;; (auth-source-pick t :host "any" :protocol 'imap :user "joe")
258 ;; (setq auth-sources '((:source (:secrets default) :host t :protocol t :user "joe")
259 ;;                   (:source (:secrets "session") :host t :protocol t :user "joe")
260 ;;                   (:source (:secrets "Login") :host t :protocol t)
261 ;;                   (:source "~/.authinfo.gpg" :host t :protocol t)))
262
263 ;; (setq auth-sources '((:source (:secrets default) :host t :protocol t :user "joe")
264 ;;                   (:source (:secrets "session") :host t :protocol t :user "joe")
265 ;;                   (:source (:secrets "Login") :host t :protocol t)
266 ;;                   ))
267
268 ;; (setq auth-sources '((:source "~/.authinfo.gpg" :host t :protocol t)))
269
270 ;; (auth-source-backend-parse "myfile.gpg")
271 ;; (auth-source-backend-parse 'default)
272 ;; (auth-source-backend-parse "secrets:Login")
273
274 (defun auth-source-backend-parse (entry)
275   "Creates an auth-source-backend from an ENTRY in `auth-sources'."
276   (auth-source-backend-parse-parameters
277    entry
278    (cond
279     ;; take 'default and recurse to get it as a Secrets API default collection
280     ;; matching any user, host, and protocol
281     ((eq entry 'default)
282      (auth-source-backend-parse '(:source (:secrets default))))
283     ;; take secrets:XYZ and recurse to get it as Secrets API collection "XYZ"
284     ;; matching any user, host, and protocol
285     ((and (stringp entry) (string-match "^secrets:\\(.+\\)" entry))
286      (auth-source-backend-parse `(:source (:secrets ,(match-string 1 entry)))))
287     ;; take just a file name and recurse to get it as a netrc file
288     ;; matching any user, host, and protocol
289     ((stringp entry)
290      (auth-source-backend-parse `(:source ,entry)))
291
292     ;; a file name with parameters
293     ((stringp (plist-get entry :source))
294      (auth-source-backend
295       (plist-get entry :source)
296       :source (plist-get entry :source)
297       :type 'netrc
298       :search-function 'auth-source-netrc-search
299       :create-function 'auth-source-netrc-create))
300
301     ;; the Secrets API.  We require the package, in order to have a
302     ;; defined value for `secrets-enabled'.
303     ((and
304       (not (null (plist-get entry :source))) ; the source must not be nil
305       (listp (plist-get entry :source))      ; and it must be a list
306       (require 'secrets nil t)               ; and we must load the Secrets API
307       secrets-enabled)                       ; and that API must be enabled
308
309      ;; the source is either the :secrets key in ENTRY or
310      ;; if that's missing or nil, it's "session"
311      (let ((source (or (plist-get (plist-get entry :source) :secrets)
312                        "session")))
313
314        ;; if the source is a symbol, we look for the alias named so,
315        ;; and if that alias is missing, we use "Login"
316        (when (symbolp source)
317          (setq source (or (secrets-get-alias (symbol-name source))
318                           "Login")))
319
320        (if (featurep 'secrets)
321            (auth-source-backend
322             (format "Secrets API (%s)" source)
323             :source source
324             :type 'secrets
325             :search-function 'auth-source-secrets-search
326             :create-function 'auth-source-secrets-create)
327          (auth-source-do-warn
328           "auth-source-backend-parse: no Secrets API, ignoring spec: %S" entry)
329          (auth-source-backend
330           (format "Ignored Secrets API (%s)" source)
331           :source ""
332           :type 'ignore))))
333
334     ;; none of them
335     (t
336      (auth-source-do-warn
337       "auth-source-backend-parse: invalid backend spec: %S" entry)
338      (auth-source-backend
339       "Empty"
340       :source ""
341       :type 'ignore)))))
342
343 (defun auth-source-backend-parse-parameters (entry backend)
344   "Fills in the extra auth-source-backend parameters of ENTRY.
345 Using the plist ENTRY, get the :host, :protocol, and :user search
346 parameters.  Accepts :port as an alias to :protocol."
347   (let ((entry (if (stringp entry)
348                    nil
349                  entry))
350         val)
351     (when (setq val (plist-get entry :host))
352       (oset backend host val))
353     (when (setq val (plist-get entry :user))
354       (oset backend user val))
355     ;; accept :port as an alias for :protocol
356     (when (setq val (or (plist-get entry :protocol) (plist-get entry :port)))
357       (oset backend protocol val)))
358   backend)
359
360 ;; (mapcar 'auth-source-backend-parse auth-sources)
361
362 (defun* auth-source-search (&rest spec
363                                   &key type max host user protocol secret
364                                   create delete
365                                   &allow-other-keys)
366   "Search or modify authentication backends according to SPEC.
367
368 This function parses `auth-sources' for matches of the SPEC
369 plist.  It can optionally create or update an authentication
370 token if requested.  A token is just a standard Emacs property
371 list with a :secret property that can be a function; all the
372 other properties will always hold scalar values.
373
374 Typically the :secret property, if present, contains a password.
375
376 Common search keys are :max, :host, :protocol, and :user.  In
377 addition, :create specifies how tokens will be or created.
378 Finally, :type can specify which backend types you want to check.
379
380 A string value is always matched literally.  A symbol is matched
381 as its string value, literally.  All the SPEC values can be
382 single values (symbol or string) or lists thereof (in which case
383 any of the search terms matches).
384
385 :create t means to create a token if possible.
386
387 A new token will be created if no matching tokens were found.
388 The new token will have only the keys the backend requires.  For
389 the netrc backend, for instance, that's the user, host, and
390 protocol keys.
391
392 Here's an example:
393
394 \(let ((auth-source-creation-defaults '((user . \"defaultUser\")
395                                         (A    . \"default A\"))))
396   (auth-source-search :host \"mine\" :type 'netrc :max 1
397                       :P \"pppp\" :Q \"qqqq\"
398                       :create t))
399
400 which says:
401
402 \"Search for any entry matching host 'mine' in backends of type
403  'netrc', maximum one result.
404
405  Create a new entry if you found none.  The netrc backend will
406  automatically require host, user, and protocol.  The host will be
407  'mine'.  We prompt for the user with default 'defaultUser' and
408  for the protocol without a default.  We will not prompt for A, Q,
409  or P.  The resulting token will only have keys user, host, and
410  protocol.\"
411
412 :create '(A B C) also means to create a token if possible.
413
414 The behavior is like :create t but if the list contains any
415 parameter, that parameter will be required in the resulting
416 token.  The value for that parameter will be obtained from the
417 search parameters or from user input.  If any queries are needed,
418 the alist `auth-source-creation-defaults' will be checked for the
419 default prompt.
420
421 Here's an example:
422
423 \(let ((auth-source-creation-defaults '((user . \"defaultUser\")
424                                         (A    . \"default A\"))))
425   (auth-source-search :host '(\"nonesuch\" \"twosuch\") :type 'netrc :max 1
426                       :P \"pppp\" :Q \"qqqq\"
427                       :create '(A B Q)))
428
429 which says:
430
431 \"Search for any entry matching host 'nonesuch'
432  or 'twosuch' in backends of type 'netrc', maximum one result.
433
434  Create a new entry if you found none.  The netrc backend will
435  automatically require host, user, and protocol.  The host will be
436  'nonesuch' and Q will be 'qqqq'.  We prompt for A with default
437  'default A', for B and protocol with default nil, and for the
438  user with default 'defaultUser'.  We will not prompt for Q.  The
439  resulting token will have keys user, host, protocol, A, B, and Q.
440  It will not have P with any value, even though P is used in the
441  search to find only entries that have P set to 'pppp'.\"
442
443 When multiple values are specified in the search parameter, the
444 first one is used for creation.  So :host (X Y Z) would create a
445 token for host X, for instance.
446
447 This creation can fail if the search was not specific enough to
448 create a new token (it's up to the backend to decide that).  You
449 should `catch' the backend-specific error as usual.  Some
450 backends (netrc, at least) will prompt the user rather than throw
451 an error.
452
453 :delete t means to delete any found entries.  nil by default.
454 Use `auth-source-delete' in ELisp code instead of calling
455 `auth-source-search' directly with this parameter.
456
457 :type (X Y Z) will check only those backend types.  'netrc and
458 'secrets are the only ones supported right now.
459
460 :max N means to try to return at most N items (defaults to 1).
461 When 0 the function will return just t or nil to indicate if any
462 matches were found.  More than N items may be returned, depending
463 on the search and the backend.
464
465 :host (X Y Z) means to match only hosts X, Y, or Z according to
466 the match rules above.  Defaults to t.
467
468 :user (X Y Z) means to match only users X, Y, or Z according to
469 the match rules above.  Defaults to t.
470
471 :protocol (P Q R) means to match only protocols P, Q, or R.
472 Defaults to t.
473
474 :K (V1 V2 V3) for any other key K will match values V1, V2, or
475 V3 (note the match rules above).
476
477 The return value is a list with at most :max tokens.  Each token
478 is a plist with keys :backend :host :protocol :user, plus any other
479 keys provided by the backend (notably :secret).  But note the
480 exception for :max 0, which see above.
481
482 The token's :secret key can hold a function.  In that case you
483 must call it to obtain the actual value."
484   (let* ((backends (mapcar 'auth-source-backend-parse auth-sources))
485          (max (or max 1))
486          (ignored-keys '(:create :delete :max))
487          (keys (loop for i below (length spec) by 2
488                      unless (memq (nth i spec) ignored-keys)
489                      collect (nth i spec)))
490          (found (auth-source-recall spec))
491          filtered-backends accessor-key found-here goal)
492
493     (if (and found auth-source-do-cache)
494         (auth-source-do-debug
495          "auth-source-search: found %d CACHED results matching %S"
496          (length found) spec)
497
498       (assert
499        (or (eq t create) (listp create)) t
500        "Invalid auth-source :create parameter (must be nil, t, or a list): %s %s")
501
502       (setq filtered-backends (copy-sequence backends))
503       (dolist (backend backends)
504         (dolist (key keys)
505           ;; ignore invalid slots
506           (condition-case signal
507               (unless (eval `(auth-source-search-collection
508                               (plist-get spec key)
509                               (oref backend ,key)))
510                 (setq filtered-backends (delq backend filtered-backends))
511                 (return))
512             (invalid-slot-name))))
513
514       (auth-source-do-debug
515        "auth-source-search: found %d backends matching %S"
516        (length filtered-backends) spec)
517
518       ;; (debug spec "filtered" filtered-backends)
519       (setq goal max)
520       (dolist (backend filtered-backends)
521         (setq found-here (apply
522                           (slot-value backend 'search-function)
523                           :backend backend
524                           :create create
525                           :delete delete
526                           spec))
527
528         ;; if max is 0, as soon as we find something, return it
529         (when (and (zerop max) (> 0 (length found-here)))
530           (return t))
531
532         ;; decrement the goal by the number of new results
533         (decf goal (length found-here))
534         ;; and append the new results to the full list
535         (setq found (append found found-here))
536
537         (auth-source-do-debug
538          "auth-source-search: found %d results (max %d/%d) in %S matching %S"
539          (length found-here) max goal backend spec)
540
541         ;; return full list if the goal is 0 or negative
542         (when (zerop (max 0 goal))
543           (return found))
544
545         ;; change the :max parameter in the spec to the goal
546         (setq spec (plist-put spec :max goal)))
547
548       (when (and found auth-source-do-cache)
549         (auth-source-remember spec found)))
550
551       found))
552
553 ;;; (auth-source-search :max 1)
554 ;;; (funcall (plist-get (nth 0 (auth-source-search :max 1)) :secret))
555 ;;; (auth-source-search :host "nonesuch" :type 'netrc :K 1)
556 ;;; (auth-source-search :host "nonesuch" :type 'secrets)
557
558 (defun* auth-source-delete (&rest spec
559                                   &key delete
560                                   &allow-other-keys)
561   "Delete entries from the authentication backends according to SPEC.
562 Calls `auth-source-search' with the :delete property in SPEC set to t.
563 The backend may not actually delete the entries.
564
565 Returns the deleted entries."
566   (auth-source-search (plist-put spec :delete t)))
567
568 (defun auth-source-search-collection (collection value)
569   "Returns t is VALUE is t or COLLECTION is t or contains VALUE."
570   (when (and (atom collection) (not (eq t collection)))
571     (setq collection (list collection)))
572
573   ;; (debug :collection collection :value value)
574   (or (eq collection t)
575       (eq value t)
576       (equal collection value)
577       (member value collection)))
578
579 (defun auth-source-forget-all-cached ()
580   "Forget all cached auth-source data."
581   (interactive)
582   (loop for sym being the symbols of password-data
583         ;; when the symbol name starts with auth-source-magic
584         when (string-match (concat "^" auth-source-magic)
585                            (symbol-name sym))
586         ;; remove that key
587         do (password-cache-remove (symbol-name sym))))
588
589 (defun auth-source-remember (spec found)
590   "Remember FOUND search results for SPEC."
591   (password-cache-add
592    (concat auth-source-magic (format "%S" spec)) found))
593
594 (defun auth-source-recall (spec)
595   "Recall FOUND search results for SPEC."
596   (password-read-from-cache
597    (concat auth-source-magic (format "%S" spec))))
598
599 (defun auth-source-forget (spec)
600   "Forget any cached data matching SPEC exactly.
601
602 This is the same SPEC you passed to `auth-source-search'.
603 Returns t or nil for forgotten or not found."
604   (password-cache-remove (concat auth-source-magic (format "%S" spec))))
605
606 ;;; (loop for sym being the symbols of password-data when (string-match (concat "^" auth-source-magic) (symbol-name sym)) collect (symbol-name sym))
607
608 ;;; (auth-source-remember '(:host "wedd") '(4 5 6))
609 ;;; (auth-source-remember '(:host "xedd") '(1 2 3))
610 ;;; (auth-source-recall '(:host "xedd"))
611 ;;; (auth-source-recall '(:host t))
612 ;;; (auth-source-forget+ :host t)
613
614 (defun* auth-source-forget+ (&rest spec &allow-other-keys)
615   "Forget any cached data matching SPEC.  Returns forgotten count.
616
617 This is not a full `auth-source-search' spec but works similarly.
618 For instance, \(:host \"myhost\" \"yourhost\") would find all the
619 cached data that was found with a search for those two hosts,
620 while \(:host t) would find all host entries."
621   (let ((count 0)
622         sname)
623     (loop for sym being the symbols of password-data
624           ;; when the symbol name matches with auth-source-magic
625           when (and (setq sname (symbol-name sym))
626                     (string-match (concat "^" auth-source-magic "\\(.+\\)")
627                                   sname)
628                     ;; and the spec matches what was stored in the cache
629                     (auth-source-specmatchp spec (read (match-string 1 sname))))
630           ;; remove that key
631           do (progn
632                (password-cache-remove sname)
633                (incf count)))
634     count))
635
636 (defun auth-source-specmatchp (spec stored)
637   (let ((keys (loop for i below (length spec) by 2
638                    collect (nth i spec))))
639     (not (eq
640           (dolist (key keys)
641             (unless (auth-source-search-collection (plist-get stored key)
642                                                    (plist-get spec key))
643               (return 'no)))
644           'no))))
645
646 ;;; Backend specific parsing: netrc/authinfo backend
647
648 ;;; (auth-source-netrc-parse "~/.authinfo.gpg")
649 (defun* auth-source-netrc-parse (&rest
650                                  spec
651                                  &key file max host user protocol delete
652                                  &allow-other-keys)
653   "Parse FILE and return a list of all entries in the file.
654 Note that the MAX parameter is used so we can exit the parse early."
655   (if (listp file)
656       ;; We got already parsed contents; just return it.
657       file
658     (when (file-exists-p file)
659       (with-temp-buffer
660         (let ((tokens '("machine" "host" "default" "login" "user"
661                         "password" "account" "macdef" "force"
662                         "port" "protocol"))
663               (max (or max 5000))       ; sanity check: default to stop at 5K
664               (modified 0)
665               alist elem result pair)
666           (insert-file-contents file)
667           (goto-char (point-min))
668           ;; Go through the file, line by line.
669           (while (and (not (eobp))
670                       (> max 0))
671
672             (narrow-to-region (point) (point-at-eol))
673             ;; For each line, get the tokens and values.
674             (while (not (eobp))
675               (skip-chars-forward "\t ")
676               ;; Skip lines that begin with a "#".
677               (if (eq (char-after) ?#)
678                   (goto-char (point-max))
679                 (unless (eobp)
680                   (setq elem
681                         (if (= (following-char) ?\")
682                             (read (current-buffer))
683                           (buffer-substring
684                            (point) (progn (skip-chars-forward "^\t ")
685                                           (point)))))
686                   (cond
687                    ((equal elem "macdef")
688                     ;; We skip past the macro definition.
689                     (widen)
690                     (while (and (zerop (forward-line 1))
691                                 (looking-at "$")))
692                     (narrow-to-region (point) (point)))
693                    ((member elem tokens)
694                     ;; Tokens that don't have a following value are ignored,
695                     ;; except "default".
696                     (when (and pair (or (cdr pair)
697                                         (equal (car pair) "default")))
698                       (push pair alist))
699                     (setq pair (list elem)))
700                    (t
701                     ;; Values that haven't got a preceding token are ignored.
702                     (when pair
703                       (setcdr pair elem)
704                       (push pair alist)
705                       (setq pair nil)))))))
706
707             (when (and alist
708                        (> max 0)
709                        (auth-source-search-collection
710                         host
711                         (or
712                          (aget alist "machine")
713                          (aget alist "host")))
714                        (auth-source-search-collection
715                         user
716                         (or
717                          (aget alist "login")
718                          (aget alist "account")
719                          (aget alist "user")))
720                        (auth-source-search-collection
721                         protocol
722                         (or
723                          (aget alist "port")
724                          (aget alist "protocol"))))
725               (decf max)
726               (push (nreverse alist) result)
727               ;; to delete a line, we just comment it out
728               (when delete
729                 (goto-char (point-min))
730                 (insert "#")
731                 (incf modified)))
732             (setq alist nil
733                   pair nil)
734             (widen)
735             (forward-line 1))
736
737           (when (< 0 modified)
738             (when auth-source-gpg-encrypt-to
739               ;; (see bug#7487) making `epa-file-encrypt-to' local to
740               ;; this buffer lets epa-file skip the key selection query
741               ;; (see the `local-variable-p' check in
742               ;; `epa-file-write-region').
743               (unless (local-variable-p 'epa-file-encrypt-to (current-buffer))
744                 (make-local-variable 'epa-file-encrypt-to))
745               (if (listp auth-source-gpg-encrypt-to)
746                   (setq epa-file-encrypt-to auth-source-gpg-encrypt-to)))
747
748             ;; ask AFTER we've successfully opened the file
749             (when (y-or-n-p (format "Save file %s? (%d modifications)"
750                                     file modified))
751               (write-region (point-min) (point-max) file nil 'silent)
752               (auth-source-do-debug
753                "auth-source-netrc-parse: modified %d lines in %s"
754                modified file)))
755
756           (nreverse result))))))
757
758 (defun auth-source-netrc-normalize (alist)
759   (mapcar (lambda (entry)
760             (let (ret item)
761               (while (setq item (pop entry))
762                 (let ((k (car item))
763                       (v (cdr item)))
764
765                   ;; apply key aliases
766                   (setq k (cond ((member k '("machine")) "host")
767                                 ((member k '("login" "account")) "user")
768                                 ((member k '("protocol")) "port")
769                                 ((member k '("password")) "secret")
770                                 (t k)))
771
772                   ;; send back the secret in a function (lexical binding)
773                   (when (equal k "secret")
774                     (setq v (lexical-let ((v v))
775                               (lambda () v))))
776
777                   (setq ret (plist-put ret
778                                        (intern (concat ":" k))
779                                        v))
780                   ))
781               ret))
782           alist))
783
784 ;;; (setq secret (plist-get (nth 0 (auth-source-search :host t :type 'netrc :K 1 :max 1)) :secret))
785 ;;; (funcall secret)
786
787 (defun* auth-source-netrc-search (&rest
788                                   spec
789                                   &key backend create delete
790                                   type max host user protocol
791                                   &allow-other-keys)
792 "Given a property list SPEC, return search matches from the :backend.
793 See `auth-source-search' for details on SPEC."
794   ;; just in case, check that the type is correct (null or same as the backend)
795   (assert (or (null type) (eq type (oref backend type)))
796           t "Invalid netrc search: %s %s")
797
798   (let ((results (auth-source-netrc-normalize
799                   (auth-source-netrc-parse
800                    :max max
801                    :delete delete
802                    :file (oref backend source)
803                    :host (or host t)
804                    :user (or user t)
805                    :protocol (or protocol t)))))
806
807     ;; if we need to create an entry AND none were found to match
808     (when (and create
809                (= 0 (length results)))
810
811       ;; create based on the spec
812       (apply (slot-value backend 'create-function) spec)
813       ;; turn off the :create key
814       (setq spec (plist-put spec :create nil))
815       ;; run the search again to get the updated data
816       ;; the result will be returned, even if the search fails
817       (setq results (apply 'auth-source-netrc-search spec)))
818
819     results))
820
821 ;;; (auth-source-search :host "nonesuch" :type 'netrc :max 1 :create t)
822 ;;; (auth-source-search :host "nonesuch" :type 'netrc :max 1 :create t :create-extra-keys '((A "default A") (B)))
823
824 (defun* auth-source-netrc-create (&rest spec
825                                         &key backend
826                                         secret host user protocol create
827                                         &allow-other-keys)
828   (let* ((base-required '(host user protocol secret))
829          ;; we know (because of an assertion in auth-source-search) that the
830          ;; :create parameter is either t or a list (which includes nil)
831          (create-extra (if (eq t create) nil create))
832          (required (append base-required create-extra))
833          (file (oref backend source))
834          (add "")
835          ;; `valist' is an alist
836          valist)
837
838     ;; only for base required elements (defined as function parameters):
839     ;; fill in the valist with whatever data we may have from the search
840     ;; we take the first value if it's a list, the whole value otherwise
841     (dolist (br base-required)
842       (when (symbol-value br)
843         (aput 'valist br (if (listp (symbol-value br))
844                              (nth 0 (symbol-value br))
845                            (symbol-value br)))))
846
847     ;; for extra required elements, see if the spec includes a value for them
848     (dolist (er create-extra)
849       (let ((name (concat ":" (symbol-name er)))
850             (keys (loop for i below (length spec) by 2
851                         collect (nth i spec))))
852         (dolist (k keys)
853           (when (equal (symbol-name k) name)
854             (aput 'valist er (plist-get spec k))))))
855
856     ;; for each required element
857     (dolist (r required)
858       (let* ((data (aget valist r))
859              (given-default (aget auth-source-creation-defaults r))
860              ;; the defaults are simple
861              (default (cond
862                        ((and (not given-default) (eq r 'user))
863                         (user-login-name))
864                        ;; note we need this empty string
865                        ((and (not given-default) (eq r 'protocol))
866                         "")
867                        (t given-default)))
868              ;; the prompt's default string depends on the data so far
869              (default-string (if (and default (< 0 (length default)))
870                                  (format " (default %s)" default)
871                                " (no default)"))
872              ;; the prompt should also show what's entered so far
873              (user-value (aget valist 'user))
874              (host-value (aget valist 'host))
875              (protocol-value (aget valist 'protocol))
876              (info-so-far (concat (if user-value
877                                       (format "%s@" user-value)
878                                     "[USER?]")
879                                   (if host-value
880                                       (format "%s" host-value)
881                                     "[HOST?]")
882                                   (if protocol-value
883                                       ;; this distinguishes protocol between
884                                       (if (zerop (length protocol-value))
885                                           "" ; 'entered as "no default"' vs.
886                                         (format ":%s" protocol-value)) ; given
887                                     ;; and this is when the protocol is unknown
888                                     "[PROTOCOL?]"))))
889
890         ;; now prompt if the search SPEC did not include a required key;
891         ;; take the result and put it in `data' AND store it in `valist'
892         (aput 'valist r
893               (setq data
894                     (cond
895                      ((and (null data) (eq r 'secret))
896                       ;; special case prompt for passwords
897                       (read-passwd (format "Password for %s: " info-so-far)))
898                      ((null data)
899                       (read-string
900                        (format "Enter %s for %s%s: "
901                                r info-so-far default-string)
902                        nil nil default))
903                      (t data))))
904
905         ;; when r is not an empty string...
906         (when (and (stringp data)
907                    (< 0 (length data)))
908           ;; append the key (the symbol name of r) and the value in r
909           (setq add (concat add
910                             (format "%s%s %S"
911                                     ;; prepend a space
912                                     (if (zerop (length add)) "" " ")
913                                     ;; remap auth-source tokens to netrc
914                                     (case r
915                                      ('user "login")
916                                      ('host "machine")
917                                      ('secret "password")
918                                      ('protocol "port")
919                                      (t (symbol-name r)))
920                                     ;; the value will be printed in %S format
921                                     data))))))
922
923     (with-temp-buffer
924       (when (file-exists-p file)
925         (insert-file-contents file))
926       (when auth-source-gpg-encrypt-to
927         ;; (see bug#7487) making `epa-file-encrypt-to' local to
928         ;; this buffer lets epa-file skip the key selection query
929         ;; (see the `local-variable-p' check in
930         ;; `epa-file-write-region').
931         (unless (local-variable-p 'epa-file-encrypt-to (current-buffer))
932           (make-local-variable 'epa-file-encrypt-to))
933         (if (listp auth-source-gpg-encrypt-to)
934             (setq epa-file-encrypt-to auth-source-gpg-encrypt-to)))
935       (goto-char (point-max))
936
937       ;; ask AFTER we've successfully opened the file
938       (when (y-or-n-p (format "Add to file %s: line [%s]" file add))
939         (unless (bolp)
940           (insert "\n"))
941         (insert add "\n")
942         (write-region (point-min) (point-max) file nil 'silent)
943         (auth-source-do-debug
944          "auth-source-netrc-create: wrote 1 new line to %s"
945          file)))))
946
947 ;;; Backend specific parsing: Secrets API backend
948
949 ;;; (let ((auth-sources '(default))) (auth-source-search :max 1 :create t))
950 ;;; (let ((auth-sources '(default))) (auth-source-search :max 1 :delete t))
951 ;;; (let ((auth-sources '(default))) (auth-source-search :max 1))
952 ;;; (let ((auth-sources '(default))) (auth-source-search))
953 ;;; (let ((auth-sources '("secrets:Login"))) (auth-source-search :max 1))
954 ;;; (let ((auth-sources '("secrets:Login"))) (auth-source-search :max 1 :signon_realm "https://git.gnus.org/Git"))
955
956 (defun* auth-source-secrets-search (&rest
957                                     spec
958                                     &key backend create delete label
959                                     type max host user protocol
960                                     &allow-other-keys)
961   "Search the Secrets API; spec is like `auth-source'.
962
963 The :label key specifies the item's label.  It is the only key
964 that can specify a substring.  Any :label value besides a string
965 will allow any label.
966
967 All other search keys must match exactly.  If you need substring
968 matching, do a wider search and narrow it down yourself.
969
970 You'll get back all the properties of the token as a plist.
971
972 Here's an example that looks for the first item in the 'Login'
973 Secrets collection:
974
975  \(let ((auth-sources '(\"secrets:Login\")))
976     (auth-source-search :max 1)
977
978 Here's another that looks for the first item in the 'Login'
979 Secrets collection whose label contains 'gnus':
980
981  \(let ((auth-sources '(\"secrets:Login\")))
982     (auth-source-search :max 1 :label \"gnus\")
983
984 And this one looks for the first item in the 'Login' Secrets
985 collection that's a Google Chrome entry for the git.gnus.org site
986 authentication tokens:
987
988  \(let ((auth-sources '(\"secrets:Login\")))
989     (auth-source-search :max 1 :signon_realm \"https://git.gnus.org/Git\"))
990 "
991
992   ;; TODO
993   (assert (not create) nil
994           "The Secrets API auth-source backend doesn't support creation yet")
995   ;; TODO
996   ;; (secrets-delete-item coll elt)
997   (assert (not delete) nil
998           "The Secrets API auth-source backend doesn't support deletion yet")
999
1000   (let* ((coll (oref backend source))
1001          (max (or max 5000))     ; sanity check: default to stop at 5K
1002          (ignored-keys '(:create :delete :max :backend :label))
1003          (search-keys (loop for i below (length spec) by 2
1004                             unless (memq (nth i spec) ignored-keys)
1005                             collect (nth i spec)))
1006          ;; build a search spec without the ignored keys
1007          ;; if a search key is nil or t (match anything), we skip it
1008          (search-spec (apply 'append (mapcar
1009                                       (lambda (k)
1010                                         (if (or (null (plist-get spec k))
1011                                                 (eq t (plist-get spec k)))
1012                                             nil
1013                                           (list k (plist-get spec k))))
1014                               search-keys)))
1015          ;; needed keys (always including host, login, protocol, and secret)
1016          (returned-keys (delete-dups (append
1017                                       '(:host :login :protocol :secret)
1018                                       search-keys)))
1019          (items (loop for item in (apply 'secrets-search-items coll search-spec)
1020                       unless (and (stringp label)
1021                                   (not (string-match label item)))
1022                       collect item))
1023          ;; TODO: respect max in `secrets-search-items', not after the fact
1024          (items (butlast items (- (length items) max)))
1025          ;; convert the item name to a full plist
1026          (items (mapcar (lambda (item)
1027                           (append
1028                            ;; make an entry for the secret (password) element
1029                            (list
1030                             :secret
1031                             (lexical-let ((v (secrets-get-secret coll item)))
1032                               (lambda () v)))
1033                            ;; rewrite the entry from ((k1 v1) (k2 v2)) to plist
1034                            (apply 'append
1035                                   (mapcar (lambda (entry)
1036                                             (list (car entry) (cdr entry)))
1037                                           (secrets-get-attributes coll item)))))
1038                         items))
1039          ;; ensure each item has each key in `returned-keys'
1040          (items (mapcar (lambda (plist)
1041                           (append
1042                            (apply 'append
1043                                   (mapcar (lambda (req)
1044                                             (if (plist-get plist req)
1045                                                 nil
1046                                               (list req nil)))
1047                                           returned-keys))
1048                            plist))
1049                         items)))
1050     items))
1051
1052 (defun* auth-source-secrets-create (&rest
1053                                     spec
1054                                     &key backend type max host user protocol
1055                                     &allow-other-keys)
1056   ;; TODO
1057   ;; (apply 'secrets-create-item (auth-get-source entry) name passwd spec)
1058   (debug spec))
1059
1060 ;;; older API
1061
1062 ;;; (auth-source-user-or-password '("login" "password") "imap.myhost.com" t "tzz")
1063
1064 ;; deprecate the old interface
1065 (make-obsolete 'auth-source-user-or-password
1066                'auth-source-search "Emacs 24.1")
1067 (make-obsolete 'auth-source-forget-user-or-password
1068                'auth-source-forget "Emacs 24.1")
1069
1070 (defun auth-source-user-or-password
1071   (mode host protocol &optional username create-missing delete-existing)
1072   "Find MODE (string or list of strings) matching HOST and PROTOCOL.
1073
1074 DEPRECATED in favor of `auth-source-search'!
1075
1076 USERNAME is optional and will be used as \"login\" in a search
1077 across the Secret Service API (see secrets.el) if the resulting
1078 items don't have a username.  This means that if you search for
1079 username \"joe\" and it matches an item but the item doesn't have
1080 a :user attribute, the username \"joe\" will be returned.
1081
1082 A non nil DELETE-EXISTING means deleting any matching password
1083 entry in the respective sources.  This is useful only when
1084 CREATE-MISSING is non nil as well; the intended use case is to
1085 remove wrong password entries.
1086
1087 If no matching entry is found, and CREATE-MISSING is non nil,
1088 the password will be retrieved interactively, and it will be
1089 stored in the password database which matches best (see
1090 `auth-sources').
1091
1092 MODE can be \"login\" or \"password\"."
1093   (auth-source-do-debug
1094    "auth-source-user-or-password: DEPRECATED get %s for %s (%s) + user=%s"
1095    mode host protocol username)
1096
1097   (let* ((listy (listp mode))
1098          (mode (if listy mode (list mode)))
1099          (cname (if username
1100                     (format "%s %s:%s %s" mode host protocol username)
1101                   (format "%s %s:%s" mode host protocol)))
1102          (search (list :host host :protocol protocol))
1103          (search (if username (append search (list :user username)) search))
1104          (search (if create-missing
1105                      (append search (list :create t))
1106                    search))
1107          (search (if delete-existing
1108                      (append search (list :delete t))
1109                    search))
1110          ;; (found (if (not delete-existing)
1111          ;;            (gethash cname auth-source-cache)
1112          ;;          (remhash cname auth-source-cache)
1113          ;;          nil)))
1114          (found nil))
1115     (if found
1116         (progn
1117           (auth-source-do-debug
1118            "auth-source-user-or-password: DEPRECATED cached %s=%s for %s (%s) + %s"
1119            mode
1120            ;; don't show the password
1121            (if (and (member "password" mode) t)
1122                "SECRET"
1123              found)
1124            host protocol username)
1125           found)                        ; return the found data
1126       ;; else, if not found, search with a max of 1
1127       (let ((choice (nth 0 (apply 'auth-source-search
1128                                   (append '(:max 1) search)))))
1129         (when choice
1130           (dolist (m mode)
1131             (cond
1132              ((equal "password" m)
1133               (push (if (plist-get choice :secret)
1134                       (funcall (plist-get choice :secret))
1135                     nil) found))
1136              ((equal "login" m)
1137               (push (plist-get choice :user) found)))))
1138         (setq found (nreverse found))
1139         (setq found (if listy found (car-safe found)))))
1140
1141         found))
1142
1143 (provide 'auth-source)
1144
1145 ;;; auth-source.el ends here