Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

password-manager/password-secret-service: Implement and document #3083

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 123 additions & 0 deletions libraries/password-manager/password-secret-service.lisp
Original file line number Diff line number Diff line change
@@ -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)
1 change: 1 addition & 0 deletions nyxt.asd
Original file line number Diff line number Diff line change
Expand Up @@ -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")))

Expand Down
39 changes: 33 additions & 6 deletions source/manual.lisp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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%)))))))

Expand Down