diff --git a/corefoundation.go b/corefoundation.go index b7ee544..2c4a840 100644 --- a/corefoundation.go +++ b/corefoundation.go @@ -28,7 +28,10 @@ CFArrayRef CFArrayCreateSafe2(CFAllocatorRef allocator, const uintptr_t *values, } */ import "C" + import ( + "bytes" + "encoding/gob" "errors" "fmt" "math" @@ -217,6 +220,18 @@ func ConvertMapToCFDictionary(attr map[string]interface{}) (C.CFDictionaryRef, e } valueRef = convertedRef defer Release(valueRef) + case map[string]any: + var b bytes.Buffer + enc := gob.NewEncoder(&b) + if err := enc.Encode(val); err != nil { + return 0, err + } + mapRef, err := BytesToCFData(b.Bytes()) + if err != nil { + return 0, err + } + valueRef = C.CFTypeRef(mapRef) + defer Release(valueRef) } keyRef, err := StringToCFString(key) if err != nil { diff --git a/keychain.go b/keychain.go index 7d0a1ac..368bd71 100644 --- a/keychain.go +++ b/keychain.go @@ -14,7 +14,10 @@ package keychain #include */ import "C" + import ( + "bytes" + "encoding/gob" "fmt" "time" ) @@ -148,11 +151,13 @@ var ( ) // SecClassKey is the key type for SecClass -var SecClassKey = attrKey(C.CFTypeRef(C.kSecClass)) -var secClassTypeRef = map[SecClass]C.CFTypeRef{ - SecClassGenericPassword: C.CFTypeRef(C.kSecClassGenericPassword), - SecClassInternetPassword: C.CFTypeRef(C.kSecClassInternetPassword), -} +var ( + SecClassKey = attrKey(C.CFTypeRef(C.kSecClass)) + secClassTypeRef = map[SecClass]C.CFTypeRef{ + SecClassGenericPassword: C.CFTypeRef(C.kSecClassGenericPassword), + SecClassInternetPassword: C.CFTypeRef(C.kSecClassInternetPassword), + } +) var ( // ServiceKey is for kSecAttrService @@ -185,6 +190,8 @@ var ( CreationDateKey = attrKey(C.CFTypeRef(C.kSecAttrCreationDate)) // ModificationDateKey is for kSecAttrModificationDate ModificationDateKey = attrKey(C.CFTypeRef(C.kSecAttrModificationDate)) + + AttrGenericKey = attrKey(C.CFTypeRef(C.kSecAttrGeneric)) ) // Synchronizable is the items synchronizable status @@ -202,12 +209,14 @@ const ( ) // SynchronizableKey is the key type for Synchronizable -var SynchronizableKey = attrKey(C.CFTypeRef(C.kSecAttrSynchronizable)) -var syncTypeRef = map[Synchronizable]C.CFTypeRef{ - SynchronizableAny: C.CFTypeRef(C.kSecAttrSynchronizableAny), - SynchronizableYes: C.CFTypeRef(C.kCFBooleanTrue), - SynchronizableNo: C.CFTypeRef(C.kCFBooleanFalse), -} +var ( + SynchronizableKey = attrKey(C.CFTypeRef(C.kSecAttrSynchronizable)) + syncTypeRef = map[Synchronizable]C.CFTypeRef{ + SynchronizableAny: C.CFTypeRef(C.kSecAttrSynchronizableAny), + SynchronizableYes: C.CFTypeRef(C.kCFBooleanTrue), + SynchronizableNo: C.CFTypeRef(C.kCFBooleanFalse), + } +) // Accessible is the items accessibility type Accessible int @@ -244,11 +253,13 @@ const ( ) // MatchLimitKey is key type for MatchLimit -var MatchLimitKey = attrKey(C.CFTypeRef(C.kSecMatchLimit)) -var matchTypeRef = map[MatchLimit]C.CFTypeRef{ - MatchLimitOne: C.CFTypeRef(C.kSecMatchLimitOne), - MatchLimitAll: C.CFTypeRef(C.kSecMatchLimitAll), -} +var ( + MatchLimitKey = attrKey(C.CFTypeRef(C.kSecMatchLimit)) + matchTypeRef = map[MatchLimit]C.CFTypeRef{ + MatchLimitOne: C.CFTypeRef(C.kSecMatchLimitOne), + MatchLimitAll: C.CFTypeRef(C.kSecMatchLimitAll), + } +) // ReturnAttributesKey is key type for kSecReturnAttributes var ReturnAttributesKey = attrKey(C.CFTypeRef(C.kSecReturnAttributes)) @@ -348,6 +359,13 @@ func (k *Item) SetData(b []byte) { } } +func (k *Item) SetGenericMetadata(m map[string]any) { + if m == nil { + return + } + k.attr[AttrGenericKey] = m +} + // SetAccessGroup sets the access group attribute func (k *Item) SetAccessGroup(ag string) { k.SetString(AccessGroupKey, ag) @@ -463,6 +481,8 @@ type QueryResult struct { Data []byte CreationDate time.Time ModificationDate time.Time + + Attributes map[string]interface{} } // QueryItemRef returns query result as CFTypeRef. You must release it when you are done. @@ -575,6 +595,17 @@ func convertResult(d C.CFDictionaryRef) (*QueryResult, error) { result.CreationDate = CFDateToTime(C.CFDateRef(v)) case ModificationDateKey: result.ModificationDate = CFDateToTime(C.CFDateRef(v)) + case AttrGenericKey: + b, err := CFDataToBytes(C.CFDataRef(v)) + if err != nil { + return nil, err + } + dec := gob.NewDecoder(bytes.NewReader(b)) + attributes := make(map[string]any) + if err := dec.Decode(&attributes); err != nil { + return nil, err + } + result.Attributes = attributes // default: // fmt.Printf("Unhandled key in conversion: %v = %v\n", cfTypeValue(k), cfTypeValue(v)) } diff --git a/macos_test.go b/macos_test.go index 7b3e57f..f5bad7e 100644 --- a/macos_test.go +++ b/macos_test.go @@ -4,7 +4,11 @@ package keychain import ( + "bytes" "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestUpdateItem(t *testing.T) { @@ -127,3 +131,76 @@ func TestInternetPassword(t *testing.T) { t.Errorf("expected comment 'this is the comment' but got %q", r.Comment) } } + +func TestGenericAttributes(t *testing.T) { + t.Run("generic password can have generic attributes", func(t *testing.T) { + service, account, label, accessGroup, password := "TestGenericPasswordRef", "test2", "generic-password", "TestGenericAttributes", "toomanysecrets" + item := NewGenericPassword(service, account, label, []byte(password), accessGroup) + attributes := map[string]any{ + "color": "green", + "large": string(bytes.Repeat([]byte{'a'}, 1024*1024)), + "score": 10, + } + t.Cleanup(func() { + queryDelete := NewItem() + queryDelete.SetAccessGroup(accessGroup) + queryDelete.SetAccount(account) + queryDelete.SetService(service) + queryDelete.SetSecClass(SecClassGenericPassword) + assert.NoError(t, DeleteItem(queryDelete)) + }) + + item.SetGenericMetadata(attributes) + require.NoError(t, AddItem(item)) + + query := NewItem() + query.SetReturnAttributes(true) + query.SetSecClass(SecClassGenericPassword) + query.SetMatchLimit(MatchLimitOne) + query.SetService(service) + query.SetAccount(account) + query.SetAccessGroup(accessGroup) + query.SetLabel(label) + + results, err := QueryItem(query) + require.NoError(t, err) + assert.Len(t, results, 1) + assert.EqualValues(t, attributes, results[0].Attributes) + }) + + t.Run("internet password cannot set generic attributes", func(t *testing.T) { + item := NewItem() + item.SetSecClass(SecClassInternetPassword) + + // Internet password-specific attributes + item.SetProtocol("htps") + item.SetServer("8xs8h5x5dfc0AI5EzT81l.com") + item.SetPort(1234) + item.SetPath("/this/is/the/path") + + item.SetAccount("this-is-the-username") + item.SetLabel("this is the label") + item.SetData([]byte("this is the password")) + item.SetGenericMetadata(map[string]any{ + "anything": "really", + }) + t.Cleanup(func() { + assert.NoError(t, DeleteItem(item)) + }) + require.NoError(t, AddItem(item)) + + query := NewItem() + query.SetSecClass(SecClassInternetPassword) + query.SetProtocol("htps") + query.SetServer("8xs8h5x5dfc0AI5EzT81l.com") + query.SetPort(1234) + query.SetPath("/this/is/the/path") + query.SetAccount("this-is-the-username") + query.SetLabel("this is the label") + query.SetReturnAttributes(true) + result, err := QueryItem(query) + assert.Len(t, result, 1) + assert.NoError(t, err) + assert.Empty(t, result[0].Attributes) + }) +}