From 4fb48f4aa618876b412376744e89a80e8eabcb35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Wid=C3=A9n?= Date: Sat, 15 Jul 2023 19:42:19 +0200 Subject: [PATCH] password-manager/password-secret-service: Implement and document An interface to https://specifications.freedesktop.org/secret-service --- .../password-secret-service.lisp | 123 ++++++++++++++++++ nyxt.asd | 1 + source/manual.lisp | 39 +++++- 3 files changed, 157 insertions(+), 6 deletions(-) create mode 100644 libraries/password-manager/password-secret-service.lisp diff --git a/libraries/password-manager/password-secret-service.lisp b/libraries/password-manager/password-secret-service.lisp new file mode 100644 index 00000000000..dcddbf517c7 --- /dev/null +++ b/libraries/password-manager/password-secret-service.lisp @@ -0,0 +1,123 @@ +;;;; SPDX-FileCopyrightText: Atlas Engineer LLC +;;;; SPDX-License-Identifier: BSD-3-Clause + +;; Search for password, get password, write password, using Secret Service API: +;; https://specifications.freedesktop.org/secret-service +;; Password entries are retrieved using python package SecretStorage: https://pypi.org/project/SecretStorage +;; This package may be provided by your linux distribution. In Ubuntu python3-secretstorage. +;; Password entries are written using python package keyring: https://pypi.org/project/keyring +;; This package may be provided by your linux distribution. In Ubuntu python3-keyring. +;; The default collection (service) in Secret Service is used, this is normally the login collection. +;; On at least Ubuntu this collection is by default open when a user is logged in. So there is no +;; need to enter a master password to unlock the collection, when logging in at a web page. +;; +;; It would have been preferable to only use keyring, and not secretstorage, but keyring does not +;; provide facilities to search the Secret Service collection. +;; +;; This interface relies on a number of command line scripts: +;; keyring: Provided by python package keyring +;; secret-service-keys, secret-service-show-password, secret-service-show-username: Implementation shown below, +;; also available from https://github.com/johanwiden/secret-service-scripts +;; +;; Limitations: +;; - Currently this interface does not generate a new password, if an empty password is saved. +;; The user must therefore use some other facility to generate a new password.] + +(in-package :password) + +(define-class password-secret-service-interface (password-interface) + ((executable (pathname->string (sera:resolve-executable "keyring")))) + (:export-class-name-p t) + (:export-accessor-names-p t)) + +(push 'password-secret-service-interface *interfaces*) + +;; Return something like ("pypi.org" "foo.org") +;; If executable "secret-service-list-keys" is available, return result from that, otherwise nil. +;; https://stackoverflow.com/questions/72020628/how-do-i-list-and-access-the-secrets-stored-in-ubuntus-keyring-from-the-command +;; In ubuntu: sudo apt get python3-secretstorage +;; Example implementation of "secret-service-list-keys": +;; #!/usr/bin/env python3 +;; import secretstorage +;; conn = secretstorage.dbus_init() +;; collection = secretstorage.get_default_collection(conn) +;; services = [] +;; for item in collection.get_all_items(): +;; attributes = item.get_attributes() +;; if 'service' in attributes: +;; services.append(attributes['service']) +;; print(' '.join(e for e in sorted(set(services)))) +(defmethod list-passwords ((password-interface password-secret-service-interface)) + (let ((secret-service-keys (pathname->string (serapeum:resolve-executable "secret-service-list-keys")))) + (when secret-service-keys + (let* ((key-string (uiop:run-program (list secret-service-keys) :output '(:string :stripped t))) + (key-list (split-sequence:split-sequence #\Space key-string :remove-empty-subseqs t))) + key-list)))) + +;; If executable "secret-service-show-password" is available, and returns result, then copy result to clipboard, return t. +;; Otherwise return nil. +;; In ubuntu: sudo apt get python3-secretstorage +;; Example implementation of "secret-service-show-password": +;; #!/usr/bin/env python3 +;; import argparse +;; import secretstorage +;; parser = argparse.ArgumentParser( +;; prog = 'secret-service-show-password', +;; description = 'Return password for password entry password_name') +;; parser.add_argument('password_name', type=str) +;; args = parser.parse_args() +;; conn = secretstorage.dbus_init() +;; collection = secretstorage.get_default_collection(conn) +;; for item in collection.get_all_items(): +;; attributes = item.get_attributes() +;; if 'service' in attributes and attributes['service'] == args.password_name: +;; print(item.get_secret().decode('utf-8')) +;; break +(defmethod clip-password ((password-interface password-secret-service-interface) &key password-name service) + (declare (ignore service)) + (let ((secret-service-show-password (pathname->string (serapeum:resolve-executable "secret-service-show-password")))) + (when secret-service-show-password + (let* ((password-str (uiop:run-program (list secret-service-show-password password-name) :output '(:string :stripped t))) + (password (if (uiop:emptyp password-str) nil password-str))) + (when password + (trivial-clipboard:text password) + t))))) + +;; If executable "secret-service-show-username" is available, and returns result, then copy result to clipboard, return t. +;; Otherwise return nil. +;; In ubuntu: sudo apt get python3-secretstorage +;; Example implementation of "secret-service-show-username": +;; #!/usr/bin/env python3 +;; import argparse +;; import secretstorage +;; parser = argparse.ArgumentParser( +;; prog = 'secret-service-show-username', +;; description = 'Return field "username" for password entry password_name') +;; parser.add_argument('password_name', type=str) +;; args = parser.parse_args() +;; conn = secretstorage.dbus_init() +;; collection = secretstorage.get_default_collection(conn) +;; for item in collection.get_all_items(): +;; attributes = item.get_attributes() +;; if 'service' in attributes and attributes['service'] == args.password_name and 'username' in attributes: +;; print(attributes['username']) +;; break +(defmethod clip-username ((password-interface password-secret-service-interface) &key password-name service) + (declare (ignore service)) + (let ((secret-service-show-username (pathname->string (serapeum:resolve-executable "secret-service-show-username")))) + (when secret-service-show-username + (let* ((username-str (uiop:run-program (list secret-service-show-username password-name) :output '(:string :stripped t))) + (username (if (uiop:emptyp username-str) nil username-str))) + (when username + (trivial-clipboard:text username) + t))))) + +;; Generate new password is not supported. +(defmethod save-password ((password-interface password-secret-service-interface) + &key password-name username password service) + (declare (ignore service)) + (with-input-from-string (st (format nil "~a~C" password #\newline)) + (execute password-interface (list "set" password-name username) :input st))) + +(defmethod password-correct-p ((password-interface password-secret-service-interface)) + t) diff --git a/nyxt.asd b/nyxt.asd index 4c63d82df20..5bcccc084e2 100644 --- a/nyxt.asd +++ b/nyxt.asd @@ -593,6 +593,7 @@ The renderer is configured from NYXT_RENDERER or `*nyxt-renderer*'.")) (:file "password") (:file "password-keepassxc") (:file "password-security") + (:file "password-secret-service") ;; Keep password-store last so that it has higher priority. (:file "password-pass"))) diff --git a/source/manual.lisp b/source/manual.lisp index 777a871ef28..2446af5f9f3 100644 --- a/source/manual.lisp +++ b/source/manual.lisp @@ -623,16 +623,18 @@ with " (:code "nyxt --profile dev --socket /tmp/nyxt.socket") ".")) (:nsection :title "Password management" (:p "Nyxt provides a uniform interface to some password managers including " (:a :href "https://keepassxc.org/" "KeepassXC") - " and " (:a :href "https://www.passwordstore.org/" "Password Store") ". " - "The supported installed password manager is automatically detected." - "See the " (:code "password-interface") " buffer slot for customization.") + ", " (:a :href "https://www.passwordstore.org/" "Password Store") + " and " (:a :href "https://specifications.freedesktop.org/secret-service" "Secret Service") ". " + "The supported installed password manager is automatically detected. See the " + (:code "password-interface") " buffer slot for customization.") (:p "You may use the " (:nxref :macro 'define-configuration) " macro with any of the password interfaces to configure them. Please make sure to use the package prefixed class name/slot designators within the " (:nxref :macro 'define-configuration) ".") (:ul (:li (:nxref :command 'nyxt/mode/password:save-new-password) ": Query for name and new password to persist in the database.") - (:li (:nxref :command 'nyxt/mode/password:copy-password) ": " (command-docstring-first-sentence 'nyxt/mode/password:copy-password))) + (:li (:nxref :command 'nyxt/mode/password:copy-password) ": " (command-docstring-first-sentence 'nyxt/mode/password:copy-password)) + (:li (:nxref :command 'nyxt/mode/password:copy-username) ": " (command-docstring-first-sentence 'nyxt/mode/password:copy-username))) (:nsection :title "KeePassXC support" (:p "The interface for KeePassXC should cover most use-cases for KeePassXC, as it @@ -644,8 +646,6 @@ supports password database locking with") (:p "To configure KeePassXC interface, you might need to add something like this snippet to your config:") (:ncode - ;; FIXME: Why does `define-configuration' not work for password - ;; interfaces? Something's fishy with user classes... '(defmethod initialize-instance :after ((interface password:keepassxc-interface) &key &allow-other-keys) "It's obviously not recommended to set master password here, as your config is likely unencrypted and can reveal your password to someone @@ -655,6 +655,33 @@ peeking at the screen." (password:yubikey-slot interface) "1:1111")) '(define-configuration nyxt/mode/password:password-mode ((nyxt/mode/password:password-interface (make-instance 'password:keepassxc-interface)))) + '(define-configuration buffer + ((default-modes (append (list 'nyxt/mode/password:password-mode) %slot-value%)))))) + + (:nsection :title "Secret Service support" + (:p "This interface accesses password entries in the default collection (service) +provided by Secret Service. This is normally the login collection. +On at least Ubuntu this collection is by default open the whole time a user is logged in. +So there is no need to unlock the collection, using a master password, when logging in at a web page.") + (:p "The interface depends on two python packages and three additional command line scripts:") + (:ul + (:li "Password entries are retrieved using python package " + (:a :href "https://pypi.org/project/SecretStorage" "SecretStorage") + ". This package may be provided by your linux distribution. In Ubuntu python3-secretstorage") + (:li "Password entries are written using python package " + (:a :href "https://pypi.org/project/keyring" "keyring") + ". This package may be provided by your linux distribution. In Ubuntu python3-keyring") + (:li "The three extra command line scripts are documented in the interface lisp file. +They may also be retrieved from " + (:a :href "https://github.com/johanwiden/secret-service-scripts" "secret-service-scripts"))) + (:p "A current limitation of the interface, is that it does not generate a new password, +if the user saves an empty password. +The user must use some other facility to generate a new password.") + (:p "To configure the Secret Service interface, you might need to add something like this +snippet to your config:") + (:ncode + '(define-configuration nyxt/mode/password:password-mode + ((nyxt/mode/password:password-interface (make-instance 'password:password-secret-service-interface)))) '(define-configuration buffer ((default-modes (append (list 'nyxt/mode/password:password-mode) %slot-value%)))))))