check / check (push) Failing after 2s
script/lint-darwin (make lint-darwin; run by script/check, and its commands by the Dockerfile lint stage) runs go vet and golangci-lint with GOOS=darwin and cgo off. Compiling cgo for macOS needs Apple's SDK, so the three functions that call go-keychain, which is cgo there, move to keychainunlocker_cgo.go; a macOS build without cgo gets keychainunlocker_nocgo.go and the macse stub, whose errors name the missing macOS build with cgo. The rest of the keychain unlocker and its plain-Go tests are now checked; their findings are fixed without changing behaviour, and lines over 88 columns in the unchecked files are wrapped. Model: opus-5-5
105 lines
3.5 KiB
Go
105 lines
3.5 KiB
Go
//go:build darwin && cgo
|
|
|
|
package secret
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/awnumar/memguard"
|
|
keychain "github.com/keybase/go-keychain"
|
|
)
|
|
|
|
// The keychain unlocker's only calls into go-keychain, which is cgo on macOS.
|
|
// A macOS build without cgo gets keychainunlocker_nocgo.go instead.
|
|
|
|
// storeInKeychain stores data in the macOS keychain using keybase/go-keychain
|
|
func storeInKeychain(itemName string, data *memguard.LockedBuffer) error {
|
|
if data == nil {
|
|
return fmt.Errorf("data buffer is nil")
|
|
}
|
|
if err := validateKeychainItemName(itemName); err != nil {
|
|
return fmt.Errorf("invalid keychain item name: %w", err)
|
|
}
|
|
|
|
item := keychain.NewItem()
|
|
item.SetSecClass(keychain.SecClassGenericPassword)
|
|
item.SetService(KEYCHAIN_APP_IDENTIFIER)
|
|
item.SetAccount(itemName)
|
|
item.SetLabel(fmt.Sprintf("%s - %s", KEYCHAIN_APP_IDENTIFIER, itemName))
|
|
item.SetDescription("Secret vault keychain data")
|
|
item.SetData(data.Bytes())
|
|
item.SetSynchronizable(keychain.SynchronizableNo)
|
|
// Use AccessibleWhenUnlockedThisDeviceOnly for better security and to trigger auth
|
|
item.SetAccessible(keychain.AccessibleWhenUnlockedThisDeviceOnly)
|
|
|
|
// First try to delete any existing item
|
|
deleteItem := keychain.NewItem()
|
|
deleteItem.SetSecClass(keychain.SecClassGenericPassword)
|
|
deleteItem.SetService(KEYCHAIN_APP_IDENTIFIER)
|
|
deleteItem.SetAccount(itemName)
|
|
_ = keychain.DeleteItem(deleteItem) // Ignore error as item might not exist
|
|
|
|
// Add the new item
|
|
if err := keychain.AddItem(item); err != nil {
|
|
return fmt.Errorf("failed to store item in keychain: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// retrieveFromKeychain retrieves data from the macOS keychain using keybase/go-keychain
|
|
func retrieveFromKeychain(itemName string) ([]byte, error) {
|
|
if err := validateKeychainItemName(itemName); err != nil {
|
|
return nil, fmt.Errorf("invalid keychain item name: %w", err)
|
|
}
|
|
|
|
query := keychain.NewItem()
|
|
query.SetSecClass(keychain.SecClassGenericPassword)
|
|
query.SetService(KEYCHAIN_APP_IDENTIFIER)
|
|
query.SetAccount(itemName)
|
|
query.SetMatchLimit(keychain.MatchLimitOne)
|
|
query.SetReturnData(true)
|
|
|
|
results, err := keychain.QueryItem(query)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to retrieve item from keychain: %w", err)
|
|
}
|
|
|
|
if len(results) == 0 {
|
|
return nil, fmt.Errorf("keychain item not found: %s", itemName)
|
|
}
|
|
|
|
return results[0].Data, nil
|
|
}
|
|
|
|
// deleteFromKeychain removes an item from the macOS keychain using keybase/go-keychain
|
|
// If the item doesn't exist, this function returns nil (not an error) since the goal
|
|
// is to ensure the item is gone, and it already being gone satisfies that goal.
|
|
func deleteFromKeychain(itemName string) error {
|
|
if err := validateKeychainItemName(itemName); err != nil {
|
|
return fmt.Errorf("invalid keychain item name: %w", err)
|
|
}
|
|
|
|
item := keychain.NewItem()
|
|
item.SetSecClass(keychain.SecClassGenericPassword)
|
|
item.SetService(KEYCHAIN_APP_IDENTIFIER)
|
|
item.SetAccount(itemName)
|
|
|
|
if err := keychain.DeleteItem(item); err != nil {
|
|
// If the item doesn't exist, that's not an error - the goal is to ensure
|
|
// the item is gone, and it already being gone satisfies that goal.
|
|
// This is important for cleaning up unlocker directories when the keychain
|
|
// item has already been removed (e.g., manually by user, or synced vault
|
|
// from a different machine).
|
|
if err == keychain.ErrorItemNotFound {
|
|
Debug("Keychain item not found during deletion, ignoring", "item_name", itemName)
|
|
|
|
return nil
|
|
}
|
|
|
|
return fmt.Errorf("failed to delete item from keychain: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|