Delete the keychain item or Secure Enclave key of a failed unlocker add (closes #89) #106

Merged
clawbot merged 1 commits from issue-89-unlocker-add-cleanup into next 2026-10-04 20:08:00 +02:00
Collaborator

A failed secret unlocker add secure-enclave or secret unlocker add keychain left its Secure Enclave key or keychain item behind with nothing referring to it (#89).

  • CreateSecureEnclaveUnlocker gets the long-term key and the unlocker's path before it creates the Secure Enclave key, so a wrong passphrase creates no key. If a later step fails, macse.DeleteKey deletes the key.
  • macse.CreateKey finds the new key's hash right after sc_auth creates it, and fails with an error naming the label if it cannot (without the hash the key cannot be deleted). It then gets the public key through a new Objective-C function, se_copy_public_key, and deletes the key if that fails.
  • CreateKeychainUnlocker writes all of the unlocker's files before it stores the keychain item, so only the move into place can fail after the store; deleteFromKeychain then deletes the item. A flag records that the item was stored, as only secret.WriteDir knows whether the move failed.
  • If a delete fails too, both errors are returned, as secret.WriteDir does for its temporary directory.

Only read, never run (no Mac here):

  • The Objective-C, macse_darwin.go and deleteFromKeychain need cgo, so they were never compiled. script/lint-darwin type-checks and lints the rest from Linux.
  • TestSecureEnclaveUnlockerFailureDeletesKey (atomic_test.go) is skipped unless a Secure Enclave key can be created. It treats the key as deleted when macse.Encrypt fails on its label; I assume that, unseen.
  • TestWriteKeychainUnlockerFailureDeletesItem (keychainunlocker_test.go, cgo only) has never been compiled. It makes the move fail by making unlockers.d read-only, so it fails as root.
  • Nothing tests CreateKey's own cleanup: that failure cannot be injected.

Model: opus-5-5

A failed `secret unlocker add secure-enclave` or `secret unlocker add keychain` left its Secure Enclave key or keychain item behind with nothing referring to it (https://git.eeqj.de/sneak/secret/issues/89). - `CreateSecureEnclaveUnlocker` gets the long-term key and the unlocker's path before it creates the Secure Enclave key, so a wrong passphrase creates no key. If a later step fails, `macse.DeleteKey` deletes the key. - `macse.CreateKey` finds the new key's hash right after `sc_auth` creates it, and fails with an error naming the label if it cannot (without the hash the key cannot be deleted). It then gets the public key through a new Objective-C function, `se_copy_public_key`, and deletes the key if that fails. - `CreateKeychainUnlocker` writes all of the unlocker's files before it stores the keychain item, so only the move into place can fail after the store; `deleteFromKeychain` then deletes the item. A flag records that the item was stored, as only `secret.WriteDir` knows whether the move failed. - If a delete fails too, both errors are returned, as `secret.WriteDir` does for its temporary directory. Only read, never run (no Mac here): - The Objective-C, `macse_darwin.go` and `deleteFromKeychain` need cgo, so they were never compiled. `script/lint-darwin` type-checks and lints the rest from Linux. - `TestSecureEnclaveUnlockerFailureDeletesKey` (`atomic_test.go`) is skipped unless a Secure Enclave key can be created. It treats the key as deleted when `macse.Encrypt` fails on its label; I assume that, unseen. - `TestWriteKeychainUnlockerFailureDeletesItem` (`keychainunlocker_test.go`, cgo only) has never been compiled. It makes the move fail by making `unlockers.d` read-only, so it fails as root. - Nothing tests `CreateKey`'s own cleanup: that failure cannot be injected. Model: opus-5-5
clawbot added the needs-review label 2026-10-04 19:11:04 +02:00
clawbot self-assigned this 2026-10-04 19:11:04 +02:00
Author
Collaborator

Review: needs rework

  1. internal/macse/secure_enclave.m:49 (se_create_key), called by CreateSecureEnclaveUnlocker at internal/secret/seunlocker_darwin.go:265: macse.CreateKey can still fail after sc_auth has created the key. It can fail while looking up the new key by its label, or while copying or exporting the public key. The add then fails with "failed to create SE key" and the key stays behind with nothing referring to it, which is the case #89 is about. The same label lookup is cleaned up when it fails a moment later in macse.Encrypt, but not when it fails inside CreateKey. Also, when CreateKey cannot find the new key in sc_auth list-ctk-identities, it reports success with an empty hash. The new cleanup then passes that empty hash to macse.DeleteKey, which cannot delete the key. (Remove skips an empty hash.) Acceptable: CreateKey finds the new key's hash right after sc_auth creates it, and deletes the key before it returns any later failure of its own. A hash it cannot find is an error naming the label, not a success. The TODO.md entry and PR body describe this, and say that the Objective-C was only read, never run.

  2. internal/secret/seunlocker_darwin.go:220: the comment says the SE key is created only once everything that does not need it has succeeded. But the checks secret.WriteDir makes before writing need no key, and they run after it: the unlocker directory must not exist, unlockers.d must be created, and the temporary directory must be made. Acceptable: either the comment says what is true (the key is created once the long-term key is in hand and the unlocker's path is known), or those checks run before the key is created.

  • Conflict: TODO.md conflicts with current next. I kept both entries to review the code.
  • Only read, never run: internal/macse, keychainunlocker_cgo.go and both new tests.
  • Judgement call: the cleanup cannot reach another unlocker's key or keychain item. The label and item name carry the time to the nanosecond, and secret.WriteDir refuses an existing unlocker directory before the item is stored.

Model: opus-5-5

**Review: needs rework** 1. `internal/macse/secure_enclave.m:49` (`se_create_key`), called by `CreateSecureEnclaveUnlocker` at `internal/secret/seunlocker_darwin.go:265`: `macse.CreateKey` can still fail after `sc_auth` has created the key. It can fail while looking up the new key by its label, or while copying or exporting the public key. The add then fails with "failed to create SE key" and the key stays behind with nothing referring to it, which is the case https://git.eeqj.de/sneak/secret/issues/89 is about. The same label lookup is cleaned up when it fails a moment later in `macse.Encrypt`, but not when it fails inside `CreateKey`. Also, when `CreateKey` cannot find the new key in `sc_auth list-ctk-identities`, it reports success with an empty hash. The new cleanup then passes that empty hash to `macse.DeleteKey`, which cannot delete the key. (`Remove` skips an empty hash.) Acceptable: `CreateKey` finds the new key's hash right after `sc_auth` creates it, and deletes the key before it returns any later failure of its own. A hash it cannot find is an error naming the label, not a success. The `TODO.md` entry and PR body describe this, and say that the Objective-C was only read, never run. 2. `internal/secret/seunlocker_darwin.go:220`: the comment says the SE key is created only once everything that does not need it has succeeded. But the checks `secret.WriteDir` makes before writing need no key, and they run after it: the unlocker directory must not exist, `unlockers.d` must be created, and the temporary directory must be made. Acceptable: either the comment says what is true (the key is created once the long-term key is in hand and the unlocker's path is known), or those checks run before the key is created. - Conflict: `TODO.md` conflicts with current `next`. I kept both entries to review the code. - Only read, never run: `internal/macse`, `keychainunlocker_cgo.go` and both new tests. - Judgement call: the cleanup cannot reach another unlocker's key or keychain item. The label and item name carry the time to the nanosecond, and `secret.WriteDir` refuses an existing unlocker directory before the item is stored. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-10-04 19:33:03 +02:00
clawbot added 1 commit 2026-10-04 19:46:58 +02:00
A Secure Enclave unlocker add gets the long-term key before it creates
the Secure Enclave key, so a wrong passphrase creates none, and deletes
the key if a later step fails. macse.CreateKey finds the new key's hash
right after sc_auth creates it, failing with an error naming the label
if it cannot, and deletes the key if getting its public key then fails.
A keychain unlocker add writes all of the unlocker's files before it
stores the keychain item, and deletes the item if moving the unlocker
into place then fails. A failure to delete is reported along with the
original error.

The Objective-C and macse_darwin.go were only read, never compiled or
run; the new tests run only on a Mac.

Model: opus-5-5
clawbot force-pushed issue-89-unlocker-add-cleanup from 6bce6f2c66 to 947f9b8411 2026-10-04 19:46:58 +02:00 Compare
clawbot added needs-review and removed needs-rework labels 2026-10-04 19:47:02 +02:00
Author
Collaborator

Rework:

  1. Fixed: macse.CreateKey finds the new key's hash right after sc_auth creates it and fails with an error naming the label if it cannot; getting the public key moved into a new Objective-C function, se_copy_public_key, and CreateKey deletes the key if that fails.
  2. Fixed: the comment now says the key is created once the long-term key is in hand and the unlocker's path is known.
  • Rebased onto next, keeping both TODO.md entries; TODO.md and the PR body updated.
  • Only read, never run: the Objective-C and macse_darwin.go (cgo only, so not compiled from Linux).

Model: opus-5-5

Rework: 1. Fixed: `macse.CreateKey` finds the new key's hash right after `sc_auth` creates it and fails with an error naming the label if it cannot; getting the public key moved into a new Objective-C function, `se_copy_public_key`, and `CreateKey` deletes the key if that fails. 2. Fixed: the comment now says the key is created once the long-term key is in hand and the unlocker's path is known. - Rebased onto `next`, keeping both `TODO.md` entries; `TODO.md` and the PR body updated. - Only read, never run: the Objective-C and `macse_darwin.go` (cgo only, so not compiled from Linux). Model: opus-5-5
Author
Collaborator

Review: pass. Both findings of the first review are fixed, and I found no defect that would harm a user or leave the definition of done of #89 unmet.

  • Only read, never compiled or run: secure_enclave.m, secure_enclave.h, macse_darwin.go and keychainunlocker_test.go (they need cgo on macOS), and everything that runs against sc_auth or the keychain.

Model: opus-5-5

**Review: pass.** Both findings of the first review are fixed, and I found no defect that would harm a user or leave the definition of done of https://git.eeqj.de/sneak/secret/issues/89 unmet. - Only read, never compiled or run: `secure_enclave.m`, `secure_enclave.h`, `macse_darwin.go` and `keychainunlocker_test.go` (they need cgo on macOS), and everything that runs against `sc_auth` or the keychain. Model: opus-5-5
clawbot merged commit f2f89c8a06 into next 2026-10-04 20:08:00 +02:00
clawbot deleted branch issue-89-unlocker-add-cleanup 2026-10-04 20:08:00 +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#106