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

mode/password: filename-based usernames for pass #3429

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

chaorace
Copy link

@chaorace chaorace commented Jul 7, 2024

Description

Introduces a new filename-based code pathway for determining the username of a GNU password-store credential. This new pathway simply uses the credential file name (not including extension) as the username.

By default, the new pathway is only used as a fallback strategy for when no matching username keys could be found using the current strategy, though users who only want the new filename-based behavior can entirely skip using the original strategy via the new scan-for-username-entries slot. Disabling scanning credential files for a key-based username is particularly useful in use-cases where the store credentials are encrypted and would require touch verification to open.

In the special case that a credential is on the root directory of the password store, we also trim off the domain signifier to match the behavior of other password-store based utilities (e.g.: "me@example.net" becomes "me", "me@example.org@example.net" becomes "me@example.org")

Fixes #3293

Checklist:

  • Git branch state is mergable.
  • Changelog is up to date (via a separate commit).
  • New dependencies are accounted for.
  • Documentation is up to date.
  • Compilation and tests ((asdf:test-system :nyxt/<renderer>))
    • No new compilation warnings.
    • Tests are sufficient.

@chaorace
Copy link
Author

chaorace commented Jul 7, 2024

Some checklist-related notes:

  1. I wasn't sure what to do about the changelog. There's no documented developer instructions and changelog.lisp, which seems organized by release number, leaves me scratching my head because I'm not sure whether features like these belong in semantically major or minor releases.
  2. Because the current password-store interface's behavior is not documented in manual.lisp, I decided to assume that my method docstrings would generally be sufficient. What I'm less sure about, however, is whether or not I need to document the newly added slot in manual.lisp.

@chaorace
Copy link
Author

chaorace commented Jul 7, 2024

Oh, and fair warning: I've never worked on a Common Lisp project before. Please assume that any fishy code you see is the product of inexperience and nothing particularly avant garde. All critique is welcome and highly useful.

Copy link
Member

@aadcg aadcg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@chaorace, I've left some minor comments.

Regarding the changelog, this change belongs to 3.12.0.

Regarding the manual, I don't think this piece of information belongs there. It's too specific.

libraries/password-manager/password-pass.lisp Outdated Show resolved Hide resolved
libraries/password-manager/password-pass.lisp Outdated Show resolved Hide resolved
libraries/password-manager/password-pass.lisp Outdated Show resolved Hide resolved
@chaorace chaorace force-pushed the password-store-filename-usernames branch 2 times, most recently from 3b3272a to c66c7a9 Compare July 9, 2024 02:11
@chaorace
Copy link
Author

chaorace commented Jul 9, 2024

@aadcg All pending feedback should now be addressed. Please take a look when you can, thanks!

Copy link
Member

@aadcg aadcg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@chaorace, thank you for taking a look.

Please help me to clarify some details. Thanks.

libraries/password-manager/password-pass.lisp Outdated Show resolved Hide resolved
@@ -69,22 +79,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'.")

