diff --git a/README.md b/README.md index e84fa4f..7179823 100644 --- a/README.md +++ b/README.md @@ -112,33 +112,6 @@ if err == keychain.ErrorNotFound { } ``` -### OS X - -Creating a new keychain and add an item to it: - -```go - -// Add a new key chain into ~/Application Support/Keychains, with the provided password -k, err := keychain.NewKeychain("mykeychain.keychain", "my keychain password") -if err != nil { - // Error creating -} - -// Create generic password item with service, account, label, password, access group -item := keychain.NewGenericPassword("MyService", "gabriel", "A label", []byte("toomanysecrets"), "A123456789.group.com.mycorp") -item.UseKeychain(k) -err := keychain.AddItem(item) -if err != nil { - // Error creating -} -``` - -Using a Keychain at path: - -```go -k, err := keychain.NewWithPath("mykeychain.keychain") -``` - ## iOS Bindable package in `bind`. iOS project in `ios`. Run that project to test iOS. diff --git a/keychain.go b/keychain.go index 2186919..556ca19 100644 --- a/keychain.go +++ b/keychain.go @@ -1,3 +1,4 @@ +//go:build darwin // +build darwin package keychain @@ -18,6 +19,20 @@ import ( "time" ) +// AccessibleKey is key for kSecAttrAccessible +var AccessibleKey = attrKey(C.CFTypeRef(C.kSecAttrAccessible)) +var accessibleTypeRef = map[Accessible]C.CFTypeRef{ + AccessibleWhenUnlocked: C.CFTypeRef(C.kSecAttrAccessibleWhenUnlocked), + AccessibleAfterFirstUnlock: C.CFTypeRef(C.kSecAttrAccessibleAfterFirstUnlock), + AccessibleAlways: C.CFTypeRef(C.kSecAttrAccessibleAlways), + AccessibleWhenUnlockedThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleWhenUnlockedThisDeviceOnly), + AccessibleAfterFirstUnlockThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly), + AccessibleAccessibleAlwaysThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleAlwaysThisDeviceOnly), + + // Only available in 10.10 + //AccessibleWhenPasscodeSetThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly), +} + // Error defines keychain errors type Error int diff --git a/macos.go b/macos.go deleted file mode 100644 index 4aaa163..0000000 --- a/macos.go +++ /dev/null @@ -1,184 +0,0 @@ -// +build darwin,!ios - -package keychain - -/* -#cgo LDFLAGS: -framework CoreFoundation -framework Security - -#include -#include -*/ -import "C" -import ( - "os" - "unsafe" -) - -// AccessibleKey is key for kSecAttrAccessible -var AccessibleKey = attrKey(C.CFTypeRef(C.kSecAttrAccessible)) -var accessibleTypeRef = map[Accessible]C.CFTypeRef{ - AccessibleWhenUnlocked: C.CFTypeRef(C.kSecAttrAccessibleWhenUnlocked), - AccessibleAfterFirstUnlock: C.CFTypeRef(C.kSecAttrAccessibleAfterFirstUnlock), - AccessibleAlways: C.CFTypeRef(C.kSecAttrAccessibleAlways), - AccessibleWhenUnlockedThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleWhenUnlockedThisDeviceOnly), - AccessibleAfterFirstUnlockThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly), - AccessibleAccessibleAlwaysThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleAlwaysThisDeviceOnly), - - // Only available in 10.10 - //AccessibleWhenPasscodeSetThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly), -} - -// DeleteItemRef deletes a keychain item reference. -func DeleteItemRef(ref C.CFTypeRef) error { - errCode := C.SecKeychainItemDelete(C.SecKeychainItemRef(ref)) - return checkError(errCode) -} - -var ( - // KeychainKey is key for kSecUseKeychain - KeychainKey = attrKey(C.CFTypeRef(C.kSecUseKeychain)) - // MatchSearchListKey is key for kSecMatchSearchList - MatchSearchListKey = attrKey(C.CFTypeRef(C.kSecMatchSearchList)) -) - -// Keychain represents the path to a specific OSX keychain -type Keychain struct { - path string -} - -// NewKeychain creates a new keychain file with a password -func NewKeychain(path string, password string) (Keychain, error) { - return newKeychain(path, password, false) -} - -// NewKeychainWithPrompt creates a new Keychain and prompts user for password -func NewKeychainWithPrompt(path string) (Keychain, error) { - return newKeychain(path, "", true) -} - -func newKeychain(path, password string, promptUser bool) (Keychain, error) { - pathRef := C.CString(path) - defer C.free(unsafe.Pointer(pathRef)) - - var errCode C.OSStatus - var kref C.SecKeychainRef - - if promptUser { - errCode = C.SecKeychainCreate(pathRef, C.UInt32(0), nil, C.Boolean(1), 0, &kref) //nolint - } else { - passwordRef := C.CString(password) - defer C.free(unsafe.Pointer(passwordRef)) - errCode = C.SecKeychainCreate(pathRef, C.UInt32(len(password)), unsafe.Pointer(passwordRef), C.Boolean(0), 0, &kref) //nolint - } - - if err := checkError(errCode); err != nil { - return Keychain{}, err - } - - // TODO: Without passing in kref I get 'One or more parameters passed to the function were not valid (-50)' - defer Release(C.CFTypeRef(kref)) - - return Keychain{ - path: path, - }, nil -} - -// NewWithPath to use an existing keychain -func NewWithPath(path string) Keychain { - return Keychain{ - path: path, - } -} - -// Status returns the status of the keychain -func (kc Keychain) Status() error { - // returns no error even if it doesn't exist - kref, err := openKeychainRef(kc.path) - if err != nil { - return err - } - defer C.CFRelease(C.CFTypeRef(kref)) - - var status C.SecKeychainStatus - return checkError(C.SecKeychainGetStatus(kref, &status)) -} - -// The returned SecKeychainRef, if non-nil, must be released via CFRelease. -func openKeychainRef(path string) (C.SecKeychainRef, error) { - pathName := C.CString(path) - defer C.free(unsafe.Pointer(pathName)) - - var kref C.SecKeychainRef - if err := checkError(C.SecKeychainOpen(pathName, &kref)); err != nil { //nolint - return 0, err - } - - return kref, nil -} - -// UnlockAtPath unlocks keychain at path -func UnlockAtPath(path string, password string) error { - kref, err := openKeychainRef(path) - defer Release(C.CFTypeRef(kref)) - if err != nil { - return err - } - passwordRef := C.CString(password) - defer C.free(unsafe.Pointer(passwordRef)) - return checkError(C.SecKeychainUnlock(kref, C.UInt32(len(password)), unsafe.Pointer(passwordRef), C.Boolean(1))) -} - -// LockAtPath locks keychain at path -func LockAtPath(path string) error { - kref, err := openKeychainRef(path) - defer Release(C.CFTypeRef(kref)) - if err != nil { - return err - } - return checkError(C.SecKeychainLock(kref)) -} - -// Delete the Keychain -func (kc *Keychain) Delete() error { - return os.Remove(kc.path) -} - -// Convert Keychain to CFTypeRef. -// The returned CFTypeRef, if non-nil, must be released via CFRelease. -func (kc Keychain) Convert() (C.CFTypeRef, error) { - keyRef, err := openKeychainRef(kc.path) - return C.CFTypeRef(keyRef), err -} - -type keychainArray []Keychain - -// Convert the keychainArray to a CFTypeRef. -// The returned CFTypeRef, if non-nil, must be released via CFRelease. -func (ka keychainArray) Convert() (C.CFTypeRef, error) { - var refs = make([]C.CFTypeRef, len(ka)) - var err error - - for idx, kc := range ka { - if refs[idx], err = kc.Convert(); err != nil { - // If we error trying to convert lets release any we converted before - for _, ref := range refs { - if ref != 0 { - Release(ref) - } - } - return 0, err - } - } - - return C.CFTypeRef(ArrayToCFArray(refs)), nil -} - -// SetMatchSearchList sets match type on keychains -func (k *Item) SetMatchSearchList(karr ...Keychain) { - k.attr[MatchSearchListKey] = keychainArray(karr) -} - -// UseKeychain tells item to use the specified Keychain -func (k *Item) UseKeychain(kc Keychain) { - k.attr[KeychainKey] = kc -} diff --git a/macos_test.go b/macos_test.go deleted file mode 100644 index 33eb032..0000000 --- a/macos_test.go +++ /dev/null @@ -1,234 +0,0 @@ -// +build darwin,!ios - -package keychain - -import ( - "fmt" - "os" - "path/filepath" - "testing" - "time" -) - -func TestUpdateItem(t *testing.T) { - var err error - - item := NewGenericPassword("TestAccess", "firsttest", "TestUpdateItem", []byte("toomanysecrets2"), "") - defer func() { _ = DeleteItem(item) }() - err = AddItem(item) - if err != nil { - t.Fatal(err) - } - - data1, err := GetGenericPassword("TestAccess", "firsttest", "TestUpdateItem", "") - if err != nil { - t.Fatal(err) - } - if string(data1) != "toomanysecrets2" { - t.Fatal("TestUpdateItem: new password does not match") - } - - updateItem := NewItem() - updateItem.SetSecClass(SecClassGenericPassword) - updateItem.SetService("TestAccess") - updateItem.SetAccount("firsttest") - updateItem.SetLabel("TestUpdateItem") - updateItem.SetData([]byte("toomanysecrets3")) - err = UpdateItem(item, updateItem) - if err != nil { - t.Fatal(err) - } - - data2, err := GetGenericPassword("TestAccess", "firsttest", "TestUpdateItem", "") - if err != nil { - t.Fatal(err) - } - if string(data2) != "toomanysecrets3" { - t.Fatal("TestUpdateItem: updated password does not match") - } -} - -func TestAddingAndQueryingNewKeychain(t *testing.T) { - keychainPath := tempPath(t) - defer func() { _ = os.Remove(keychainPath) }() - - service, account, label, accessGroup, password := "TestAddingAndQueryingNewKeychain", "test", "", "", "toomanysecrets" - - k, err := NewKeychain(keychainPath, "my password") - if err != nil { - t.Fatal(err) - } - - item := NewGenericPassword(service, account, label, []byte(password), accessGroup) - item.UseKeychain(k) - if err = AddItem(item); err != nil { - t.Fatal(err) - } - - query := NewItem() - query.SetSecClass(SecClassGenericPassword) - query.SetMatchSearchList(k) - query.SetService(service) - query.SetAccount(account) - query.SetLabel(label) - query.SetAccessGroup(accessGroup) - query.SetMatchLimit(MatchLimitOne) - query.SetReturnData(true) - - results, err := QueryItem(query) - if err != nil { - t.Fatal(err) - } - - if len(results) != 1 { - t.Fatalf("Expected 1 result, got %d", len(results)) - } else if string(results[0].Data) != password { - t.Fatalf("Expected password to be %s, got %s", password, results[0].Data) - } - - // Search default keychain to make sure it's not there - queryDefault := NewItem() - queryDefault.SetSecClass(SecClassGenericPassword) - queryDefault.SetService(service) - queryDefault.SetMatchLimit(MatchLimitOne) - queryDefault.SetReturnData(true) - resultsDefault, err := QueryItem(queryDefault) - if err != nil { - t.Fatal(err) - } - if len(resultsDefault) != 0 { - t.Fatalf("Expected no results") - } -} - -func tempPath(t *testing.T) string { - temp, err := RandomID("go-keychain-test-") - if err != nil { - t.Fatal(err) - } - return filepath.Join(os.TempDir(), temp+".keychain") -} - -func TestNewWithPath(t *testing.T) { - path := tempPath(t) - defer func() { _ = os.Remove(path) }() - kc, newErr := NewKeychain(path, "testkeychainpassword") - if newErr != nil { - t.Fatal(newErr) - } - - item := NewGenericPassword("MyService2", "gabriel2", "", []byte("toomanysecrets2"), "") - item.UseKeychain(kc) - if err := AddItem(item); err != nil { - t.Fatal(err) - } - - kc2 := NewWithPath(path) - - if lockErr := LockAtPath(path); lockErr != nil { - t.Fatal(lockErr) - } - - if unlockErr := UnlockAtPath(path, "testkeychainpassword"); unlockErr != nil { - t.Fatal(unlockErr) - } - - query := NewItem() - query.SetMatchSearchList(kc2) - query.SetService("MyService2") - query.SetAccount("gabriel2") - query.SetSecClass(SecClassGenericPassword) - query.SetMatchLimit(MatchLimitOne) - query.SetReturnAttributes(true) - query.SetReturnData(true) - results, err := QueryItem(query) - if err != nil { - t.Fatal(err) - } - if len(results) != 1 { - t.Fatalf("Should have 1 result, had %d", len(results)) - } - if string(results[0].Data) != "toomanysecrets2" { - t.Fatalf("Invalid password: %s", results[0].Data) - } - if results[0].CreationDate.Equal(time.Time{}) { - t.Fatal("Got null creation date") - } - if results[0].ModificationDate.Equal(time.Time{}) { - t.Fatal("Got null creation date") - } -} - -func TestStatus(t *testing.T) { - path := tempPath(t) - defer func() { _ = os.Remove(path) }() - k, newErr := NewKeychain(path, "testkeychainpassword") - if newErr != nil { - t.Fatal(newErr) - } - - if err := k.Status(); err != nil { - t.Fatal(err) - } - - nonexistent := NewWithPath(fmt.Sprintf("this_shouldnt_exist_%s", time.Now())) - if err := nonexistent.Status(); err != ErrorNoSuchKeychain { - t.Fatalf("Expected %v, get %v", ErrorNoSuchKeychain, err) - } -} - -func TestGenericPasswordRef(t *testing.T) { - service, account, label, accessGroup, password := "TestGenericPasswordRef", "test", "", "", "toomanysecrets" - - item := NewGenericPassword(service, account, label, []byte(password), accessGroup) - defer func() { _ = DeleteItem(item) }() - err := AddItem(item) - if err != nil { - t.Fatal(err) - } - - // Query reference and delete by reference - query := NewItem() - query.SetSecClass(SecClassGenericPassword) - query.SetService(service) - query.SetAccount(account) - query.SetMatchLimit(MatchLimitOne) - query.SetReturnRef(true) - ref, err := QueryItemRef(query) - if err != nil { - t.Fatal(err) - } else if ref == 0 { - t.Fatal("Missing result") - } else { - err = DeleteItemRef(ref) - if err != nil { - t.Fatal(err) - } - Release(ref) - } - - passwordAfter, err := GetGenericPassword(service, account, label, accessGroup) - if err != nil { - t.Fatal(err) - } - if passwordAfter != nil { - t.Fatal("Shouldn't have password") - } -} - -func TestInternetPassword(t *testing.T) { - query := NewItem() - query.SetSecClass(SecClassInternetPassword) - query.SetLabel("github.com") - query.SetMatchLimit(MatchLimitOne) - query.SetReturnAttributes(true) - results, err := QueryItem(query) - if err != nil { - // Error - t.Errorf("Query Error: %v", err) - } else { - for _, r := range results { - fmt.Printf("%#v\n", r.Account) - } - } -}