Commit | Line | Data |
---|---|---|
8f7abae3 MB |
1 | ;;; auth-source.el --- authentication sources for Gnus and Emacs |
2 | ||
ba318903 | 3 | ;; Copyright (C) 2008-2014 Free Software Foundation, Inc. |
8f7abae3 MB |
4 | |
5 | ;; Author: Ted Zlatanov <tzz@lifelogs.com> | |
6 | ;; Keywords: news | |
7 | ||
8 | ;; This file is part of GNU Emacs. | |
9 | ||
5e809f55 | 10 | ;; GNU Emacs is free software: you can redistribute it and/or modify |
8f7abae3 | 11 | ;; it under the terms of the GNU General Public License as published by |
5e809f55 GM |
12 | ;; the Free Software Foundation, either version 3 of the License, or |
13 | ;; (at your option) any later version. | |
8f7abae3 MB |
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 | |
5e809f55 | 17 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
8f7abae3 MB |
18 | ;; GNU General Public License for more details. |
19 | ||
20 | ;; You should have received a copy of the GNU General Public License | |
5e809f55 | 21 | ;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>. |
8f7abae3 MB |
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. | |
d55fe5bb | 29 | |
554a69b8 | 30 | ;; See the auth.info Info documentation for details. |
4079589f | 31 | |
cbabe91f TZ |
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 | ||
8f7abae3 MB |
40 | ;;; Code: |
41 | ||
b8e0f0cd | 42 | (require 'password-cache) |
d638ac9e | 43 | (require 'mm-util) |
e952b711 | 44 | (require 'gnus-util) |
936d08bb | 45 | |
8f7abae3 | 46 | (eval-when-compile (require 'cl)) |
b68bbc23 | 47 | (require 'eieio) |
b8e0f0cd | 48 | |
0e4966fb MA |
49 | (autoload 'secrets-create-item "secrets") |
50 | (autoload 'secrets-delete-item "secrets") | |
ec7995fa | 51 | (autoload 'secrets-get-alias "secrets") |
b8e0f0cd | 52 | (autoload 'secrets-get-attributes "secrets") |
fb178e4c | 53 | (autoload 'secrets-get-secret "secrets") |
0e4966fb MA |
54 | (autoload 'secrets-list-collections "secrets") |
55 | (autoload 'secrets-search-items "secrets") | |
8f7abae3 | 56 | |
4248cca2 TZ |
57 | (autoload 'rfc2104-hash "rfc2104") |
58 | ||
8977de27 DU |
59 | (autoload 'plstore-open "plstore") |
60 | (autoload 'plstore-find "plstore") | |
61 | (autoload 'plstore-put "plstore") | |
f3078a00 | 62 | (autoload 'plstore-delete "plstore") |
8977de27 | 63 | (autoload 'plstore-save "plstore") |
c302199d | 64 | (autoload 'plstore-get-file "plstore") |
8977de27 | 65 | |
b09c3fe0 G |
66 | (autoload 'epg-make-context "epg") |
67 | (autoload 'epg-context-set-passphrase-callback "epg") | |
68 | (autoload 'epg-decrypt-string "epg") | |
69 | (autoload 'epg-context-set-armor "epg") | |
70 | (autoload 'epg-encrypt-string "epg") | |
71 | ||
b0de839f KY |
72 | (autoload 'help-mode "help-mode" nil t) |
73 | ||
b8e0f0cd G |
74 | (defvar secrets-enabled) |
75 | ||
8f7abae3 MB |
76 | (defgroup auth-source nil |
77 | "Authentication sources." | |
9b3ebcb6 | 78 | :version "23.1" ;; No Gnus |
8f7abae3 MB |
79 | :group 'gnus) |
80 | ||
584c9d3f G |
81 | ;;;###autoload |
82 | (defcustom auth-source-cache-expiry 7200 | |
83 | "How many seconds passwords are cached, or nil to disable | |
84 | expiring. Overrides `password-cache-expiry' through a | |
85 | let-binding." | |
2bed3f04 | 86 | :version "24.1" |
584c9d3f G |
87 | :group 'auth-source |
88 | :type '(choice (const :tag "Never" nil) | |
89 | (const :tag "All Day" 86400) | |
90 | (const :tag "2 Hours" 7200) | |
91 | (const :tag "30 Minutes" 1800) | |
92 | (integer :tag "Seconds"))) | |
93 | ||
cbffd0bd SM |
94 | ;; The slots below correspond with the `auth-source-search' spec, |
95 | ;; so a backend with :host set, for instance, would match only | |
96 | ;; searches for that host. Normally they are nil. | |
b8e0f0cd G |
97 | (defclass auth-source-backend () |
98 | ((type :initarg :type | |
99 | :initform 'netrc | |
100 | :type symbol | |
101 | :custom symbol | |
102 | :documentation "The backend type.") | |
103 | (source :initarg :source | |
104 | :type string | |
105 | :custom string | |
106 | :documentation "The backend source.") | |
107 | (host :initarg :host | |
108 | :initform t | |
109 | :type t | |
110 | :custom string | |
111 | :documentation "The backend host.") | |
112 | (user :initarg :user | |
113 | :initform t | |
114 | :type t | |
115 | :custom string | |
116 | :documentation "The backend user.") | |
35123c04 TZ |
117 | (port :initarg :port |
118 | :initform t | |
119 | :type t | |
120 | :custom string | |
121 | :documentation "The backend protocol.") | |
65afde5c | 122 | (data :initarg :data |
b09c3fe0 G |
123 | :initform nil |
124 | :documentation "Internal backend data.") | |
b8e0f0cd G |
125 | (create-function :initarg :create-function |
126 | :initform ignore | |
127 | :type function | |
128 | :custom function | |
129 | :documentation "The create function.") | |
130 | (search-function :initarg :search-function | |
131 | :initform ignore | |
132 | :type function | |
133 | :custom function | |
134 | :documentation "The search function."))) | |
135 | ||
9b3ebcb6 | 136 | (defcustom auth-source-protocols '((imap "imap" "imaps" "143" "993") |
cbabe91f TZ |
137 | (pop3 "pop3" "pop" "pop3s" "110" "995") |
138 | (ssh "ssh" "22") | |
139 | (sftp "sftp" "115") | |
140 | (smtp "smtp" "25")) | |
9b3ebcb6 MB |
141 | "List of authentication protocols and their names" |
142 | ||
143 | :group 'auth-source | |
ec7995fa | 144 | :version "23.2" ;; No Gnus |
9b3ebcb6 | 145 | :type '(repeat :tag "Authentication Protocols" |
cbabe91f TZ |
146 | (cons :tag "Protocol Entry" |
147 | (symbol :tag "Protocol") | |
148 | (repeat :tag "Names" | |
149 | (string :tag "Name"))))) | |
9b3ebcb6 | 150 | |
cbffd0bd SM |
151 | ;; Generate all the protocols in a format Customize can use. |
152 | ;; TODO: generate on the fly from auth-source-protocols | |
9b3ebcb6 MB |
153 | (defconst auth-source-protocols-customize |
154 | (mapcar (lambda (a) | |
cbabe91f TZ |
155 | (let ((p (car-safe a))) |
156 | (list 'const | |
157 | :tag (upcase (symbol-name p)) | |
158 | p))) | |
159 | auth-source-protocols)) | |
9b3ebcb6 | 160 | |
b8e0f0cd G |
161 | (defvar auth-source-creation-defaults nil |
162 | "Defaults for creating token values. Usually let-bound.") | |
163 | ||
003522ce G |
164 | (defvar auth-source-creation-prompts nil |
165 | "Default prompts for token values. Usually let-bound.") | |
166 | ||
b8e0f0cd G |
167 | (make-obsolete 'auth-source-hide-passwords nil "Emacs 24.1") |
168 | ||
003522ce G |
169 | (defcustom auth-source-save-behavior 'ask |
170 | "If set, auth-source will respect it for save behavior." | |
171 | :group 'auth-source | |
172 | :version "23.2" ;; No Gnus | |
173 | :type `(choice | |
174 | :tag "auth-source new token save behavior" | |
175 | (const :tag "Always save" t) | |
176 | (const :tag "Never save" nil) | |
177 | (const :tag "Ask" ask))) | |
178 | ||
61e6a0ac TZ |
179 | ;; TODO: make the default (setq auth-source-netrc-use-gpg-tokens `((,(if (boundp 'epa-file-auto-mode-alist-entry) (car (symbol-value 'epa-file-auto-mode-alist-entry)) "\\.gpg\\'") never) (t gpg))) |
180 | ;; TODO: or maybe leave as (setq auth-source-netrc-use-gpg-tokens 'never) | |
181 | ||
182 | (defcustom auth-source-netrc-use-gpg-tokens 'never | |
183 | "Set this to tell auth-source when to create GPG password | |
b09c3fe0 G |
184 | tokens in netrc files. It's either an alist or `never'. |
185 | Note that if EPA/EPG is not available, this should NOT be used." | |
2b8c5660 TZ |
186 | :group 'auth-source |
187 | :version "23.2" ;; No Gnus | |
188 | :type `(choice | |
61e6a0ac TZ |
189 | (const :tag "Always use GPG password tokens" (t gpg)) |
190 | (const :tag "Never use GPG password tokens" never) | |
191 | (repeat :tag "Use a lookup list" | |
192 | (list | |
193 | (choice :tag "Matcher" | |
194 | (const :tag "Match anything" t) | |
195 | (const :tag "The EPA encrypted file extensions" | |
196 | ,(if (boundp 'epa-file-auto-mode-alist-entry) | |
197 | (car (symbol-value | |
198 | 'epa-file-auto-mode-alist-entry)) | |
199 | "\\.gpg\\'")) | |
200 | (regexp :tag "Regular expression")) | |
201 | (choice :tag "What to do" | |
202 | (const :tag "Save GPG-encrypted password tokens" gpg) | |
203 | (const :tag "Don't encrypt tokens" never)))))) | |
2b8c5660 | 204 | |
b8e0f0cd | 205 | (defvar auth-source-magic "auth-source-magic ") |
ed778fad MB |
206 | |
207 | (defcustom auth-source-do-cache t | |
b8e0f0cd | 208 | "Whether auth-source should cache information with `password-cache'." |
ed778fad | 209 | :group 'auth-source |
ec7995fa | 210 | :version "23.2" ;; No Gnus |
ed778fad MB |
211 | :type `boolean) |
212 | ||
a202ff49 | 213 | (defcustom auth-source-debug nil |
554a69b8 | 214 | "Whether auth-source should log debug messages. |
554a69b8 KY |
215 | |
216 | If the value is nil, debug messages are not logged. | |
ca6ddb88 TZ |
217 | |
218 | If the value is t, debug messages are logged with `message'. In | |
219 | that case, your authentication data will be in the clear (except | |
220 | for passwords). | |
221 | ||
554a69b8 KY |
222 | If the value is a function, debug messages are logged by calling |
223 | that function using the same arguments as `message'." | |
224 | :group 'auth-source | |
ec7995fa | 225 | :version "23.2" ;; No Gnus |
cbabe91f TZ |
226 | :type `(choice |
227 | :tag "auth-source debugging mode" | |
228 | (const :tag "Log using `message' to the *Messages* buffer" t) | |
4a3988d5 G |
229 | (const :tag "Log all trivia with `message' to the *Messages* buffer" |
230 | trivia) | |
cbabe91f TZ |
231 | (function :tag "Function that takes arguments like `message'") |
232 | (const :tag "Don't log anything" nil))) | |
554a69b8 | 233 | |
6a6e4d93 | 234 | (defcustom auth-sources '("~/.authinfo" "~/.authinfo.gpg" "~/.netrc") |
8f7abae3 | 235 | "List of authentication sources. |
ec7995fa | 236 | Each entry is the authentication type with optional properties. |
36f1351e GM |
237 | Entries are tried in the order in which they appear. |
238 | See Info node `(auth)Help for users' for details. | |
ec7995fa | 239 | |
019ff874 TZ |
240 | If an entry names a file with the \".gpg\" extension and you have |
241 | EPA/EPG set up, the file will be encrypted and decrypted | |
242 | automatically. See Info node `(epa)Encrypting/decrypting gpg files' | |
243 | for details. | |
244 | ||
ec7995fa KY |
245 | It's best to customize this with `M-x customize-variable' because the choices |
246 | can get pretty complex." | |
8f7abae3 | 247 | :group 'auth-source |
b8e0f0cd | 248 | :version "24.1" ;; No Gnus |
9b3ebcb6 | 249 | :type `(repeat :tag "Authentication Sources" |
b8e0f0cd G |
250 | (choice |
251 | (string :tag "Just a file") | |
9c61f806 | 252 | (const :tag "Default Secrets API Collection" default) |
5415d076 | 253 | (const :tag "Login Secrets API Collection" "secrets:Login") |
b8e0f0cd | 254 | (const :tag "Temp Secrets API Collection" "secrets:session") |
d7fcec5d TZ |
255 | |
256 | (const :tag "Default internet Mac OS Keychain" | |
c20643e2 | 257 | macos-keychain-internet) |
d7fcec5d TZ |
258 | |
259 | (const :tag "Default generic Mac OS Keychain" | |
c20643e2 | 260 | macos-keychain-generic) |
d7fcec5d | 261 | |
b8e0f0cd G |
262 | (list :tag "Source definition" |
263 | (const :format "" :value :source) | |
264 | (choice :tag "Authentication backend choice" | |
265 | (string :tag "Authentication Source (file)") | |
266 | (list | |
267 | :tag "Secret Service API/KWallet/GNOME Keyring" | |
268 | (const :format "" :value :secrets) | |
269 | (choice :tag "Collection to use" | |
270 | (string :tag "Collection name") | |
9c61f806 | 271 | (const :tag "Default" default) |
5415d076 | 272 | (const :tag "Login" "Login") |
b8e0f0cd | 273 | (const |
d7fcec5d TZ |
274 | :tag "Temporary" "session"))) |
275 | (list | |
276 | :tag "Mac OS internet Keychain" | |
277 | (const :format "" | |
278 | :value :macos-keychain-internet) | |
279 | (choice :tag "Collection to use" | |
280 | (string :tag "internet Keychain path") | |
9c61f806 | 281 | (const :tag "default" default))) |
d7fcec5d TZ |
282 | (list |
283 | :tag "Mac OS generic Keychain" | |
284 | (const :format "" | |
285 | :value :macos-keychain-generic) | |
286 | (choice :tag "Collection to use" | |
287 | (string :tag "generic Keychain path") | |
9c61f806 | 288 | (const :tag "default" default)))) |
b8e0f0cd G |
289 | (repeat :tag "Extra Parameters" :inline t |
290 | (choice :tag "Extra parameter" | |
291 | (list | |
292 | :tag "Host" | |
293 | (const :format "" :value :host) | |
294 | (choice :tag "Host (machine) choice" | |
295 | (const :tag "Any" t) | |
296 | (regexp | |
297 | :tag "Regular expression"))) | |
298 | (list | |
299 | :tag "Protocol" | |
35123c04 | 300 | (const :format "" :value :port) |
b8e0f0cd G |
301 | (choice |
302 | :tag "Protocol" | |
303 | (const :tag "Any" t) | |
304 | ,@auth-source-protocols-customize)) | |
305 | (list :tag "User" :inline t | |
306 | (const :format "" :value :user) | |
61e6a0ac TZ |
307 | (choice |
308 | :tag "Personality/Username" | |
e9cb4479 DU |
309 | (const :tag "Any" t) |
310 | (string | |
311 | :tag "Name"))))))))) | |
8f7abae3 | 312 | |
549c9aed G |
313 | (defcustom auth-source-gpg-encrypt-to t |
314 | "List of recipient keys that `authinfo.gpg' encrypted to. | |
315 | If the value is not a list, symmetric encryption will be used." | |
316 | :group 'auth-source | |
b8e0f0cd | 317 | :version "24.1" ;; No Gnus |
549c9aed | 318 | :type '(choice (const :tag "Symmetric encryption" t) |
b8e0f0cd G |
319 | (repeat :tag "Recipient public keys" |
320 | (string :tag "Recipient public key")))) | |
549c9aed | 321 | |
8f7abae3 | 322 | ;; temp for debugging |
9b3ebcb6 MB |
323 | ;; (unintern 'auth-source-protocols) |
324 | ;; (unintern 'auth-sources) | |
325 | ;; (customize-variable 'auth-sources) | |
326 | ;; (setq auth-sources nil) | |
327 | ;; (format "%S" auth-sources) | |
328 | ;; (customize-variable 'auth-source-protocols) | |
329 | ;; (setq auth-source-protocols nil) | |
330 | ;; (format "%S" auth-source-protocols) | |
fb178e4c | 331 | ;; (auth-source-pick nil :host "a" :port 'imap) |
9b3ebcb6 MB |
332 | ;; (auth-source-user-or-password "login" "imap.myhost.com" 'imap) |
333 | ;; (auth-source-user-or-password "password" "imap.myhost.com" 'imap) | |
334 | ;; (auth-source-user-or-password-imap "login" "imap.myhost.com") | |
335 | ;; (auth-source-user-or-password-imap "password" "imap.myhost.com") | |
336 | ;; (auth-source-protocol-defaults 'imap) | |
337 | ||
ca6ddb88 TZ |
338 | ;; (let ((auth-source-debug 'debug)) (auth-source-do-debug "hello")) |
339 | ;; (let ((auth-source-debug t)) (auth-source-do-debug "hello")) | |
340 | ;; (let ((auth-source-debug nil)) (auth-source-do-debug "hello")) | |
554a69b8 | 341 | (defun auth-source-do-debug (&rest msg) |
554a69b8 | 342 | (when auth-source-debug |
ca6ddb88 TZ |
343 | (apply 'auth-source-do-warn msg))) |
344 | ||
4a3988d5 G |
345 | (defun auth-source-do-trivia (&rest msg) |
346 | (when (or (eq auth-source-debug 'trivia) | |
347 | (functionp auth-source-debug)) | |
348 | (apply 'auth-source-do-warn msg))) | |
349 | ||
ca6ddb88 TZ |
350 | (defun auth-source-do-warn (&rest msg) |
351 | (apply | |
e9cb4479 DU |
352 | ;; set logger to either the function in auth-source-debug or 'message |
353 | ;; note that it will be 'message if auth-source-debug is nil | |
ca6ddb88 TZ |
354 | (if (functionp auth-source-debug) |
355 | auth-source-debug | |
356 | 'message) | |
357 | msg)) | |
358 | ||
554a69b8 | 359 | |
cbffd0bd | 360 | ;; (auth-source-read-char-choice "enter choice? " '(?a ?b ?q)) |
733afdf4 TZ |
361 | (defun auth-source-read-char-choice (prompt choices) |
362 | "Read one of CHOICES by `read-char-choice', or `read-char'. | |
363 | `dropdown-list' support is disabled because it doesn't work reliably. | |
364 | Only one of CHOICES will be returned. The PROMPT is augmented | |
365 | with \"[a/b/c] \" if CHOICES is '\(?a ?b ?c\)." | |
366 | (when choices | |
367 | (let* ((prompt-choices | |
368 | (apply 'concat (loop for c in choices | |
369 | collect (format "%c/" c)))) | |
370 | (prompt-choices (concat "[" (substring prompt-choices 0 -1) "] ")) | |
371 | (full-prompt (concat prompt prompt-choices)) | |
372 | k) | |
373 | ||
374 | (while (not (memq k choices)) | |
375 | (setq k (cond | |
733afdf4 TZ |
376 | ((fboundp 'read-char-choice) |
377 | (read-char-choice full-prompt choices)) | |
378 | (t (message "%s" full-prompt) | |
379 | (setq k (read-char)))))) | |
380 | k))) | |
381 | ||
35123c04 TZ |
382 | ;; (auth-source-pick nil :host "any" :port 'imap :user "joe") |
383 | ;; (auth-source-pick t :host "any" :port 'imap :user "joe") | |
384 | ;; (setq auth-sources '((:source (:secrets default) :host t :port t :user "joe") | |
385 | ;; (:source (:secrets "session") :host t :port t :user "joe") | |
386 | ;; (:source (:secrets "Login") :host t :port t) | |
387 | ;; (:source "~/.authinfo.gpg" :host t :port t))) | |
fb178e4c | 388 | |
35123c04 TZ |
389 | ;; (setq auth-sources '((:source (:secrets default) :host t :port t :user "joe") |
390 | ;; (:source (:secrets "session") :host t :port t :user "joe") | |
391 | ;; (:source (:secrets "Login") :host t :port t) | |
cbabe91f | 392 | ;; )) |
fb178e4c | 393 | |
35123c04 | 394 | ;; (setq auth-sources '((:source "~/.authinfo.gpg" :host t :port t))) |
fb178e4c | 395 | |
b8e0f0cd G |
396 | ;; (auth-source-backend-parse "myfile.gpg") |
397 | ;; (auth-source-backend-parse 'default) | |
5415d076 | 398 | ;; (auth-source-backend-parse "secrets:Login") |
d7fcec5d TZ |
399 | ;; (auth-source-backend-parse 'macos-keychain-internet) |
400 | ;; (auth-source-backend-parse 'macos-keychain-generic) | |
401 | ;; (auth-source-backend-parse "macos-keychain-internet:/path/here.keychain") | |
402 | ;; (auth-source-backend-parse "macos-keychain-generic:/path/here.keychain") | |
b8e0f0cd G |
403 | |
404 | (defun auth-source-backend-parse (entry) | |
405 | "Creates an auth-source-backend from an ENTRY in `auth-sources'." | |
406 | (auth-source-backend-parse-parameters | |
407 | entry | |
408 | (cond | |
409 | ;; take 'default and recurse to get it as a Secrets API default collection | |
410 | ;; matching any user, host, and protocol | |
411 | ((eq entry 'default) | |
412 | (auth-source-backend-parse '(:source (:secrets default)))) | |
413 | ;; take secrets:XYZ and recurse to get it as Secrets API collection "XYZ" | |
414 | ;; matching any user, host, and protocol | |
415 | ((and (stringp entry) (string-match "^secrets:\\(.+\\)" entry)) | |
416 | (auth-source-backend-parse `(:source (:secrets ,(match-string 1 entry))))) | |
d7fcec5d TZ |
417 | |
418 | ;; take 'macos-keychain-internet and recurse to get it as a Mac OS | |
419 | ;; Keychain collection matching any user, host, and protocol | |
420 | ((eq entry 'macos-keychain-internet) | |
421 | (auth-source-backend-parse '(:source (:macos-keychain-internet default)))) | |
422 | ;; take 'macos-keychain-generic and recurse to get it as a Mac OS | |
423 | ;; Keychain collection matching any user, host, and protocol | |
424 | ((eq entry 'macos-keychain-generic) | |
425 | (auth-source-backend-parse '(:source (:macos-keychain-generic default)))) | |
426 | ;; take macos-keychain-internet:XYZ and recurse to get it as MacOS | |
427 | ;; Keychain "XYZ" matching any user, host, and protocol | |
428 | ((and (stringp entry) (string-match "^macos-keychain-internet:\\(.+\\)" | |
429 | entry)) | |
430 | (auth-source-backend-parse `(:source (:macos-keychain-internet | |
431 | ,(match-string 1 entry))))) | |
432 | ;; take macos-keychain-generic:XYZ and recurse to get it as MacOS | |
433 | ;; Keychain "XYZ" matching any user, host, and protocol | |
434 | ((and (stringp entry) (string-match "^macos-keychain-generic:\\(.+\\)" | |
435 | entry)) | |
436 | (auth-source-backend-parse `(:source (:macos-keychain-generic | |
437 | ,(match-string 1 entry))))) | |
438 | ||
b8e0f0cd G |
439 | ;; take just a file name and recurse to get it as a netrc file |
440 | ;; matching any user, host, and protocol | |
441 | ((stringp entry) | |
442 | (auth-source-backend-parse `(:source ,entry))) | |
443 | ||
444 | ;; a file name with parameters | |
445 | ((stringp (plist-get entry :source)) | |
8977de27 | 446 | (if (equal (file-name-extension (plist-get entry :source)) "plist") |
e9cb4479 DU |
447 | (auth-source-backend |
448 | (plist-get entry :source) | |
449 | :source (plist-get entry :source) | |
450 | :type 'plstore | |
451 | :search-function 'auth-source-plstore-search | |
452 | :create-function 'auth-source-plstore-create | |
453 | :data (plstore-open (plist-get entry :source))) | |
8977de27 | 454 | (auth-source-backend |
e9cb4479 DU |
455 | (plist-get entry :source) |
456 | :source (plist-get entry :source) | |
457 | :type 'netrc | |
458 | :search-function 'auth-source-netrc-search | |
459 | :create-function 'auth-source-netrc-create))) | |
b8e0f0cd | 460 | |
d7fcec5d TZ |
461 | ;; the MacOS Keychain |
462 | ((and | |
463 | (not (null (plist-get entry :source))) ; the source must not be nil | |
464 | (listp (plist-get entry :source)) ; and it must be a list | |
465 | (or | |
466 | (plist-get (plist-get entry :source) :macos-keychain-generic) | |
467 | (plist-get (plist-get entry :source) :macos-keychain-internet))) | |
468 | ||
469 | (let* ((source-spec (plist-get entry :source)) | |
470 | (keychain-generic (plist-get source-spec :macos-keychain-generic)) | |
471 | (keychain-type (if keychain-generic | |
472 | 'macos-keychain-generic | |
473 | 'macos-keychain-internet)) | |
474 | (source (plist-get source-spec (if keychain-generic | |
475 | :macos-keychain-generic | |
476 | :macos-keychain-internet)))) | |
477 | ||
478 | (when (symbolp source) | |
479 | (setq source (symbol-name source))) | |
480 | ||
481 | (auth-source-backend | |
482 | (format "Mac OS Keychain (%s)" source) | |
483 | :source source | |
484 | :type keychain-type | |
485 | :search-function 'auth-source-macos-keychain-search | |
486 | :create-function 'auth-source-macos-keychain-create))) | |
487 | ||
b8e0f0cd G |
488 | ;; the Secrets API. We require the package, in order to have a |
489 | ;; defined value for `secrets-enabled'. | |
490 | ((and | |
491 | (not (null (plist-get entry :source))) ; the source must not be nil | |
492 | (listp (plist-get entry :source)) ; and it must be a list | |
493 | (require 'secrets nil t) ; and we must load the Secrets API | |
494 | secrets-enabled) ; and that API must be enabled | |
495 | ||
496 | ;; the source is either the :secrets key in ENTRY or | |
497 | ;; if that's missing or nil, it's "session" | |
498 | (let ((source (or (plist-get (plist-get entry :source) :secrets) | |
499 | "session"))) | |
500 | ||
501 | ;; if the source is a symbol, we look for the alias named so, | |
5415d076 | 502 | ;; and if that alias is missing, we use "Login" |
b8e0f0cd G |
503 | (when (symbolp source) |
504 | (setq source (or (secrets-get-alias (symbol-name source)) | |
5415d076 | 505 | "Login"))) |
b8e0f0cd | 506 | |
ca6ddb88 TZ |
507 | (if (featurep 'secrets) |
508 | (auth-source-backend | |
509 | (format "Secrets API (%s)" source) | |
510 | :source source | |
511 | :type 'secrets | |
512 | :search-function 'auth-source-secrets-search | |
513 | :create-function 'auth-source-secrets-create) | |
514 | (auth-source-do-warn | |
515 | "auth-source-backend-parse: no Secrets API, ignoring spec: %S" entry) | |
516 | (auth-source-backend | |
517 | (format "Ignored Secrets API (%s)" source) | |
518 | :source "" | |
519 | :type 'ignore)))) | |
b8e0f0cd G |
520 | |
521 | ;; none of them | |
522 | (t | |
ca6ddb88 | 523 | (auth-source-do-warn |
b8e0f0cd G |
524 | "auth-source-backend-parse: invalid backend spec: %S" entry) |
525 | (auth-source-backend | |
526 | "Empty" | |
527 | :source "" | |
528 | :type 'ignore))))) | |
529 | ||
530 | (defun auth-source-backend-parse-parameters (entry backend) | |
531 | "Fills in the extra auth-source-backend parameters of ENTRY. | |
35123c04 TZ |
532 | Using the plist ENTRY, get the :host, :port, and :user search |
533 | parameters." | |
e45de620 TZ |
534 | (let ((entry (if (stringp entry) |
535 | nil | |
536 | entry)) | |
537 | val) | |
b8e0f0cd G |
538 | (when (setq val (plist-get entry :host)) |
539 | (oset backend host val)) | |
540 | (when (setq val (plist-get entry :user)) | |
541 | (oset backend user val)) | |
35123c04 TZ |
542 | (when (setq val (plist-get entry :port)) |
543 | (oset backend port val))) | |
b8e0f0cd G |
544 | backend) |
545 | ||
546 | ;; (mapcar 'auth-source-backend-parse auth-sources) | |
547 | ||
548 | (defun* auth-source-search (&rest spec | |
35123c04 | 549 | &key type max host user port secret |
733afdf4 | 550 | require create delete |
b8e0f0cd G |
551 | &allow-other-keys) |
552 | "Search or modify authentication backends according to SPEC. | |
553 | ||
554 | This function parses `auth-sources' for matches of the SPEC | |
555 | plist. It can optionally create or update an authentication | |
556 | token if requested. A token is just a standard Emacs property | |
557 | list with a :secret property that can be a function; all the | |
558 | other properties will always hold scalar values. | |
559 | ||
560 | Typically the :secret property, if present, contains a password. | |
561 | ||
35123c04 | 562 | Common search keys are :max, :host, :port, and :user. In |
b8e0f0cd G |
563 | addition, :create specifies how tokens will be or created. |
564 | Finally, :type can specify which backend types you want to check. | |
565 | ||
566 | A string value is always matched literally. A symbol is matched | |
567 | as its string value, literally. All the SPEC values can be | |
568 | single values (symbol or string) or lists thereof (in which case | |
569 | any of the search terms matches). | |
570 | ||
571 | :create t means to create a token if possible. | |
572 | ||
573 | A new token will be created if no matching tokens were found. | |
574 | The new token will have only the keys the backend requires. For | |
575 | the netrc backend, for instance, that's the user, host, and | |
35123c04 | 576 | port keys. |
b8e0f0cd G |
577 | |
578 | Here's an example: | |
579 | ||
580 | \(let ((auth-source-creation-defaults '((user . \"defaultUser\") | |
581 | (A . \"default A\")))) | |
582 | (auth-source-search :host \"mine\" :type 'netrc :max 1 | |
583 | :P \"pppp\" :Q \"qqqq\" | |
584 | :create t)) | |
585 | ||
586 | which says: | |
587 | ||
588 | \"Search for any entry matching host 'mine' in backends of type | |
589 | 'netrc', maximum one result. | |
590 | ||
591 | Create a new entry if you found none. The netrc backend will | |
35123c04 | 592 | automatically require host, user, and port. The host will be |
b8e0f0cd | 593 | 'mine'. We prompt for the user with default 'defaultUser' and |
35123c04 | 594 | for the port without a default. We will not prompt for A, Q, |
b8e0f0cd | 595 | or P. The resulting token will only have keys user, host, and |
35123c04 | 596 | port.\" |
b8e0f0cd G |
597 | |
598 | :create '(A B C) also means to create a token if possible. | |
599 | ||
600 | The behavior is like :create t but if the list contains any | |
601 | parameter, that parameter will be required in the resulting | |
602 | token. The value for that parameter will be obtained from the | |
603 | search parameters or from user input. If any queries are needed, | |
604 | the alist `auth-source-creation-defaults' will be checked for the | |
003522ce G |
605 | default value. If the user, host, or port are missing, the alist |
606 | `auth-source-creation-prompts' will be used to look up the | |
607 | prompts IN THAT ORDER (so the 'user prompt will be queried first, | |
608 | then 'host, then 'port, and finally 'secret). Each prompt string | |
609 | can use %u, %h, and %p to show the user, host, and port. | |
b8e0f0cd G |
610 | |
611 | Here's an example: | |
612 | ||
613 | \(let ((auth-source-creation-defaults '((user . \"defaultUser\") | |
003522ce G |
614 | (A . \"default A\"))) |
615 | (auth-source-creation-prompts | |
616 | '((password . \"Enter IMAP password for %h:%p: \")))) | |
b8e0f0cd G |
617 | (auth-source-search :host '(\"nonesuch\" \"twosuch\") :type 'netrc :max 1 |
618 | :P \"pppp\" :Q \"qqqq\" | |
619 | :create '(A B Q))) | |
620 | ||
621 | which says: | |
622 | ||
623 | \"Search for any entry matching host 'nonesuch' | |
624 | or 'twosuch' in backends of type 'netrc', maximum one result. | |
625 | ||
626 | Create a new entry if you found none. The netrc backend will | |
35123c04 | 627 | automatically require host, user, and port. The host will be |
003522ce G |
628 | 'nonesuch' and Q will be 'qqqq'. We prompt for the password |
629 | with the shown prompt. We will not prompt for Q. The resulting | |
630 | token will have keys user, host, port, A, B, and Q. It will not | |
631 | have P with any value, even though P is used in the search to | |
632 | find only entries that have P set to 'pppp'.\" | |
b8e0f0cd G |
633 | |
634 | When multiple values are specified in the search parameter, the | |
7ba93e94 G |
635 | user is prompted for which one. So :host (X Y Z) would ask the |
636 | user to choose between X, Y, and Z. | |
b8e0f0cd G |
637 | |
638 | This creation can fail if the search was not specific enough to | |
639 | create a new token (it's up to the backend to decide that). You | |
640 | should `catch' the backend-specific error as usual. Some | |
641 | backends (netrc, at least) will prompt the user rather than throw | |
642 | an error. | |
643 | ||
733afdf4 TZ |
644 | :require (A B C) means that only results that contain those |
645 | tokens will be returned. Thus for instance requiring :secret | |
646 | will ensure that any results will actually have a :secret | |
647 | property. | |
648 | ||
b8e0f0cd G |
649 | :delete t means to delete any found entries. nil by default. |
650 | Use `auth-source-delete' in ELisp code instead of calling | |
651 | `auth-source-search' directly with this parameter. | |
652 | ||
653 | :type (X Y Z) will check only those backend types. 'netrc and | |
654 | 'secrets are the only ones supported right now. | |
655 | ||
656 | :max N means to try to return at most N items (defaults to 1). | |
8f25c2bc TZ |
657 | More than N items may be returned, depending on the search and |
658 | the backend. | |
659 | ||
660 | When :max is 0 the function will return just t or nil to indicate | |
661 | if any matches were found. | |
b8e0f0cd G |
662 | |
663 | :host (X Y Z) means to match only hosts X, Y, or Z according to | |
664 | the match rules above. Defaults to t. | |
665 | ||
666 | :user (X Y Z) means to match only users X, Y, or Z according to | |
667 | the match rules above. Defaults to t. | |
668 | ||
35123c04 | 669 | :port (P Q R) means to match only protocols P, Q, or R. |
b8e0f0cd G |
670 | Defaults to t. |
671 | ||
672 | :K (V1 V2 V3) for any other key K will match values V1, V2, or | |
673 | V3 (note the match rules above). | |
674 | ||
675 | The return value is a list with at most :max tokens. Each token | |
35123c04 | 676 | is a plist with keys :backend :host :port :user, plus any other |
b8e0f0cd G |
677 | keys provided by the backend (notably :secret). But note the |
678 | exception for :max 0, which see above. | |
679 | ||
733afdf4 TZ |
680 | The token can hold a :save-function key. If you call that, the |
681 | user will be prompted to save the data to the backend. You can't | |
682 | request that this should happen right after creation, because | |
683 | `auth-source-search' has no way of knowing if the token is | |
684 | actually useful. So the caller must arrange to call this function. | |
685 | ||
b8e0f0cd G |
686 | The token's :secret key can hold a function. In that case you |
687 | must call it to obtain the actual value." | |
688 | (let* ((backends (mapcar 'auth-source-backend-parse auth-sources)) | |
689 | (max (or max 1)) | |
733afdf4 | 690 | (ignored-keys '(:require :create :delete :max)) |
b8e0f0cd G |
691 | (keys (loop for i below (length spec) by 2 |
692 | unless (memq (nth i spec) ignored-keys) | |
693 | collect (nth i spec))) | |
61e9662e TZ |
694 | (cached (auth-source-remembered-p spec)) |
695 | ;; note that we may have cached results but found is still nil | |
696 | ;; (there were no results from the search) | |
b8e0f0cd | 697 | (found (auth-source-recall spec)) |
4a3988d5 | 698 | filtered-backends accessor-key backend) |
b8e0f0cd | 699 | |
61e9662e | 700 | (if (and cached auth-source-do-cache) |
b8e0f0cd G |
701 | (auth-source-do-debug |
702 | "auth-source-search: found %d CACHED results matching %S" | |
703 | (length found) spec) | |
704 | ||
705 | (assert | |
706 | (or (eq t create) (listp create)) t | |
4a3988d5 | 707 | "Invalid auth-source :create parameter (must be t or a list): %s %s") |
b8e0f0cd | 708 | |
733afdf4 TZ |
709 | (assert |
710 | (listp require) t | |
711 | "Invalid auth-source :require parameter (must be a list): %s") | |
712 | ||
6ce6c742 | 713 | (setq filtered-backends (copy-sequence backends)) |
b8e0f0cd G |
714 | (dolist (backend backends) |
715 | (dolist (key keys) | |
716 | ;; ignore invalid slots | |
717 | (condition-case signal | |
718 | (unless (eval `(auth-source-search-collection | |
719 | (plist-get spec key) | |
720 | (oref backend ,key))) | |
721 | (setq filtered-backends (delq backend filtered-backends)) | |
722 | (return)) | |
723 | (invalid-slot-name)))) | |
724 | ||
4a3988d5 | 725 | (auth-source-do-trivia |
b8e0f0cd G |
726 | "auth-source-search: found %d backends matching %S" |
727 | (length filtered-backends) spec) | |
728 | ||
729 | ;; (debug spec "filtered" filtered-backends) | |
1d2c4a49 LI |
730 | ;; First go through all the backends without :create, so we can |
731 | ;; query them all. | |
4a3988d5 G |
732 | (setq found (auth-source-search-backends filtered-backends |
733 | spec | |
734 | ;; to exit early | |
735 | max | |
733afdf4 TZ |
736 | ;; create is always nil here |
737 | nil delete | |
738 | require)) | |
4a3988d5 G |
739 | |
740 | (auth-source-do-debug | |
741 | "auth-source-search: found %d results (max %d) matching %S" | |
742 | (length found) max spec) | |
743 | ||
1d2c4a49 LI |
744 | ;; If we didn't find anything, then we allow the backend(s) to |
745 | ;; create the entries. | |
38046520 | 746 | (when (and create |
4a3988d5 G |
747 | (not found)) |
748 | (setq found (auth-source-search-backends filtered-backends | |
749 | spec | |
750 | ;; to exit early | |
751 | max | |
733afdf4 TZ |
752 | create delete |
753 | require)) | |
754 | (auth-source-do-debug | |
4a3988d5 G |
755 | "auth-source-search: CREATED %d results (max %d) matching %S" |
756 | (length found) max spec)) | |
757 | ||
61e9662e TZ |
758 | ;; note we remember the lack of result too, if it's applicable |
759 | (when auth-source-do-cache | |
4a3988d5 G |
760 | (auth-source-remember spec found))) |
761 | ||
8f25c2bc TZ |
762 | (if (zerop max) |
763 | (not (null found)) | |
764 | found))) | |
4a3988d5 | 765 | |
733afdf4 | 766 | (defun auth-source-search-backends (backends spec max create delete require) |
8f25c2bc TZ |
767 | (let ((max (if (zerop max) 1 max)) ; stop with 1 match if we're asked for zero |
768 | matches) | |
4a3988d5 | 769 | (dolist (backend backends) |
8f25c2bc | 770 | (when (> max (length matches)) ; if we need more matches... |
733afdf4 TZ |
771 | (let* ((bmatches (apply |
772 | (slot-value backend 'search-function) | |
773 | :backend backend | |
d7fcec5d | 774 | :type (slot-value backend :type) |
733afdf4 | 775 | ;; note we're overriding whatever the spec |
8f25c2bc TZ |
776 | ;; has for :max, :require, :create, and :delete |
777 | :max max | |
733afdf4 TZ |
778 | :require require |
779 | :create create | |
780 | :delete delete | |
781 | spec))) | |
4a3988d5 G |
782 | (when bmatches |
783 | (auth-source-do-trivia | |
784 | "auth-source-search-backend: got %d (max %d) in %s:%s matching %S" | |
785 | (length bmatches) max | |
786 | (slot-value backend :type) | |
787 | (slot-value backend :source) | |
788 | spec) | |
789 | (setq matches (append matches bmatches)))))) | |
790 | matches)) | |
b8e0f0cd | 791 | |
8f25c2bc | 792 | ;; (auth-source-search :max 0) |
cbffd0bd SM |
793 | ;; (auth-source-search :max 1) |
794 | ;; (funcall (plist-get (nth 0 (auth-source-search :max 1)) :secret)) | |
795 | ;; (auth-source-search :host "nonesuch" :type 'netrc :K 1) | |
796 | ;; (auth-source-search :host "nonesuch" :type 'secrets) | |
b8e0f0cd G |
797 | |
798 | (defun* auth-source-delete (&rest spec | |
799 | &key delete | |
800 | &allow-other-keys) | |
801 | "Delete entries from the authentication backends according to SPEC. | |
802 | Calls `auth-source-search' with the :delete property in SPEC set to t. | |
803 | The backend may not actually delete the entries. | |
804 | ||
805 | Returns the deleted entries." | |
806 | (auth-source-search (plist-put spec :delete t))) | |
807 | ||
808 | (defun auth-source-search-collection (collection value) | |
2809512e | 809 | "Returns t is VALUE is t or COLLECTION is t or COLLECTION contains VALUE." |
b8e0f0cd G |
810 | (when (and (atom collection) (not (eq t collection))) |
811 | (setq collection (list collection))) | |
812 | ||
813 | ;; (debug :collection collection :value value) | |
814 | (or (eq collection t) | |
815 | (eq value t) | |
816 | (equal collection value) | |
817 | (member value collection))) | |
ed778fad | 818 | |
74e8193b KY |
819 | (defvar auth-source-netrc-cache nil) |
820 | ||
3b36c17e | 821 | (defun auth-source-forget-all-cached () |
b8e0f0cd | 822 | "Forget all cached auth-source data." |
3b36c17e | 823 | (interactive) |
b8e0f0cd G |
824 | (loop for sym being the symbols of password-data |
825 | ;; when the symbol name starts with auth-source-magic | |
826 | when (string-match (concat "^" auth-source-magic) | |
827 | (symbol-name sym)) | |
828 | ;; remove that key | |
ddb7ffee LMI |
829 | do (password-cache-remove (symbol-name sym))) |
830 | (setq auth-source-netrc-cache nil)) | |
b8e0f0cd | 831 | |
cf499a1a JD |
832 | (defun auth-source-format-cache-entry (spec) |
833 | "Format SPEC entry to put it in the password cache." | |
834 | (concat auth-source-magic (format "%S" spec))) | |
835 | ||
b8e0f0cd G |
836 | (defun auth-source-remember (spec found) |
837 | "Remember FOUND search results for SPEC." | |
584c9d3f G |
838 | (let ((password-cache-expiry auth-source-cache-expiry)) |
839 | (password-cache-add | |
cf499a1a | 840 | (auth-source-format-cache-entry spec) found))) |
b8e0f0cd G |
841 | |
842 | (defun auth-source-recall (spec) | |
843 | "Recall FOUND search results for SPEC." | |
cf499a1a | 844 | (password-read-from-cache (auth-source-format-cache-entry spec))) |
b8e0f0cd | 845 | |
61e9662e TZ |
846 | (defun auth-source-remembered-p (spec) |
847 | "Check if SPEC is remembered." | |
848 | (password-in-cache-p | |
cf499a1a | 849 | (auth-source-format-cache-entry spec))) |
61e9662e | 850 | |
b8e0f0cd G |
851 | (defun auth-source-forget (spec) |
852 | "Forget any cached data matching SPEC exactly. | |
853 | ||
854 | This is the same SPEC you passed to `auth-source-search'. | |
855 | Returns t or nil for forgotten or not found." | |
cf499a1a | 856 | (password-cache-remove (auth-source-format-cache-entry spec))) |
b8e0f0cd | 857 | |
cbffd0bd | 858 | ;; (loop for sym being the symbols of password-data when (string-match (concat "^" auth-source-magic) (symbol-name sym)) collect (symbol-name sym)) |
b8e0f0cd | 859 | |
cbffd0bd SM |
860 | ;; (auth-source-remember '(:host "wedd") '(4 5 6)) |
861 | ;; (auth-source-remembered-p '(:host "wedd")) | |
862 | ;; (auth-source-remember '(:host "xedd") '(1 2 3)) | |
863 | ;; (auth-source-remembered-p '(:host "xedd")) | |
864 | ;; (auth-source-remembered-p '(:host "zedd")) | |
865 | ;; (auth-source-recall '(:host "xedd")) | |
866 | ;; (auth-source-recall '(:host t)) | |
867 | ;; (auth-source-forget+ :host t) | |
b8e0f0cd G |
868 | |
869 | (defun* auth-source-forget+ (&rest spec &allow-other-keys) | |
870 | "Forget any cached data matching SPEC. Returns forgotten count. | |
871 | ||
872 | This is not a full `auth-source-search' spec but works similarly. | |
873 | For instance, \(:host \"myhost\" \"yourhost\") would find all the | |
874 | cached data that was found with a search for those two hosts, | |
875 | while \(:host t) would find all host entries." | |
876 | (let ((count 0) | |
877 | sname) | |
878 | (loop for sym being the symbols of password-data | |
879 | ;; when the symbol name matches with auth-source-magic | |
880 | when (and (setq sname (symbol-name sym)) | |
881 | (string-match (concat "^" auth-source-magic "\\(.+\\)") | |
882 | sname) | |
883 | ;; and the spec matches what was stored in the cache | |
884 | (auth-source-specmatchp spec (read (match-string 1 sname)))) | |
885 | ;; remove that key | |
886 | do (progn | |
887 | (password-cache-remove sname) | |
888 | (incf count))) | |
889 | count)) | |
890 | ||
891 | (defun auth-source-specmatchp (spec stored) | |
892 | (let ((keys (loop for i below (length spec) by 2 | |
e9cb4479 | 893 | collect (nth i spec)))) |
b8e0f0cd G |
894 | (not (eq |
895 | (dolist (key keys) | |
896 | (unless (auth-source-search-collection (plist-get stored key) | |
897 | (plist-get spec key)) | |
898 | (return 'no))) | |
899 | 'no)))) | |
900 | ||
cbffd0bd SM |
901 | ;; (auth-source-pick-first-password :host "z.lifelogs.com") |
902 | ;; (auth-source-pick-first-password :port "imap") | |
f3b54b0e TZ |
903 | (defun auth-source-pick-first-password (&rest spec) |
904 | "Pick the first secret found from applying SPEC to `auth-source-search'." | |
905 | (let* ((result (nth 0 (apply 'auth-source-search (plist-put spec :max 1)))) | |
906 | (secret (plist-get result :secret))) | |
907 | ||
908 | (if (functionp secret) | |
909 | (funcall secret) | |
910 | secret))) | |
911 | ||
912 | ;; (auth-source-format-prompt "test %u %h %p" '((?u "user") (?h "host"))) | |
913 | (defun auth-source-format-prompt (prompt alist) | |
914 | "Format PROMPT using %x (for any character x) specifiers in ALIST." | |
915 | (dolist (cell alist) | |
916 | (let ((c (nth 0 cell)) | |
917 | (v (nth 1 cell))) | |
918 | (when (and c v) | |
4248cca2 TZ |
919 | (setq prompt (replace-regexp-in-string (format "%%%c" c) |
920 | (format "%s" v) | |
17d14f7e | 921 | prompt nil t))))) |
f3b54b0e | 922 | prompt) |
b8e0f0cd | 923 | |
8e22bee0 G |
924 | (defun auth-source-ensure-strings (values) |
925 | (unless (listp values) | |
926 | (setq values (list values))) | |
927 | (mapcar (lambda (value) | |
e9cb4479 DU |
928 | (if (numberp value) |
929 | (format "%s" value) | |
930 | value)) | |
931 | values)) | |
8e22bee0 | 932 | |
f3b54b0e TZ |
933 | ;;; Backend specific parsing: netrc/authinfo backend |
934 | ||
8b6c19f4 SM |
935 | (defun auth-source--aput-1 (alist key val) |
936 | (let ((seen ()) | |
937 | (rest alist)) | |
938 | (while (and (consp rest) (not (equal key (caar rest)))) | |
939 | (push (pop rest) seen)) | |
940 | (cons (cons key val) | |
941 | (if (null rest) alist | |
942 | (nconc (nreverse seen) | |
943 | (if (equal key (caar rest)) (cdr rest) rest)))))) | |
944 | (defmacro auth-source--aput (var key val) | |
945 | `(setq ,var (auth-source--aput-1 ,var ,key ,val))) | |
946 | ||
947 | (defun auth-source--aget (alist key) | |
948 | (cdr (assoc key alist))) | |
949 | ||
2809512e | 950 | ;; (auth-source-netrc-parse :file "~/.authinfo.gpg") |
b8e0f0cd G |
951 | (defun* auth-source-netrc-parse (&rest |
952 | spec | |
733afdf4 | 953 | &key file max host user port delete require |
b8e0f0cd G |
954 | &allow-other-keys) |
955 | "Parse FILE and return a list of all entries in the file. | |
956 | Note that the MAX parameter is used so we can exit the parse early." | |
957 | (if (listp file) | |
958 | ;; We got already parsed contents; just return it. | |
959 | file | |
960 | (when (file-exists-p file) | |
8e22bee0 | 961 | (setq port (auth-source-ensure-strings port)) |
b8e0f0cd | 962 | (with-temp-buffer |
2809512e | 963 | (let* ((max (or max 5000)) ; sanity check: default to stop at 5K |
4a3988d5 G |
964 | (modified 0) |
965 | (cached (cdr-safe (assoc file auth-source-netrc-cache))) | |
966 | (cached-mtime (plist-get cached :mtime)) | |
967 | (cached-secrets (plist-get cached :secret)) | |
2809512e TZ |
968 | (check (lambda(alist) |
969 | (and alist | |
970 | (auth-source-search-collection | |
971 | host | |
972 | (or | |
973 | (auth-source--aget alist "machine") | |
974 | (auth-source--aget alist "host") | |
975 | t)) | |
976 | (auth-source-search-collection | |
977 | user | |
978 | (or | |
979 | (auth-source--aget alist "login") | |
980 | (auth-source--aget alist "account") | |
981 | (auth-source--aget alist "user") | |
982 | t)) | |
983 | (auth-source-search-collection | |
984 | port | |
985 | (or | |
986 | (auth-source--aget alist "port") | |
987 | (auth-source--aget alist "protocol") | |
988 | t)) | |
989 | (or | |
990 | ;; the required list of keys is nil, or | |
991 | (null require) | |
992 | ;; every element of require is in n(ormalized) | |
993 | (let ((n (nth 0 (auth-source-netrc-normalize | |
994 | (list alist) file)))) | |
995 | (loop for req in require | |
996 | always (plist-get n req))))))) | |
997 | result) | |
4a3988d5 G |
998 | |
999 | (if (and (functionp cached-secrets) | |
1000 | (equal cached-mtime | |
1001 | (nth 5 (file-attributes file)))) | |
1002 | (progn | |
1003 | (auth-source-do-trivia | |
1004 | "auth-source-netrc-parse: using CACHED file data for %s" | |
1005 | file) | |
1006 | (insert (funcall cached-secrets))) | |
1007 | (insert-file-contents file) | |
1008 | ;; cache all netrc files (used to be just .gpg files) | |
1009 | ;; Store the contents of the file heavily encrypted in memory. | |
1010 | ;; (note for the irony-impaired: they are just obfuscated) | |
8b6c19f4 SM |
1011 | (auth-source--aput |
1012 | auth-source-netrc-cache file | |
1013 | (list :mtime (nth 5 (file-attributes file)) | |
1014 | :secret (lexical-let ((v (mapcar '1+ (buffer-string)))) | |
1015 | (lambda () (apply 'string (mapcar '1- v))))))) | |
b8e0f0cd | 1016 | (goto-char (point-min)) |
2809512e TZ |
1017 | (let ((entries (auth-source-netrc-parse-entries check max)) |
1018 | alist) | |
1019 | (while (setq alist (pop entries)) | |
1020 | (push (nreverse alist) result))) | |
b8e0f0cd G |
1021 | |
1022 | (when (< 0 modified) | |
1023 | (when auth-source-gpg-encrypt-to | |
1024 | ;; (see bug#7487) making `epa-file-encrypt-to' local to | |
1025 | ;; this buffer lets epa-file skip the key selection query | |
1026 | ;; (see the `local-variable-p' check in | |
1027 | ;; `epa-file-write-region'). | |
1028 | (unless (local-variable-p 'epa-file-encrypt-to (current-buffer)) | |
1029 | (make-local-variable 'epa-file-encrypt-to)) | |
1030 | (if (listp auth-source-gpg-encrypt-to) | |
1031 | (setq epa-file-encrypt-to auth-source-gpg-encrypt-to))) | |
1032 | ||
1033 | ;; ask AFTER we've successfully opened the file | |
733afdf4 | 1034 | (when (y-or-n-p (format "Save file %s? (%d deletions)" |
b8e0f0cd G |
1035 | file modified)) |
1036 | (write-region (point-min) (point-max) file nil 'silent) | |
1037 | (auth-source-do-debug | |
1038 | "auth-source-netrc-parse: modified %d lines in %s" | |
1039 | modified file))) | |
1040 | ||
1041 | (nreverse result)))))) | |
1042 | ||
2809512e TZ |
1043 | (defun auth-source-netrc-parse-next-interesting () |
1044 | "Advance to the next interesting position in the current buffer." | |
1045 | ;; If we're looking at a comment or are at the end of the line, move forward | |
1046 | (while (or (looking-at "#") | |
1047 | (and (eolp) | |
1048 | (not (eobp)))) | |
1049 | (forward-line 1)) | |
1050 | (skip-chars-forward "\t ")) | |
1051 | ||
1052 | (defun auth-source-netrc-parse-one () | |
1053 | "Read one thing from the current buffer." | |
1054 | (auth-source-netrc-parse-next-interesting) | |
1055 | ||
fa7f427c DK |
1056 | (when (or (looking-at "'\\([^']*\\)'") |
1057 | (looking-at "\"\\([^\"]*\\)\"") | |
2809512e TZ |
1058 | (looking-at "\\([^ \t\n]+\\)")) |
1059 | (forward-char (length (match-string 0))) | |
1060 | (auth-source-netrc-parse-next-interesting) | |
1061 | (match-string-no-properties 1))) | |
1062 | ||
cc52b6cc TZ |
1063 | ;; with thanks to org-mode |
1064 | (defsubst auth-source-current-line (&optional pos) | |
1065 | (save-excursion | |
1066 | (and pos (goto-char pos)) | |
1067 | ;; works also in narrowed buffer, because we start at 1, not point-min | |
1068 | (+ (if (bolp) 1 0) (count-lines 1 (point))))) | |
1069 | ||
2809512e TZ |
1070 | (defun auth-source-netrc-parse-entries(check max) |
1071 | "Parse up to MAX netrc entries, passed by CHECK, from the current buffer." | |
1072 | (let ((adder (lambda(check alist all) | |
1073 | (when (and | |
1074 | alist | |
1075 | (> max (length all)) | |
1076 | (funcall check alist)) | |
1077 | (push alist all)) | |
1078 | all)) | |
1079 | item item2 all alist default) | |
1080 | (while (setq item (auth-source-netrc-parse-one)) | |
1081 | (setq default (equal item "default")) | |
1082 | ;; We're starting a new machine. Save the old one. | |
1083 | (when (and alist | |
1084 | (or default | |
1085 | (equal item "machine"))) | |
924d6997 G |
1086 | ;; (auth-source-do-trivia |
1087 | ;; "auth-source-netrc-parse-entries: got entry %S" alist) | |
2809512e TZ |
1088 | (setq all (funcall adder check alist all) |
1089 | alist nil)) | |
1090 | ;; In default entries, we don't have a next token. | |
1091 | ;; We store them as ("machine" . t) | |
1092 | (if default | |
1093 | (push (cons "machine" t) alist) | |
1094 | ;; Not a default entry. Grab the next item. | |
1095 | (when (setq item2 (auth-source-netrc-parse-one)) | |
cc52b6cc TZ |
1096 | ;; Did we get a "machine" value? |
1097 | (if (equal item2 "machine") | |
1098 | (progn | |
1099 | (gnus-error 1 | |
1100 | "%s: Unexpected 'machine' token at line %d" | |
1101 | "auth-source-netrc-parse-entries" | |
1102 | (auth-source-current-line)) | |
1103 | (forward-line 1)) | |
1104 | (push (cons item item2) alist))))) | |
2809512e TZ |
1105 | |
1106 | ;; Clean up: if there's an entry left over, use it. | |
1107 | (when alist | |
cc52b6cc | 1108 | (setq all (funcall adder check alist all)) |
924d6997 G |
1109 | ;; (auth-source-do-trivia |
1110 | ;; "auth-source-netrc-parse-entries: got2 entry %S" alist) | |
1111 | ) | |
2809512e TZ |
1112 | (nreverse all))) |
1113 | ||
936d08bb G |
1114 | (defvar auth-source-passphrase-alist nil) |
1115 | ||
936d08bb | 1116 | (defun auth-source-token-passphrase-callback-function (context key-id file) |
7f6d634a DU |
1117 | (let* ((file (file-truename file)) |
1118 | (entry (assoc file auth-source-passphrase-alist)) | |
1119 | passphrase) | |
1120 | ;; return the saved passphrase, calling a function if needed | |
1121 | (or (copy-sequence (if (functionp (cdr entry)) | |
1122 | (funcall (cdr entry)) | |
1123 | (cdr entry))) | |
1124 | (progn | |
1125 | (unless entry | |
1126 | (setq entry (list file)) | |
1127 | (push entry auth-source-passphrase-alist)) | |
1128 | (setq passphrase | |
1129 | (read-passwd | |
1130 | (format "Passphrase for %s tokens: " file) | |
1131 | t)) | |
1132 | (setcdr entry (lexical-let ((p (copy-sequence passphrase))) | |
1133 | (lambda () p))) | |
1134 | passphrase)))) | |
936d08bb G |
1135 | |
1136 | ;; (auth-source-epa-extract-gpg-token "gpg:LS0tLS1CRUdJTiBQR1AgTUVTU0FHRS0tLS0tClZlcnNpb246IEdudVBHIHYxLjQuMTEgKEdOVS9MaW51eCkKCmpBMEVBd01DT25qMjB1ak9rZnRneVI3K21iNm9aZWhuLzRad3cySkdlbnVaKzRpeEswWDY5di9icDI1U1dsQT0KPS9yc2wKLS0tLS1FTkQgUEdQIE1FU1NBR0UtLS0tLQo=" "~/.netrc") | |
1137 | (defun auth-source-epa-extract-gpg-token (secret file) | |
1138 | "Pass either the decoded SECRET or the gpg:BASE64DATA version. | |
1139 | FILE is the file from which we obtained this token." | |
1140 | (when (string-match "^gpg:\\(.+\\)" secret) | |
1141 | (setq secret (base64-decode-string (match-string 1 secret)))) | |
1142 | (let ((context (epg-make-context 'OpenPGP)) | |
1143 | plain) | |
1144 | (epg-context-set-passphrase-callback | |
1145 | context | |
1146 | (cons #'auth-source-token-passphrase-callback-function | |
1147 | file)) | |
1148 | (epg-decrypt-string context secret))) | |
1149 | ||
1150 | ;; (insert (auth-source-epa-make-gpg-token "mysecret" "~/.netrc")) | |
2b8c5660 | 1151 | (defun auth-source-epa-make-gpg-token (secret file) |
936d08bb G |
1152 | (let ((context (epg-make-context 'OpenPGP)) |
1153 | (pp-escape-newlines nil) | |
1154 | cipher) | |
1155 | (epg-context-set-armor context t) | |
1156 | (epg-context-set-passphrase-callback | |
1157 | context | |
1158 | (cons #'auth-source-token-passphrase-callback-function | |
1159 | file)) | |
1160 | (setq cipher (epg-encrypt-string context secret nil)) | |
1161 | (with-temp-buffer | |
1162 | (insert cipher) | |
1163 | (base64-encode-region (point-min) (point-max) t) | |
1164 | (concat "gpg:" (buffer-substring-no-properties | |
1165 | (point-min) | |
1166 | (point-max)))))) | |
2b8c5660 TZ |
1167 | |
1168 | (defun auth-source-netrc-normalize (alist filename) | |
b8e0f0cd G |
1169 | (mapcar (lambda (entry) |
1170 | (let (ret item) | |
1171 | (while (setq item (pop entry)) | |
1172 | (let ((k (car item)) | |
1173 | (v (cdr item))) | |
1174 | ||
1175 | ;; apply key aliases | |
1176 | (setq k (cond ((member k '("machine")) "host") | |
1177 | ((member k '("login" "account")) "user") | |
1178 | ((member k '("protocol")) "port") | |
1179 | ((member k '("password")) "secret") | |
1180 | (t k))) | |
1181 | ||
1182 | ;; send back the secret in a function (lexical binding) | |
1183 | (when (equal k "secret") | |
936d08bb G |
1184 | (setq v (lexical-let ((lexv v) |
1185 | (token-decoder nil)) | |
1186 | (when (string-match "^gpg:" lexv) | |
1187 | ;; it's a GPG token: create a token decoder | |
1188 | ;; which unsets itself once | |
1189 | (setq token-decoder | |
1190 | (lambda (val) | |
1191 | (prog1 | |
1192 | (auth-source-epa-extract-gpg-token | |
1193 | val | |
1194 | filename) | |
1195 | (setq token-decoder nil))))) | |
1196 | (lambda () | |
1197 | (when token-decoder | |
1198 | (setq lexv (funcall token-decoder lexv))) | |
1199 | lexv)))) | |
e9cb4479 DU |
1200 | (setq ret (plist-put ret |
1201 | (intern (concat ":" k)) | |
1202 | v)))) | |
1203 | ret)) | |
1204 | alist)) | |
b8e0f0cd | 1205 | |
cbffd0bd SM |
1206 | ;; (setq secret (plist-get (nth 0 (auth-source-search :host t :type 'netrc :K 1 :max 1)) :secret)) |
1207 | ;; (funcall secret) | |
b8e0f0cd G |
1208 | |
1209 | (defun* auth-source-netrc-search (&rest | |
1210 | spec | |
733afdf4 | 1211 | &key backend require create delete |
35123c04 | 1212 | type max host user port |
b8e0f0cd | 1213 | &allow-other-keys) |
e9cb4479 | 1214 | "Given a property list SPEC, return search matches from the :backend. |
b8e0f0cd G |
1215 | See `auth-source-search' for details on SPEC." |
1216 | ;; just in case, check that the type is correct (null or same as the backend) | |
1217 | (assert (or (null type) (eq type (oref backend type))) | |
d5e9a4e9 | 1218 | t "Invalid netrc search: %s %s") |
b8e0f0cd G |
1219 | |
1220 | (let ((results (auth-source-netrc-normalize | |
1221 | (auth-source-netrc-parse | |
1222 | :max max | |
733afdf4 | 1223 | :require require |
b8e0f0cd G |
1224 | :delete delete |
1225 | :file (oref backend source) | |
1226 | :host (or host t) | |
1227 | :user (or user t) | |
2b8c5660 TZ |
1228 | :port (or port t)) |
1229 | (oref backend source)))) | |
b8e0f0cd G |
1230 | |
1231 | ;; if we need to create an entry AND none were found to match | |
1232 | (when (and create | |
4a3988d5 | 1233 | (not results)) |
b8e0f0cd | 1234 | |
584c9d3f G |
1235 | ;; create based on the spec and record the value |
1236 | (setq results (or | |
1237 | ;; if the user did not want to create the entry | |
1238 | ;; in the file, it will be returned | |
1239 | (apply (slot-value backend 'create-function) spec) | |
1240 | ;; if not, we do the search again without :create | |
1241 | ;; to get the updated data. | |
1242 | ||
1243 | ;; the result will be returned, even if the search fails | |
1244 | (apply 'auth-source-netrc-search | |
1245 | (plist-put spec :create nil))))) | |
b8e0f0cd G |
1246 | results)) |
1247 | ||
fa41748c G |
1248 | (defun auth-source-netrc-element-or-first (v) |
1249 | (if (listp v) | |
1250 | (nth 0 v) | |
1251 | v)) | |
1252 | ||
cbffd0bd SM |
1253 | ;; (auth-source-search :host "nonesuch" :type 'netrc :max 1 :create t) |
1254 | ;; (auth-source-search :host "nonesuch" :type 'netrc :max 1 :create t :create-extra-keys '((A "default A") (B))) | |
b8e0f0cd G |
1255 | |
1256 | (defun* auth-source-netrc-create (&rest spec | |
1257 | &key backend | |
35123c04 | 1258 | secret host user port create |
b8e0f0cd | 1259 | &allow-other-keys) |
35123c04 | 1260 | (let* ((base-required '(host user port secret)) |
b8e0f0cd G |
1261 | ;; we know (because of an assertion in auth-source-search) that the |
1262 | ;; :create parameter is either t or a list (which includes nil) | |
1263 | (create-extra (if (eq t create) nil create)) | |
e9cb4479 DU |
1264 | (current-data (car (auth-source-search :max 1 |
1265 | :host host | |
1266 | :port port))) | |
b8e0f0cd G |
1267 | (required (append base-required create-extra)) |
1268 | (file (oref backend source)) | |
1269 | (add "") | |
1270 | ;; `valist' is an alist | |
584c9d3f G |
1271 | valist |
1272 | ;; `artificial' will be returned if no creation is needed | |
1273 | artificial) | |
b8e0f0cd G |
1274 | |
1275 | ;; only for base required elements (defined as function parameters): | |
1276 | ;; fill in the valist with whatever data we may have from the search | |
7ba93e94 | 1277 | ;; we complete the first value if it's a list and use the value otherwise |
b8e0f0cd G |
1278 | (dolist (br base-required) |
1279 | (when (symbol-value br) | |
7ba93e94 G |
1280 | (let ((br-choice (cond |
1281 | ;; all-accepting choice (predicate is t) | |
1282 | ((eq t (symbol-value br)) nil) | |
1283 | ;; just the value otherwise | |
1284 | (t (symbol-value br))))) | |
1285 | (when br-choice | |
8b6c19f4 | 1286 | (auth-source--aput valist br br-choice))))) |
b8e0f0cd G |
1287 | |
1288 | ;; for extra required elements, see if the spec includes a value for them | |
1289 | (dolist (er create-extra) | |
1290 | (let ((name (concat ":" (symbol-name er))) | |
1291 | (keys (loop for i below (length spec) by 2 | |
1292 | collect (nth i spec)))) | |
1293 | (dolist (k keys) | |
1294 | (when (equal (symbol-name k) name) | |
8b6c19f4 | 1295 | (auth-source--aput valist er (plist-get spec k)))))) |
b8e0f0cd G |
1296 | |
1297 | ;; for each required element | |
1298 | (dolist (r required) | |
8b6c19f4 | 1299 | (let* ((data (auth-source--aget valist r)) |
4a3988d5 | 1300 | ;; take the first element if the data is a list |
ddb7ffee | 1301 | (data (or (auth-source-netrc-element-or-first data) |
e9cb4479 DU |
1302 | (plist-get current-data |
1303 | (intern (format ":%s" r) obarray)))) | |
4a3988d5 | 1304 | ;; this is the default to be offered |
8b6c19f4 SM |
1305 | (given-default (auth-source--aget |
1306 | auth-source-creation-defaults r)) | |
733afdf4 TZ |
1307 | ;; the default supplementals are simple: |
1308 | ;; for the user, try `given-default' and then (user-login-name); | |
1309 | ;; otherwise take `given-default' | |
b8e0f0cd | 1310 | (default (cond |
733afdf4 TZ |
1311 | ((and (not given-default) (eq r 'user)) |
1312 | (user-login-name)) | |
003522ce G |
1313 | (t given-default))) |
1314 | (printable-defaults (list | |
1315 | (cons 'user | |
1316 | (or | |
1317 | (auth-source-netrc-element-or-first | |
8b6c19f4 | 1318 | (auth-source--aget valist 'user)) |
003522ce G |
1319 | (plist-get artificial :user) |
1320 | "[any user]")) | |
1321 | (cons 'host | |
1322 | (or | |
1323 | (auth-source-netrc-element-or-first | |
8b6c19f4 | 1324 | (auth-source--aget valist 'host)) |
003522ce G |
1325 | (plist-get artificial :host) |
1326 | "[any host]")) | |
1327 | (cons 'port | |
1328 | (or | |
1329 | (auth-source-netrc-element-or-first | |
8b6c19f4 | 1330 | (auth-source--aget valist 'port)) |
003522ce G |
1331 | (plist-get artificial :port) |
1332 | "[any port]")))) | |
8b6c19f4 | 1333 | (prompt (or (auth-source--aget auth-source-creation-prompts r) |
003522ce | 1334 | (case r |
733afdf4 TZ |
1335 | (secret "%p password for %u@%h: ") |
1336 | (user "%p user name for %h: ") | |
1337 | (host "%p host name for user %u: ") | |
1338 | (port "%p port for %u@%h: ")) | |
003522ce G |
1339 | (format "Enter %s (%%u@%%h:%%p): " r))) |
1340 | (prompt (auth-source-format-prompt | |
1341 | prompt | |
8b6c19f4 SM |
1342 | `((?u ,(auth-source--aget printable-defaults 'user)) |
1343 | (?h ,(auth-source--aget printable-defaults 'host)) | |
1344 | (?p ,(auth-source--aget printable-defaults 'port)))))) | |
4a3988d5 | 1345 | |
aa2ebce9 | 1346 | ;; Store the data, prompting for the password if needed. |
5c7f66a0 G |
1347 | (setq data (or data |
1348 | (if (eq r 'secret) | |
1349 | ;; Special case prompt for passwords. | |
1350 | ;; TODO: make the default (setq auth-source-netrc-use-gpg-tokens `((,(if (boundp 'epa-file-auto-mode-alist-entry) (car (symbol-value 'epa-file-auto-mode-alist-entry)) "\\.gpg\\'") nil) (t gpg))) | |
1351 | ;; TODO: or maybe leave as (setq auth-source-netrc-use-gpg-tokens 'never) | |
1352 | (let* ((ep (format "Use GPG password tokens in %s?" file)) | |
1353 | (gpg-encrypt | |
1354 | (cond | |
1355 | ((eq auth-source-netrc-use-gpg-tokens 'never) | |
1356 | 'never) | |
1357 | ((listp auth-source-netrc-use-gpg-tokens) | |
1358 | (let ((check (copy-sequence | |
1359 | auth-source-netrc-use-gpg-tokens)) | |
1360 | item ret) | |
1361 | (while check | |
1362 | (setq item (pop check)) | |
1363 | (when (or (eq (car item) t) | |
1364 | (string-match (car item) file)) | |
1365 | (setq ret (cdr item)) | |
1366 | (setq check nil))))) | |
1367 | (t 'never))) | |
1368 | (plain (or (eval default) (read-passwd prompt)))) | |
1369 | ;; ask if we don't know what to do (in which case | |
1370 | ;; auth-source-netrc-use-gpg-tokens must be a list) | |
1371 | (unless gpg-encrypt | |
1372 | (setq gpg-encrypt (if (y-or-n-p ep) 'gpg 'never)) | |
1373 | ;; TODO: save the defcustom now? or ask? | |
1374 | (setq auth-source-netrc-use-gpg-tokens | |
1375 | (cons `(,file ,gpg-encrypt) | |
1376 | auth-source-netrc-use-gpg-tokens))) | |
1377 | (if (eq gpg-encrypt 'gpg) | |
1378 | (auth-source-epa-make-gpg-token plain file) | |
1379 | plain)) | |
1380 | (if (stringp default) | |
1381 | (read-string (if (string-match ": *\\'" prompt) | |
1382 | (concat (substring prompt 0 (match-beginning 0)) | |
1383 | " (default " default "): ") | |
1384 | (concat prompt "(default " default ") ")) | |
1385 | nil nil default) | |
1386 | (eval default))))) | |
b8e0f0cd | 1387 | |
584c9d3f G |
1388 | (when data |
1389 | (setq artificial (plist-put artificial | |
1390 | (intern (concat ":" (symbol-name r))) | |
1391 | (if (eq r 'secret) | |
1392 | (lexical-let ((data data)) | |
1393 | (lambda () data)) | |
1394 | data)))) | |
1395 | ||
aa2ebce9 | 1396 | ;; When r is not an empty string... |
b8e0f0cd G |
1397 | (when (and (stringp data) |
1398 | (< 0 (length data))) | |
4a3988d5 G |
1399 | ;; this function is not strictly necessary but I think it |
1400 | ;; makes the code clearer -tzz | |
1401 | (let ((printer (lambda () | |
7ba93e94 G |
1402 | ;; append the key (the symbol name of r) |
1403 | ;; and the value in r | |
6a6e4d93 | 1404 | (format "%s%s %s" |
7ba93e94 G |
1405 | ;; prepend a space |
1406 | (if (zerop (length add)) "" " ") | |
1407 | ;; remap auth-source tokens to netrc | |
1408 | (case r | |
0adf5618 SM |
1409 | (user "login") |
1410 | (host "machine") | |
1411 | (secret "password") | |
1412 | (port "port") ; redundant but clearer | |
b8e0f0cd | 1413 | (t (symbol-name r))) |
005a89ff | 1414 | (if (string-match "[\"# ]" data) |
e9cb4479 DU |
1415 | (format "%S" data) |
1416 | data))))) | |
4a3988d5 | 1417 | (setq add (concat add (funcall printer))))))) |
b8e0f0cd | 1418 | |
733afdf4 TZ |
1419 | (plist-put |
1420 | artificial | |
1421 | :save-function | |
1422 | (lexical-let ((file file) | |
1423 | (add add)) | |
1424 | (lambda () (auth-source-netrc-saver file add)))) | |
1425 | ||
1426 | (list artificial))) | |
1427 | ||
4248cca2 | 1428 | ;;(funcall (plist-get (nth 0 (auth-source-search :host '("nonesuch2") :user "tzz" :port "imap" :create t :max 1)) :save-function)) |
733afdf4 TZ |
1429 | (defun auth-source-netrc-saver (file add) |
1430 | "Save a line ADD in FILE, prompting along the way. | |
4248cca2 TZ |
1431 | Respects `auth-source-save-behavior'. Uses |
1432 | `auth-source-netrc-cache' to avoid prompting more than once." | |
1433 | (let* ((key (format "%s %s" file (rfc2104-hash 'md5 64 16 file add))) | |
1434 | (cached (assoc key auth-source-netrc-cache))) | |
1435 | ||
1436 | (if cached | |
1437 | (auth-source-do-trivia | |
1438 | "auth-source-netrc-saver: found previous run for key %s, returning" | |
1439 | key) | |
1440 | (with-temp-buffer | |
1441 | (when (file-exists-p file) | |
1442 | (insert-file-contents file)) | |
1443 | (when auth-source-gpg-encrypt-to | |
1444 | ;; (see bug#7487) making `epa-file-encrypt-to' local to | |
1445 | ;; this buffer lets epa-file skip the key selection query | |
1446 | ;; (see the `local-variable-p' check in | |
1447 | ;; `epa-file-write-region'). | |
1448 | (unless (local-variable-p 'epa-file-encrypt-to (current-buffer)) | |
1449 | (make-local-variable 'epa-file-encrypt-to)) | |
1450 | (if (listp auth-source-gpg-encrypt-to) | |
1451 | (setq epa-file-encrypt-to auth-source-gpg-encrypt-to))) | |
1452 | ;; we want the new data to be found first, so insert at beginning | |
1453 | (goto-char (point-min)) | |
1454 | ||
aa2ebce9 | 1455 | ;; Ask AFTER we've successfully opened the file. |
4248cca2 TZ |
1456 | (let ((prompt (format "Save auth info to file %s? " file)) |
1457 | (done (not (eq auth-source-save-behavior 'ask))) | |
1458 | (bufname "*auth-source Help*") | |
1459 | k) | |
1460 | (while (not done) | |
1461 | (setq k (auth-source-read-char-choice prompt '(?y ?n ?N ?e ??))) | |
1462 | (case k | |
1463 | (?y (setq done t)) | |
1464 | (?? (save-excursion | |
1465 | (with-output-to-temp-buffer bufname | |
1466 | (princ | |
1467 | (concat "(y)es, save\n" | |
1468 | "(n)o but use the info\n" | |
1469 | "(N)o and don't ask to save again\n" | |
1470 | "(e)dit the line\n" | |
1471 | "(?) for help as you can see.\n")) | |
aa2ebce9 SM |
1472 | ;; Why? Doesn't with-output-to-temp-buffer already do |
1473 | ;; the exact same thing anyway? --Stef | |
4248cca2 TZ |
1474 | (set-buffer standard-output) |
1475 | (help-mode)))) | |
1476 | (?n (setq add "" | |
1477 | done t)) | |
3451795c | 1478 | (?N |
e9cb4479 DU |
1479 | (setq add "" |
1480 | done t) | |
1481 | (customize-save-variable 'auth-source-save-behavior nil)) | |
4248cca2 TZ |
1482 | (?e (setq add (read-string "Line to add: " add))) |
1483 | (t nil))) | |
1484 | ||
1485 | (when (get-buffer-window bufname) | |
1486 | (delete-window (get-buffer-window bufname))) | |
1487 | ||
aa2ebce9 | 1488 | ;; Make sure the info is not saved. |
4248cca2 TZ |
1489 | (when (null auth-source-save-behavior) |
1490 | (setq add "")) | |
1491 | ||
1492 | (when (< 0 (length add)) | |
1493 | (progn | |
1494 | (unless (bolp) | |
1495 | (insert "\n")) | |
1496 | (insert add "\n") | |
1497 | (write-region (point-min) (point-max) file nil 'silent) | |
4dcb0d7a LMI |
1498 | ;; Make the .authinfo file non-world-readable. |
1499 | (set-file-modes file #o600) | |
4248cca2 TZ |
1500 | (auth-source-do-debug |
1501 | "auth-source-netrc-create: wrote 1 new line to %s" | |
1502 | file) | |
1503 | (message "Saved new authentication information to %s" file) | |
1504 | nil)))) | |
8b6c19f4 | 1505 | (auth-source--aput auth-source-netrc-cache key "ran")))) |
b8e0f0cd G |
1506 | |
1507 | ;;; Backend specific parsing: Secrets API backend | |
1508 | ||
cbffd0bd SM |
1509 | ;; (let ((auth-sources '(default))) (auth-source-search :max 1 :create t)) |
1510 | ;; (let ((auth-sources '(default))) (auth-source-search :max 1 :delete t)) | |
1511 | ;; (let ((auth-sources '(default))) (auth-source-search :max 1)) | |
1512 | ;; (let ((auth-sources '(default))) (auth-source-search)) | |
1513 | ;; (let ((auth-sources '("secrets:Login"))) (auth-source-search :max 1)) | |
1514 | ;; (let ((auth-sources '("secrets:Login"))) (auth-source-search :max 1 :signon_realm "https://git.gnus.org/Git")) | |
b8e0f0cd | 1515 | |
d3cac061 DC |
1516 | (defun auth-source-secrets-listify-pattern (pattern) |
1517 | "Convert a pattern with lists to a list of string patterns. | |
1518 | ||
1519 | auth-source patterns can have values of the form :foo (\"bar\" | |
1520 | \"qux\"), which means to match any secret with :foo equal to | |
16f4c9f1 | 1521 | \"bar\" or :foo equal to \"qux\". The secrets backend supports |
d3cac061 DC |
1522 | only string values for patterns, so this routine returns a list |
1523 | of patterns that is equivalent to the single original pattern | |
1524 | when interpreted such that if a secret matches any pattern in the | |
16f4c9f1 | 1525 | list, it matches the original pattern." |
d3cac061 DC |
1526 | (if (null pattern) |
1527 | '(nil) | |
1528 | (let* ((key (pop pattern)) | |
1529 | (value (pop pattern)) | |
1530 | (tails (auth-source-secrets-listify-pattern pattern)) | |
1531 | (heads (if (stringp value) | |
1532 | (list (list key value)) | |
1533 | (mapcar (lambda (v) (list key v)) value)))) | |
4d2226bf | 1534 | (loop |
d3cac061 DC |
1535 | for h in heads |
1536 | nconc | |
4d2226bf | 1537 | (loop |
d3cac061 DC |
1538 | for tl in tails |
1539 | collect (append h tl)))))) | |
1540 | ||
b8e0f0cd G |
1541 | (defun* auth-source-secrets-search (&rest |
1542 | spec | |
1543 | &key backend create delete label | |
35123c04 | 1544 | type max host user port |
b8e0f0cd G |
1545 | &allow-other-keys) |
1546 | "Search the Secrets API; spec is like `auth-source'. | |
1547 | ||
1548 | The :label key specifies the item's label. It is the only key | |
1549 | that can specify a substring. Any :label value besides a string | |
1550 | will allow any label. | |
1551 | ||
1552 | All other search keys must match exactly. If you need substring | |
1553 | matching, do a wider search and narrow it down yourself. | |
1554 | ||
1555 | You'll get back all the properties of the token as a plist. | |
1556 | ||
5415d076 | 1557 | Here's an example that looks for the first item in the 'Login' |
b8e0f0cd G |
1558 | Secrets collection: |
1559 | ||
5415d076 | 1560 | \(let ((auth-sources '(\"secrets:Login\"))) |
b8e0f0cd G |
1561 | (auth-source-search :max 1) |
1562 | ||
5415d076 | 1563 | Here's another that looks for the first item in the 'Login' |
b8e0f0cd G |
1564 | Secrets collection whose label contains 'gnus': |
1565 | ||
5415d076 | 1566 | \(let ((auth-sources '(\"secrets:Login\"))) |
b8e0f0cd G |
1567 | (auth-source-search :max 1 :label \"gnus\") |
1568 | ||
5415d076 | 1569 | And this one looks for the first item in the 'Login' Secrets |
b8e0f0cd | 1570 | collection that's a Google Chrome entry for the git.gnus.org site |
5415d076 | 1571 | authentication tokens: |
b8e0f0cd | 1572 | |
5415d076 | 1573 | \(let ((auth-sources '(\"secrets:Login\"))) |
b8e0f0cd G |
1574 | (auth-source-search :max 1 :signon_realm \"https://git.gnus.org/Git\")) |
1575 | " | |
1576 | ||
1577 | ;; TODO | |
1578 | (assert (not create) nil | |
1579 | "The Secrets API auth-source backend doesn't support creation yet") | |
1580 | ;; TODO | |
1581 | ;; (secrets-delete-item coll elt) | |
1582 | (assert (not delete) nil | |
1583 | "The Secrets API auth-source backend doesn't support deletion yet") | |
1584 | ||
1585 | (let* ((coll (oref backend source)) | |
1586 | (max (or max 5000)) ; sanity check: default to stop at 5K | |
a3095f42 | 1587 | (ignored-keys '(:create :delete :max :backend :label :require :type)) |
b8e0f0cd G |
1588 | (search-keys (loop for i below (length spec) by 2 |
1589 | unless (memq (nth i spec) ignored-keys) | |
1590 | collect (nth i spec))) | |
1591 | ;; build a search spec without the ignored keys | |
1592 | ;; if a search key is nil or t (match anything), we skip it | |
d3cac061 DC |
1593 | (search-specs (auth-source-secrets-listify-pattern |
1594 | (apply 'append (mapcar | |
5415d076 G |
1595 | (lambda (k) |
1596 | (if (or (null (plist-get spec k)) | |
1597 | (eq t (plist-get spec k))) | |
1598 | nil | |
1599 | (list k (plist-get spec k)))) | |
d3cac061 | 1600 | search-keys)))) |
35123c04 | 1601 | ;; needed keys (always including host, login, port, and secret) |
d638ac9e | 1602 | (returned-keys (mm-delete-duplicates (append |
e9cb4479 DU |
1603 | '(:host :login :port :secret) |
1604 | search-keys))) | |
d3cac061 DC |
1605 | (items |
1606 | (loop for search-spec in search-specs | |
1607 | nconc | |
1608 | (loop for item in (apply 'secrets-search-items coll search-spec) | |
1609 | unless (and (stringp label) | |
1610 | (not (string-match label item))) | |
1611 | collect item))) | |
b8e0f0cd | 1612 | ;; TODO: respect max in `secrets-search-items', not after the fact |
5415d076 | 1613 | (items (butlast items (- (length items) max))) |
b8e0f0cd G |
1614 | ;; convert the item name to a full plist |
1615 | (items (mapcar (lambda (item) | |
1616 | (append | |
1617 | ;; make an entry for the secret (password) element | |
1618 | (list | |
1619 | :secret | |
1620 | (lexical-let ((v (secrets-get-secret coll item))) | |
1621 | (lambda () v))) | |
1622 | ;; rewrite the entry from ((k1 v1) (k2 v2)) to plist | |
5415d076 G |
1623 | (apply 'append |
1624 | (mapcar (lambda (entry) | |
1625 | (list (car entry) (cdr entry))) | |
1626 | (secrets-get-attributes coll item))))) | |
b8e0f0cd G |
1627 | items)) |
1628 | ;; ensure each item has each key in `returned-keys' | |
1629 | (items (mapcar (lambda (plist) | |
1630 | (append | |
5415d076 G |
1631 | (apply 'append |
1632 | (mapcar (lambda (req) | |
1633 | (if (plist-get plist req) | |
1634 | nil | |
1635 | (list req nil))) | |
1636 | returned-keys)) | |
b8e0f0cd G |
1637 | plist)) |
1638 | items))) | |
1639 | items)) | |
1640 | ||
1641 | (defun* auth-source-secrets-create (&rest | |
1642 | spec | |
35123c04 | 1643 | &key backend type max host user port |
b8e0f0cd G |
1644 | &allow-other-keys) |
1645 | ;; TODO | |
1646 | ;; (apply 'secrets-create-item (auth-get-source entry) name passwd spec) | |
1647 | (debug spec)) | |
1648 | ||
d7fcec5d TZ |
1649 | ;;; Backend specific parsing: Mac OS Keychain (using /usr/bin/security) backend |
1650 | ||
1651 | ;; (let ((auth-sources '(macos-keychain-internet))) (auth-source-search :max 1 :create t)) | |
1652 | ;; (let ((auth-sources '(macos-keychain-internet))) (auth-source-search :max 1 :delete t)) | |
1653 | ;; (let ((auth-sources '(macos-keychain-internet))) (auth-source-search :max 1)) | |
1654 | ;; (let ((auth-sources '(macos-keychain-internet))) (auth-source-search)) | |
1655 | ||
1656 | ;; (let ((auth-sources '(macos-keychain-generic))) (auth-source-search :max 1 :create t)) | |
1657 | ;; (let ((auth-sources '(macos-keychain-generic))) (auth-source-search :max 1 :delete t)) | |
1658 | ;; (let ((auth-sources '(macos-keychain-generic))) (auth-source-search :max 1)) | |
1659 | ;; (let ((auth-sources '(macos-keychain-generic))) (auth-source-search)) | |
1660 | ||
1661 | ;; (let ((auth-sources '("macos-keychain-internet:/Users/tzz/Library/Keychains/login.keychain"))) (auth-source-search :max 1)) | |
1662 | ;; (let ((auth-sources '("macos-keychain-generic:Login"))) (auth-source-search :max 1 :host "git.gnus.org")) | |
8f25c2bc | 1663 | ;; (let ((auth-sources '("macos-keychain-generic:Login"))) (auth-source-search :max 1)) |
d7fcec5d TZ |
1664 | |
1665 | (defun* auth-source-macos-keychain-search (&rest | |
1666 | spec | |
1667 | &key backend create delete label | |
1668 | type max host user port | |
1669 | &allow-other-keys) | |
1670 | "Search the MacOS Keychain; spec is like `auth-source'. | |
1671 | ||
1672 | All search keys must match exactly. If you need substring | |
1673 | matching, do a wider search and narrow it down yourself. | |
1674 | ||
1675 | You'll get back all the properties of the token as a plist. | |
1676 | ||
1677 | The :type key is either 'macos-keychain-internet or | |
1678 | 'macos-keychain-generic. | |
1679 | ||
1680 | For the internet keychain type, the :label key searches the | |
1681 | item's labels (\"-l LABEL\" passed to \"/usr/bin/security\"). | |
1682 | Similarly, :host maps to \"-s HOST\", :user maps to \"-a USER\", | |
1683 | and :port maps to \"-P PORT\" or \"-r PROT\" | |
1684 | (note PROT has to be a 4-character string). | |
1685 | ||
1686 | For the generic keychain type, the :label key searches the item's | |
1687 | labels (\"-l LABEL\" passed to \"/usr/bin/security\"). | |
1688 | Similarly, :host maps to \"-c HOST\" (the \"creator\" keychain | |
1689 | field), :user maps to \"-a USER\", and :port maps to \"-s PORT\". | |
1690 | ||
1691 | Here's an example that looks for the first item in the default | |
1692 | generic MacOS Keychain: | |
1693 | ||
1694 | \(let ((auth-sources '(macos-keychain-generic))) | |
1695 | (auth-source-search :max 1) | |
1696 | ||
1697 | Here's another that looks for the first item in the internet | |
1698 | MacOS Keychain collection whose label is 'gnus': | |
1699 | ||
1700 | \(let ((auth-sources '(macos-keychain-internet))) | |
1701 | (auth-source-search :max 1 :label \"gnus\") | |
1702 | ||
1703 | And this one looks for the first item in the internet keychain | |
1704 | entries for git.gnus.org: | |
1705 | ||
1706 | \(let ((auth-sources '(macos-keychain-internet\"))) | |
1707 | (auth-source-search :max 1 :host \"git.gnus.org\")) | |
1708 | " | |
1709 | ;; TODO | |
1710 | (assert (not create) nil | |
1711 | "The MacOS Keychain auth-source backend doesn't support creation yet") | |
1712 | ;; TODO | |
1713 | ;; (macos-keychain-delete-item coll elt) | |
1714 | (assert (not delete) nil | |
1715 | "The MacOS Keychain auth-source backend doesn't support deletion yet") | |
1716 | ||
1717 | (let* ((coll (oref backend source)) | |
1718 | (max (or max 5000)) ; sanity check: default to stop at 5K | |
1719 | (ignored-keys '(:create :delete :max :backend :label)) | |
1720 | (search-keys (loop for i below (length spec) by 2 | |
1721 | unless (memq (nth i spec) ignored-keys) | |
1722 | collect (nth i spec))) | |
1723 | ;; build a search spec without the ignored keys | |
1724 | ;; if a search key is nil or t (match anything), we skip it | |
1725 | (search-spec (apply 'append (mapcar | |
1726 | (lambda (k) | |
1727 | (if (or (null (plist-get spec k)) | |
1728 | (eq t (plist-get spec k))) | |
1729 | nil | |
1730 | (list k (plist-get spec k)))) | |
1731 | search-keys))) | |
1732 | ;; needed keys (always including host, login, port, and secret) | |
1733 | (returned-keys (mm-delete-duplicates (append | |
1734 | '(:host :login :port :secret) | |
1735 | search-keys))) | |
1736 | (items (apply 'auth-source-macos-keychain-search-items | |
1737 | coll | |
1738 | type | |
1739 | max | |
1740 | search-spec)) | |
1741 | ||
1742 | ;; ensure each item has each key in `returned-keys' | |
1743 | (items (mapcar (lambda (plist) | |
1744 | (append | |
1745 | (apply 'append | |
1746 | (mapcar (lambda (req) | |
1747 | (if (plist-get plist req) | |
1748 | nil | |
1749 | (list req nil))) | |
1750 | returned-keys)) | |
1751 | plist)) | |
1752 | items))) | |
1753 | items)) | |
1754 | ||
1755 | (defun* auth-source-macos-keychain-search-items (coll type max | |
1756 | &rest spec | |
1757 | &key label type | |
1758 | host user port | |
1759 | &allow-other-keys) | |
1760 | ||
1761 | (let* ((keychain-generic (eq type 'macos-keychain-generic)) | |
1762 | (args `(,(if keychain-generic | |
1763 | "find-generic-password" | |
1764 | "find-internet-password") | |
1765 | "-g")) | |
1766 | (ret (list :type type))) | |
1767 | (when label | |
1768 | (setq args (append args (list "-l" label)))) | |
1769 | (when host | |
1770 | (setq args (append args (list (if keychain-generic "-c" "-s") host)))) | |
1771 | (when user | |
1772 | (setq args (append args (list "-a" user)))) | |
1773 | ||
1774 | (when port | |
1775 | (if keychain-generic | |
1776 | (setq args (append args (list "-s" port))) | |
1777 | (setq args (append args (list | |
1778 | (if (string-match "[0-9]+" port) "-P" "-r") | |
1779 | port))))) | |
1780 | ||
1781 | (unless (equal coll "default") | |
1782 | (setq args (append args (list coll)))) | |
1783 | ||
1784 | (with-temp-buffer | |
1785 | (apply 'call-process "/usr/bin/security" nil t nil args) | |
1786 | (goto-char (point-min)) | |
1787 | (while (not (eobp)) | |
1788 | (cond | |
1789 | ((looking-at "^password: \"\\(.+\\)\"$") | |
1790 | (auth-source-macos-keychain-result-append | |
1791 | ret | |
1792 | keychain-generic | |
1793 | "secret" | |
1794 | (lexical-let ((v (match-string 1))) | |
1795 | (lambda () v)))) | |
1796 | ;; TODO: check if this is really the label | |
1797 | ;; match 0x00000007 <blob>="AppleID" | |
1798 | ((looking-at "^[ ]+0x00000007 <blob>=\"\\(.+\\)\"") | |
1799 | (auth-source-macos-keychain-result-append | |
1800 | ret | |
1801 | keychain-generic | |
1802 | "label" | |
1803 | (match-string 1))) | |
1804 | ;; match "crtr"<uint32>="aapl" | |
1805 | ;; match "svce"<blob>="AppleID" | |
1806 | ((looking-at "^[ ]+\"\\([a-z]+\\)\"[^=]+=\"\\(.+\\)\"") | |
1807 | (auth-source-macos-keychain-result-append | |
1808 | ret | |
1809 | keychain-generic | |
1810 | (match-string 1) | |
1811 | (match-string 2)))) | |
1812 | (forward-line))) | |
1813 | ;; return `ret' iff it has the :secret key | |
1814 | (and (plist-get ret :secret) (list ret)))) | |
1815 | ||
1816 | (defun auth-source-macos-keychain-result-append (result generic k v) | |
d6e7c17b | 1817 | (push v result) |
d7fcec5d TZ |
1818 | (setq k (cond |
1819 | ((equal k "acct") "user") | |
1820 | ;; for generic keychains, creator is host, service is port | |
1821 | ((and generic (equal k "crtr")) "host") | |
1822 | ((and generic (equal k "svce")) "port") | |
1823 | ;; for internet keychains, protocol is port, server is host | |
1824 | ((and (not generic) (equal k "ptcl")) "port") | |
1825 | ((and (not generic) (equal k "srvr")) "host") | |
1826 | (t k))) | |
1827 | ||
d6e7c17b | 1828 | (push (intern (format ":%s" k)) result)) |
d7fcec5d TZ |
1829 | |
1830 | (defun* auth-source-macos-keychain-create (&rest | |
1831 | spec | |
1832 | &key backend type max host user port | |
1833 | &allow-other-keys) | |
1834 | ;; TODO | |
1835 | (debug spec)) | |
1836 | ||
8977de27 DU |
1837 | ;;; Backend specific parsing: PLSTORE backend |
1838 | ||
1839 | (defun* auth-source-plstore-search (&rest | |
1840 | spec | |
1841 | &key backend create delete label | |
1842 | type max host user port | |
1843 | &allow-other-keys) | |
1844 | "Search the PLSTORE; spec is like `auth-source'." | |
b09c3fe0 | 1845 | (let* ((store (oref backend data)) |
8977de27 | 1846 | (max (or max 5000)) ; sanity check: default to stop at 5K |
a3095f42 | 1847 | (ignored-keys '(:create :delete :max :backend :label :require :type)) |
8977de27 DU |
1848 | (search-keys (loop for i below (length spec) by 2 |
1849 | unless (memq (nth i spec) ignored-keys) | |
1850 | collect (nth i spec))) | |
1851 | ;; build a search spec without the ignored keys | |
1852 | ;; if a search key is nil or t (match anything), we skip it | |
1853 | (search-spec (apply 'append (mapcar | |
1854 | (lambda (k) | |
e9cb4479 DU |
1855 | (let ((v (plist-get spec k))) |
1856 | (if (or (null v) | |
1857 | (eq t v)) | |
1858 | nil | |
1859 | (if (stringp v) | |
1860 | (setq v (list v))) | |
1861 | (list k v)))) | |
1862 | search-keys))) | |
8977de27 DU |
1863 | ;; needed keys (always including host, login, port, and secret) |
1864 | (returned-keys (mm-delete-duplicates (append | |
e9cb4479 DU |
1865 | '(:host :login :port :secret) |
1866 | search-keys))) | |
8977de27 | 1867 | (items (plstore-find store search-spec)) |
e9cb4479 | 1868 | (item-names (mapcar #'car items)) |
8977de27 DU |
1869 | (items (butlast items (- (length items) max))) |
1870 | ;; convert the item to a full plist | |
1871 | (items (mapcar (lambda (item) | |
e9cb4479 DU |
1872 | (let* ((plist (copy-tree (cdr item))) |
1873 | (secret (plist-member plist :secret))) | |
1874 | (if secret | |
1875 | (setcar | |
1876 | (cdr secret) | |
1877 | (lexical-let ((v (car (cdr secret)))) | |
1878 | (lambda () v)))) | |
1879 | plist)) | |
8977de27 DU |
1880 | items)) |
1881 | ;; ensure each item has each key in `returned-keys' | |
1882 | (items (mapcar (lambda (plist) | |
1883 | (append | |
1884 | (apply 'append | |
1885 | (mapcar (lambda (req) | |
1886 | (if (plist-get plist req) | |
1887 | nil | |
1888 | (list req nil))) | |
1889 | returned-keys)) | |
1890 | plist)) | |
1891 | items))) | |
f3078a00 DU |
1892 | (cond |
1893 | ;; if we need to create an entry AND none were found to match | |
1894 | ((and create | |
e9cb4479 | 1895 | (not items)) |
8977de27 DU |
1896 | |
1897 | ;; create based on the spec and record the value | |
1898 | (setq items (or | |
e9cb4479 DU |
1899 | ;; if the user did not want to create the entry |
1900 | ;; in the file, it will be returned | |
1901 | (apply (slot-value backend 'create-function) spec) | |
1902 | ;; if not, we do the search again without :create | |
1903 | ;; to get the updated data. | |
1904 | ||
1905 | ;; the result will be returned, even if the search fails | |
1906 | (apply 'auth-source-plstore-search | |
1907 | (plist-put spec :create nil))))) | |
f3078a00 | 1908 | ((and delete |
e9cb4479 | 1909 | item-names) |
f3078a00 | 1910 | (dolist (item-name item-names) |
e9cb4479 | 1911 | (plstore-delete store item-name)) |
f3078a00 | 1912 | (plstore-save store))) |
8977de27 DU |
1913 | items)) |
1914 | ||
1915 | (defun* auth-source-plstore-create (&rest spec | |
e9cb4479 DU |
1916 | &key backend |
1917 | secret host user port create | |
1918 | &allow-other-keys) | |
8977de27 | 1919 | (let* ((base-required '(host user port secret)) |
e9cb4479 | 1920 | (base-secret '(secret)) |
8977de27 DU |
1921 | ;; we know (because of an assertion in auth-source-search) that the |
1922 | ;; :create parameter is either t or a list (which includes nil) | |
1923 | (create-extra (if (eq t create) nil create)) | |
e9cb4479 DU |
1924 | (current-data (car (auth-source-search :max 1 |
1925 | :host host | |
1926 | :port port))) | |
8977de27 DU |
1927 | (required (append base-required create-extra)) |
1928 | (file (oref backend source)) | |
1929 | (add "") | |
1930 | ;; `valist' is an alist | |
1931 | valist | |
1932 | ;; `artificial' will be returned if no creation is needed | |
1933 | artificial | |
e9cb4479 | 1934 | secret-artificial) |
8977de27 DU |
1935 | |
1936 | ;; only for base required elements (defined as function parameters): | |
1937 | ;; fill in the valist with whatever data we may have from the search | |
1938 | ;; we complete the first value if it's a list and use the value otherwise | |
1939 | (dolist (br base-required) | |
1940 | (when (symbol-value br) | |
1941 | (let ((br-choice (cond | |
1942 | ;; all-accepting choice (predicate is t) | |
1943 | ((eq t (symbol-value br)) nil) | |
1944 | ;; just the value otherwise | |
1945 | (t (symbol-value br))))) | |
1946 | (when br-choice | |
8b6c19f4 | 1947 | (auth-source--aput valist br br-choice))))) |
8977de27 DU |
1948 | |
1949 | ;; for extra required elements, see if the spec includes a value for them | |
1950 | (dolist (er create-extra) | |
1951 | (let ((name (concat ":" (symbol-name er))) | |
1952 | (keys (loop for i below (length spec) by 2 | |
1953 | collect (nth i spec)))) | |
1954 | (dolist (k keys) | |
1955 | (when (equal (symbol-name k) name) | |
8b6c19f4 | 1956 | (auth-source--aput valist er (plist-get spec k)))))) |
8977de27 DU |
1957 | |
1958 | ;; for each required element | |
1959 | (dolist (r required) | |
8b6c19f4 | 1960 | (let* ((data (auth-source--aget valist r)) |
8977de27 DU |
1961 | ;; take the first element if the data is a list |
1962 | (data (or (auth-source-netrc-element-or-first data) | |
e9cb4479 DU |
1963 | (plist-get current-data |
1964 | (intern (format ":%s" r) obarray)))) | |
8977de27 | 1965 | ;; this is the default to be offered |
8b6c19f4 SM |
1966 | (given-default (auth-source--aget |
1967 | auth-source-creation-defaults r)) | |
8977de27 DU |
1968 | ;; the default supplementals are simple: |
1969 | ;; for the user, try `given-default' and then (user-login-name); | |
1970 | ;; otherwise take `given-default' | |
1971 | (default (cond | |
1972 | ((and (not given-default) (eq r 'user)) | |
1973 | (user-login-name)) | |
1974 | (t given-default))) | |
1975 | (printable-defaults (list | |
1976 | (cons 'user | |
1977 | (or | |
1978 | (auth-source-netrc-element-or-first | |
8b6c19f4 | 1979 | (auth-source--aget valist 'user)) |
8977de27 DU |
1980 | (plist-get artificial :user) |
1981 | "[any user]")) | |
1982 | (cons 'host | |
1983 | (or | |
1984 | (auth-source-netrc-element-or-first | |
8b6c19f4 | 1985 | (auth-source--aget valist 'host)) |
8977de27 DU |
1986 | (plist-get artificial :host) |
1987 | "[any host]")) | |
1988 | (cons 'port | |
1989 | (or | |
1990 | (auth-source-netrc-element-or-first | |
8b6c19f4 | 1991 | (auth-source--aget valist 'port)) |
8977de27 DU |
1992 | (plist-get artificial :port) |
1993 | "[any port]")))) | |
8b6c19f4 | 1994 | (prompt (or (auth-source--aget auth-source-creation-prompts r) |
8977de27 DU |
1995 | (case r |
1996 | (secret "%p password for %u@%h: ") | |
1997 | (user "%p user name for %h: ") | |
1998 | (host "%p host name for user %u: ") | |
1999 | (port "%p port for %u@%h: ")) | |
2000 | (format "Enter %s (%%u@%%h:%%p): " r))) | |
2001 | (prompt (auth-source-format-prompt | |
2002 | prompt | |
8b6c19f4 SM |
2003 | `((?u ,(auth-source--aget printable-defaults 'user)) |
2004 | (?h ,(auth-source--aget printable-defaults 'host)) | |
2005 | (?p ,(auth-source--aget printable-defaults 'port)))))) | |
8977de27 DU |
2006 | |
2007 | ;; Store the data, prompting for the password if needed. | |
5c7f66a0 G |
2008 | (setq data (or data |
2009 | (if (eq r 'secret) | |
2010 | (or (eval default) (read-passwd prompt)) | |
2011 | (if (stringp default) | |
8b6c19f4 SM |
2012 | (read-string |
2013 | (if (string-match ": *\\'" prompt) | |
2014 | (concat (substring prompt 0 (match-beginning 0)) | |
2015 | " (default " default "): ") | |
2016 | (concat prompt "(default " default ") ")) | |
2017 | nil nil default) | |
5c7f66a0 | 2018 | (eval default))))) |
8977de27 DU |
2019 | |
2020 | (when data | |
e9cb4479 DU |
2021 | (if (member r base-secret) |
2022 | (setq secret-artificial | |
2023 | (plist-put secret-artificial | |
2024 | (intern (concat ":" (symbol-name r))) | |
2025 | data)) | |
2026 | (setq artificial (plist-put artificial | |
2027 | (intern (concat ":" (symbol-name r))) | |
2028 | data)))))) | |
b09c3fe0 | 2029 | (plstore-put (oref backend data) |
e9cb4479 DU |
2030 | (sha1 (format "%s@%s:%s" |
2031 | (plist-get artificial :user) | |
2032 | (plist-get artificial :host) | |
2033 | (plist-get artificial :port))) | |
2034 | artificial secret-artificial) | |
8977de27 | 2035 | (if (y-or-n-p (format "Save auth info to file %s? " |
e9cb4479 DU |
2036 | (plstore-get-file (oref backend data)))) |
2037 | (plstore-save (oref backend data))))) | |
8977de27 | 2038 | |
b8e0f0cd G |
2039 | ;;; older API |
2040 | ||
cbffd0bd | 2041 | ;; (auth-source-user-or-password '("login" "password") "imap.myhost.com" t "tzz") |
b8e0f0cd G |
2042 | |
2043 | ;; deprecate the old interface | |
2044 | (make-obsolete 'auth-source-user-or-password | |
2045 | 'auth-source-search "Emacs 24.1") | |
2046 | (make-obsolete 'auth-source-forget-user-or-password | |
2047 | 'auth-source-forget "Emacs 24.1") | |
fb178e4c | 2048 | |
0e4966fb | 2049 | (defun auth-source-user-or-password |
35123c04 TZ |
2050 | (mode host port &optional username create-missing delete-existing) |
2051 | "Find MODE (string or list of strings) matching HOST and PORT. | |
fb178e4c | 2052 | |
b8e0f0cd G |
2053 | DEPRECATED in favor of `auth-source-search'! |
2054 | ||
fb178e4c KY |
2055 | USERNAME is optional and will be used as \"login\" in a search |
2056 | across the Secret Service API (see secrets.el) if the resulting | |
2057 | items don't have a username. This means that if you search for | |
2058 | username \"joe\" and it matches an item but the item doesn't have | |
2059 | a :user attribute, the username \"joe\" will be returned. | |
2060 | ||
0e4966fb MA |
2061 | A non nil DELETE-EXISTING means deleting any matching password |
2062 | entry in the respective sources. This is useful only when | |
2063 | CREATE-MISSING is non nil as well; the intended use case is to | |
2064 | remove wrong password entries. | |
2065 | ||
2066 | If no matching entry is found, and CREATE-MISSING is non nil, | |
2067 | the password will be retrieved interactively, and it will be | |
2068 | stored in the password database which matches best (see | |
2069 | `auth-sources'). | |
2070 | ||
2071 | MODE can be \"login\" or \"password\"." | |
554a69b8 | 2072 | (auth-source-do-debug |
b8e0f0cd | 2073 | "auth-source-user-or-password: DEPRECATED get %s for %s (%s) + user=%s" |
35123c04 | 2074 | mode host port username) |
b8e0f0cd | 2075 | |
3b36c17e | 2076 | (let* ((listy (listp mode)) |
cbabe91f TZ |
2077 | (mode (if listy mode (list mode))) |
2078 | (cname (if username | |
35123c04 TZ |
2079 | (format "%s %s:%s %s" mode host port username) |
2080 | (format "%s %s:%s" mode host port))) | |
2081 | (search (list :host host :port port)) | |
cbabe91f | 2082 | (search (if username (append search (list :user username)) search)) |
b8e0f0cd G |
2083 | (search (if create-missing |
2084 | (append search (list :create t)) | |
2085 | search)) | |
2086 | (search (if delete-existing | |
2087 | (append search (list :delete t)) | |
2088 | search)) | |
2089 | ;; (found (if (not delete-existing) | |
2090 | ;; (gethash cname auth-source-cache) | |
2091 | ;; (remhash cname auth-source-cache) | |
2092 | ;; nil))) | |
2093 | (found nil)) | |
ed778fad | 2094 | (if found |
cbabe91f TZ |
2095 | (progn |
2096 | (auth-source-do-debug | |
b8e0f0cd | 2097 | "auth-source-user-or-password: DEPRECATED cached %s=%s for %s (%s) + %s" |
cbabe91f TZ |
2098 | mode |
2099 | ;; don't show the password | |
b8e0f0cd | 2100 | (if (and (member "password" mode) t) |
cbabe91f TZ |
2101 | "SECRET" |
2102 | found) | |
35123c04 | 2103 | host port username) |
cbabe91f | 2104 | found) ; return the found data |
b8e0f0cd G |
2105 | ;; else, if not found, search with a max of 1 |
2106 | (let ((choice (nth 0 (apply 'auth-source-search | |
2107 | (append '(:max 1) search))))) | |
2108 | (when choice | |
2109 | (dolist (m mode) | |
2110 | (cond | |
2111 | ((equal "password" m) | |
2112 | (push (if (plist-get choice :secret) | |
e9cb4479 DU |
2113 | (funcall (plist-get choice :secret)) |
2114 | nil) found)) | |
b8e0f0cd G |
2115 | ((equal "login" m) |
2116 | (push (plist-get choice :user) found))))) | |
2117 | (setq found (nreverse found)) | |
2118 | (setq found (if listy found (car-safe found))))) | |
9b3ebcb6 | 2119 | |
e9cb4479 | 2120 | found)) |
8f7abae3 | 2121 | |
fb7e9e05 TZ |
2122 | (defun auth-source-user-and-password (host &optional user) |
2123 | (let* ((auth-info (car | |
2124 | (if user | |
2125 | (auth-source-search | |
2126 | :host host | |
2127 | :user "yourusername" | |
2128 | :max 1 | |
2129 | :require '(:user :secret) | |
2130 | :create nil) | |
2131 | (auth-source-search | |
2132 | :host host | |
2133 | :max 1 | |
2134 | :require '(:user :secret) | |
2135 | :create nil)))) | |
2136 | (user (plist-get auth-info :user)) | |
2137 | (password (plist-get auth-info :secret))) | |
2138 | (when (functionp password) | |
2139 | (setq password (funcall password))) | |
2140 | (list user password auth-info))) | |
2141 | ||
8f7abae3 MB |
2142 | (provide 'auth-source) |
2143 | ||
8f7abae3 | 2144 | ;;; auth-source.el ends here |