1
0
mirror of https://git.savannah.gnu.org/git/emacs.git synced 2024-11-21 06:55:39 +00:00
emacs/lisp/auth-source-pass.el

329 lines
13 KiB
EmacsLisp
Raw Normal View History

;;; auth-source-pass.el --- Integrate auth-source with password-store -*- lexical-binding: t -*-
;; Copyright (C) 2015, 2017-2019 Free Software Foundation, Inc.
;; Author: Damien Cassou <damien@cassou.me>,
;; Nicolas Petton <nicolas@petton.fr>
;; Version: 4.0.2
;; Package-Requires: ((emacs "25"))
;; Url: https://github.com/DamienCassou/auth-password-store
;; Created: 07 Jun 2015
;; This file is part of GNU Emacs.
;; GNU Emacs is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; GNU Emacs is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;; Integrates password-store (http://passwordstore.org/) within
;; auth-source.
;;; Code:
(require 'seq)
(eval-when-compile (require 'subr-x))
(eval-when-compile
(require 'cl-lib))
(require 'auth-source)
(require 'url-parse)
(defgroup auth-source-pass nil
"password-store integration within auth-source."
:prefix "auth-source-pass-"
:group 'auth-source
:version "27.1")
(defcustom auth-source-pass-filename "~/.password-store"
"Filename of the password-store folder."
:type 'directory
:version "27.1")
(defcustom auth-source-pass-port-separator ":"
"Separator string between host and port in entry filename."
:type 'string
:version "27.1")
(cl-defun auth-source-pass-search (&rest spec
&key backend type host user port
&allow-other-keys)
"Given a property list SPEC, return search matches from the :backend.
See `auth-source-search' for details on SPEC."
(cl-assert (or (null type) (eq type (oref backend type)))
t "Invalid password-store search: %s %s")
(when (consp host)
(warn "auth-source-pass ignores all but first host in spec.")
;; Take the first non-nil item of the list of hosts
(setq host (seq-find #'identity host)))
(cond ((eq host t)
(warn "auth-source-pass does not handle host wildcards.")
nil)
((null host)
;; Do not build a result, as none will match when HOST is nil
nil)
(t
(when-let ((result (auth-source-pass--build-result host port user)))
(list result)))))
(defun auth-source-pass--build-result (host port user)
"Build auth-source-pass entry matching HOST, PORT and USER."
Minimize entry parsing in auth-source-pass Prior to this commit, while searching for the most applicable entry password-store entries were decrypted and parsed to ensure they were valid. The entries were parsed in the order they were found on the filesystem and all applicable entries would be decrypted and parsed, which varied based on the contents of the password-store and the entry to be found. This is fine when the GPG key is cached and each entry can be decrypted without user interaction. However, for security some people have their GPG on a hardware token like a Yubikey setup so that they have to touch a sensor on the toke for every cryptographic operation, in which case it becomes inconvenient as each attempt to find an entry requires a variable number of touches of the hardware token. The implementation already assumes that names which contain more of the information in the search key should be preferred so there is an ordering of preference of applicable entries. If the decrypt and parsing is removed from the initial identification of applicable entries in the store then in most cases a single decrypt and parse of the most preferred entry will suffice, improving the experience for hardware token users that require interaction with the token. This commit implements that strategy. It is in spirit a refactor of the existing code. * lisp/auth-source-pass.el (auth-source-pass--matching-entries): New function, generate an ordered list of regular expression matchers for all possible names that could be in the password-store for the entry to be found and then makes a pass over the password-store entry names accumulating the matching entries in a list after the regexp that matched. This implementation ensures the password-store entry list still only has to be scanned once. (auth-source-pass--find-match-unambiguous): Use it to obtain candidate entries and then parse them one by one until an entry containing the desired information is located. When complete, return the parsed data of the entry instead of the entry name so that the information can be used directly to construct the auth-source response. (auth-source-pass--build-result): Update accordingly. (auth-source-pass--find-match): Update docstring accordingly. (auth-source-pass--select-one-entry) (auth-source-pass--entry-valid-p) (auth-source-pass--find-all-by-entry-name) (auth-source-pass--find-one-by-entry-name): Remove. (auth-source-pass--select-from-entries) (auth-source-pass--accumulate-matches) (auth-source-pass--entry-reducer) (auth-source-pass--generate-entry-suffixes) (auth-source-pass--domains) (auth-source-pass--name-port-user-suffixes): New functions. * test/lisp/auth-source-pass-tests.el: One test case was added to the test suite to verify that only the minimal number of entries are parsed in common cases. The auth-source-pass-only-return-entries-that-can-be-open test case had to be re-implemented because the function it was used eliminated as the functionality is provided elsewhere. All the other fairly substantial changes to the test suite are the result of mechanical changes that were required to adapt to auth-source-pass--find-match returning the data from a parsed password-store entry instead of the entry name.
2019-05-06 03:21:43 +00:00
(let ((entry-data (auth-source-pass--find-match host user port)))
(when entry-data
(let ((retval (list
:host host
:port (or (auth-source-pass--get-attr "port" entry-data) port)
:user (or (auth-source-pass--get-attr "user" entry-data) user)
:secret (lambda () (auth-source-pass--get-attr 'secret entry-data)))))
(auth-source-pass--do-debug "return %s as final result (plus hidden password)"
(seq-subseq retval 0 -2)) ;; remove password
retval))))
;;;###autoload
(defun auth-source-pass-enable ()
"Enable auth-source-password-store."
;; To add password-store to the list of sources, evaluate the following:
(add-to-list 'auth-sources 'password-store)
;; clear the cache (required after each change to #'auth-source-pass-search)
(auth-source-forget-all-cached))
(defvar auth-source-pass-backend
(auth-source-backend
(when (<= emacs-major-version 25) "password-store")
:source "." ;; not used
:type 'password-store
:search-function #'auth-source-pass-search)
"Auth-source backend for password-store.")
(defun auth-source-pass-backend-parse (entry)
"Create a password-store auth-source backend from ENTRY."
(when (eq entry 'password-store)
(auth-source-backend-parse-parameters entry auth-source-pass-backend)))
(if (boundp 'auth-source-backend-parser-functions)
(add-hook 'auth-source-backend-parser-functions #'auth-source-pass-backend-parse)
(advice-add 'auth-source-backend-parse :before-until #'auth-source-pass-backend-parse))
;;;###autoload
(defun auth-source-pass-get (key entry)
"Return the value associated to KEY in the password-store entry ENTRY.
ENTRY is the name of a password-store entry.
The key used to retrieve the password is the symbol `secret'.
The convention used as the format for a password-store file is
the following (see http://www.passwordstore.org/#organization):
secret
key1: value1
key2: value2"
(let ((data (auth-source-pass-parse-entry entry)))
(auth-source-pass--get-attr key data)))
(defun auth-source-pass--get-attr (key entry-data)
"Return value associated with KEY in an ENTRY-DATA.
ENTRY-DATA is the data from a parsed password-store entry.
The key used to retrieve the password is the symbol `secret'.
See `auth-source-pass-get'."
(or (cdr (assoc key entry-data))
(and (string= key "user")
(cdr (assoc "username" entry-data)))))
(defun auth-source-pass--read-entry (entry)
"Return a string with the file content of ENTRY."
(with-temp-buffer
(insert-file-contents (expand-file-name
(format "%s.gpg" entry)
auth-source-pass-filename))
(buffer-substring-no-properties (point-min) (point-max))))
(defun auth-source-pass-parse-entry (entry)
"Return an alist of the data associated with ENTRY.
ENTRY is the name of a password-store entry."
(let ((file-contents (ignore-errors (auth-source-pass--read-entry entry))))
(and file-contents
(cons `(secret . ,(auth-source-pass--parse-secret file-contents))
(auth-source-pass--parse-data file-contents)))))
(defun auth-source-pass--parse-secret (contents)
"Parse the password-store data in the string CONTENTS and return its secret.
The secret is the first line of CONTENTS."
Fix more regular expression typos Problem reported by Mattias Engdegård in: https://lists.gnu.org/r/emacs-devel/2019-03/msg00548.html except that I didn’t address the issues involving Hebrew, or involving comint-prompt-regexp. * lisp/align.el (align-rules-list, align-exclude-rules-list): * lisp/auth-source-pass.el (auth-source-pass--parse-secret) (auth-source-pass--parse-data): * lisp/cedet/data-debug.el (data-debug-next) (data-debug-prev, data-debug-expand-or-contract): * lisp/comint.el (comint-within-quotes): * lisp/emacs-lisp/checkdoc.el (checkdoc-this-string-valid-engine): * lisp/emulation/viper-ex.el (ex-cmd-complete): * lisp/gnus/gnus-cite.el (gnus-message-search-citation-line): * lisp/gnus/nnir.el (nnir-imap-end-of-input): * lisp/mail/mail-extr.el (mail-extr-all-letters): * lisp/minibuffer.el (minibuffer-maybe-quote-filename): * lisp/nxml/rng-nxml.el (rng-complete-tag) (rng-complete-end-tag, rng-complete-attribute-name): * lisp/obsolete/vip.el (vip-get-ex-token, vip-get-ex-pat): * lisp/org/org-pcomplete.el (org-thing-at-point): * lisp/org/org.el (org-set-tags) (org-increase-number-at-point) (org-fill-line-break-nobreak-p): * lisp/pcomplete.el (pcomplete-parse-comint-arguments): * lisp/progmodes/ada-mode.el (ada-compile-goto-error): * lisp/progmodes/cperl-mode.el (cperl-highlight-charclass) (cperl-find-pods-heres, cperl-not-bad-style-regexp) (cperl-regext-to-level-start): * lisp/progmodes/ebnf-yac.el (ebnf-yac-skip-spaces): * lisp/progmodes/flymake-proc.el (flymake-proc-master-tex-init): * lisp/progmodes/flymake.el (flymake-diag-region): * lisp/progmodes/fortran.el (fortran-current-line-indentation): * lisp/progmodes/idlw-complete-structtag.el: (idlwave-complete-structure-tag): * lisp/progmodes/idlwave.el (idlwave-complete-sysvar-or-tag): * lisp/progmodes/prolog.el (prolog-pred-end) (prolog-clause-info): * lisp/progmodes/ruby-mode.el (ruby-forward-sexp) (ruby-backward-sexp): * lisp/progmodes/verilog-mode.el (verilog-repair-open-comma): * lisp/term.el (term-within-quotes): * lisp/textmodes/bib-mode.el (bib-capitalize-title-stop-words): * lisp/textmodes/refbib.el (r2b-capitalize-title-stop-words): * lisp/textmodes/reftex-parse.el (reftex-nth-arg): * lisp/textmodes/rst.el (rst-svn-rev): * lisp/url/url-http.el (url-http-parse-response): * test/lisp/progmodes/f90-tests.el (f90-test-bug3730): Fix regular expression typos.
2019-03-19 00:02:01 +00:00
(car (split-string contents "\n" t)))
(defun auth-source-pass--parse-data (contents)
"Parse the password-store data in the string CONTENTS and return an alist.
CONTENTS is the contents of a password-store formatted file."
(let ((lines (split-string contents "\n" t "[ \t]+")))
(seq-remove #'null
(mapcar (lambda (line)
(let ((pair (mapcar (lambda (s) (string-trim s))
(split-string line ":"))))
(when (> (length pair) 1)
(cons (car pair)
(mapconcat #'identity (cdr pair) ":")))))
(cdr lines)))))
(defun auth-source-pass--do-debug (&rest msg)
"Call `auth-source-do-debug` with MSG and a prefix."
(apply #'auth-source-do-debug
(cons (concat "auth-source-pass: " (car msg))
(cdr msg))))
;; TODO: add tests for that when `assess-with-filesystem' is included
;; in Emacs
(defun auth-source-pass-entries ()
"Return a list of all password store entries."
(let ((store-dir (expand-file-name auth-source-pass-filename)))
(mapcar
(lambda (file) (file-name-sans-extension (file-relative-name file store-dir)))
(directory-files-recursively store-dir "\\.gpg$"))))
(defun auth-source-pass--find-match (host user port)
Minimize entry parsing in auth-source-pass Prior to this commit, while searching for the most applicable entry password-store entries were decrypted and parsed to ensure they were valid. The entries were parsed in the order they were found on the filesystem and all applicable entries would be decrypted and parsed, which varied based on the contents of the password-store and the entry to be found. This is fine when the GPG key is cached and each entry can be decrypted without user interaction. However, for security some people have their GPG on a hardware token like a Yubikey setup so that they have to touch a sensor on the toke for every cryptographic operation, in which case it becomes inconvenient as each attempt to find an entry requires a variable number of touches of the hardware token. The implementation already assumes that names which contain more of the information in the search key should be preferred so there is an ordering of preference of applicable entries. If the decrypt and parsing is removed from the initial identification of applicable entries in the store then in most cases a single decrypt and parse of the most preferred entry will suffice, improving the experience for hardware token users that require interaction with the token. This commit implements that strategy. It is in spirit a refactor of the existing code. * lisp/auth-source-pass.el (auth-source-pass--matching-entries): New function, generate an ordered list of regular expression matchers for all possible names that could be in the password-store for the entry to be found and then makes a pass over the password-store entry names accumulating the matching entries in a list after the regexp that matched. This implementation ensures the password-store entry list still only has to be scanned once. (auth-source-pass--find-match-unambiguous): Use it to obtain candidate entries and then parse them one by one until an entry containing the desired information is located. When complete, return the parsed data of the entry instead of the entry name so that the information can be used directly to construct the auth-source response. (auth-source-pass--build-result): Update accordingly. (auth-source-pass--find-match): Update docstring accordingly. (auth-source-pass--select-one-entry) (auth-source-pass--entry-valid-p) (auth-source-pass--find-all-by-entry-name) (auth-source-pass--find-one-by-entry-name): Remove. (auth-source-pass--select-from-entries) (auth-source-pass--accumulate-matches) (auth-source-pass--entry-reducer) (auth-source-pass--generate-entry-suffixes) (auth-source-pass--domains) (auth-source-pass--name-port-user-suffixes): New functions. * test/lisp/auth-source-pass-tests.el: One test case was added to the test suite to verify that only the minimal number of entries are parsed in common cases. The auth-source-pass-only-return-entries-that-can-be-open test case had to be re-implemented because the function it was used eliminated as the functionality is provided elsewhere. All the other fairly substantial changes to the test suite are the result of mechanical changes that were required to adapt to auth-source-pass--find-match returning the data from a parsed password-store entry instead of the entry name.
2019-05-06 03:21:43 +00:00
"Return password-store entry data matching HOST, USER and PORT.
Disambiguate between user provided inside HOST (e.g., user@server.com) and
inside USER by giving priority to USER. Same for PORT."
(let* ((url (url-generic-parse-url (if (string-match-p ".*://" host)
host
(format "https://%s" host)))))
(auth-source-pass--find-match-unambiguous
(or (url-host url) host)
(or user (url-user url))
;; url-port returns 443 (because of the https:// above) by default
(or port (number-to-string (url-port url))))))
(defun auth-source-pass--find-match-unambiguous (hostname user port)
Minimize entry parsing in auth-source-pass Prior to this commit, while searching for the most applicable entry password-store entries were decrypted and parsed to ensure they were valid. The entries were parsed in the order they were found on the filesystem and all applicable entries would be decrypted and parsed, which varied based on the contents of the password-store and the entry to be found. This is fine when the GPG key is cached and each entry can be decrypted without user interaction. However, for security some people have their GPG on a hardware token like a Yubikey setup so that they have to touch a sensor on the toke for every cryptographic operation, in which case it becomes inconvenient as each attempt to find an entry requires a variable number of touches of the hardware token. The implementation already assumes that names which contain more of the information in the search key should be preferred so there is an ordering of preference of applicable entries. If the decrypt and parsing is removed from the initial identification of applicable entries in the store then in most cases a single decrypt and parse of the most preferred entry will suffice, improving the experience for hardware token users that require interaction with the token. This commit implements that strategy. It is in spirit a refactor of the existing code. * lisp/auth-source-pass.el (auth-source-pass--matching-entries): New function, generate an ordered list of regular expression matchers for all possible names that could be in the password-store for the entry to be found and then makes a pass over the password-store entry names accumulating the matching entries in a list after the regexp that matched. This implementation ensures the password-store entry list still only has to be scanned once. (auth-source-pass--find-match-unambiguous): Use it to obtain candidate entries and then parse them one by one until an entry containing the desired information is located. When complete, return the parsed data of the entry instead of the entry name so that the information can be used directly to construct the auth-source response. (auth-source-pass--build-result): Update accordingly. (auth-source-pass--find-match): Update docstring accordingly. (auth-source-pass--select-one-entry) (auth-source-pass--entry-valid-p) (auth-source-pass--find-all-by-entry-name) (auth-source-pass--find-one-by-entry-name): Remove. (auth-source-pass--select-from-entries) (auth-source-pass--accumulate-matches) (auth-source-pass--entry-reducer) (auth-source-pass--generate-entry-suffixes) (auth-source-pass--domains) (auth-source-pass--name-port-user-suffixes): New functions. * test/lisp/auth-source-pass-tests.el: One test case was added to the test suite to verify that only the minimal number of entries are parsed in common cases. The auth-source-pass-only-return-entries-that-can-be-open test case had to be re-implemented because the function it was used eliminated as the functionality is provided elsewhere. All the other fairly substantial changes to the test suite are the result of mechanical changes that were required to adapt to auth-source-pass--find-match returning the data from a parsed password-store entry instead of the entry name.
2019-05-06 03:21:43 +00:00
"Return password-store entry data matching HOSTNAME, USER and PORT.
If many matches are found, return the first one. If no match is found,
return nil.
HOSTNAME should not contain any username or port number."
Minimize entry parsing in auth-source-pass Prior to this commit, while searching for the most applicable entry password-store entries were decrypted and parsed to ensure they were valid. The entries were parsed in the order they were found on the filesystem and all applicable entries would be decrypted and parsed, which varied based on the contents of the password-store and the entry to be found. This is fine when the GPG key is cached and each entry can be decrypted without user interaction. However, for security some people have their GPG on a hardware token like a Yubikey setup so that they have to touch a sensor on the toke for every cryptographic operation, in which case it becomes inconvenient as each attempt to find an entry requires a variable number of touches of the hardware token. The implementation already assumes that names which contain more of the information in the search key should be preferred so there is an ordering of preference of applicable entries. If the decrypt and parsing is removed from the initial identification of applicable entries in the store then in most cases a single decrypt and parse of the most preferred entry will suffice, improving the experience for hardware token users that require interaction with the token. This commit implements that strategy. It is in spirit a refactor of the existing code. * lisp/auth-source-pass.el (auth-source-pass--matching-entries): New function, generate an ordered list of regular expression matchers for all possible names that could be in the password-store for the entry to be found and then makes a pass over the password-store entry names accumulating the matching entries in a list after the regexp that matched. This implementation ensures the password-store entry list still only has to be scanned once. (auth-source-pass--find-match-unambiguous): Use it to obtain candidate entries and then parse them one by one until an entry containing the desired information is located. When complete, return the parsed data of the entry instead of the entry name so that the information can be used directly to construct the auth-source response. (auth-source-pass--build-result): Update accordingly. (auth-source-pass--find-match): Update docstring accordingly. (auth-source-pass--select-one-entry) (auth-source-pass--entry-valid-p) (auth-source-pass--find-all-by-entry-name) (auth-source-pass--find-one-by-entry-name): Remove. (auth-source-pass--select-from-entries) (auth-source-pass--accumulate-matches) (auth-source-pass--entry-reducer) (auth-source-pass--generate-entry-suffixes) (auth-source-pass--domains) (auth-source-pass--name-port-user-suffixes): New functions. * test/lisp/auth-source-pass-tests.el: One test case was added to the test suite to verify that only the minimal number of entries are parsed in common cases. The auth-source-pass-only-return-entries-that-can-be-open test case had to be re-implemented because the function it was used eliminated as the functionality is provided elsewhere. All the other fairly substantial changes to the test suite are the result of mechanical changes that were required to adapt to auth-source-pass--find-match returning the data from a parsed password-store entry instead of the entry name.
2019-05-06 03:21:43 +00:00
(cl-reduce
(lambda (result entries)
(or result
(pcase (length entries)
(0 nil)
(1 (auth-source-pass-parse-entry (car entries)))
(_ (auth-source-pass--select-from-entries entries user)))))
(auth-source-pass--matching-entries hostname user port)
:initial-value nil))
(defun auth-source-pass--select-from-entries (entries user)
"Return best matching password-store entry data from ENTRIES.
If USER is non nil, give precedence to entries containing a user field
matching USER."
(cl-reduce
(lambda (result entry)
(let ((entry-data (auth-source-pass-parse-entry entry)))
(cond ((equal (auth-source-pass--get-attr "user" result) user)
result)
((equal (auth-source-pass--get-attr "user" entry-data) user)
entry-data)
(t
result))))
entries
:initial-value (auth-source-pass-parse-entry (car entries))))
(defun auth-source-pass--matching-entries (hostname user port)
"Return all matching password-store entries for HOSTNAME, USER, & PORT.
The result is a list of lists of password-store entries, where
each sublist contains entries that actually exist in the
password-store matching one of the entry name formats that
auth-source-pass expects, most specific to least specific."
(let* ((entries-lists (mapcar
#'cdr
(auth-source-pass--accumulate-matches hostname user port)))
(entries (apply #'cl-concatenate (cons 'list entries-lists))))
(if entries
(auth-source-pass--do-debug (format "found: %S" entries))
(auth-source-pass--do-debug "no matches found"))
entries-lists))
(defun auth-source-pass--accumulate-matches (hostname user port)
"Accumulate matching password-store entries into sublists.
Entries matching supported formats that combine HOSTNAME, USER, &
PORT are accumulated into sublists where the car of each sublist
is a regular expression for matching paths in the password-store
and the remainder is the list of matching entries."
(let ((suffix-match-lists
(mapcar (lambda (suffix) (list (format "\\(^\\|/\\)%s$" suffix)))
(auth-source-pass--generate-entry-suffixes hostname user port))))
(cl-reduce #'auth-source-pass--entry-reducer
(auth-source-pass-entries)
:initial-value suffix-match-lists)))
(defun auth-source-pass--entry-reducer (match-lists entry)
"Match MATCH-LISTS sublists against ENTRY.
The result is a copy of match-lists with the entry added to the
end of any sublists for which the regular expression at the head
of the list matches the entry name."
(mapcar (lambda (match-list)
(if (string-match (car match-list) entry)
(append match-list (list entry))
match-list))
match-lists))
(defun auth-source-pass--generate-entry-suffixes (hostname user port)
"Return a list of possible entry path suffixes in the password-store.
Based on the supported pathname patterns for HOSTNAME, USER, &
PORT, return a list of possible suffixes for matching entries in
the password-store."
(let ((domains (auth-source-pass--domains (split-string hostname "\\."))))
(seq-mapcat (lambda (n)
(auth-source-pass--name-port-user-suffixes n user port))
domains)))
(defun auth-source-pass--domains (name-components)
"Return a list of possible domain names matching the hostname.
This function takes a list of NAME-COMPONENTS, the strings
separated by periods in the hostname, and returns a list of full
domain names containing the trailing sequences of those
components, from longest to shortest."
(cl-maplist (lambda (components) (mapconcat #'identity components "."))
name-components))
(defun auth-source-pass--name-port-user-suffixes (name user port)
"Return a list of possible path suffixes for NAME, USER, & PORT.
The resulting list is ordered from most specifc to least
specific, with paths matching all of NAME, USER, & PORT first,
then NAME & USER, then NAME & PORT, then just NAME."
(seq-mapcat
#'identity
(list
(when (and user port)
(list
(format "%s@%s%s%s" user name auth-source-pass-port-separator port)
(format "%s%s%s/%s" name auth-source-pass-port-separator port user)))
(when user
(list
(format "%s@%s" user name)
(format "%s/%s" name user)))
(when port
(list
(format "%s%s%s" name auth-source-pass-port-separator port)))
(list
(format "%s" name)))))
(provide 'auth-source-pass)
;;; auth-source-pass.el ends here