diff --git a/TODO.md b/TODO.md index 2a714f5..0017d73 100644 --- a/TODO.md +++ b/TODO.md @@ -25,6 +25,18 @@ Bring the repo into policy compliance in one commit: # Completed Steps +- 2026-10-04: A failed `secret unlocker add keychain` or + `secret unlocker add secure-enclave` no longer leaves its keychain item or + Secure Enclave key behind (https://git.eeqj.de/sneak/secret/issues/89). + `CreateSecureEnclaveUnlocker` gets the long-term key before it creates the + Secure Enclave key, so that a wrong passphrase creates none, and deletes the + key again if encrypting with it or writing the unlocker then fails. + `CreateKeychainUnlocker` writes all of the unlocker's files, the metadata + among them, before it stores the item in the keychain, and deletes the item + again if moving the unlocker into place then fails. A failure to delete is + reported along with the first error. The tests of this run only on macOS: + the Secure Enclave one in a build with cgo on a Mac with a Secure Enclave, + the keychain one in a build with cgo. - 2026-10-04: An age identity's private key goes into a locked buffer through `secret.IdentityToLockedBuffer` everywhere (https://git.eeqj.de/sneak/secret/issues/38): the vault's long-term key diff --git a/internal/secret/atomic_test.go b/internal/secret/atomic_test.go index cf9792c..a976205 100644 --- a/internal/secret/atomic_test.go +++ b/internal/secret/atomic_test.go @@ -8,6 +8,7 @@ import ( "testing" "filippo.io/age" + "git.eeqj.de/sneak/secret/internal/macse" "git.eeqj.de/sneak/secret/internal/secret" "git.eeqj.de/sneak/secret/internal/vault" "github.com/awnumar/memguard" @@ -899,3 +900,42 @@ func TestWriteDirRefusesExistingDir(t *testing.T) { }) } } + +// TestSecureEnclaveUnlockerFailureDeletesKey makes moving a new Secure +// Enclave unlocker into place fail after its Secure Enclave key is created: +// the key must be deleted again. Skipped when the add fails before that, as +// it does everywhere but in a macOS build with cgo on a Mac with a Secure +// Enclave. +func TestSecureEnclaveUnlockerFailureDeletesKey(t *testing.T) { + t.Parallel() + + mnemonic := testMnemonicBuffer(t) + base := afero.NewMemMapFs() + _, err := vault.CreateVault(base, testVaultStateDir, testVaultName, mnemonic) + require.NoError(t, err) + + // The unlocker's directory is named se-