| 1 | ;;; auth-source.el --- authentication sources for Gnus and Emacs |
| 2 | |
| 3 | ;; Copyright (C) 2008-2014 Free Software Foundation, Inc. |
| 4 | |
| 5 | ;; Author: Ted Zlatanov <tzz@lifelogs.com> |
| 6 | ;; Keywords: news |
| 7 | |
| 8 | ;; This file is part of GNU Emacs. |
| 9 | |
| 10 | ;; GNU Emacs is free software: you can redistribute it and/or modify |
| 11 | ;; it under the terms of the GNU General Public License as published by |
| 12 | ;; the Free Software Foundation, either version 3 of the License, or |
| 13 | ;; (at your option) any later version. |
| 14 | |
| 15 | ;; GNU Emacs is distributed in the hope that it will be useful, |
| 16 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 17 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 18 | ;; GNU General Public License for more details. |
| 19 | |
| 20 | ;; You should have received a copy of the GNU General Public License |
| 21 | ;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>. |
| 22 | |
| 23 | ;;; Commentary: |
| 24 | |
| 25 | ;; This is the auth-source.el package. It lets users tell Gnus how to |
| 26 | ;; authenticate in a single place. Simplicity is the goal. Instead |
| 27 | ;; of providing 5000 options, we'll stick to simple, easy to |
| 28 | ;; understand options. |
| 29 | |
| 30 | ;; See the auth.info Info documentation for details. |
| 31 | |
| 32 | ;; TODO: |
| 33 | |
| 34 | ;; - never decode the backend file unless it's necessary |
| 35 | ;; - a more generic way to match backends and search backend contents |
| 36 | ;; - absorb netrc.el and simplify it |
| 37 | ;; - protect passwords better |
| 38 | ;; - allow creating and changing netrc lines (not files) e.g. change a password |
| 39 | |
| 40 | ;;; Code: |
| 41 | |
| 42 | (require 'password-cache) |
| 43 | (require 'mm-util) |
| 44 | (require 'gnus-util) |
| 45 | |
| 46 | (eval-when-compile (require 'cl)) |
| 47 | (require 'eieio) |
| 48 | |
| 49 | (autoload 'secrets-create-item "secrets") |
| 50 | (autoload 'secrets-delete-item "secrets") |
| 51 | (autoload 'secrets-get-alias "secrets") |
| 52 | (autoload 'secrets-get-attributes "secrets") |
| 53 | (autoload 'secrets-get-secret "secrets") |
| 54 | (autoload 'secrets-list-collections "secrets") |
| 55 | (autoload 'secrets-search-items "secrets") |
| 56 | |
| 57 | (autoload 'rfc2104-hash "rfc2104") |
| 58 | |
| 59 | (autoload 'plstore-open "plstore") |
| 60 | (autoload 'plstore-find "plstore") |
| 61 | (autoload 'plstore-put "plstore") |
| 62 | (autoload 'plstore-delete "plstore") |
| 63 | (autoload 'plstore-save "plstore") |
| 64 | (autoload 'plstore-get-file "plstore") |
| 65 | |
| 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 | |
| 72 | (autoload 'help-mode "help-mode" nil t) |
| 73 | |
| 74 | (defvar secrets-enabled) |
| 75 | |
| 76 | (defgroup auth-source nil |
| 77 | "Authentication sources." |
| 78 | :version "23.1" ;; No Gnus |
| 79 | :group 'gnus) |
| 80 | |
| 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." |
| 86 | :version "24.1" |
| 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 | |
| 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. |
| 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.") |
| 117 | (port :initarg :port |
| 118 | :initform t |
| 119 | :type t |
| 120 | :custom string |
| 121 | :documentation "The backend protocol.") |
| 122 | (data :initarg :data |
| 123 | :initform nil |
| 124 | :documentation "Internal backend data.") |
| 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 | |
| 136 | (defcustom auth-source-protocols '((imap "imap" "imaps" "143" "993") |
| 137 | (pop3 "pop3" "pop" "pop3s" "110" "995") |
| 138 | (ssh "ssh" "22") |
| 139 | (sftp "sftp" "115") |
| 140 | (smtp "smtp" "25")) |
| 141 | "List of authentication protocols and their names" |
| 142 | |
| 143 | :group 'auth-source |
| 144 | :version "23.2" ;; No Gnus |
| 145 | :type '(repeat :tag "Authentication Protocols" |
| 146 | (cons :tag "Protocol Entry" |
| 147 | (symbol :tag "Protocol") |
| 148 | (repeat :tag "Names" |
| 149 | (string :tag "Name"))))) |
| 150 | |
| 151 | ;; Generate all the protocols in a format Customize can use. |
| 152 | ;; TODO: generate on the fly from auth-source-protocols |
| 153 | (defconst auth-source-protocols-customize |
| 154 | (mapcar (lambda (a) |
| 155 | (let ((p (car-safe a))) |
| 156 | (list 'const |
| 157 | :tag (upcase (symbol-name p)) |
| 158 | p))) |
| 159 | auth-source-protocols)) |
| 160 | |
| 161 | (defvar auth-source-creation-defaults nil |
| 162 | "Defaults for creating token values. Usually let-bound.") |
| 163 | |
| 164 | (defvar auth-source-creation-prompts nil |
| 165 | "Default prompts for token values. Usually let-bound.") |
| 166 | |
| 167 | (make-obsolete 'auth-source-hide-passwords nil "Emacs 24.1") |
| 168 | |
| 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 | |
| 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 |
| 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." |
| 186 | :group 'auth-source |
| 187 | :version "23.2" ;; No Gnus |
| 188 | :type `(choice |
| 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)))))) |
| 204 | |
| 205 | (defvar auth-source-magic "auth-source-magic ") |
| 206 | |
| 207 | (defcustom auth-source-do-cache t |
| 208 | "Whether auth-source should cache information with `password-cache'." |
| 209 | :group 'auth-source |
| 210 | :version "23.2" ;; No Gnus |
| 211 | :type `boolean) |
| 212 | |
| 213 | (defcustom auth-source-debug nil |
| 214 | "Whether auth-source should log debug messages. |
| 215 | |
| 216 | If the value is nil, debug messages are not logged. |
| 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 | |
| 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 |
| 225 | :version "23.2" ;; No Gnus |
| 226 | :type `(choice |
| 227 | :tag "auth-source debugging mode" |
| 228 | (const :tag "Log using `message' to the *Messages* buffer" t) |
| 229 | (const :tag "Log all trivia with `message' to the *Messages* buffer" |
| 230 | trivia) |
| 231 | (function :tag "Function that takes arguments like `message'") |
| 232 | (const :tag "Don't log anything" nil))) |
| 233 | |
| 234 | (defcustom auth-sources '("~/.authinfo" "~/.authinfo.gpg" "~/.netrc") |
| 235 | "List of authentication sources. |
| 236 | Each entry is the authentication type with optional properties. |
| 237 | Entries are tried in the order in which they appear. |
| 238 | See Info node `(auth)Help for users' for details. |
| 239 | |
| 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 | |
| 245 | It's best to customize this with `M-x customize-variable' because the choices |
| 246 | can get pretty complex." |
| 247 | :group 'auth-source |
| 248 | :version "24.1" ;; No Gnus |
| 249 | :type `(repeat :tag "Authentication Sources" |
| 250 | (choice |
| 251 | (string :tag "Just a file") |
| 252 | (const :tag "Default Secrets API Collection" default) |
| 253 | (const :tag "Login Secrets API Collection" "secrets:Login") |
| 254 | (const :tag "Temp Secrets API Collection" "secrets:session") |
| 255 | |
| 256 | (const :tag "Default internet Mac OS Keychain" |
| 257 | macos-keychain-internet) |
| 258 | |
| 259 | (const :tag "Default generic Mac OS Keychain" |
| 260 | macos-keychain-generic) |
| 261 | |
| 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") |
| 271 | (const :tag "Default" default) |
| 272 | (const :tag "Login" "Login") |
| 273 | (const |
| 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") |
| 281 | (const :tag "default" default))) |
| 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") |
| 288 | (const :tag "default" default)))) |
| 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" |
| 300 | (const :format "" :value :port) |
| 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) |
| 307 | (choice |
| 308 | :tag "Personality/Username" |
| 309 | (const :tag "Any" t) |
| 310 | (string |
| 311 | :tag "Name"))))))))) |
| 312 | |
| 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 |
| 317 | :version "24.1" ;; No Gnus |
| 318 | :type '(choice (const :tag "Symmetric encryption" t) |
| 319 | (repeat :tag "Recipient public keys" |
| 320 | (string :tag "Recipient public key")))) |
| 321 | |
| 322 | ;; temp for debugging |
| 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) |
| 331 | ;; (auth-source-pick nil :host "a" :port 'imap) |
| 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 | |
| 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")) |
| 341 | (defun auth-source-do-debug (&rest msg) |
| 342 | (when auth-source-debug |
| 343 | (apply 'auth-source-do-warn msg))) |
| 344 | |
| 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 | |
| 350 | (defun auth-source-do-warn (&rest msg) |
| 351 | (apply |
| 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 |
| 354 | (if (functionp auth-source-debug) |
| 355 | auth-source-debug |
| 356 | 'message) |
| 357 | msg)) |
| 358 | |
| 359 | |
| 360 | ;; (auth-source-read-char-choice "enter choice? " '(?a ?b ?q)) |
| 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 |
| 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 | |
| 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))) |
| 388 | |
| 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) |
| 392 | ;; )) |
| 393 | |
| 394 | ;; (setq auth-sources '((:source "~/.authinfo.gpg" :host t :port t))) |
| 395 | |
| 396 | ;; (auth-source-backend-parse "myfile.gpg") |
| 397 | ;; (auth-source-backend-parse 'default) |
| 398 | ;; (auth-source-backend-parse "secrets:Login") |
| 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") |
| 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))))) |
| 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 | |
| 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)) |
| 446 | (if (equal (file-name-extension (plist-get entry :source)) "plist") |
| 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))) |
| 454 | (auth-source-backend |
| 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))) |
| 460 | |
| 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 | |
| 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, |
| 502 | ;; and if that alias is missing, we use "Login" |
| 503 | (when (symbolp source) |
| 504 | (setq source (or (secrets-get-alias (symbol-name source)) |
| 505 | "Login"))) |
| 506 | |
| 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)))) |
| 520 | |
| 521 | ;; none of them |
| 522 | (t |
| 523 | (auth-source-do-warn |
| 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. |
| 532 | Using the plist ENTRY, get the :host, :port, and :user search |
| 533 | parameters." |
| 534 | (let ((entry (if (stringp entry) |
| 535 | nil |
| 536 | entry)) |
| 537 | val) |
| 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)) |
| 542 | (when (setq val (plist-get entry :port)) |
| 543 | (oset backend port val))) |
| 544 | backend) |
| 545 | |
| 546 | ;; (mapcar 'auth-source-backend-parse auth-sources) |
| 547 | |
| 548 | (defun* auth-source-search (&rest spec |
| 549 | &key type max host user port secret |
| 550 | require create delete |
| 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 | |
| 562 | Common search keys are :max, :host, :port, and :user. In |
| 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 |
| 576 | port keys. |
| 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 |
| 592 | automatically require host, user, and port. The host will be |
| 593 | 'mine'. We prompt for the user with default 'defaultUser' and |
| 594 | for the port without a default. We will not prompt for A, Q, |
| 595 | or P. The resulting token will only have keys user, host, and |
| 596 | port.\" |
| 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 |
| 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. |
| 610 | |
| 611 | Here's an example: |
| 612 | |
| 613 | \(let ((auth-source-creation-defaults '((user . \"defaultUser\") |
| 614 | (A . \"default A\"))) |
| 615 | (auth-source-creation-prompts |
| 616 | '((password . \"Enter IMAP password for %h:%p: \")))) |
| 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 |
| 627 | automatically require host, user, and port. The host will be |
| 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'.\" |
| 633 | |
| 634 | When multiple values are specified in the search parameter, the |
| 635 | user is prompted for which one. So :host (X Y Z) would ask the |
| 636 | user to choose between X, Y, and Z. |
| 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 | |
| 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 | |
| 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). |
| 657 | When 0 the function will return just t or nil to indicate if any |
| 658 | matches were found. More than N items may be returned, depending |
| 659 | on the search and the backend. |
| 660 | |
| 661 | :host (X Y Z) means to match only hosts X, Y, or Z according to |
| 662 | the match rules above. Defaults to t. |
| 663 | |
| 664 | :user (X Y Z) means to match only users X, Y, or Z according to |
| 665 | the match rules above. Defaults to t. |
| 666 | |
| 667 | :port (P Q R) means to match only protocols P, Q, or R. |
| 668 | Defaults to t. |
| 669 | |
| 670 | :K (V1 V2 V3) for any other key K will match values V1, V2, or |
| 671 | V3 (note the match rules above). |
| 672 | |
| 673 | The return value is a list with at most :max tokens. Each token |
| 674 | is a plist with keys :backend :host :port :user, plus any other |
| 675 | keys provided by the backend (notably :secret). But note the |
| 676 | exception for :max 0, which see above. |
| 677 | |
| 678 | The token can hold a :save-function key. If you call that, the |
| 679 | user will be prompted to save the data to the backend. You can't |
| 680 | request that this should happen right after creation, because |
| 681 | `auth-source-search' has no way of knowing if the token is |
| 682 | actually useful. So the caller must arrange to call this function. |
| 683 | |
| 684 | The token's :secret key can hold a function. In that case you |
| 685 | must call it to obtain the actual value." |
| 686 | (let* ((backends (mapcar 'auth-source-backend-parse auth-sources)) |
| 687 | (max (or max 1)) |
| 688 | (ignored-keys '(:require :create :delete :max)) |
| 689 | (keys (loop for i below (length spec) by 2 |
| 690 | unless (memq (nth i spec) ignored-keys) |
| 691 | collect (nth i spec))) |
| 692 | (cached (auth-source-remembered-p spec)) |
| 693 | ;; note that we may have cached results but found is still nil |
| 694 | ;; (there were no results from the search) |
| 695 | (found (auth-source-recall spec)) |
| 696 | filtered-backends accessor-key backend) |
| 697 | |
| 698 | (if (and cached auth-source-do-cache) |
| 699 | (auth-source-do-debug |
| 700 | "auth-source-search: found %d CACHED results matching %S" |
| 701 | (length found) spec) |
| 702 | |
| 703 | (assert |
| 704 | (or (eq t create) (listp create)) t |
| 705 | "Invalid auth-source :create parameter (must be t or a list): %s %s") |
| 706 | |
| 707 | (assert |
| 708 | (listp require) t |
| 709 | "Invalid auth-source :require parameter (must be a list): %s") |
| 710 | |
| 711 | (setq filtered-backends (copy-sequence backends)) |
| 712 | (dolist (backend backends) |
| 713 | (dolist (key keys) |
| 714 | ;; ignore invalid slots |
| 715 | (condition-case signal |
| 716 | (unless (eval `(auth-source-search-collection |
| 717 | (plist-get spec key) |
| 718 | (oref backend ,key))) |
| 719 | (setq filtered-backends (delq backend filtered-backends)) |
| 720 | (return)) |
| 721 | (invalid-slot-name)))) |
| 722 | |
| 723 | (auth-source-do-trivia |
| 724 | "auth-source-search: found %d backends matching %S" |
| 725 | (length filtered-backends) spec) |
| 726 | |
| 727 | ;; (debug spec "filtered" filtered-backends) |
| 728 | ;; First go through all the backends without :create, so we can |
| 729 | ;; query them all. |
| 730 | (setq found (auth-source-search-backends filtered-backends |
| 731 | spec |
| 732 | ;; to exit early |
| 733 | max |
| 734 | ;; create is always nil here |
| 735 | nil delete |
| 736 | require)) |
| 737 | |
| 738 | (auth-source-do-debug |
| 739 | "auth-source-search: found %d results (max %d) matching %S" |
| 740 | (length found) max spec) |
| 741 | |
| 742 | ;; If we didn't find anything, then we allow the backend(s) to |
| 743 | ;; create the entries. |
| 744 | (when (and create |
| 745 | (not found)) |
| 746 | (setq found (auth-source-search-backends filtered-backends |
| 747 | spec |
| 748 | ;; to exit early |
| 749 | max |
| 750 | create delete |
| 751 | require)) |
| 752 | (auth-source-do-debug |
| 753 | "auth-source-search: CREATED %d results (max %d) matching %S" |
| 754 | (length found) max spec)) |
| 755 | |
| 756 | ;; note we remember the lack of result too, if it's applicable |
| 757 | (when auth-source-do-cache |
| 758 | (auth-source-remember spec found))) |
| 759 | |
| 760 | found)) |
| 761 | |
| 762 | (defun auth-source-search-backends (backends spec max create delete require) |
| 763 | (let (matches) |
| 764 | (dolist (backend backends) |
| 765 | (when (> max (length matches)) ; when we need more matches... |
| 766 | (let* ((bmatches (apply |
| 767 | (slot-value backend 'search-function) |
| 768 | :backend backend |
| 769 | :type (slot-value backend :type) |
| 770 | ;; note we're overriding whatever the spec |
| 771 | ;; has for :require, :create, and :delete |
| 772 | :require require |
| 773 | :create create |
| 774 | :delete delete |
| 775 | spec))) |
| 776 | (when bmatches |
| 777 | (auth-source-do-trivia |
| 778 | "auth-source-search-backend: got %d (max %d) in %s:%s matching %S" |
| 779 | (length bmatches) max |
| 780 | (slot-value backend :type) |
| 781 | (slot-value backend :source) |
| 782 | spec) |
| 783 | (setq matches (append matches bmatches)))))) |
| 784 | matches)) |
| 785 | |
| 786 | ;; (auth-source-search :max 1) |
| 787 | ;; (funcall (plist-get (nth 0 (auth-source-search :max 1)) :secret)) |
| 788 | ;; (auth-source-search :host "nonesuch" :type 'netrc :K 1) |
| 789 | ;; (auth-source-search :host "nonesuch" :type 'secrets) |
| 790 | |
| 791 | (defun* auth-source-delete (&rest spec |
| 792 | &key delete |
| 793 | &allow-other-keys) |
| 794 | "Delete entries from the authentication backends according to SPEC. |
| 795 | Calls `auth-source-search' with the :delete property in SPEC set to t. |
| 796 | The backend may not actually delete the entries. |
| 797 | |
| 798 | Returns the deleted entries." |
| 799 | (auth-source-search (plist-put spec :delete t))) |
| 800 | |
| 801 | (defun auth-source-search-collection (collection value) |
| 802 | "Returns t is VALUE is t or COLLECTION is t or COLLECTION contains VALUE." |
| 803 | (when (and (atom collection) (not (eq t collection))) |
| 804 | (setq collection (list collection))) |
| 805 | |
| 806 | ;; (debug :collection collection :value value) |
| 807 | (or (eq collection t) |
| 808 | (eq value t) |
| 809 | (equal collection value) |
| 810 | (member value collection))) |
| 811 | |
| 812 | (defvar auth-source-netrc-cache nil) |
| 813 | |
| 814 | (defun auth-source-forget-all-cached () |
| 815 | "Forget all cached auth-source data." |
| 816 | (interactive) |
| 817 | (loop for sym being the symbols of password-data |
| 818 | ;; when the symbol name starts with auth-source-magic |
| 819 | when (string-match (concat "^" auth-source-magic) |
| 820 | (symbol-name sym)) |
| 821 | ;; remove that key |
| 822 | do (password-cache-remove (symbol-name sym))) |
| 823 | (setq auth-source-netrc-cache nil)) |
| 824 | |
| 825 | (defun auth-source-format-cache-entry (spec) |
| 826 | "Format SPEC entry to put it in the password cache." |
| 827 | (concat auth-source-magic (format "%S" spec))) |
| 828 | |
| 829 | (defun auth-source-remember (spec found) |
| 830 | "Remember FOUND search results for SPEC." |
| 831 | (let ((password-cache-expiry auth-source-cache-expiry)) |
| 832 | (password-cache-add |
| 833 | (auth-source-format-cache-entry spec) found))) |
| 834 | |
| 835 | (defun auth-source-recall (spec) |
| 836 | "Recall FOUND search results for SPEC." |
| 837 | (password-read-from-cache (auth-source-format-cache-entry spec))) |
| 838 | |
| 839 | (defun auth-source-remembered-p (spec) |
| 840 | "Check if SPEC is remembered." |
| 841 | (password-in-cache-p |
| 842 | (auth-source-format-cache-entry spec))) |
| 843 | |
| 844 | (defun auth-source-forget (spec) |
| 845 | "Forget any cached data matching SPEC exactly. |
| 846 | |
| 847 | This is the same SPEC you passed to `auth-source-search'. |
| 848 | Returns t or nil for forgotten or not found." |
| 849 | (password-cache-remove (auth-source-format-cache-entry spec))) |
| 850 | |
| 851 | ;; (loop for sym being the symbols of password-data when (string-match (concat "^" auth-source-magic) (symbol-name sym)) collect (symbol-name sym)) |
| 852 | |
| 853 | ;; (auth-source-remember '(:host "wedd") '(4 5 6)) |
| 854 | ;; (auth-source-remembered-p '(:host "wedd")) |
| 855 | ;; (auth-source-remember '(:host "xedd") '(1 2 3)) |
| 856 | ;; (auth-source-remembered-p '(:host "xedd")) |
| 857 | ;; (auth-source-remembered-p '(:host "zedd")) |
| 858 | ;; (auth-source-recall '(:host "xedd")) |
| 859 | ;; (auth-source-recall '(:host t)) |
| 860 | ;; (auth-source-forget+ :host t) |
| 861 | |
| 862 | (defun* auth-source-forget+ (&rest spec &allow-other-keys) |
| 863 | "Forget any cached data matching SPEC. Returns forgotten count. |
| 864 | |
| 865 | This is not a full `auth-source-search' spec but works similarly. |
| 866 | For instance, \(:host \"myhost\" \"yourhost\") would find all the |
| 867 | cached data that was found with a search for those two hosts, |
| 868 | while \(:host t) would find all host entries." |
| 869 | (let ((count 0) |
| 870 | sname) |
| 871 | (loop for sym being the symbols of password-data |
| 872 | ;; when the symbol name matches with auth-source-magic |
| 873 | when (and (setq sname (symbol-name sym)) |
| 874 | (string-match (concat "^" auth-source-magic "\\(.+\\)") |
| 875 | sname) |
| 876 | ;; and the spec matches what was stored in the cache |
| 877 | (auth-source-specmatchp spec (read (match-string 1 sname)))) |
| 878 | ;; remove that key |
| 879 | do (progn |
| 880 | (password-cache-remove sname) |
| 881 | (incf count))) |
| 882 | count)) |
| 883 | |
| 884 | (defun auth-source-specmatchp (spec stored) |
| 885 | (let ((keys (loop for i below (length spec) by 2 |
| 886 | collect (nth i spec)))) |
| 887 | (not (eq |
| 888 | (dolist (key keys) |
| 889 | (unless (auth-source-search-collection (plist-get stored key) |
| 890 | (plist-get spec key)) |
| 891 | (return 'no))) |
| 892 | 'no)))) |
| 893 | |
| 894 | ;; (auth-source-pick-first-password :host "z.lifelogs.com") |
| 895 | ;; (auth-source-pick-first-password :port "imap") |
| 896 | (defun auth-source-pick-first-password (&rest spec) |
| 897 | "Pick the first secret found from applying SPEC to `auth-source-search'." |
| 898 | (let* ((result (nth 0 (apply 'auth-source-search (plist-put spec :max 1)))) |
| 899 | (secret (plist-get result :secret))) |
| 900 | |
| 901 | (if (functionp secret) |
| 902 | (funcall secret) |
| 903 | secret))) |
| 904 | |
| 905 | ;; (auth-source-format-prompt "test %u %h %p" '((?u "user") (?h "host"))) |
| 906 | (defun auth-source-format-prompt (prompt alist) |
| 907 | "Format PROMPT using %x (for any character x) specifiers in ALIST." |
| 908 | (dolist (cell alist) |
| 909 | (let ((c (nth 0 cell)) |
| 910 | (v (nth 1 cell))) |
| 911 | (when (and c v) |
| 912 | (setq prompt (replace-regexp-in-string (format "%%%c" c) |
| 913 | (format "%s" v) |
| 914 | prompt nil t))))) |
| 915 | prompt) |
| 916 | |
| 917 | (defun auth-source-ensure-strings (values) |
| 918 | (unless (listp values) |
| 919 | (setq values (list values))) |
| 920 | (mapcar (lambda (value) |
| 921 | (if (numberp value) |
| 922 | (format "%s" value) |
| 923 | value)) |
| 924 | values)) |
| 925 | |
| 926 | ;;; Backend specific parsing: netrc/authinfo backend |
| 927 | |
| 928 | (defun auth-source--aput-1 (alist key val) |
| 929 | (let ((seen ()) |
| 930 | (rest alist)) |
| 931 | (while (and (consp rest) (not (equal key (caar rest)))) |
| 932 | (push (pop rest) seen)) |
| 933 | (cons (cons key val) |
| 934 | (if (null rest) alist |
| 935 | (nconc (nreverse seen) |
| 936 | (if (equal key (caar rest)) (cdr rest) rest)))))) |
| 937 | (defmacro auth-source--aput (var key val) |
| 938 | `(setq ,var (auth-source--aput-1 ,var ,key ,val))) |
| 939 | |
| 940 | (defun auth-source--aget (alist key) |
| 941 | (cdr (assoc key alist))) |
| 942 | |
| 943 | ;; (auth-source-netrc-parse :file "~/.authinfo.gpg") |
| 944 | (defun* auth-source-netrc-parse (&rest |
| 945 | spec |
| 946 | &key file max host user port delete require |
| 947 | &allow-other-keys) |
| 948 | "Parse FILE and return a list of all entries in the file. |
| 949 | Note that the MAX parameter is used so we can exit the parse early." |
| 950 | (if (listp file) |
| 951 | ;; We got already parsed contents; just return it. |
| 952 | file |
| 953 | (when (file-exists-p file) |
| 954 | (setq port (auth-source-ensure-strings port)) |
| 955 | (with-temp-buffer |
| 956 | (let* ((max (or max 5000)) ; sanity check: default to stop at 5K |
| 957 | (modified 0) |
| 958 | (cached (cdr-safe (assoc file auth-source-netrc-cache))) |
| 959 | (cached-mtime (plist-get cached :mtime)) |
| 960 | (cached-secrets (plist-get cached :secret)) |
| 961 | (check (lambda(alist) |
| 962 | (and alist |
| 963 | (auth-source-search-collection |
| 964 | host |
| 965 | (or |
| 966 | (auth-source--aget alist "machine") |
| 967 | (auth-source--aget alist "host") |
| 968 | t)) |
| 969 | (auth-source-search-collection |
| 970 | user |
| 971 | (or |
| 972 | (auth-source--aget alist "login") |
| 973 | (auth-source--aget alist "account") |
| 974 | (auth-source--aget alist "user") |
| 975 | t)) |
| 976 | (auth-source-search-collection |
| 977 | port |
| 978 | (or |
| 979 | (auth-source--aget alist "port") |
| 980 | (auth-source--aget alist "protocol") |
| 981 | t)) |
| 982 | (or |
| 983 | ;; the required list of keys is nil, or |
| 984 | (null require) |
| 985 | ;; every element of require is in n(ormalized) |
| 986 | (let ((n (nth 0 (auth-source-netrc-normalize |
| 987 | (list alist) file)))) |
| 988 | (loop for req in require |
| 989 | always (plist-get n req))))))) |
| 990 | result) |
| 991 | |
| 992 | (if (and (functionp cached-secrets) |
| 993 | (equal cached-mtime |
| 994 | (nth 5 (file-attributes file)))) |
| 995 | (progn |
| 996 | (auth-source-do-trivia |
| 997 | "auth-source-netrc-parse: using CACHED file data for %s" |
| 998 | file) |
| 999 | (insert (funcall cached-secrets))) |
| 1000 | (insert-file-contents file) |
| 1001 | ;; cache all netrc files (used to be just .gpg files) |
| 1002 | ;; Store the contents of the file heavily encrypted in memory. |
| 1003 | ;; (note for the irony-impaired: they are just obfuscated) |
| 1004 | (auth-source--aput |
| 1005 | auth-source-netrc-cache file |
| 1006 | (list :mtime (nth 5 (file-attributes file)) |
| 1007 | :secret (lexical-let ((v (mapcar '1+ (buffer-string)))) |
| 1008 | (lambda () (apply 'string (mapcar '1- v))))))) |
| 1009 | (goto-char (point-min)) |
| 1010 | (let ((entries (auth-source-netrc-parse-entries check max)) |
| 1011 | alist) |
| 1012 | (while (setq alist (pop entries)) |
| 1013 | (push (nreverse alist) result))) |
| 1014 | |
| 1015 | (when (< 0 modified) |
| 1016 | (when auth-source-gpg-encrypt-to |
| 1017 | ;; (see bug#7487) making `epa-file-encrypt-to' local to |
| 1018 | ;; this buffer lets epa-file skip the key selection query |
| 1019 | ;; (see the `local-variable-p' check in |
| 1020 | ;; `epa-file-write-region'). |
| 1021 | (unless (local-variable-p 'epa-file-encrypt-to (current-buffer)) |
| 1022 | (make-local-variable 'epa-file-encrypt-to)) |
| 1023 | (if (listp auth-source-gpg-encrypt-to) |
| 1024 | (setq epa-file-encrypt-to auth-source-gpg-encrypt-to))) |
| 1025 | |
| 1026 | ;; ask AFTER we've successfully opened the file |
| 1027 | (when (y-or-n-p (format "Save file %s? (%d deletions)" |
| 1028 | file modified)) |
| 1029 | (write-region (point-min) (point-max) file nil 'silent) |
| 1030 | (auth-source-do-debug |
| 1031 | "auth-source-netrc-parse: modified %d lines in %s" |
| 1032 | modified file))) |
| 1033 | |
| 1034 | (nreverse result)))))) |
| 1035 | |
| 1036 | (defun auth-source-netrc-parse-next-interesting () |
| 1037 | "Advance to the next interesting position in the current buffer." |
| 1038 | ;; If we're looking at a comment or are at the end of the line, move forward |
| 1039 | (while (or (looking-at "#") |
| 1040 | (and (eolp) |
| 1041 | (not (eobp)))) |
| 1042 | (forward-line 1)) |
| 1043 | (skip-chars-forward "\t ")) |
| 1044 | |
| 1045 | (defun auth-source-netrc-parse-one () |
| 1046 | "Read one thing from the current buffer." |
| 1047 | (auth-source-netrc-parse-next-interesting) |
| 1048 | |
| 1049 | (when (or (looking-at "'\\([^']*\\)'") |
| 1050 | (looking-at "\"\\([^\"]*\\)\"") |
| 1051 | (looking-at "\\([^ \t\n]+\\)")) |
| 1052 | (forward-char (length (match-string 0))) |
| 1053 | (auth-source-netrc-parse-next-interesting) |
| 1054 | (match-string-no-properties 1))) |
| 1055 | |
| 1056 | ;; with thanks to org-mode |
| 1057 | (defsubst auth-source-current-line (&optional pos) |
| 1058 | (save-excursion |
| 1059 | (and pos (goto-char pos)) |
| 1060 | ;; works also in narrowed buffer, because we start at 1, not point-min |
| 1061 | (+ (if (bolp) 1 0) (count-lines 1 (point))))) |
| 1062 | |
| 1063 | (defun auth-source-netrc-parse-entries(check max) |
| 1064 | "Parse up to MAX netrc entries, passed by CHECK, from the current buffer." |
| 1065 | (let ((adder (lambda(check alist all) |
| 1066 | (when (and |
| 1067 | alist |
| 1068 | (> max (length all)) |
| 1069 | (funcall check alist)) |
| 1070 | (push alist all)) |
| 1071 | all)) |
| 1072 | item item2 all alist default) |
| 1073 | (while (setq item (auth-source-netrc-parse-one)) |
| 1074 | (setq default (equal item "default")) |
| 1075 | ;; We're starting a new machine. Save the old one. |
| 1076 | (when (and alist |
| 1077 | (or default |
| 1078 | (equal item "machine"))) |
| 1079 | ;; (auth-source-do-trivia |
| 1080 | ;; "auth-source-netrc-parse-entries: got entry %S" alist) |
| 1081 | (setq all (funcall adder check alist all) |
| 1082 | alist nil)) |
| 1083 | ;; In default entries, we don't have a next token. |
| 1084 | ;; We store them as ("machine" . t) |
| 1085 | (if default |
| 1086 | (push (cons "machine" t) alist) |
| 1087 | ;; Not a default entry. Grab the next item. |
| 1088 | (when (setq item2 (auth-source-netrc-parse-one)) |
| 1089 | ;; Did we get a "machine" value? |
| 1090 | (if (equal item2 "machine") |
| 1091 | (progn |
| 1092 | (gnus-error 1 |
| 1093 | "%s: Unexpected 'machine' token at line %d" |
| 1094 | "auth-source-netrc-parse-entries" |
| 1095 | (auth-source-current-line)) |
| 1096 | (forward-line 1)) |
| 1097 | (push (cons item item2) alist))))) |
| 1098 | |
| 1099 | ;; Clean up: if there's an entry left over, use it. |
| 1100 | (when alist |
| 1101 | (setq all (funcall adder check alist all)) |
| 1102 | ;; (auth-source-do-trivia |
| 1103 | ;; "auth-source-netrc-parse-entries: got2 entry %S" alist) |
| 1104 | ) |
| 1105 | (nreverse all))) |
| 1106 | |
| 1107 | (defvar auth-source-passphrase-alist nil) |
| 1108 | |
| 1109 | (defun auth-source-token-passphrase-callback-function (context key-id file) |
| 1110 | (let* ((file (file-truename file)) |
| 1111 | (entry (assoc file auth-source-passphrase-alist)) |
| 1112 | passphrase) |
| 1113 | ;; return the saved passphrase, calling a function if needed |
| 1114 | (or (copy-sequence (if (functionp (cdr entry)) |
| 1115 | (funcall (cdr entry)) |
| 1116 | (cdr entry))) |
| 1117 | (progn |
| 1118 | (unless entry |
| 1119 | (setq entry (list file)) |
| 1120 | (push entry auth-source-passphrase-alist)) |
| 1121 | (setq passphrase |
| 1122 | (read-passwd |
| 1123 | (format "Passphrase for %s tokens: " file) |
| 1124 | t)) |
| 1125 | (setcdr entry (lexical-let ((p (copy-sequence passphrase))) |
| 1126 | (lambda () p))) |
| 1127 | passphrase)))) |
| 1128 | |
| 1129 | ;; (auth-source-epa-extract-gpg-token "gpg:LS0tLS1CRUdJTiBQR1AgTUVTU0FHRS0tLS0tClZlcnNpb246IEdudVBHIHYxLjQuMTEgKEdOVS9MaW51eCkKCmpBMEVBd01DT25qMjB1ak9rZnRneVI3K21iNm9aZWhuLzRad3cySkdlbnVaKzRpeEswWDY5di9icDI1U1dsQT0KPS9yc2wKLS0tLS1FTkQgUEdQIE1FU1NBR0UtLS0tLQo=" "~/.netrc") |
| 1130 | (defun auth-source-epa-extract-gpg-token (secret file) |
| 1131 | "Pass either the decoded SECRET or the gpg:BASE64DATA version. |
| 1132 | FILE is the file from which we obtained this token." |
| 1133 | (when (string-match "^gpg:\\(.+\\)" secret) |
| 1134 | (setq secret (base64-decode-string (match-string 1 secret)))) |
| 1135 | (let ((context (epg-make-context 'OpenPGP)) |
| 1136 | plain) |
| 1137 | (epg-context-set-passphrase-callback |
| 1138 | context |
| 1139 | (cons #'auth-source-token-passphrase-callback-function |
| 1140 | file)) |
| 1141 | (epg-decrypt-string context secret))) |
| 1142 | |
| 1143 | ;; (insert (auth-source-epa-make-gpg-token "mysecret" "~/.netrc")) |
| 1144 | (defun auth-source-epa-make-gpg-token (secret file) |
| 1145 | (let ((context (epg-make-context 'OpenPGP)) |
| 1146 | (pp-escape-newlines nil) |
| 1147 | cipher) |
| 1148 | (epg-context-set-armor context t) |
| 1149 | (epg-context-set-passphrase-callback |
| 1150 | context |
| 1151 | (cons #'auth-source-token-passphrase-callback-function |
| 1152 | file)) |
| 1153 | (setq cipher (epg-encrypt-string context secret nil)) |
| 1154 | (with-temp-buffer |
| 1155 | (insert cipher) |
| 1156 | (base64-encode-region (point-min) (point-max) t) |
| 1157 | (concat "gpg:" (buffer-substring-no-properties |
| 1158 | (point-min) |
| 1159 | (point-max)))))) |
| 1160 | |
| 1161 | (defun auth-source-netrc-normalize (alist filename) |
| 1162 | (mapcar (lambda (entry) |
| 1163 | (let (ret item) |
| 1164 | (while (setq item (pop entry)) |
| 1165 | (let ((k (car item)) |
| 1166 | (v (cdr item))) |
| 1167 | |
| 1168 | ;; apply key aliases |
| 1169 | (setq k (cond ((member k '("machine")) "host") |
| 1170 | ((member k '("login" "account")) "user") |
| 1171 | ((member k '("protocol")) "port") |
| 1172 | ((member k '("password")) "secret") |
| 1173 | (t k))) |
| 1174 | |
| 1175 | ;; send back the secret in a function (lexical binding) |
| 1176 | (when (equal k "secret") |
| 1177 | (setq v (lexical-let ((lexv v) |
| 1178 | (token-decoder nil)) |
| 1179 | (when (string-match "^gpg:" lexv) |
| 1180 | ;; it's a GPG token: create a token decoder |
| 1181 | ;; which unsets itself once |
| 1182 | (setq token-decoder |
| 1183 | (lambda (val) |
| 1184 | (prog1 |
| 1185 | (auth-source-epa-extract-gpg-token |
| 1186 | val |
| 1187 | filename) |
| 1188 | (setq token-decoder nil))))) |
| 1189 | (lambda () |
| 1190 | (when token-decoder |
| 1191 | (setq lexv (funcall token-decoder lexv))) |
| 1192 | lexv)))) |
| 1193 | (setq ret (plist-put ret |
| 1194 | (intern (concat ":" k)) |
| 1195 | v)))) |
| 1196 | ret)) |
| 1197 | alist)) |
| 1198 | |
| 1199 | ;; (setq secret (plist-get (nth 0 (auth-source-search :host t :type 'netrc :K 1 :max 1)) :secret)) |
| 1200 | ;; (funcall secret) |
| 1201 | |
| 1202 | (defun* auth-source-netrc-search (&rest |
| 1203 | spec |
| 1204 | &key backend require create delete |
| 1205 | type max host user port |
| 1206 | &allow-other-keys) |
| 1207 | "Given a property list SPEC, return search matches from the :backend. |
| 1208 | See `auth-source-search' for details on SPEC." |
| 1209 | ;; just in case, check that the type is correct (null or same as the backend) |
| 1210 | (assert (or (null type) (eq type (oref backend type))) |
| 1211 | t "Invalid netrc search: %s %s") |
| 1212 | |
| 1213 | (let ((results (auth-source-netrc-normalize |
| 1214 | (auth-source-netrc-parse |
| 1215 | :max max |
| 1216 | :require require |
| 1217 | :delete delete |
| 1218 | :file (oref backend source) |
| 1219 | :host (or host t) |
| 1220 | :user (or user t) |
| 1221 | :port (or port t)) |
| 1222 | (oref backend source)))) |
| 1223 | |
| 1224 | ;; if we need to create an entry AND none were found to match |
| 1225 | (when (and create |
| 1226 | (not results)) |
| 1227 | |
| 1228 | ;; create based on the spec and record the value |
| 1229 | (setq results (or |
| 1230 | ;; if the user did not want to create the entry |
| 1231 | ;; in the file, it will be returned |
| 1232 | (apply (slot-value backend 'create-function) spec) |
| 1233 | ;; if not, we do the search again without :create |
| 1234 | ;; to get the updated data. |
| 1235 | |
| 1236 | ;; the result will be returned, even if the search fails |
| 1237 | (apply 'auth-source-netrc-search |
| 1238 | (plist-put spec :create nil))))) |
| 1239 | results)) |
| 1240 | |
| 1241 | (defun auth-source-netrc-element-or-first (v) |
| 1242 | (if (listp v) |
| 1243 | (nth 0 v) |
| 1244 | v)) |
| 1245 | |
| 1246 | ;; (auth-source-search :host "nonesuch" :type 'netrc :max 1 :create t) |
| 1247 | ;; (auth-source-search :host "nonesuch" :type 'netrc :max 1 :create t :create-extra-keys '((A "default A") (B))) |
| 1248 | |
| 1249 | (defun* auth-source-netrc-create (&rest spec |
| 1250 | &key backend |
| 1251 | secret host user port create |
| 1252 | &allow-other-keys) |
| 1253 | (let* ((base-required '(host user port secret)) |
| 1254 | ;; we know (because of an assertion in auth-source-search) that the |
| 1255 | ;; :create parameter is either t or a list (which includes nil) |
| 1256 | (create-extra (if (eq t create) nil create)) |
| 1257 | (current-data (car (auth-source-search :max 1 |
| 1258 | :host host |
| 1259 | :port port))) |
| 1260 | (required (append base-required create-extra)) |
| 1261 | (file (oref backend source)) |
| 1262 | (add "") |
| 1263 | ;; `valist' is an alist |
| 1264 | valist |
| 1265 | ;; `artificial' will be returned if no creation is needed |
| 1266 | artificial) |
| 1267 | |
| 1268 | ;; only for base required elements (defined as function parameters): |
| 1269 | ;; fill in the valist with whatever data we may have from the search |
| 1270 | ;; we complete the first value if it's a list and use the value otherwise |
| 1271 | (dolist (br base-required) |
| 1272 | (when (symbol-value br) |
| 1273 | (let ((br-choice (cond |
| 1274 | ;; all-accepting choice (predicate is t) |
| 1275 | ((eq t (symbol-value br)) nil) |
| 1276 | ;; just the value otherwise |
| 1277 | (t (symbol-value br))))) |
| 1278 | (when br-choice |
| 1279 | (auth-source--aput valist br br-choice))))) |
| 1280 | |
| 1281 | ;; for extra required elements, see if the spec includes a value for them |
| 1282 | (dolist (er create-extra) |
| 1283 | (let ((name (concat ":" (symbol-name er))) |
| 1284 | (keys (loop for i below (length spec) by 2 |
| 1285 | collect (nth i spec)))) |
| 1286 | (dolist (k keys) |
| 1287 | (when (equal (symbol-name k) name) |
| 1288 | (auth-source--aput valist er (plist-get spec k)))))) |
| 1289 | |
| 1290 | ;; for each required element |
| 1291 | (dolist (r required) |
| 1292 | (let* ((data (auth-source--aget valist r)) |
| 1293 | ;; take the first element if the data is a list |
| 1294 | (data (or (auth-source-netrc-element-or-first data) |
| 1295 | (plist-get current-data |
| 1296 | (intern (format ":%s" r) obarray)))) |
| 1297 | ;; this is the default to be offered |
| 1298 | (given-default (auth-source--aget |
| 1299 | auth-source-creation-defaults r)) |
| 1300 | ;; the default supplementals are simple: |
| 1301 | ;; for the user, try `given-default' and then (user-login-name); |
| 1302 | ;; otherwise take `given-default' |
| 1303 | (default (cond |
| 1304 | ((and (not given-default) (eq r 'user)) |
| 1305 | (user-login-name)) |
| 1306 | (t given-default))) |
| 1307 | (printable-defaults (list |
| 1308 | (cons 'user |
| 1309 | (or |
| 1310 | (auth-source-netrc-element-or-first |
| 1311 | (auth-source--aget valist 'user)) |
| 1312 | (plist-get artificial :user) |
| 1313 | "[any user]")) |
| 1314 | (cons 'host |
| 1315 | (or |
| 1316 | (auth-source-netrc-element-or-first |
| 1317 | (auth-source--aget valist 'host)) |
| 1318 | (plist-get artificial :host) |
| 1319 | "[any host]")) |
| 1320 | (cons 'port |
| 1321 | (or |
| 1322 | (auth-source-netrc-element-or-first |
| 1323 | (auth-source--aget valist 'port)) |
| 1324 | (plist-get artificial :port) |
| 1325 | "[any port]")))) |
| 1326 | (prompt (or (auth-source--aget auth-source-creation-prompts r) |
| 1327 | (case r |
| 1328 | (secret "%p password for %u@%h: ") |
| 1329 | (user "%p user name for %h: ") |
| 1330 | (host "%p host name for user %u: ") |
| 1331 | (port "%p port for %u@%h: ")) |
| 1332 | (format "Enter %s (%%u@%%h:%%p): " r))) |
| 1333 | (prompt (auth-source-format-prompt |
| 1334 | prompt |
| 1335 | `((?u ,(auth-source--aget printable-defaults 'user)) |
| 1336 | (?h ,(auth-source--aget printable-defaults 'host)) |
| 1337 | (?p ,(auth-source--aget printable-defaults 'port)))))) |
| 1338 | |
| 1339 | ;; Store the data, prompting for the password if needed. |
| 1340 | (setq data (or data |
| 1341 | (if (eq r 'secret) |
| 1342 | ;; Special case prompt for passwords. |
| 1343 | ;; 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))) |
| 1344 | ;; TODO: or maybe leave as (setq auth-source-netrc-use-gpg-tokens 'never) |
| 1345 | (let* ((ep (format "Use GPG password tokens in %s?" file)) |
| 1346 | (gpg-encrypt |
| 1347 | (cond |
| 1348 | ((eq auth-source-netrc-use-gpg-tokens 'never) |
| 1349 | 'never) |
| 1350 | ((listp auth-source-netrc-use-gpg-tokens) |
| 1351 | (let ((check (copy-sequence |
| 1352 | auth-source-netrc-use-gpg-tokens)) |
| 1353 | item ret) |
| 1354 | (while check |
| 1355 | (setq item (pop check)) |
| 1356 | (when (or (eq (car item) t) |
| 1357 | (string-match (car item) file)) |
| 1358 | (setq ret (cdr item)) |
| 1359 | (setq check nil))))) |
| 1360 | (t 'never))) |
| 1361 | (plain (or (eval default) (read-passwd prompt)))) |
| 1362 | ;; ask if we don't know what to do (in which case |
| 1363 | ;; auth-source-netrc-use-gpg-tokens must be a list) |
| 1364 | (unless gpg-encrypt |
| 1365 | (setq gpg-encrypt (if (y-or-n-p ep) 'gpg 'never)) |
| 1366 | ;; TODO: save the defcustom now? or ask? |
| 1367 | (setq auth-source-netrc-use-gpg-tokens |
| 1368 | (cons `(,file ,gpg-encrypt) |
| 1369 | auth-source-netrc-use-gpg-tokens))) |
| 1370 | (if (eq gpg-encrypt 'gpg) |
| 1371 | (auth-source-epa-make-gpg-token plain file) |
| 1372 | plain)) |
| 1373 | (if (stringp default) |
| 1374 | (read-string (if (string-match ": *\\'" prompt) |
| 1375 | (concat (substring prompt 0 (match-beginning 0)) |
| 1376 | " (default " default "): ") |
| 1377 | (concat prompt "(default " default ") ")) |
| 1378 | nil nil default) |
| 1379 | (eval default))))) |
| 1380 | |
| 1381 | (when data |
| 1382 | (setq artificial (plist-put artificial |
| 1383 | (intern (concat ":" (symbol-name r))) |
| 1384 | (if (eq r 'secret) |
| 1385 | (lexical-let ((data data)) |
| 1386 | (lambda () data)) |
| 1387 | data)))) |
| 1388 | |
| 1389 | ;; When r is not an empty string... |
| 1390 | (when (and (stringp data) |
| 1391 | (< 0 (length data))) |
| 1392 | ;; this function is not strictly necessary but I think it |
| 1393 | ;; makes the code clearer -tzz |
| 1394 | (let ((printer (lambda () |
| 1395 | ;; append the key (the symbol name of r) |
| 1396 | ;; and the value in r |
| 1397 | (format "%s%s %s" |
| 1398 | ;; prepend a space |
| 1399 | (if (zerop (length add)) "" " ") |
| 1400 | ;; remap auth-source tokens to netrc |
| 1401 | (case r |
| 1402 | (user "login") |
| 1403 | (host "machine") |
| 1404 | (secret "password") |
| 1405 | (port "port") ; redundant but clearer |
| 1406 | (t (symbol-name r))) |
| 1407 | (if (string-match "[\"# ]" data) |
| 1408 | (format "%S" data) |
| 1409 | data))))) |
| 1410 | (setq add (concat add (funcall printer))))))) |
| 1411 | |
| 1412 | (plist-put |
| 1413 | artificial |
| 1414 | :save-function |
| 1415 | (lexical-let ((file file) |
| 1416 | (add add)) |
| 1417 | (lambda () (auth-source-netrc-saver file add)))) |
| 1418 | |
| 1419 | (list artificial))) |
| 1420 | |
| 1421 | ;;(funcall (plist-get (nth 0 (auth-source-search :host '("nonesuch2") :user "tzz" :port "imap" :create t :max 1)) :save-function)) |
| 1422 | (defun auth-source-netrc-saver (file add) |
| 1423 | "Save a line ADD in FILE, prompting along the way. |
| 1424 | Respects `auth-source-save-behavior'. Uses |
| 1425 | `auth-source-netrc-cache' to avoid prompting more than once." |
| 1426 | (let* ((key (format "%s %s" file (rfc2104-hash 'md5 64 16 file add))) |
| 1427 | (cached (assoc key auth-source-netrc-cache))) |
| 1428 | |
| 1429 | (if cached |
| 1430 | (auth-source-do-trivia |
| 1431 | "auth-source-netrc-saver: found previous run for key %s, returning" |
| 1432 | key) |
| 1433 | (with-temp-buffer |
| 1434 | (when (file-exists-p file) |
| 1435 | (insert-file-contents file)) |
| 1436 | (when auth-source-gpg-encrypt-to |
| 1437 | ;; (see bug#7487) making `epa-file-encrypt-to' local to |
| 1438 | ;; this buffer lets epa-file skip the key selection query |
| 1439 | ;; (see the `local-variable-p' check in |
| 1440 | ;; `epa-file-write-region'). |
| 1441 | (unless (local-variable-p 'epa-file-encrypt-to (current-buffer)) |
| 1442 | (make-local-variable 'epa-file-encrypt-to)) |
| 1443 | (if (listp auth-source-gpg-encrypt-to) |
| 1444 | (setq epa-file-encrypt-to auth-source-gpg-encrypt-to))) |
| 1445 | ;; we want the new data to be found first, so insert at beginning |
| 1446 | (goto-char (point-min)) |
| 1447 | |
| 1448 | ;; Ask AFTER we've successfully opened the file. |
| 1449 | (let ((prompt (format "Save auth info to file %s? " file)) |
| 1450 | (done (not (eq auth-source-save-behavior 'ask))) |
| 1451 | (bufname "*auth-source Help*") |
| 1452 | k) |
| 1453 | (while (not done) |
| 1454 | (setq k (auth-source-read-char-choice prompt '(?y ?n ?N ?e ??))) |
| 1455 | (case k |
| 1456 | (?y (setq done t)) |
| 1457 | (?? (save-excursion |
| 1458 | (with-output-to-temp-buffer bufname |
| 1459 | (princ |
| 1460 | (concat "(y)es, save\n" |
| 1461 | "(n)o but use the info\n" |
| 1462 | "(N)o and don't ask to save again\n" |
| 1463 | "(e)dit the line\n" |
| 1464 | "(?) for help as you can see.\n")) |
| 1465 | ;; Why? Doesn't with-output-to-temp-buffer already do |
| 1466 | ;; the exact same thing anyway? --Stef |
| 1467 | (set-buffer standard-output) |
| 1468 | (help-mode)))) |
| 1469 | (?n (setq add "" |
| 1470 | done t)) |
| 1471 | (?N |
| 1472 | (setq add "" |
| 1473 | done t) |
| 1474 | (customize-save-variable 'auth-source-save-behavior nil)) |
| 1475 | (?e (setq add (read-string "Line to add: " add))) |
| 1476 | (t nil))) |
| 1477 | |
| 1478 | (when (get-buffer-window bufname) |
| 1479 | (delete-window (get-buffer-window bufname))) |
| 1480 | |
| 1481 | ;; Make sure the info is not saved. |
| 1482 | (when (null auth-source-save-behavior) |
| 1483 | (setq add "")) |
| 1484 | |
| 1485 | (when (< 0 (length add)) |
| 1486 | (progn |
| 1487 | (unless (bolp) |
| 1488 | (insert "\n")) |
| 1489 | (insert add "\n") |
| 1490 | (write-region (point-min) (point-max) file nil 'silent) |
| 1491 | ;; Make the .authinfo file non-world-readable. |
| 1492 | (set-file-modes file #o600) |
| 1493 | (auth-source-do-debug |
| 1494 | "auth-source-netrc-create: wrote 1 new line to %s" |
| 1495 | file) |
| 1496 | (message "Saved new authentication information to %s" file) |
| 1497 | nil)))) |
| 1498 | (auth-source--aput auth-source-netrc-cache key "ran")))) |
| 1499 | |
| 1500 | ;;; Backend specific parsing: Secrets API backend |
| 1501 | |
| 1502 | ;; (let ((auth-sources '(default))) (auth-source-search :max 1 :create t)) |
| 1503 | ;; (let ((auth-sources '(default))) (auth-source-search :max 1 :delete t)) |
| 1504 | ;; (let ((auth-sources '(default))) (auth-source-search :max 1)) |
| 1505 | ;; (let ((auth-sources '(default))) (auth-source-search)) |
| 1506 | ;; (let ((auth-sources '("secrets:Login"))) (auth-source-search :max 1)) |
| 1507 | ;; (let ((auth-sources '("secrets:Login"))) (auth-source-search :max 1 :signon_realm "https://git.gnus.org/Git")) |
| 1508 | |
| 1509 | (defun auth-source-secrets-listify-pattern (pattern) |
| 1510 | "Convert a pattern with lists to a list of string patterns. |
| 1511 | |
| 1512 | auth-source patterns can have values of the form :foo (\"bar\" |
| 1513 | \"qux\"), which means to match any secret with :foo equal to |
| 1514 | \"bar\" or :foo equal to \"qux\". The secrets backend supports |
| 1515 | only string values for patterns, so this routine returns a list |
| 1516 | of patterns that is equivalent to the single original pattern |
| 1517 | when interpreted such that if a secret matches any pattern in the |
| 1518 | list, it matches the original pattern." |
| 1519 | (if (null pattern) |
| 1520 | '(nil) |
| 1521 | (let* ((key (pop pattern)) |
| 1522 | (value (pop pattern)) |
| 1523 | (tails (auth-source-secrets-listify-pattern pattern)) |
| 1524 | (heads (if (stringp value) |
| 1525 | (list (list key value)) |
| 1526 | (mapcar (lambda (v) (list key v)) value)))) |
| 1527 | (cl-loop |
| 1528 | for h in heads |
| 1529 | nconc |
| 1530 | (cl-loop |
| 1531 | for tl in tails |
| 1532 | collect (append h tl)))))) |
| 1533 | |
| 1534 | (defun* auth-source-secrets-search (&rest |
| 1535 | spec |
| 1536 | &key backend create delete label |
| 1537 | type max host user port |
| 1538 | &allow-other-keys) |
| 1539 | "Search the Secrets API; spec is like `auth-source'. |
| 1540 | |
| 1541 | The :label key specifies the item's label. It is the only key |
| 1542 | that can specify a substring. Any :label value besides a string |
| 1543 | will allow any label. |
| 1544 | |
| 1545 | All other search keys must match exactly. If you need substring |
| 1546 | matching, do a wider search and narrow it down yourself. |
| 1547 | |
| 1548 | You'll get back all the properties of the token as a plist. |
| 1549 | |
| 1550 | Here's an example that looks for the first item in the 'Login' |
| 1551 | Secrets collection: |
| 1552 | |
| 1553 | \(let ((auth-sources '(\"secrets:Login\"))) |
| 1554 | (auth-source-search :max 1) |
| 1555 | |
| 1556 | Here's another that looks for the first item in the 'Login' |
| 1557 | Secrets collection whose label contains 'gnus': |
| 1558 | |
| 1559 | \(let ((auth-sources '(\"secrets:Login\"))) |
| 1560 | (auth-source-search :max 1 :label \"gnus\") |
| 1561 | |
| 1562 | And this one looks for the first item in the 'Login' Secrets |
| 1563 | collection that's a Google Chrome entry for the git.gnus.org site |
| 1564 | authentication tokens: |
| 1565 | |
| 1566 | \(let ((auth-sources '(\"secrets:Login\"))) |
| 1567 | (auth-source-search :max 1 :signon_realm \"https://git.gnus.org/Git\")) |
| 1568 | " |
| 1569 | |
| 1570 | ;; TODO |
| 1571 | (assert (not create) nil |
| 1572 | "The Secrets API auth-source backend doesn't support creation yet") |
| 1573 | ;; TODO |
| 1574 | ;; (secrets-delete-item coll elt) |
| 1575 | (assert (not delete) nil |
| 1576 | "The Secrets API auth-source backend doesn't support deletion yet") |
| 1577 | |
| 1578 | (let* ((coll (oref backend source)) |
| 1579 | (max (or max 5000)) ; sanity check: default to stop at 5K |
| 1580 | (ignored-keys '(:create :delete :max :backend :label :require :type)) |
| 1581 | (search-keys (loop for i below (length spec) by 2 |
| 1582 | unless (memq (nth i spec) ignored-keys) |
| 1583 | collect (nth i spec))) |
| 1584 | ;; build a search spec without the ignored keys |
| 1585 | ;; if a search key is nil or t (match anything), we skip it |
| 1586 | (search-specs (auth-source-secrets-listify-pattern |
| 1587 | (apply 'append (mapcar |
| 1588 | (lambda (k) |
| 1589 | (if (or (null (plist-get spec k)) |
| 1590 | (eq t (plist-get spec k))) |
| 1591 | nil |
| 1592 | (list k (plist-get spec k)))) |
| 1593 | search-keys)))) |
| 1594 | ;; needed keys (always including host, login, port, and secret) |
| 1595 | (returned-keys (mm-delete-duplicates (append |
| 1596 | '(:host :login :port :secret) |
| 1597 | search-keys))) |
| 1598 | (items |
| 1599 | (loop for search-spec in search-specs |
| 1600 | nconc |
| 1601 | (loop for item in (apply 'secrets-search-items coll search-spec) |
| 1602 | unless (and (stringp label) |
| 1603 | (not (string-match label item))) |
| 1604 | collect item))) |
| 1605 | ;; TODO: respect max in `secrets-search-items', not after the fact |
| 1606 | (items (butlast items (- (length items) max))) |
| 1607 | ;; convert the item name to a full plist |
| 1608 | (items (mapcar (lambda (item) |
| 1609 | (append |
| 1610 | ;; make an entry for the secret (password) element |
| 1611 | (list |
| 1612 | :secret |
| 1613 | (lexical-let ((v (secrets-get-secret coll item))) |
| 1614 | (lambda () v))) |
| 1615 | ;; rewrite the entry from ((k1 v1) (k2 v2)) to plist |
| 1616 | (apply 'append |
| 1617 | (mapcar (lambda (entry) |
| 1618 | (list (car entry) (cdr entry))) |
| 1619 | (secrets-get-attributes coll item))))) |
| 1620 | items)) |
| 1621 | ;; ensure each item has each key in `returned-keys' |
| 1622 | (items (mapcar (lambda (plist) |
| 1623 | (append |
| 1624 | (apply 'append |
| 1625 | (mapcar (lambda (req) |
| 1626 | (if (plist-get plist req) |
| 1627 | nil |
| 1628 | (list req nil))) |
| 1629 | returned-keys)) |
| 1630 | plist)) |
| 1631 | items))) |
| 1632 | items)) |
| 1633 | |
| 1634 | (defun* auth-source-secrets-create (&rest |
| 1635 | spec |
| 1636 | &key backend type max host user port |
| 1637 | &allow-other-keys) |
| 1638 | ;; TODO |
| 1639 | ;; (apply 'secrets-create-item (auth-get-source entry) name passwd spec) |
| 1640 | (debug spec)) |
| 1641 | |
| 1642 | ;;; Backend specific parsing: Mac OS Keychain (using /usr/bin/security) backend |
| 1643 | |
| 1644 | ;; (let ((auth-sources '(macos-keychain-internet))) (auth-source-search :max 1 :create t)) |
| 1645 | ;; (let ((auth-sources '(macos-keychain-internet))) (auth-source-search :max 1 :delete t)) |
| 1646 | ;; (let ((auth-sources '(macos-keychain-internet))) (auth-source-search :max 1)) |
| 1647 | ;; (let ((auth-sources '(macos-keychain-internet))) (auth-source-search)) |
| 1648 | |
| 1649 | ;; (let ((auth-sources '(macos-keychain-generic))) (auth-source-search :max 1 :create t)) |
| 1650 | ;; (let ((auth-sources '(macos-keychain-generic))) (auth-source-search :max 1 :delete t)) |
| 1651 | ;; (let ((auth-sources '(macos-keychain-generic))) (auth-source-search :max 1)) |
| 1652 | ;; (let ((auth-sources '(macos-keychain-generic))) (auth-source-search)) |
| 1653 | |
| 1654 | ;; (let ((auth-sources '("macos-keychain-internet:/Users/tzz/Library/Keychains/login.keychain"))) (auth-source-search :max 1)) |
| 1655 | ;; (let ((auth-sources '("macos-keychain-generic:Login"))) (auth-source-search :max 1 :host "git.gnus.org")) |
| 1656 | |
| 1657 | (defun* auth-source-macos-keychain-search (&rest |
| 1658 | spec |
| 1659 | &key backend create delete label |
| 1660 | type max host user port |
| 1661 | &allow-other-keys) |
| 1662 | "Search the MacOS Keychain; spec is like `auth-source'. |
| 1663 | |
| 1664 | All search keys must match exactly. If you need substring |
| 1665 | matching, do a wider search and narrow it down yourself. |
| 1666 | |
| 1667 | You'll get back all the properties of the token as a plist. |
| 1668 | |
| 1669 | The :type key is either 'macos-keychain-internet or |
| 1670 | 'macos-keychain-generic. |
| 1671 | |
| 1672 | For the internet keychain type, the :label key searches the |
| 1673 | item's labels (\"-l LABEL\" passed to \"/usr/bin/security\"). |
| 1674 | Similarly, :host maps to \"-s HOST\", :user maps to \"-a USER\", |
| 1675 | and :port maps to \"-P PORT\" or \"-r PROT\" |
| 1676 | (note PROT has to be a 4-character string). |
| 1677 | |
| 1678 | For the generic keychain type, the :label key searches the item's |
| 1679 | labels (\"-l LABEL\" passed to \"/usr/bin/security\"). |
| 1680 | Similarly, :host maps to \"-c HOST\" (the \"creator\" keychain |
| 1681 | field), :user maps to \"-a USER\", and :port maps to \"-s PORT\". |
| 1682 | |
| 1683 | Here's an example that looks for the first item in the default |
| 1684 | generic MacOS Keychain: |
| 1685 | |
| 1686 | \(let ((auth-sources '(macos-keychain-generic))) |
| 1687 | (auth-source-search :max 1) |
| 1688 | |
| 1689 | Here's another that looks for the first item in the internet |
| 1690 | MacOS Keychain collection whose label is 'gnus': |
| 1691 | |
| 1692 | \(let ((auth-sources '(macos-keychain-internet))) |
| 1693 | (auth-source-search :max 1 :label \"gnus\") |
| 1694 | |
| 1695 | And this one looks for the first item in the internet keychain |
| 1696 | entries for git.gnus.org: |
| 1697 | |
| 1698 | \(let ((auth-sources '(macos-keychain-internet\"))) |
| 1699 | (auth-source-search :max 1 :host \"git.gnus.org\")) |
| 1700 | " |
| 1701 | ;; TODO |
| 1702 | (assert (not create) nil |
| 1703 | "The MacOS Keychain auth-source backend doesn't support creation yet") |
| 1704 | ;; TODO |
| 1705 | ;; (macos-keychain-delete-item coll elt) |
| 1706 | (assert (not delete) nil |
| 1707 | "The MacOS Keychain auth-source backend doesn't support deletion yet") |
| 1708 | |
| 1709 | (let* ((coll (oref backend source)) |
| 1710 | (max (or max 5000)) ; sanity check: default to stop at 5K |
| 1711 | (ignored-keys '(:create :delete :max :backend :label)) |
| 1712 | (search-keys (loop for i below (length spec) by 2 |
| 1713 | unless (memq (nth i spec) ignored-keys) |
| 1714 | collect (nth i spec))) |
| 1715 | ;; build a search spec without the ignored keys |
| 1716 | ;; if a search key is nil or t (match anything), we skip it |
| 1717 | (search-spec (apply 'append (mapcar |
| 1718 | (lambda (k) |
| 1719 | (if (or (null (plist-get spec k)) |
| 1720 | (eq t (plist-get spec k))) |
| 1721 | nil |
| 1722 | (list k (plist-get spec k)))) |
| 1723 | search-keys))) |
| 1724 | ;; needed keys (always including host, login, port, and secret) |
| 1725 | (returned-keys (mm-delete-duplicates (append |
| 1726 | '(:host :login :port :secret) |
| 1727 | search-keys))) |
| 1728 | (items (apply 'auth-source-macos-keychain-search-items |
| 1729 | coll |
| 1730 | type |
| 1731 | max |
| 1732 | search-spec)) |
| 1733 | |
| 1734 | ;; ensure each item has each key in `returned-keys' |
| 1735 | (items (mapcar (lambda (plist) |
| 1736 | (append |
| 1737 | (apply 'append |
| 1738 | (mapcar (lambda (req) |
| 1739 | (if (plist-get plist req) |
| 1740 | nil |
| 1741 | (list req nil))) |
| 1742 | returned-keys)) |
| 1743 | plist)) |
| 1744 | items))) |
| 1745 | items)) |
| 1746 | |
| 1747 | (defun* auth-source-macos-keychain-search-items (coll type max |
| 1748 | &rest spec |
| 1749 | &key label type |
| 1750 | host user port |
| 1751 | &allow-other-keys) |
| 1752 | |
| 1753 | (let* ((keychain-generic (eq type 'macos-keychain-generic)) |
| 1754 | (args `(,(if keychain-generic |
| 1755 | "find-generic-password" |
| 1756 | "find-internet-password") |
| 1757 | "-g")) |
| 1758 | (ret (list :type type))) |
| 1759 | (when label |
| 1760 | (setq args (append args (list "-l" label)))) |
| 1761 | (when host |
| 1762 | (setq args (append args (list (if keychain-generic "-c" "-s") host)))) |
| 1763 | (when user |
| 1764 | (setq args (append args (list "-a" user)))) |
| 1765 | |
| 1766 | (when port |
| 1767 | (if keychain-generic |
| 1768 | (setq args (append args (list "-s" port))) |
| 1769 | (setq args (append args (list |
| 1770 | (if (string-match "[0-9]+" port) "-P" "-r") |
| 1771 | port))))) |
| 1772 | |
| 1773 | (unless (equal coll "default") |
| 1774 | (setq args (append args (list coll)))) |
| 1775 | |
| 1776 | (with-temp-buffer |
| 1777 | (apply 'call-process "/usr/bin/security" nil t nil args) |
| 1778 | (goto-char (point-min)) |
| 1779 | (while (not (eobp)) |
| 1780 | (cond |
| 1781 | ((looking-at "^password: \"\\(.+\\)\"$") |
| 1782 | (auth-source-macos-keychain-result-append |
| 1783 | ret |
| 1784 | keychain-generic |
| 1785 | "secret" |
| 1786 | (lexical-let ((v (match-string 1))) |
| 1787 | (lambda () v)))) |
| 1788 | ;; TODO: check if this is really the label |
| 1789 | ;; match 0x00000007 <blob>="AppleID" |
| 1790 | ((looking-at "^[ ]+0x00000007 <blob>=\"\\(.+\\)\"") |
| 1791 | (auth-source-macos-keychain-result-append |
| 1792 | ret |
| 1793 | keychain-generic |
| 1794 | "label" |
| 1795 | (match-string 1))) |
| 1796 | ;; match "crtr"<uint32>="aapl" |
| 1797 | ;; match "svce"<blob>="AppleID" |
| 1798 | ((looking-at "^[ ]+\"\\([a-z]+\\)\"[^=]+=\"\\(.+\\)\"") |
| 1799 | (auth-source-macos-keychain-result-append |
| 1800 | ret |
| 1801 | keychain-generic |
| 1802 | (match-string 1) |
| 1803 | (match-string 2)))) |
| 1804 | (forward-line))) |
| 1805 | ;; return `ret' iff it has the :secret key |
| 1806 | (and (plist-get ret :secret) (list ret)))) |
| 1807 | |
| 1808 | (defun auth-source-macos-keychain-result-append (result generic k v) |
| 1809 | (push v result) |
| 1810 | (setq k (cond |
| 1811 | ((equal k "acct") "user") |
| 1812 | ;; for generic keychains, creator is host, service is port |
| 1813 | ((and generic (equal k "crtr")) "host") |
| 1814 | ((and generic (equal k "svce")) "port") |
| 1815 | ;; for internet keychains, protocol is port, server is host |
| 1816 | ((and (not generic) (equal k "ptcl")) "port") |
| 1817 | ((and (not generic) (equal k "srvr")) "host") |
| 1818 | (t k))) |
| 1819 | |
| 1820 | (push (intern (format ":%s" k)) result)) |
| 1821 | |
| 1822 | (defun* auth-source-macos-keychain-create (&rest |
| 1823 | spec |
| 1824 | &key backend type max host user port |
| 1825 | &allow-other-keys) |
| 1826 | ;; TODO |
| 1827 | (debug spec)) |
| 1828 | |
| 1829 | ;;; Backend specific parsing: PLSTORE backend |
| 1830 | |
| 1831 | (defun* auth-source-plstore-search (&rest |
| 1832 | spec |
| 1833 | &key backend create delete label |
| 1834 | type max host user port |
| 1835 | &allow-other-keys) |
| 1836 | "Search the PLSTORE; spec is like `auth-source'." |
| 1837 | (let* ((store (oref backend data)) |
| 1838 | (max (or max 5000)) ; sanity check: default to stop at 5K |
| 1839 | (ignored-keys '(:create :delete :max :backend :label :require :type)) |
| 1840 | (search-keys (loop for i below (length spec) by 2 |
| 1841 | unless (memq (nth i spec) ignored-keys) |
| 1842 | collect (nth i spec))) |
| 1843 | ;; build a search spec without the ignored keys |
| 1844 | ;; if a search key is nil or t (match anything), we skip it |
| 1845 | (search-spec (apply 'append (mapcar |
| 1846 | (lambda (k) |
| 1847 | (let ((v (plist-get spec k))) |
| 1848 | (if (or (null v) |
| 1849 | (eq t v)) |
| 1850 | nil |
| 1851 | (if (stringp v) |
| 1852 | (setq v (list v))) |
| 1853 | (list k v)))) |
| 1854 | search-keys))) |
| 1855 | ;; needed keys (always including host, login, port, and secret) |
| 1856 | (returned-keys (mm-delete-duplicates (append |
| 1857 | '(:host :login :port :secret) |
| 1858 | search-keys))) |
| 1859 | (items (plstore-find store search-spec)) |
| 1860 | (item-names (mapcar #'car items)) |
| 1861 | (items (butlast items (- (length items) max))) |
| 1862 | ;; convert the item to a full plist |
| 1863 | (items (mapcar (lambda (item) |
| 1864 | (let* ((plist (copy-tree (cdr item))) |
| 1865 | (secret (plist-member plist :secret))) |
| 1866 | (if secret |
| 1867 | (setcar |
| 1868 | (cdr secret) |
| 1869 | (lexical-let ((v (car (cdr secret)))) |
| 1870 | (lambda () v)))) |
| 1871 | plist)) |
| 1872 | items)) |
| 1873 | ;; ensure each item has each key in `returned-keys' |
| 1874 | (items (mapcar (lambda (plist) |
| 1875 | (append |
| 1876 | (apply 'append |
| 1877 | (mapcar (lambda (req) |
| 1878 | (if (plist-get plist req) |
| 1879 | nil |
| 1880 | (list req nil))) |
| 1881 | returned-keys)) |
| 1882 | plist)) |
| 1883 | items))) |
| 1884 | (cond |
| 1885 | ;; if we need to create an entry AND none were found to match |
| 1886 | ((and create |
| 1887 | (not items)) |
| 1888 | |
| 1889 | ;; create based on the spec and record the value |
| 1890 | (setq items (or |
| 1891 | ;; if the user did not want to create the entry |
| 1892 | ;; in the file, it will be returned |
| 1893 | (apply (slot-value backend 'create-function) spec) |
| 1894 | ;; if not, we do the search again without :create |
| 1895 | ;; to get the updated data. |
| 1896 | |
| 1897 | ;; the result will be returned, even if the search fails |
| 1898 | (apply 'auth-source-plstore-search |
| 1899 | (plist-put spec :create nil))))) |
| 1900 | ((and delete |
| 1901 | item-names) |
| 1902 | (dolist (item-name item-names) |
| 1903 | (plstore-delete store item-name)) |
| 1904 | (plstore-save store))) |
| 1905 | items)) |
| 1906 | |
| 1907 | (defun* auth-source-plstore-create (&rest spec |
| 1908 | &key backend |
| 1909 | secret host user port create |
| 1910 | &allow-other-keys) |
| 1911 | (let* ((base-required '(host user port secret)) |
| 1912 | (base-secret '(secret)) |
| 1913 | ;; we know (because of an assertion in auth-source-search) that the |
| 1914 | ;; :create parameter is either t or a list (which includes nil) |
| 1915 | (create-extra (if (eq t create) nil create)) |
| 1916 | (current-data (car (auth-source-search :max 1 |
| 1917 | :host host |
| 1918 | :port port))) |
| 1919 | (required (append base-required create-extra)) |
| 1920 | (file (oref backend source)) |
| 1921 | (add "") |
| 1922 | ;; `valist' is an alist |
| 1923 | valist |
| 1924 | ;; `artificial' will be returned if no creation is needed |
| 1925 | artificial |
| 1926 | secret-artificial) |
| 1927 | |
| 1928 | ;; only for base required elements (defined as function parameters): |
| 1929 | ;; fill in the valist with whatever data we may have from the search |
| 1930 | ;; we complete the first value if it's a list and use the value otherwise |
| 1931 | (dolist (br base-required) |
| 1932 | (when (symbol-value br) |
| 1933 | (let ((br-choice (cond |
| 1934 | ;; all-accepting choice (predicate is t) |
| 1935 | ((eq t (symbol-value br)) nil) |
| 1936 | ;; just the value otherwise |
| 1937 | (t (symbol-value br))))) |
| 1938 | (when br-choice |
| 1939 | (auth-source--aput valist br br-choice))))) |
| 1940 | |
| 1941 | ;; for extra required elements, see if the spec includes a value for them |
| 1942 | (dolist (er create-extra) |
| 1943 | (let ((name (concat ":" (symbol-name er))) |
| 1944 | (keys (loop for i below (length spec) by 2 |
| 1945 | collect (nth i spec)))) |
| 1946 | (dolist (k keys) |
| 1947 | (when (equal (symbol-name k) name) |
| 1948 | (auth-source--aput valist er (plist-get spec k)))))) |
| 1949 | |
| 1950 | ;; for each required element |
| 1951 | (dolist (r required) |
| 1952 | (let* ((data (auth-source--aget valist r)) |
| 1953 | ;; take the first element if the data is a list |
| 1954 | (data (or (auth-source-netrc-element-or-first data) |
| 1955 | (plist-get current-data |
| 1956 | (intern (format ":%s" r) obarray)))) |
| 1957 | ;; this is the default to be offered |
| 1958 | (given-default (auth-source--aget |
| 1959 | auth-source-creation-defaults r)) |
| 1960 | ;; the default supplementals are simple: |
| 1961 | ;; for the user, try `given-default' and then (user-login-name); |
| 1962 | ;; otherwise take `given-default' |
| 1963 | (default (cond |
| 1964 | ((and (not given-default) (eq r 'user)) |
| 1965 | (user-login-name)) |
| 1966 | (t given-default))) |
| 1967 | (printable-defaults (list |
| 1968 | (cons 'user |
| 1969 | (or |
| 1970 | (auth-source-netrc-element-or-first |
| 1971 | (auth-source--aget valist 'user)) |
| 1972 | (plist-get artificial :user) |
| 1973 | "[any user]")) |
| 1974 | (cons 'host |
| 1975 | (or |
| 1976 | (auth-source-netrc-element-or-first |
| 1977 | (auth-source--aget valist 'host)) |
| 1978 | (plist-get artificial :host) |
| 1979 | "[any host]")) |
| 1980 | (cons 'port |
| 1981 | (or |
| 1982 | (auth-source-netrc-element-or-first |
| 1983 | (auth-source--aget valist 'port)) |
| 1984 | (plist-get artificial :port) |
| 1985 | "[any port]")))) |
| 1986 | (prompt (or (auth-source--aget auth-source-creation-prompts r) |
| 1987 | (case r |
| 1988 | (secret "%p password for %u@%h: ") |
| 1989 | (user "%p user name for %h: ") |
| 1990 | (host "%p host name for user %u: ") |
| 1991 | (port "%p port for %u@%h: ")) |
| 1992 | (format "Enter %s (%%u@%%h:%%p): " r))) |
| 1993 | (prompt (auth-source-format-prompt |
| 1994 | prompt |
| 1995 | `((?u ,(auth-source--aget printable-defaults 'user)) |
| 1996 | (?h ,(auth-source--aget printable-defaults 'host)) |
| 1997 | (?p ,(auth-source--aget printable-defaults 'port)))))) |
| 1998 | |
| 1999 | ;; Store the data, prompting for the password if needed. |
| 2000 | (setq data (or data |
| 2001 | (if (eq r 'secret) |
| 2002 | (or (eval default) (read-passwd prompt)) |
| 2003 | (if (stringp default) |
| 2004 | (read-string |
| 2005 | (if (string-match ": *\\'" prompt) |
| 2006 | (concat (substring prompt 0 (match-beginning 0)) |
| 2007 | " (default " default "): ") |
| 2008 | (concat prompt "(default " default ") ")) |
| 2009 | nil nil default) |
| 2010 | (eval default))))) |
| 2011 | |
| 2012 | (when data |
| 2013 | (if (member r base-secret) |
| 2014 | (setq secret-artificial |
| 2015 | (plist-put secret-artificial |
| 2016 | (intern (concat ":" (symbol-name r))) |
| 2017 | data)) |
| 2018 | (setq artificial (plist-put artificial |
| 2019 | (intern (concat ":" (symbol-name r))) |
| 2020 | data)))))) |
| 2021 | (plstore-put (oref backend data) |
| 2022 | (sha1 (format "%s@%s:%s" |
| 2023 | (plist-get artificial :user) |
| 2024 | (plist-get artificial :host) |
| 2025 | (plist-get artificial :port))) |
| 2026 | artificial secret-artificial) |
| 2027 | (if (y-or-n-p (format "Save auth info to file %s? " |
| 2028 | (plstore-get-file (oref backend data)))) |
| 2029 | (plstore-save (oref backend data))))) |
| 2030 | |
| 2031 | ;;; older API |
| 2032 | |
| 2033 | ;; (auth-source-user-or-password '("login" "password") "imap.myhost.com" t "tzz") |
| 2034 | |
| 2035 | ;; deprecate the old interface |
| 2036 | (make-obsolete 'auth-source-user-or-password |
| 2037 | 'auth-source-search "Emacs 24.1") |
| 2038 | (make-obsolete 'auth-source-forget-user-or-password |
| 2039 | 'auth-source-forget "Emacs 24.1") |
| 2040 | |
| 2041 | (defun auth-source-user-or-password |
| 2042 | (mode host port &optional username create-missing delete-existing) |
| 2043 | "Find MODE (string or list of strings) matching HOST and PORT. |
| 2044 | |
| 2045 | DEPRECATED in favor of `auth-source-search'! |
| 2046 | |
| 2047 | USERNAME is optional and will be used as \"login\" in a search |
| 2048 | across the Secret Service API (see secrets.el) if the resulting |
| 2049 | items don't have a username. This means that if you search for |
| 2050 | username \"joe\" and it matches an item but the item doesn't have |
| 2051 | a :user attribute, the username \"joe\" will be returned. |
| 2052 | |
| 2053 | A non nil DELETE-EXISTING means deleting any matching password |
| 2054 | entry in the respective sources. This is useful only when |
| 2055 | CREATE-MISSING is non nil as well; the intended use case is to |
| 2056 | remove wrong password entries. |
| 2057 | |
| 2058 | If no matching entry is found, and CREATE-MISSING is non nil, |
| 2059 | the password will be retrieved interactively, and it will be |
| 2060 | stored in the password database which matches best (see |
| 2061 | `auth-sources'). |
| 2062 | |
| 2063 | MODE can be \"login\" or \"password\"." |
| 2064 | (auth-source-do-debug |
| 2065 | "auth-source-user-or-password: DEPRECATED get %s for %s (%s) + user=%s" |
| 2066 | mode host port username) |
| 2067 | |
| 2068 | (let* ((listy (listp mode)) |
| 2069 | (mode (if listy mode (list mode))) |
| 2070 | (cname (if username |
| 2071 | (format "%s %s:%s %s" mode host port username) |
| 2072 | (format "%s %s:%s" mode host port))) |
| 2073 | (search (list :host host :port port)) |
| 2074 | (search (if username (append search (list :user username)) search)) |
| 2075 | (search (if create-missing |
| 2076 | (append search (list :create t)) |
| 2077 | search)) |
| 2078 | (search (if delete-existing |
| 2079 | (append search (list :delete t)) |
| 2080 | search)) |
| 2081 | ;; (found (if (not delete-existing) |
| 2082 | ;; (gethash cname auth-source-cache) |
| 2083 | ;; (remhash cname auth-source-cache) |
| 2084 | ;; nil))) |
| 2085 | (found nil)) |
| 2086 | (if found |
| 2087 | (progn |
| 2088 | (auth-source-do-debug |
| 2089 | "auth-source-user-or-password: DEPRECATED cached %s=%s for %s (%s) + %s" |
| 2090 | mode |
| 2091 | ;; don't show the password |
| 2092 | (if (and (member "password" mode) t) |
| 2093 | "SECRET" |
| 2094 | found) |
| 2095 | host port username) |
| 2096 | found) ; return the found data |
| 2097 | ;; else, if not found, search with a max of 1 |
| 2098 | (let ((choice (nth 0 (apply 'auth-source-search |
| 2099 | (append '(:max 1) search))))) |
| 2100 | (when choice |
| 2101 | (dolist (m mode) |
| 2102 | (cond |
| 2103 | ((equal "password" m) |
| 2104 | (push (if (plist-get choice :secret) |
| 2105 | (funcall (plist-get choice :secret)) |
| 2106 | nil) found)) |
| 2107 | ((equal "login" m) |
| 2108 | (push (plist-get choice :user) found))))) |
| 2109 | (setq found (nreverse found)) |
| 2110 | (setq found (if listy found (car-safe found))))) |
| 2111 | |
| 2112 | found)) |
| 2113 | |
| 2114 | (defun auth-source-user-and-password (host &optional user) |
| 2115 | (let* ((auth-info (car |
| 2116 | (if user |
| 2117 | (auth-source-search |
| 2118 | :host host |
| 2119 | :user "yourusername" |
| 2120 | :max 1 |
| 2121 | :require '(:user :secret) |
| 2122 | :create nil) |
| 2123 | (auth-source-search |
| 2124 | :host host |
| 2125 | :max 1 |
| 2126 | :require '(:user :secret) |
| 2127 | :create nil)))) |
| 2128 | (user (plist-get auth-info :user)) |
| 2129 | (password (plist-get auth-info :secret))) |
| 2130 | (when (functionp password) |
| 2131 | (setq password (funcall password))) |
| 2132 | (list user password auth-info))) |
| 2133 | |
| 2134 | (provide 'auth-source) |
| 2135 | |
| 2136 | ;;; auth-source.el ends here |