Delete the keychain item or Secure Enclave key of a failed unlocker add (closes #89)
check / check (push) Failing after 2s

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
This commit was merged in pull request #106.
This commit is contained in:
2026-10-04 20:07:59 +02:00
parent 015730fb05
commit f2f89c8a06
8 changed files with 240 additions and 72 deletions
+27
View File
@@ -4,10 +4,13 @@ package secret
import (
"encoding/hex"
"os"
"path/filepath"
"runtime"
"testing"
"github.com/awnumar/memguard"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -185,3 +188,27 @@ func TestDeleteNonExistentKeychainItem(t *testing.T) {
assert.NoError(t, err,
"Deleting non-existent keychain item should not return an error")
}
// TestWriteKeychainUnlockerFailureDeletesItem makes moving a new keychain
// unlocker into place fail after its data is stored in the keychain: the
// keychain item must be deleted again.
func TestWriteKeychainUnlockerFailureDeletesItem(t *testing.T) {
testItemName := "test-secret-keychain-unlocker-cleanup"
_ = deleteFromKeychain(testItemName)
// Moving the unlocker into a read-only directory fails
unlockersDir := filepath.Join(t.TempDir(), "unlockers.d")
require.NoError(t, os.Mkdir(unlockersDir, 0o500))
testBuffer := memguard.NewBufferFromBytes([]byte("test-keychain-data"))
defer testBuffer.Destroy()
_, err := writeKeychainUnlocker(afero.NewOsFs(),
filepath.Join(unlockersDir, testItemName), testItemName, "age1test",
[]byte("test-priv"), []byte("test-longterm"), testBuffer)
require.ErrorIs(t, err, os.ErrPermission,
"moving the unlocker into place should fail")
_, err = retrieveFromKeychain(testItemName)
assert.Error(t, err, "keychain item left behind")
}