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