Do smarter auth-source creation if needed and still work without creation.
[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 and record the value
812       (setq results (or
813                      ;; if the user did not want to create the entry
814                      ;; in the file, it will be returned
815                      (apply (slot-value backend 'create-function) spec)
816                      ;; if not, we do the search again without :create
817                      ;; to get the updated data.
818
819                      ;; the result will be returned, even if the search fails
820                      (apply 'auth-source-netrc-search
821                             (plist-put spec :create nil)))))
822     results))
823
824 ;;; (auth-source-search :host "nonesuch" :type 'netrc :max 1 :create t)
825 ;;; (auth-source-search :host "nonesuch" :type 'netrc :max 1 :create t :create-extra-keys '((A "default A") (B)))
826
827 (defun* auth-source-netrc-create (&rest spec
828                                         &key backend
829                                         secret host user protocol create
830                                         &allow-other-keys)
831   (let* ((base-required '(host user protocol secret))
832          ;; we know (because of an assertion in auth-source-search) that the
833          ;; :create parameter is either t or a list (which includes nil)
834          (create-extra (if (eq t create) nil create))
835          (required (append base-required create-extra))
836          (file (oref backend source))
837          (add "")
838          ;; `valist' is an alist
839          valist
840          ;; `artificial' will be returned if no creation is needed
841          artificial)
842
843     ;; only for base required elements (defined as function parameters):
844     ;; fill in the valist with whatever data we may have from the search
845     ;; we take the first value if it's a list, the whole value otherwise
846     (dolist (br base-required)
847       (when (symbol-value br)
848         (aput 'valist br (if (listp (symbol-value br))
849                              (nth 0 (symbol-value br))
850                            (symbol-value br)))))
851
852     ;; for extra required elements, see if the spec includes a value for them
853     (dolist (er create-extra)
854       (let ((name (concat ":" (symbol-name er)))
855             (keys (loop for i below (length spec) by 2
856                         collect (nth i spec))))
857         (dolist (k keys)
858           (when (equal (symbol-name k) name)
859             (aput 'valist er (plist-get spec k))))))
860
861     ;; for each required element
862     (dolist (r required)
863       (let* ((data (aget valist r))
864              (given-default (aget auth-source-creation-defaults r))
865              ;; the defaults are simple
866              (default (cond
867                        ((and (not given-default) (eq r 'user))
868                         (user-login-name))
869                        ;; note we need this empty string
870                        ((and (not given-default) (eq r 'protocol))
871                         "")
872                        (t given-default)))
873              ;; the prompt's default string depends on the data so far
874              (default-string (if (and default (< 0 (length default)))
875                                  (format " (default %s)" default)
876                                " (no default)"))
877              ;; the prompt should also show what's entered so far
878              (user-value (aget valist 'user))
879              (host-value (aget valist 'host))
880              (protocol-value (aget valist 'protocol))
881              (info-so-far (concat (if user-value
882                                       (format "%s@" user-value)
883                                     "[USER?]")
884                                   (if host-value
885                                       (format "%s" host-value)
886                                     "[HOST?]")
887                                   (if protocol-value
888                                       ;; this distinguishes protocol between
889                                       (if (zerop (length protocol-value))
890                                           "" ; 'entered as "no default"' vs.
891                                         (format ":%s" protocol-value)) ; given
892                                     ;; and this is when the protocol is unknown
893                                     "[PROTOCOL?]"))))
894
895         ;; now prompt if the search SPEC did not include a required key;
896         ;; take the result and put it in `data' AND store it in `valist'
897         (aput 'valist r
898               (setq data
899                     (cond
900                      ((and (null data) (eq r 'secret))
901                       ;; special case prompt for passwords
902                       (read-passwd (format "Password for %s: " info-so-far)))
903                      ((null data)
904                       (read-string
905                        (format "Enter %s for %s%s: "
906                                r info-so-far default-string)
907                        nil nil default))
908                      (t data))))
909
910         (when data
911           (setq artificial (plist-put artificial
912                                       (intern (concat ":" (symbol-name r)))
913                                       (if (eq r 'secret)
914                                           (lexical-let ((data data))
915                                             (lambda () data))
916                                         data))))
917
918         ;; when r is not an empty string...
919         (when (and (stringp data)
920                    (< 0 (length data)))
921           ;; append the key (the symbol name of r) and the value in r
922           (setq add (concat add
923                             (format "%s%s %S"
924                                     ;; prepend a space
925                                     (if (zerop (length add)) "" " ")
926                                     ;; remap auth-source tokens to netrc
927                                     (case r
928                                      ('user "login")
929                                      ('host "machine")
930                                      ('secret "password")
931                                      ('protocol "port")
932                                      (t (symbol-name r)))
933                                     ;; the value will be printed in %S format
934                                     data))))))
935
936     (with-temp-buffer
937       (when (file-exists-p file)
938         (insert-file-contents file))
939       (when auth-source-gpg-encrypt-to
940         ;; (see bug#7487) making `epa-file-encrypt-to' local to
941         ;; this buffer lets epa-file skip the key selection query
942         ;; (see the `local-variable-p' check in
943         ;; `epa-file-write-region').
944         (unless (local-variable-p 'epa-file-encrypt-to (current-buffer))
945           (make-local-variable 'epa-file-encrypt-to))
946         (if (listp auth-source-gpg-encrypt-to)
947             (setq epa-file-encrypt-to auth-source-gpg-encrypt-to)))
948       (goto-char (point-max))
949
950       ;; ask AFTER we've successfully opened the file
951       (if (y-or-n-p (format "Add to file %s: line [%s]" file add))
952           (progn
953             (unless (bolp)
954               (insert "\n"))
955             (insert add "\n")
956             (write-region (point-min) (point-max) file nil 'silent)
957             (auth-source-do-debug
958              "auth-source-netrc-create: wrote 1 new line to %s"
959              file)
960             nil)
961         (list artificial)))))
962
963 ;;; Backend specific parsing: Secrets API backend
964
965 ;;; (let ((auth-sources '(default))) (auth-source-search :max 1 :create t))
966 ;;; (let ((auth-sources '(default))) (auth-source-search :max 1 :delete t))
967 ;;; (let ((auth-sources '(default))) (auth-source-search :max 1))
968 ;;; (let ((auth-sources '(default))) (auth-source-search))
969 ;;; (let ((auth-sources '("secrets:Login"))) (auth-source-search :max 1))
970 ;;; (let ((auth-sources '("secrets:Login"))) (auth-source-search :max 1 :signon_realm "https://git.gnus.org/Git"))
971
972 (defun* auth-source-secrets-search (&rest
973                                     spec
974                                     &key backend create delete label
975                                     type max host user protocol
976                                     &allow-other-keys)
977   "Search the Secrets API; spec is like `auth-source'.
978
979 The :label key specifies the item's label.  It is the only key
980 that can specify a substring.  Any :label value besides a string
981 will allow any label.
982
983 All other search keys must match exactly.  If you need substring
984 matching, do a wider search and narrow it down yourself.
985
986 You'll get back all the properties of the token as a plist.
987
988 Here's an example that looks for the first item in the 'Login'
989 Secrets collection:
990
991  \(let ((auth-sources '(\"secrets:Login\")))
992     (auth-source-search :max 1)
993
994 Here's another that looks for the first item in the 'Login'
995 Secrets collection whose label contains 'gnus':
996
997  \(let ((auth-sources '(\"secrets:Login\")))
998     (auth-source-search :max 1 :label \"gnus\")
999
1000 And this one looks for the first item in the 'Login' Secrets
1001 collection that's a Google Chrome entry for the git.gnus.org site
1002 authentication tokens:
1003
1004  \(let ((auth-sources '(\"secrets:Login\")))
1005     (auth-source-search :max 1 :signon_realm \"https://git.gnus.org/Git\"))
1006 "
1007
1008   ;; TODO
1009   (assert (not create) nil
1010           "The Secrets API auth-source backend doesn't support creation yet")
1011   ;; TODO
1012   ;; (secrets-delete-item coll elt)
1013   (assert (not delete) nil
1014           "The Secrets API auth-source backend doesn't support deletion yet")
1015
1016   (let* ((coll (oref backend source))
1017          (max (or max 5000))     ; sanity check: default to stop at 5K
1018          (ignored-keys '(:create :delete :max :backend :label))
1019          (search-keys (loop for i below (length spec) by 2
1020                             unless (memq (nth i spec) ignored-keys)
1021                             collect (nth i spec)))
1022          ;; build a search spec without the ignored keys
1023          ;; if a search key is nil or t (match anything), we skip it
1024          (search-spec (apply 'append (mapcar
1025                                       (lambda (k)
1026                                         (if (or (null (plist-get spec k))
1027                                                 (eq t (plist-get spec k)))
1028                                             nil
1029                                           (list k (plist-get spec k))))
1030                               search-keys)))
1031          ;; needed keys (always including host, login, protocol, and secret)
1032          (returned-keys (delete-dups (append
1033                                       '(:host :login :protocol :secret)
1034                                       search-keys)))
1035          (items (loop for item in (apply 'secrets-search-items coll search-spec)
1036                       unless (and (stringp label)
1037                                   (not (string-match label item)))
1038                       collect item))
1039          ;; TODO: respect max in `secrets-search-items', not after the fact
1040          (items (butlast items (- (length items) max)))
1041          ;; convert the item name to a full plist
1042          (items (mapcar (lambda (item)
1043                           (append
1044                            ;; make an entry for the secret (password) element
1045                            (list
1046                             :secret
1047                             (lexical-let ((v (secrets-get-secret coll item)))
1048                               (lambda () v)))
1049                            ;; rewrite the entry from ((k1 v1) (k2 v2)) to plist
1050                            (apply 'append
1051                                   (mapcar (lambda (entry)
1052                                             (list (car entry) (cdr entry)))
1053                                           (secrets-get-attributes coll item)))))
1054                         items))
1055          ;; ensure each item has each key in `returned-keys'
1056          (items (mapcar (lambda (plist)
1057                           (append
1058                            (apply 'append
1059                                   (mapcar (lambda (req)
1060                                             (if (plist-get plist req)
1061                                                 nil
1062                                               (list req nil)))
1063                                           returned-keys))
1064                            plist))
1065                         items)))
1066     items))
1067
1068 (defun* auth-source-secrets-create (&rest
1069                                     spec
1070                                     &key backend type max host user protocol
1071                                     &allow-other-keys)
1072   ;; TODO
1073   ;; (apply 'secrets-create-item (auth-get-source entry) name passwd spec)
1074   (debug spec))
1075
1076 ;;; older API
1077
1078 ;;; (auth-source-user-or-password '("login" "password") "imap.myhost.com" t "tzz")
1079
1080 ;; deprecate the old interface
1081 (make-obsolete 'auth-source-user-or-password
1082                'auth-source-search "Emacs 24.1")
1083 (make-obsolete 'auth-source-forget-user-or-password
1084                'auth-source-forget "Emacs 24.1")
1085
1086 (defun auth-source-user-or-password
1087   (mode host protocol &optional username create-missing delete-existing)
1088   "Find MODE (string or list of strings) matching HOST and PROTOCOL.
1089
1090 DEPRECATED in favor of `auth-source-search'!
1091
1092 USERNAME is optional and will be used as \"login\" in a search
1093 across the Secret Service API (see secrets.el) if the resulting
1094 items don't have a username.  This means that if you search for
1095 username \"joe\" and it matches an item but the item doesn't have
1096 a :user attribute, the username \"joe\" will be returned.
1097
1098 A non nil DELETE-EXISTING means deleting any matching password
1099 entry in the respective sources.  This is useful only when
1100 CREATE-MISSING is non nil as well; the intended use case is to
1101 remove wrong password entries.
1102
1103 If no matching entry is found, and CREATE-MISSING is non nil,
1104 the password will be retrieved interactively, and it will be
1105 stored in the password database which matches best (see
1106 `auth-sources').
1107
1108 MODE can be \"login\" or \"password\"."
1109   (auth-source-do-debug
1110    "auth-source-user-or-password: DEPRECATED get %s for %s (%s) + user=%s"
1111    mode host protocol username)
1112
1113   (let* ((listy (listp mode))
1114          (mode (if listy mode (list mode)))
1115          (cname (if username
1116                     (format "%s %s:%s %s" mode host protocol username)
1117                   (format "%s %s:%s" mode host protocol)))
1118          (search (list :host host :protocol protocol))
1119          (search (if username (append search (list :user username)) search))
1120          (search (if create-missing
1121                      (append search (list :create t))
1122                    search))
1123          (search (if delete-existing
1124                      (append search (list :delete t))
1125                    search))
1126          ;; (found (if (not delete-existing)
1127          ;;            (gethash cname auth-source-cache)
1128          ;;          (remhash cname auth-source-cache)
1129          ;;          nil)))
1130          (found nil))
1131     (if found
1132         (progn
1133           (auth-source-do-debug
1134            "auth-source-user-or-password: DEPRECATED cached %s=%s for %s (%s) + %s"
1135            mode
1136            ;; don't show the password
1137            (if (and (member "password" mode) t)
1138                "SECRET"
1139              found)
1140            host protocol username)
1141           found)                        ; return the found data
1142       ;; else, if not found, search with a max of 1
1143       (let ((choice (nth 0 (apply 'auth-source-search
1144                                   (append '(:max 1) search)))))
1145         (when choice
1146           (dolist (m mode)
1147             (cond
1148              ((equal "password" m)
1149               (push (if (plist-get choice :secret)
1150                       (funcall (plist-get choice :secret))
1151                     nil) found))
1152              ((equal "login" m)
1153               (push (plist-get choice :user) found)))))
1154         (setq found (nreverse found))
1155         (setq found (if listy found (car-safe found)))))
1156
1157         found))
1158
1159 (provide 'auth-source)
1160
1161 ;;; auth-source.el ends here