(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.
(defun username-from-name (password-name)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we come up with a better name for the function and argument? It seems a bit misleading.

Copy link
Author

@chaorace chaorace Jul 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@aadcg Here's some candidates, do any stand out to you?

  • username-from-filename
  • username-from-path
  • username-fallback

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As for the argument name... I don't know what to say. It's identical to the name given to the exact same value via the pre-existing clip-username, clip-password functions. I don't really have a horse in this race but I think choosing a new name for it is beyond the scope of my responsibilities and the work I'm doing for this PR.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since it takes a path as a string I'd suggest calling it username-from-path and renaming the argument to path.

Indeed, as you correctly note, it is called in clip-username with an argument called password-name (which I find odd), but that is beyond the scope of this PR. My goal is to guarantee that the added code is maintainable and easily understood in the future. Note that I am not the author of the password-manager library.

2. If no match could be found, use the credential filename as a fallback.

When nil, Nyxt always immediately uses the fallback strategy.
If your store doesn't utilize username keys, this skips credential decryption."))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I gave it a second thought.

The slot name needs to be named as a predicate, i.e. it must have the suffic -p.

Since I am not a pass user please help me figure out an adequate default behavior. Does it make sense to scan for both the content and the directory tree structure? Why is that better than scanning the content only, as before this PR? Am I right that you'd use this mode with this slot set to nil, such that the username is computed exclusively via username-from-name?

Upon answering, we can decide the slot name.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pass is -- in what might be described as typical GNU fashion -- highly unprescriptive. As far as the software is concerned, there are exactly two distinct types of data:

  1. The filename & path relative to the root of the store
  2. The password text

Higher-level concepts, such as URLs and usernames, exist at a level of abstraction that pass is unconcerned with -- note how the manual entry contains zero mentions of the word "username". The original author's website does touch on the author's personally preferred schema for storing extra metadata...

One approach is to use the multi-line functionality of pass (--multiline or -m in insert), and store the password itself on the first line of the file, and the additional information on subsequent lines

... though it should be noted that this statement is immediately preceded by the disclaimer that the software itself prescribes no notion of schema whatsoever:

The password store does not impose any particular schema or type of organization of your data, as it is simply a flat text file, which can contain arbitrary data.

I lay this all out so that I can assert a very important point: it is impossible to use pass as an internet password manager without making opinionated assumptions about how the user chooses to utilize pass. At best, all one can do is study the conventions invented by the surrounding pass-based tooling ecosystem and accomodate the most common approaches.

The most common convention is to place the website URL & username metadata directly in the path of the credential. The case for storing these like so are, respectively:

  • URL: If the URL metadata were instead stored within the password text, then password managers would be required to decrypt every single password in the store when attempting to produce an exhausting list of credentials for any given website. This scales very badly with larger password stores.
  • Username: There exist several common use-cases where users may have multiple credentials associated with the same website (e.g.: "Single Sign On" portals). For this reason, some additional disambiguating value needs to exist in order for the pattern to accomodate the concept of multiple credential files being applicable to the same URL. True, the disambiguating value doesn't strictly need to be the username... but something needs to be used and usernames make for good unique identifiers.

The main point of contention here among implementations has historically been how these two metadata elements are organized in the credential path. Some implementations used flat directory structures while others used nested structures. Luckily, these two approaches are not mutually exclusive, so the tooling ecosystem eventually settled on supporting use of either of these patterns (even both simultaneously!). This dual-support convention is most canonically codified in the Emacs documentation, though it should be noted that many password management tools in the pass ecosystem expect the same patterns (example, example).

As you may have noticed, both of the previously linked examples also support getting the username via a "field value". These "field values" are yet another invented convention with various competing implementations. The general consensus for how such username keys should be named is relatively weak, so implementations which support extracting usernames via field value generally try to pack in a customizable list of commonly used key variants.

Right, so with all of that baggage out of the way, let's circle back and address your questions...

Does it make sense to scan for both the content and the directory tree structure?

Yes, the reason for this being that it's the easiest way to support both styles without making the user do additional configuration. Indeed, there are many common GPG configurations out there (some of them even secure!) which do not require additional input from the user in order to decrypt credentials once the keyring (and/or hardware key) has been unlocked. For these users, simply attempting both approaches is painless and not something they would be overly concerned about.

Why is that better than scanning the content only, as before this PR?

Because storing usernames as "field values" is no more official or correct than storing usernames as part of the file path. More importantly, I use the pathname-based approach and I want Nyxt to support my (quite common!) password store schema. The only way to logically square excluding support for this on the grounds of canonicity would be removing support for both schemas and always returning "" for the username.

Am I right that you'd use this mode with this slot set to nil, such that the username is computed exclusively via username-from-name?

Correct. This is because I use hardware-based encryption which actively withholds decryption from GPG until I manually reach across my desk and press a physical button. In other words: without this feature, I'd have to engage in this awkward ritual twice as often as would be otherwise necessary -- once (unnecessarily!) for the username and once again for the password. Indeed, this is one of the better justifications for not storing the username as a credential field value.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the detailed explanation.

I suggest the following.

Name the slot username-scan-method. It takes the following keywords as values: :credential-content-and-path, :credential-content and :credential-path. By default it is set to :credential-content-and-path and it follows the logic you have implemented (checks the content and fallbacks on the path approach). For your use-case, you'd set it to credential-path.

Does that make sense? I wonder if it makes sense to allow setting it to :credential-content.

Copy link
Author

@chaorace chaorace Jul 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@aadcg Hmm... I can't imagine any hypothetical reasons anyone would want such a :credential-content option other than offering strict reverse-compatibility.

If you ask me, I don't personally think the added level of technical completeness really justifies the (admitedly small) amount of additional code complexity... but I'll happily ignore that instinct and implement the option should you deem it important -- it's your codebase after all 😉

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feel free to let the implementation handle :credential-content-and-path and :credential-path exclusively. Thanks!

Fixes atlas-engineer#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.
@chaorace chaorace force-pushed the password-store-filename-usernames branch from d5f99b0 to beaf3e6 Compare July 9, 2024 22:04
@chaorace chaorace requested a review from aadcg July 15, 2024 15:15
Copy link
Member

@aadcg aadcg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@chaorace Sorry for the late reply.

Thanks for the information shared and check my relies.

Note that your first commit isn't using the correct pathname in the commit message. Suggestion:

libraries/password-manager/password-pass: Refactor username scan.

@aadcg
Copy link
Member

aadcg commented Jul 29, 2024

@chaorace would you like to finish the PR or should I take over it? Thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

Password Store: Support harvesting username via filename without decrypting
2 participants