From 52ef38284dd47752f1e5d17ed70f4d645b3c19ca Mon Sep 17 00:00:00 2001 From: Dominik Schulz Date: Sun, 3 May 2026 13:37:03 +0200 Subject: [PATCH] [chore] Follow up to PR 3383 Signed-off-by: Dominik Schulz --- docs/commands/generate.md | 8 +- docs/exit-codes.md | 277 ++++++++++++++++++++++++++--- docs/security.md | 37 ++++ internal/action/commands.go | 2 +- internal/action/pwgen/commands.go | 4 +- internal/action/show.go | 2 +- internal/backend/crypto/age/ssh.go | 9 - 7 files changed, 301 insertions(+), 38 deletions(-) diff --git a/docs/commands/generate.md b/docs/commands/generate.md index 26862a2c4d..ab3b348ce2 100644 --- a/docs/commands/generate.md +++ b/docs/commands/generate.md @@ -6,9 +6,9 @@ Note: If you only want generate a password without storing it in the store, use ## Synopsis -``` -$ gopass generate entry [length] -$ gopass generate entry key [length] +```sh +gopass generate entry [length] +gopass generate entry key [length] ``` ## Modes of operation @@ -41,7 +41,7 @@ Use `--generator` to select one of the available password generators: | Generator | Description | |-------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `cryptic` | The default generator yields cryptic passwords that should work with most sites. Use `--symbols` and `--strict` if the site has specific requirements. Please note that we auto-detect the correct rules for some sites. The length argument specifies the number of characters. | -| `--xkcd` | Use an [XKCD#936](https://xkcd.com/936/) style password. Use `--xkcd-lang` and `--xkcd-sep` to refine its behaviour. The length argument specifies the number of words. | +| `xkcd` | Use an [XKCD#936](https://xkcd.com/936/) style password. Use `--xkcd-lang` and `--xkcd-sep` to refine its behaviour. The length argument specifies the number of words. | | `memorable` | Generate a memorable password. The length argument specifies the minimum lenght of characters. Please note that the password might be longer if not all necessary rules were satisfied by the minimum length solution. | | `external` | Use the external generator from `$GOPASS_EXTERNAL_PWGEN` | diff --git a/docs/exit-codes.md b/docs/exit-codes.md index 7e790c27d0..b1f6cc2d28 100644 --- a/docs/exit-codes.md +++ b/docs/exit-codes.md @@ -38,63 +38,298 @@ Run `gopass --exit-codes` to print this table at any time. ## Per-Command Summary -### `show` +### `audit` | Code | When | |-----:|------| -| 0 | Secret displayed successfully | -| 10 | Secret not found | -| 11 | Secret could not be decrypted | +| 0 | No issues found | +| 1 | Audit run itself failed | +| 13 | Store contents could not be listed | +| 14 | One or more weak passwords or issues detected | +| 18 | Report file could not be written | -### `insert` +### `cat` | Code | When | |-----:|------| -| 0 | Secret inserted successfully | -| 11 | Existing secret could not be read for append/key-insert | -| 12 | Secret could not be encrypted and saved | +| 0 | Content read or written successfully | +| 9 | No secret name provided | +| 11 | Secret could not be decrypted for output | +| 18 | I/O error reading from stdin or writing content | -### `generate` +### `clone` | Code | When | |-----:|------| -| 0 | Password generated and stored successfully | -| 12 | Generated secret could not be encrypted and saved | +| 0 | Store cloned successfully | +| 2 | No repository URL provided | +| 5 | Root store is already initialized (cannot clone over it) | +| 6 | Root store not initialized when trying to add a mount | +| 7 | Git clone operation failed | +| 8 | Adding the cloned store as a mount failed | +| 18 | Could not read repository URL or mount point interactively | -### `find` +### `config` | Code | When | |-----:|------| -| 0 | Matches found (or single match displayed) | -| 10 | No matching secret found | +| 0 | Config value displayed or set successfully | +| 2 | Wrong number of arguments | +| 1 | Config value could not be set | -### `delete` +### `convert` + +| Code | When | +|-----:|------| +| 0 | Store converted successfully | +| 2 | Unknown backend name given for `--storage` or `--crypto` | +| 10 | Named store not found | +| 1 | Conversion failed | + +### `copy` / `cp` + +| Code | When | +|-----:|------| +| 0 | Secret or directory copied successfully | +| 2 | Not enough arguments | +| 3 | Destination exists and user declined overwrite | +| 10 | Source path does not exist | +| 13 | Could not list source subtree | +| 18 | Copy operation failed | + +### `create` + +| Code | When | +|-----:|------| +| 0 | Secret created successfully | +| 1 | Interactive create wizard failed to initialize | +| 3 | User cancelled the create wizard | +| 18 | Generated password could not be copied to clipboard | + +### `delete` / `rm` | Code | When | |-----:|------| | 0 | Secret deleted successfully | +| 2 | No name provided; or multiple names with `-r`; or target is a directory without `-r` | +| 4 | `--key` value conflicts with an existing secret name | | 10 | Secret not found | +| 18 | Delete or YAML-key removal failed | +| 20 | Post-delete hook execution failed | -### `audit` +### `doctor` | Code | When | |-----:|------| -| 0 | No issues found | -| 14 | One or more weak passwords or issues detected | +| 0 | All checks passed | +| 21 | One or more checks failed | + +### `edit` + +| Code | When | +|-----:|------| +| 0 | Secret saved successfully | +| 2 | No name provided | +| 11 | Existing secret could not be decrypted before editing | +| 12 | Edited secret could not be encrypted and saved | +| 17 | Recipients for the secret are invalid | +| 20 | Pre-edit hook execution failed | + +### `env` + +| Code | When | +|-----:|------| +| 0 | Program executed successfully with secrets in environment | +| 2 | No program to execute; conflicting input-mode flags; non-secret path used with `--stdin` | +| 10 | Named secret not found | +| 13 | Store contents could not be listed | + +### `find` + +| Code | When | +|-----:|------| +| 0 | Matches found (or single match displayed) | +| 2 | No search pattern provided; or invalid regular expression | +| 3 | User aborted interactive selection | +| 10 | No matching secret found | +| 13 | Store contents could not be listed | ### `fsck` | Code | When | |-----:|------| | 0 | Store integrity OK | +| 10 | Specified filter path not found | | 15 | One or more integrity errors found | -### `doctor` +### `generate` | Code | When | |-----:|------| -| 0 | All checks passed | -| 21 | One or more checks failed | +| 0 | Password generated and stored successfully | +| 2 | Length argument is not a valid positive integer | +| 3 | User declined to overwrite existing secret | +| 9 | No secret name provided | +| 12 | Generated secret could not be encrypted and saved | +| 18 | Generated password could not be copied to clipboard | + +### `git` + +| Code | When | +|-----:|------| +| 0 | Git operation completed successfully | +| 2 | Not enough arguments for `git remote add` or `git remote rm` | +| 7 | VCS init or remote push operation failed | + +### `grep` + +| Code | When | +|-----:|------| +| 0 | Search completed (results printed or nothing matched) | +| 2 | No search argument provided; or invalid regular expression | +| 13 | Store contents could not be listed | + +### `history` + +| Code | When | +|-----:|------| +| 0 | Revision history displayed successfully | +| 2 | No secret name provided | +| 10 | Secret does not exist | +| 1 | Revision list could not be retrieved | + +### `init` + +| Code | When | +|-----:|------| +| 0 | Store initialized successfully | +| 1 | Store initialization failed | +| 6 | Store is not initialized (checked via `IsInitialized`) | + +### `insert` + +| Code | When | +|-----:|------| +| 0 | Secret inserted successfully | +| 1 | Editor could not be launched for buffer-based insert | +| 2 | YAML key could not be parsed | +| 3 | Secret exists and user declined overwrite | +| 9 | No secret name provided | +| 11 | Existing secret could not be read for append/key-insert | +| 12 | Secret could not be encrypted and saved | +| 18 | I/O error reading from stdin or prompting for password | + +### `link` + +| Code | When | +|-----:|------| +| 0 | Link created successfully | +| 2 | Not enough arguments | + +### `list` / `ls` + +| Code | When | +|-----:|------| +| 0 | Store contents listed successfully | +| 10 | Specified filter path not found | +| 13 | Store tree could not be built | + +### `merge` + +| Code | When | +|-----:|------| +| 0 | Secrets merged successfully | +| 2 | Missing source or destination argument | +| 11 | Source secret could not be decrypted | +| 12 | Merged secret could not be encrypted and saved | + +### `mounts` + +| Code | When | +|-----:|------| +| 0 | Mount added or removed successfully | +| 2 | No alias provided for `mounts remove`; or wrong argument count for `mounts add` | +| 8 | Mount operation failed | + +### `move` / `mv` + +| Code | When | +|-----:|------| +| 0 | Secret or directory moved successfully | +| 2 | Not exactly two arguments provided | +| 3 | Destination exists and user declined overwrite | +| 1 | Move operation failed | + +### `otp` + +| Code | When | +|-----:|------| +| 0 | OTP token generated successfully | +| 2 | No secret name provided | +| 10 | Secret contains no OTP key | +| 1 | OTP URI not found or token calculation failed | +| 18 | Token could not be copied to clipboard | + +### `process` + +| Code | When | +|-----:|------| +| 0 | Template processed and output written successfully | +| 2 | No file argument provided | +| 18 | Template file could not be read or processed | + +### `recipients` + +| Code | When | +|-----:|------| +| 0 | Recipient operation completed successfully | +| 3 | User aborted interactive key selection | +| 13 | Recipient list could not be retrieved | +| 17 | Recipient could not be added or removed | + +### `reorg` + +| Code | When | +|-----:|------| +| 0 | Store reorganized successfully | +| 2 | Secret count changed in editor; or invalid move in editor | +| 3 | User aborted confirmation | +| 4 | Running in non-interactive mode | +| 7 | Git commit after reorganization failed | +| 13 | Store contents could not be listed | + +### `show` + +| Code | When | +|-----:|------| +| 0 | Secret displayed successfully | +| 1 | Revision list could not be retrieved; or QR encoding failed | +| 2 | No name provided | +| 10 | Secret not found; or requested YAML key, line, or password field not found | +| 11 | Secret could not be decrypted | + +### `sync` + +| Code | When | +|-----:|------| +| 0 | Synchronization completed (sync does not emit specific non-zero codes; errors are logged as warnings) | + +### `templates` + +| Code | When | +|-----:|------| +| 0 | Template operation completed successfully | +| 2 | No template name provided for `templates rm` | +| 10 | Template not found for `templates rm` | +| 13 | Template list could not be retrieved | +| 18 | Template could not be read or written | + +### `update` + +| Code | When | +|-----:|------| +| 0 | gopass is up to date or update applied successfully | +| 1 | Update check or download failed | ## Scripting Example diff --git a/docs/security.md b/docs/security.md index 205fcd8525..c8ee2e5704 100644 --- a/docs/security.md +++ b/docs/security.md @@ -131,6 +131,43 @@ checks for expiration, minimum trust level, and the presence of an encryption sub-capability. Expired or untrusted keys are rejected, preventing silent encryption to keys that can no longer decrypt. +### Recipient File Integrity (Hash Pinning) + +The recipients file (`.gpg-id`) lives inside the git repository and lists the +public-key fingerprints/IDs that secrets are encrypted to. Because the file is +git-tracked, a collaborator — or an attacker with write access to the remote +repository — could push a commit that silently adds their own key, causing +future encrypt operations to include them as a recipient. + +gopass defends against this with a hash-pinning mechanism: + +1. **Hash computation.** Every time gopass writes the recipients file (via + `gopass recipients add`, `gopass recipients rm`, or `gopass init`), it + computes the SHA-256 digest of the serialised recipients list. + +2. **Local storage.** The digest is written to the **global** (machine-local) + gopass config file, stored outside the git repository, under the key + `recipients.hash` (or `recipients..hash` for mounted sub-stores). + Because the global config file is never committed to git, a remote attacker + cannot alter the expected value. + +3. **Verification on load.** When `recipients.check` is set to `true` (in the + global config or per-mount config), every load of the recipients file + recomputes the digest and compares it to the stored value. If they differ, + `ErrInvalidHash` is returned and the operation is aborted, alerting the + operator that the recipients file was modified outside of gopass. + +4. **Explicit acknowledgement.** A legitimate change — for example, pulling an + update that a teammate pushed after running `gopass recipients add` on their + machine — can be accepted with `gopass recipients ack`. This command + verifies the new recipient list interactively and then updates the stored + hash, re-pinning to the new value. + +The combination of out-of-band hash storage and explicit acknowledgement ensures +that the recipients list can only grow silently if the attacker also has +write access to the operator's local machine — at which point the entire +threat model has already collapsed. + ### OpenBSD Pledge On OpenBSD, gopass calls `protect.Pledge("stdio rpath wpath cpath tty proc diff --git a/internal/action/commands.go b/internal/action/commands.go index 0d0cb54d7e..b216debaee 100644 --- a/internal/action/commands.go +++ b/internal/action/commands.go @@ -91,7 +91,7 @@ func ShowFlags() []cli.Flag { }, &cli.BoolFlag{ Name: "unsafe", - Aliases: []string{"u"}, + Aliases: []string{"u", "f"}, // -f is deprecated, but we keep it for backward compatibility. Usage: "Display unsafe content (e.g. the password) even if safecontent is enabled", }, &cli.BoolFlag{ diff --git a/internal/action/pwgen/commands.go b/internal/action/pwgen/commands.go index 18b84b0bad..94df44d6e4 100644 --- a/internal/action/pwgen/commands.go +++ b/internal/action/pwgen/commands.go @@ -46,13 +46,13 @@ func GetCommands() []*cli.Command { }, &cli.StringFlag{ Name: "xkcd-sep", - Aliases: []string{"sep", "xkcdsep"}, + Aliases: []string{"sep", "xkcdsep", "xs"}, Usage: "Word separator for generated xkcd style password. If no separator is specified, the words are combined without spaces/separator and the first character of words is capitalised. This flag implies -xkcd", Value: " ", }, &cli.StringFlag{ Name: "xkcd-lang", - Aliases: []string{"lang", "xkcdlang"}, + Aliases: []string{"lang", "xkcdlang", "xl"}, Usage: "Language to generate password from, currently only en (english, default) or de are supported", Value: "en", }, diff --git a/internal/action/show.go b/internal/action/show.go index 9184bef3a5..7e164c39e4 100644 --- a/internal/action/show.go +++ b/internal/action/show.go @@ -254,7 +254,7 @@ func (s *secretHandler) showHandleOutput(ctx context.Context, name string, sec g if pw == "" && body == "" { if config.Bool(ctx, "show.safecontent") && !ctxutil.IsForce(ctx) { - out.Warning(ctx, "show.safecontent=true. Use -f to display password, if any") + out.Warning(ctx, "show.safecontent=true. Use -u to display password, if any") } return exit.Error(exit.NotFound, store.ErrEmptySecret, "%v", store.ErrEmptySecret) diff --git a/internal/backend/crypto/age/ssh.go b/internal/backend/crypto/age/ssh.go index 53ea1f5dd7..9339b21c90 100644 --- a/internal/backend/crypto/age/ssh.go +++ b/internal/backend/crypto/age/ssh.go @@ -27,15 +27,6 @@ var ( // getSSHIdentities returns all SSH identities available for the current user. func (a *Age) getSSHIdentities(ctx context.Context) (map[string]age.Identity, error) { - sshCacheMu.RLock() - if sshCache != nil { - defer sshCacheMu.RUnlock() - debug.Log("using sshCache") - - return sshCache, nil - } - sshCacheMu.RUnlock() - sshCacheMu.Lock() defer sshCacheMu.Unlock() // Re-check after acquiring the write lock (another goroutine may have