From 4fd53ffcbdb804a83c48b017f26fd300272a0efa Mon Sep 17 00:00:00 2001 From: Christopher Crockett Date: Sun, 7 Jul 2024 12:16:57 -0400 Subject: [PATCH] mode/password: filename-based usernames for pass Fixes #3293 The new filename-based username check is used as a fallback to the current key-based username behavior. Users who only want the filename-based behavior can disable scanning the credential for username keys via the new scan-for-username-entries slot. Disabling scanning for a key-based username is particularly useful in use-cases where credential files are encrypted and require touch verification to open. --- libraries/password-manager/password-pass.lisp | 59 +++++++++++++++---- 1 file changed, 46 insertions(+), 13 deletions(-) diff --git a/libraries/password-manager/password-pass.lisp b/libraries/password-manager/password-pass.lisp index 136fe908146..981f2e0df27 100644 --- a/libraries/password-manager/password-pass.lisp +++ b/libraries/password-manager/password-pass.lisp @@ -9,7 +9,14 @@ (password-directory (or (uiop:getenv "PASSWORD_STORE_DIR") (format nil "~a/.password-store" (uiop:getenv "HOME"))) :type string - :reader password-directory)) + :reader password-directory) + (scan-for-username-entries t + :type boolean + :documentation "If t, Nyxt uses a two-step process to determine a credential's username: +1. Open the credential file and scan for an entry with a key matching `*username-keys*'. +2. If no username key could be found, fall back to determining username based on the credential's filename. + +If nil, Nyxt skips the first step and instead always uses the fallback strategy. This is useful if you don't use key-based usernames in your password store as it skips unnecessarily decrypting the credential file (if originally encrypted).")) (:export-class-name-p t) (:export-accessor-names-p t)) @@ -69,22 +76,48 @@ The first line (the password) is skipped." (defvar *username-keys* '("login" "user" "username") "A list of string keys used to find the `pass' username in `clip-username'.") +(defun username-from-name (password-name) + "Select a username using the path of the credential file. +The strategy for deriving the username is context-dependent: +- If the credential exists in a subdirectory of the password store (e.g.: example.com/me@example.net), +username is taken as-is from the filename (me@example.net) +- If the credential exists in the root of the password store (e.g.: me@example.net@example.com), +username will be the first half of the filename (me@example.net) + +This is meant to handle the naming patterns described in Emacs documentation +https://www.gnu.org/software/emacs/manual/html_node/auth/The-Unix-password-store.html +" + (multiple-value-bind (_ parent-dirs credential-name) + (uiop/pathname:split-unix-namestring-directory-components password-name) + (declare (ignore _)) + (if parent-dirs + credential-name + (subseq credential-name 0 (position #\@ credential-name :from-end t))))) + +(defun username-from-content (password-interface password-name) + "Select a username from the first entry within the `password-name' credential matching `*username-keys*'" + (let* ((content (execute password-interface (list "show" password-name) + :output '(:string :stripped t))) + (entries (parse-multiline content)) + (username-entry (when entries + (some (lambda (key) + (find key entries :test #'string-equal :key #'first)) + *username-keys*)))) + (when username-entry (second username-entry)))) + (defmethod clip-username ((password-interface password-store-interface) &key password-name service) - "Save the multiline entry that's prefixed with on of the `*username-keys*' to clipboard. + "Save the username of the `password-name' credential to clipboard. See the `scan-for-usename-entries' slot for details. Case is ignored. -The prefix is discarded from the result and returned." +The resulting username is also returned." (declare (ignore service)) (when password-name - (let* ((content (execute password-interface (list "show" password-name) - :output '(:string :stripped t))) - (entries (parse-multiline content)) - (username-entry (when entries - (some (lambda (key) - (find key entries :test #'string-equal :key #'first)) - *username-keys*)))) - (when username-entry - (trivial-clipboard:text (second username-entry)) - (second username-entry))))) + (let ((username + (or (when (scan-for-username-entries password-interface) + (username-from-content password-interface password-name)) + (username-from-name password-name)))) + (when username + (trivial-clipboard:text username) + username)))) (defmethod save-password ((password-interface password-store-interface) &key password-name username password service)