A secret add --force that fails to unlock the vault still leaves a new version behind #136

Open
opened 2026-10-13 00:29:40 +02:00 by clawbot · 0 comments
Collaborator

Severity: major
A secret add --force that fails reports an error yet has changed the secret: it leaves a new version behind that version list shows, so a failed command is not a no-op and the version history gains an entry the user never saw succeed. Nothing is lost.

What happens: addVersion writes the new version directory first, then unlocks the vault to stamp notAfter on the previous version, then rewrites current. The unlock is the step that fails in normal use: a mistyped passphrase, or a wrong SB_SECRET_MNEMONIC. By then the version is already in versions/, named as the newest serial, with the new value encrypted in it; current still names the old version. A crash between the first and last step leaves the same state.

Reproduction:

SB_UNLOCK_PASSPHRASE=pw SB_SECRET_MNEMONIC="$M" secret init
echo -n v1 | SB_UNLOCK_PASSPHRASE=pw secret add foo
echo -n v2 | SB_UNLOCK_PASSPHRASE=wrong secret add --force foo
# Error: failed to get long-term key: ... no identity matched any of the recipients ...; exit 1
SB_UNLOCK_PASSPHRASE=pw secret version list foo
# 20261012.002  ...  expired  (the failed add's version, notAfter unset)
# 20261012.001  ...  current
SB_UNLOCK_PASSPHRASE=pw secret get foo    # v1

Every further failed add --force adds one more. generate secret --force and import --force take the same path.

Where:

func (v *Vault) addVersion(
(addVersion): createAndSaveVersion at L225 runs before updatePreviousVersion at L232, which is where GetOrDeriveLongTermKey first runs for an existing secret.

Correct behaviour: a failed add changes nothing. Get the long-term key (and load the previous version's metadata) before writing the new version, so a wrong passphrase or mnemonic fails before anything is written; only the rename of current can then still fail.

Definition of done

  • After add --force fails to unlock the vault, version list shows the same versions as before and the versions directory is unchanged.
  • A regression test records the secret directory, runs AddSecret with force and a wrong passphrase, and requires the directory unchanged; it fails before the fix.

Model: fable-5-1

Severity: major A `secret add --force` that fails reports an error yet has changed the secret: it leaves a new version behind that `version list` shows, so a failed command is not a no-op and the version history gains an entry the user never saw succeed. Nothing is lost. What happens: `addVersion` writes the new version directory first, then unlocks the vault to stamp `notAfter` on the previous version, then rewrites `current`. The unlock is the step that fails in normal use: a mistyped passphrase, or a wrong `SB_SECRET_MNEMONIC`. By then the version is already in `versions/`, named as the newest serial, with the new value encrypted in it; `current` still names the old version. A crash between the first and last step leaves the same state. Reproduction: ``` SB_UNLOCK_PASSPHRASE=pw SB_SECRET_MNEMONIC="$M" secret init echo -n v1 | SB_UNLOCK_PASSPHRASE=pw secret add foo echo -n v2 | SB_UNLOCK_PASSPHRASE=wrong secret add --force foo # Error: failed to get long-term key: ... no identity matched any of the recipients ...; exit 1 SB_UNLOCK_PASSPHRASE=pw secret version list foo # 20261012.002 ... expired (the failed add's version, notAfter unset) # 20261012.001 ... current SB_UNLOCK_PASSPHRASE=pw secret get foo # v1 ``` Every further failed `add --force` adds one more. `generate secret --force` and `import --force` take the same path. Where: https://git.eeqj.de/sneak/secret/src/commit/ef0ae90768c8b18a829033b3ebd4cf98fcaeb3d1/internal/vault/secrets.go#L218 (`addVersion`): `createAndSaveVersion` at L225 runs before `updatePreviousVersion` at L232, which is where `GetOrDeriveLongTermKey` first runs for an existing secret. Correct behaviour: a failed `add` changes nothing. Get the long-term key (and load the previous version's metadata) before writing the new version, so a wrong passphrase or mnemonic fails before anything is written; only the rename of `current` can then still fail. **Definition of done** - After `add --force` fails to unlock the vault, `version list` shows the same versions as before and the versions directory is unchanged. - A regression test records the secret directory, runs `AddSecret` with `force` and a wrong passphrase, and requires the directory unchanged; it fails before the fix. Model: fable-5-1
sneak was assigned by clawbot 2026-10-13 00:29:41 +02:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/secret#136