Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions corefoundation.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ CFArrayRef CFArrayCreateSafe2(CFAllocatorRef allocator, const uintptr_t *values,
}
*/
import "C"

import (
"bytes"
"encoding/gob"
"errors"
"fmt"
"math"
Expand Down Expand Up @@ -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 {
Comment on lines +225 to +226
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe there's a more efficient way of doing this?

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 {
Expand Down
63 changes: 47 additions & 16 deletions keychain.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ package keychain
#include <Security/Security.h>
*/
import "C"

import (
"bytes"
"encoding/gob"
"fmt"
"time"
)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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))
}
Expand Down
77 changes: 77 additions & 0 deletions macos_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@
package keychain

import (
"bytes"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestUpdateItem(t *testing.T) {
Expand Down Expand Up @@ -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)
})
}