Skip to content

Store initialization ignores hierarchical  .gpg-id  and passless's own scope #491

Description

@HGBits

Store initialization ignores hierarchical .gpg-id and passless's own scope

Summary

storage/pass/init/* (the type-state initialization flow) treats the
physical root of the password store (store_path) as if it were also
passless's own operating scope. It never receives the path value (e.g.
"fido2") that PassStorageAdapter otherwise uses for everything else
(get_fido2_path() = store_path.join(&self.path)).

This causes two related problems on any store where passless's scope is a
subdirectory with its own .gpg-id, rather than the store root:

  1. False "not initialized" prompt. Uninitialized::check_if_initialized
    only checks store_path.join(".gpg-id") directly. If that file doesn't
    exist — even though store_path/<path>/.gpg-id does — it concludes the
    store isn't initialized and proceeds to prompt_user, which shows a
    "Password Store Not Initialized" notification even though the store is
    fully functional and already has a valid .gpg-id for passless's scope.

  2. Risk of writing a spurious root .gpg-id. If the user goes through
    with the (unnecessary) prompt, GpgKeySelected::write_gpg_id writes
    unconditionally to store_path.join(".gpg-id"). For a store where other
    subtrees already manage independent .gpg-id policies (a common pass
    pattern — see resolve_recipients_for_target / find_nearest_gpg_id,
    which already implement hierarchical, closest-wins resolution for
    reading recipients), this introduces a new ancestor .gpg-id that acts
    as a fallback for any target under store_path that doesn't have a more
    specific one — an unintended, implicit change to the crypto policy of
    parts of the store passless has no business touching.

Root cause

ensure_initialized (storage/pass/init/mod.rs) has this signature:

pub fn ensure_initialized(
    store_path: &Path,
    gpg_backend: GpgBackend,
    allow_create_without_prompt: bool,
) -> Result<()>

It's called from storage/pass/mod.rs as:

self::init::ensure_initialized(&store_path, gpg_backend, allow_create_without_prompt)?;

path (the adapter's own scope, used everywhere else via get_fido2_path())
is simply never passed in. Every state in the chain
(Uninitialized → DirectoryCreated → GpgKeySelected → StoreInitialized → Complete) only carries store_path, so nothing in the initialization flow
can distinguish "the physical root of the repo" from "the directory passless
actually operates on."

Meanwhile, resolve_recipients_for_target / find_nearest_gpg_id in
storage/pass/gpg_id.rs already implement the correct hierarchical lookup
for resolving recipients of a file — closest .gpg-id wins, walking up to
store_root. Initialization doesn't reuse this at all.

Reproduction

Given a store like:

~/.password-store/
├── fido2/
│   └── .gpg-id
├── ID-Pessoal/
│   └── .../.gpg-id
└── ID-Work/
    └── .../.gpg-id

(no .gpg-id directly at ~/.password-store/), with passless configured
with path = "fido2":

  • Expected: passless recognizes fido2/.gpg-id as its applicable config and
    starts up silently.
  • Actual: check_if_initialized finds no ~/.password-store/.gpg-id,
    concludes "not initialized", and fires a desktop notification via dunst
    ("Password Store Not Initialized") on every relevant startup, even though
    the store is correctly configured and has been in use for a while.

Suggested direction

  • Thread path through ensure_initialized and every state struct in
    storage/pass/init/ (Uninitialized, DirectoryCreated,
    GpgKeySelected, StoreInitialized).
  • check_if_initialized and write_gpg_id should resolve against
    store_path.join(path) using the same walk-up-to-root logic already used
    for recipient resolution (a directory-oriented variant that also checks
    the scope directory itself, not just its parent, since
    find_nearest_gpg_id is written for a file target).
  • Only write a new .gpg-id at store_path.join(path) if nothing
    applicable is found anywhere between there and store_path — never write
    at store_path directly just because it's the physical root.
  • initialize_git_repo's git add .gpg-id assumes the file lives directly
    at store_path too; it should git add whatever path the .gpg-id
    actually ended up at.

Happy to submit a PR with this if it's useful, but wanted to flag the
architecture question first since I don't have visibility into every caller
of ensure_initialized or the test suite.

Unrelated minor findings in gpg_id.rs::parse_gpg_id_content

Not the cause of the above, but noticed while reading:

  • The 0x/0X prefix is stripped for hex validation, but the stored
    fingerprint keeps whatever prefix form the user wrote — worth deciding on
    one canonical internal format.
  • The check that rejects short key IDs only checks for exactly 8 hex chars;
    IDs of, say, 9–15 characters pass through even though the error message
    implies only 16-char long IDs or 40-char fingerprints are accepted.

Note: I used Claude to help me inspect the code and locate the point of failure; the problem is real and exists in my current pass file structure. I discovered this after deciding to delete the .gpg-id from the root and create a new one inside the fido2 directory.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions