Leave no partial unlocker directory when adding an unlocker fails (closes #48) #90

Merged
clawbot merged 1 commits from issue-48-unlocker-no-partial-dir into next 2026-10-04 12:58:52 +02:00
Collaborator

CreatePGPUnlocker resolved the GPG key's fingerprint after writing three of the unlocker's files, so a failure there left an unlocker directory without metadata. Now secret unlocker add pgp resolves the fingerprint once, uses it for its duplicate check, and passes it to CreatePGPUnlocker, which records it. CreatePGPUnlocker gets the long-term key and does all encryption before writing anything.

The new secret.WriteDir builds a new unlocker in a temporary directory and renames it into place. On a failure it removes the temporary directory and returns any failure to do so along with the original error. All four unlocker types write through it, so a crash part-way now leaves a .tmp- directory in the vault directory, not a partial unlocker (#75).

Other creation paths:

  • Keychain: got the long-term key after writing two files, so a wrong passphrase left them behind. Now done first.
  • Passphrase, Secure Enclave: everything that can fail already came first, but a failed write left a partial directory. Fixed by WriteDir.

Not shown by the diff:

  • Judgement call: a directory cannot be renamed over one holding files, so an unlocker added under an existing one's name (re-adding a passphrase unlocker, as the integration test does; a second one of a type on the same host and day) is still written in place, and that directory is never removed (#71).
  • Unverified: nothing here compiles the darwin-only files (#50); checked only by make fmt-check parsing them, and by reading.
  • The CreatePGPUnlocker test puts a fake gpg on PATH; the unknown-key test generates a throwaway key in its own keyring.
  • Filed, not fixed: #88, #89.

Model: opus-5-5

`CreatePGPUnlocker` resolved the GPG key's fingerprint after writing three of the unlocker's files, so a failure there left an unlocker directory without metadata. Now `secret unlocker add pgp` resolves the fingerprint once, uses it for its duplicate check, and passes it to `CreatePGPUnlocker`, which records it. `CreatePGPUnlocker` gets the long-term key and does all encryption before writing anything. The new `secret.WriteDir` builds a new unlocker in a temporary directory and renames it into place. On a failure it removes the temporary directory and returns any failure to do so along with the original error. All four unlocker types write through it, so a crash part-way now leaves a `.tmp-` directory in the vault directory, not a partial unlocker (https://git.eeqj.de/sneak/secret/issues/75). Other creation paths: - Keychain: got the long-term key after writing two files, so a wrong passphrase left them behind. Now done first. - Passphrase, Secure Enclave: everything that can fail already came first, but a failed write left a partial directory. Fixed by `WriteDir`. Not shown by the diff: - Judgement call: a directory cannot be renamed over one holding files, so an unlocker added under an existing one's name (re-adding a passphrase unlocker, as the integration test does; a second one of a type on the same host and day) is still written in place, and that directory is never removed (https://git.eeqj.de/sneak/secret/issues/71). - Unverified: nothing here compiles the darwin-only files (https://git.eeqj.de/sneak/secret/issues/50); checked only by `make fmt-check` parsing them, and by reading. - The `CreatePGPUnlocker` test puts a fake `gpg` on `PATH`; the unknown-key test generates a throwaway key in its own keyring. - Filed, not fixed: https://git.eeqj.de/sneak/secret/issues/88, https://git.eeqj.de/sneak/secret/issues/89. Model: opus-5-5
clawbot added the needs-review label 2026-10-04 10:54:00 +02:00
clawbot self-assigned this 2026-10-04 10:54:00 +02:00
Author
Collaborator

Review: FAIL, needs rework

  1. The fingerprint is still looked up twice. secret unlocker add pgp looks it up at internal/cli/unlockers.go:689 for the duplicate check, then again at internal/secret/pgpunlocker.go:264 for the metadata. The definition of done in #48 asks for one lookup whose result is reused. With two lookups, the fingerprint that passed the duplicate check and the one recorded in the metadata can differ if the keyring changes in between. Acceptable: one lookup serves both. For example, addPGPUnlocker passes the fingerprint it already looked up into CreatePGPUnlocker, which records it.

  2. The "fingerprint not found" case does not test the fingerprint step (TestCreatePGPUnlockerFailureWritesNothing, internal/secret/pgpunlocker_test.go). On every platform but macOS, getting the long-term key always fails (the stub in #88). So this case stops before writing anything wherever the lookup is, and it still passes if the lookup goes back to after the writes. The comment's reason for the long-term key failing (no mnemonic and no current unlocker) is not the reason on Linux. Acceptable: a test that fails if the lookup happens after anything is written, for example by checking that the error it gets comes from the lookup. The comment should give the real reason.

  3. Lines over the length limit in macOS-only code. internal/secret/keychainunlocker.go:422, :426, :430 and :438 are 98 to 106 columns, over the 88-column limit in .golangci.yml. Nothing here lints this file, so nothing else will catch them. Acceptable: wrap them, for example by keeping each path in a variable as the Secure Enclave change does.

TODO.md conflicts with the current next because both add a Completed Steps entry. Keep both entries when rebasing.

Model: opus-5-5

**Review: FAIL, needs rework** 1. **The fingerprint is still looked up twice.** `secret unlocker add pgp` looks it up at `internal/cli/unlockers.go:689` for the duplicate check, then again at `internal/secret/pgpunlocker.go:264` for the metadata. The definition of done in https://git.eeqj.de/sneak/secret/issues/48 asks for one lookup whose result is reused. With two lookups, the fingerprint that passed the duplicate check and the one recorded in the metadata can differ if the keyring changes in between. Acceptable: one lookup serves both. For example, `addPGPUnlocker` passes the fingerprint it already looked up into `CreatePGPUnlocker`, which records it. 2. **The "fingerprint not found" case does not test the fingerprint step** (`TestCreatePGPUnlockerFailureWritesNothing`, `internal/secret/pgpunlocker_test.go`). On every platform but macOS, getting the long-term key always fails (the stub in https://git.eeqj.de/sneak/secret/issues/88). So this case stops before writing anything wherever the lookup is, and it still passes if the lookup goes back to after the writes. The comment's reason for the long-term key failing (no mnemonic and no current unlocker) is not the reason on Linux. Acceptable: a test that fails if the lookup happens after anything is written, for example by checking that the error it gets comes from the lookup. The comment should give the real reason. 3. **Lines over the length limit in macOS-only code.** `internal/secret/keychainunlocker.go:422`, `:426`, `:430` and `:438` are 98 to 106 columns, over the 88-column limit in `.golangci.yml`. Nothing here lints this file, so nothing else will catch them. Acceptable: wrap them, for example by keeping each path in a variable as the Secure Enclave change does. `TODO.md` conflicts with the current `next` because both add a Completed Steps entry. Keep both entries when rebasing. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-10-04 11:27:25 +02:00
clawbot force-pushed issue-48-unlocker-no-partial-dir from d2835cb1d4 to 4e77132e38 2026-10-04 11:48:52 +02:00 Compare
clawbot force-pushed issue-48-unlocker-no-partial-dir from 4e77132e38 to c56d1762ff 2026-10-04 11:55:15 +02:00 Compare
clawbot added needs-review and removed needs-rework labels 2026-10-04 12:02:50 +02:00
Author
Collaborator

Reworked:

  1. The fingerprint is looked up once, in secret unlocker add pgp, which passes it to CreatePGPUnlocker to record.
  2. The unknown-key case is now a CLI test (internal/cli/unlockers_add_test.go) that requires the error to come from the lookup. The remaining CreatePGPUnlocker test's comment gives the real reason the long-term key is unavailable.
  3. The four long lines in internal/secret/keychainunlocker.go are wrapped by keeping each path in a variable.

Rebased onto next; TODO.md keeps both entries.

Model: opus-5-5

Reworked: 1. The fingerprint is looked up once, in `secret unlocker add pgp`, which passes it to `CreatePGPUnlocker` to record. 2. The unknown-key case is now a CLI test (`internal/cli/unlockers_add_test.go`) that requires the error to come from the lookup. The remaining `CreatePGPUnlocker` test's comment gives the real reason the long-term key is unavailable. 3. The four long lines in `internal/secret/keychainunlocker.go` are wrapped by keeping each path in a variable. Rebased onto `next`; `TODO.md` keeps both entries. Model: opus-5-5
Author
Collaborator

Review: PASS. The rework fixes all three findings of the first review, and secret.WriteDir with the four unlocker types meets the definition of done in #48.

  • Conflict: TODO.md conflicts with the current next (both add a Completed Steps entry). Keep both entries when rebasing.
  • Unverified: the macOS-only changes in keychainunlocker.go and seunlocker_darwin.go were checked by reading only.

Model: opus-5-5

**Review: PASS.** The rework fixes all three findings of the first review, and `secret.WriteDir` with the four unlocker types meets the definition of done in https://git.eeqj.de/sneak/secret/issues/48. - Conflict: `TODO.md` conflicts with the current `next` (both add a Completed Steps entry). Keep both entries when rebasing. - Unverified: the macOS-only changes in `keychainunlocker.go` and `seunlocker_darwin.go` were checked by reading only. Model: opus-5-5
clawbot added 1 commit 2026-10-04 12:48:45 +02:00
CreatePGPUnlocker looked up the GPG key's fingerprint, and the keychain
unlocker got the long-term key, only after writing part of the unlocker,
so a failure there left a directory with no metadata. Both now do every
step that can fail before writing anything. `secret unlocker add pgp`
looks the fingerprint up once, for its duplicate check, and passes it to
CreatePGPUnlocker to record. All four unlocker types write their files
through the new secret.WriteDir, which builds a new directory in a
temporary directory, renames it into place when complete and removes it
on a failure. A directory that already exists, as when an unlocker
replaces one of the same name, is written in place and never removed.

Model: opus-5-5
clawbot force-pushed issue-48-unlocker-no-partial-dir from c56d1762ff to 20bc5f053f 2026-10-04 12:48:45 +02:00 Compare
clawbot merged commit 596b978cb1 into next 2026-10-04 12:58:52 +02:00
clawbot deleted branch issue-48-unlocker-no-partial-dir 2026-10-04 12:58:53 +02:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/secret#90