From 4f19d685f146286ba19084d59de0608a8727226b Mon Sep 17 00:00:00 2001 From: Pete Woods Date: Thu, 18 May 2023 19:58:16 +0100 Subject: [PATCH] Add support for internet password items (#95) --- corefoundation.go | 10 ++++++ keychain.go | 81 +++++++++++++++++++++++++++++++++++++++++++++-- macos_test.go | 57 ++++++++++++++++++++++++++++----- 3 files changed, 138 insertions(+), 10 deletions(-) diff --git a/corefoundation.go b/corefoundation.go index 4153fd4..b7ee544 100644 --- a/corefoundation.go +++ b/corefoundation.go @@ -101,6 +101,13 @@ func CFDictionaryToMap(cfDict C.CFDictionaryRef) (m map[C.CFTypeRef]C.CFTypeRef) return } +// Int32ToCFNumber will return a CFNumberRef, must be released with Release(ref). +func Int32ToCFNumber(u int32) C.CFNumberRef { + sint := C.SInt32(u) + p := unsafe.Pointer(&sint) + return C.CFNumberCreate(C.kCFAllocatorDefault, C.kCFNumberSInt32Type, p) +} + // StringToCFString will return a CFStringRef and if non-nil, must be released with // Release(ref). func StringToCFString(s string) (C.CFStringRef, error) { @@ -186,6 +193,9 @@ func ConvertMapToCFDictionary(attr map[string]interface{}) (C.CFDictionaryRef, e } else { valueRef = C.CFTypeRef(C.kCFBooleanFalse) } + case int32: + valueRef = C.CFTypeRef(Int32ToCFNumber(val)) + defer Release(valueRef) case []byte: bytesRef, err := BytesToCFData(val) if err != nil { diff --git a/keychain.go b/keychain.go index a8ece3f..7d0a1ac 100644 --- a/keychain.go +++ b/keychain.go @@ -157,6 +157,18 @@ var secClassTypeRef = map[SecClass]C.CFTypeRef{ var ( // ServiceKey is for kSecAttrService ServiceKey = attrKey(C.CFTypeRef(C.kSecAttrService)) + + // ServerKey is for kSecAttrServer + ServerKey = attrKey(C.CFTypeRef(C.kSecAttrServer)) + // ProtocolKey is for kSecAttrProtocol + ProtocolKey = attrKey(C.CFTypeRef(C.kSecAttrProtocol)) + // AuthenticationTypeKey is for kSecAttrAuthenticationType + AuthenticationTypeKey = attrKey(C.CFTypeRef(C.kSecAttrAuthenticationType)) + // PortKey is for kSecAttrPort + PortKey = attrKey(C.CFTypeRef(C.kSecAttrPort)) + // PathKey is for kSecAttrPath + PathKey = attrKey(C.CFTypeRef(C.kSecAttrPath)) + // LabelKey is for kSecAttrLabel LabelKey = attrKey(C.CFTypeRef(C.kSecAttrLabel)) // AccountKey is for kSecAttrAccount @@ -167,6 +179,8 @@ var ( DataKey = attrKey(C.CFTypeRef(C.kSecValueData)) // DescriptionKey is for kSecAttrDescription DescriptionKey = attrKey(C.CFTypeRef(C.kSecAttrDescription)) + // CommentKey is for kSecAttrComment + CommentKey = attrKey(C.CFTypeRef(C.kSecAttrComment)) // CreationDateKey is for kSecAttrCreationDate CreationDateKey = attrKey(C.CFTypeRef(C.kSecAttrCreationDate)) // ModificationDateKey is for kSecAttrModificationDate @@ -256,6 +270,15 @@ func (k *Item) SetSecClass(sc SecClass) { k.attr[SecClassKey] = secClassTypeRef[sc] } +// SetInt32 sets an int32 attribute for a string key +func (k *Item) SetInt32(key string, v int32) { + if v != 0 { + k.attr[key] = v + } else { + delete(k.attr, key) + } +} + // SetString sets a string attibute for a string key func (k *Item) SetString(key string, s string) { if s != "" { @@ -265,11 +288,37 @@ func (k *Item) SetString(key string, s string) { } } -// SetService sets the service attribute +// SetService sets the service attribute (for generic application items) func (k *Item) SetService(s string) { k.SetString(ServiceKey, s) } +// SetServer sets the server attribute (for internet password items) +func (k *Item) SetServer(s string) { + k.SetString(ServerKey, s) +} + +// SetProtocol sets the protocol attribute (for internet password items) +// Example values are: "htps", "http", "smb " +func (k *Item) SetProtocol(s string) { + k.SetString(ProtocolKey, s) +} + +// SetAuthenticationType sets the authentication type attribute (for internet password items) +func (k *Item) SetAuthenticationType(s string) { + k.SetString(AuthenticationTypeKey, s) +} + +// SetPort sets the port attribute (for internet password items) +func (k *Item) SetPort(v int32) { + k.SetInt32(PortKey, v) +} + +// SetPath sets the path attribute (for internet password items) +func (k *Item) SetPath(s string) { + k.SetString(PathKey, s) +} + // SetAccount sets the account attribute func (k *Item) SetAccount(a string) { k.SetString(AccountKey, a) @@ -285,6 +334,11 @@ func (k *Item) SetDescription(s string) { k.SetString(DescriptionKey, s) } +// SetComment sets the comment attribute +func (k *Item) SetComment(s string) { + k.SetString(CommentKey, s) +} + // SetData sets the data attribute func (k *Item) SetData(b []byte) { if b != nil { @@ -391,11 +445,21 @@ func UpdateItem(queryItem Item, updateItem Item) error { // QueryResult stores all possible results from queries. // Not all fields are applicable all the time. Results depend on query. type QueryResult struct { - Service string + // For generic application items + Service string + + // For internet password items + Server string + Protocol string + AuthenticationType string + Port int32 + Path string + Account string AccessGroup string Label string Description string + Comment string Data []byte CreationDate time.Time ModificationDate time.Time @@ -480,6 +544,17 @@ func convertResult(d C.CFDictionaryRef) (*QueryResult, error) { switch attrKey(k) { case ServiceKey: result.Service = CFStringToString(C.CFStringRef(v)) + case ServerKey: + result.Server = CFStringToString(C.CFStringRef(v)) + case ProtocolKey: + result.Protocol = CFStringToString(C.CFStringRef(v)) + case AuthenticationTypeKey: + result.AuthenticationType = CFStringToString(C.CFStringRef(v)) + case PortKey: + val := CFNumberToInterface(C.CFNumberRef(v)) + result.Port = val.(int32) + case PathKey: + result.Path = CFStringToString(C.CFStringRef(v)) case AccountKey: result.Account = CFStringToString(C.CFStringRef(v)) case AccessGroupKey: @@ -488,6 +563,8 @@ func convertResult(d C.CFDictionaryRef) (*QueryResult, error) { result.Label = CFStringToString(C.CFStringRef(v)) case DescriptionKey: result.Description = CFStringToString(C.CFStringRef(v)) + case CommentKey: + result.Comment = CFStringToString(C.CFStringRef(v)) case DataKey: b, err := CFDataToBytes(C.CFDataRef(v)) if err != nil { diff --git a/macos_test.go b/macos_test.go index 16e78f5..ee249f4 100644 --- a/macos_test.go +++ b/macos_test.go @@ -4,7 +4,6 @@ package keychain import ( - "fmt" "testing" ) @@ -86,18 +85,60 @@ func TestGenericPasswordRef(t *testing.T) { } func TestInternetPassword(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.SetComment("this is the comment") + defer func() { _ = DeleteItem(item) }() + err := AddItem(item) + if err != nil { + t.Fatal(err) + } + query := NewItem() query.SetSecClass(SecClassInternetPassword) - query.SetLabel("github.com") + query.SetServer("8xs8h5x5dfc0AI5EzT81l.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) - } + t.Fatalf("Query Error: %v", err) + } + + if len(results) != 1 { + t.Fatalf("expected 1 result, got %d", len(results)) + } + + r := results[0] + if r.Protocol != "htps" { + t.Errorf("expected protocol 'htps' but got %q", r.Protocol) + } + if r.Server != "8xs8h5x5dfc0AI5EzT81l.com" { + t.Errorf("expected server '8xs8h5x5dfc0AI5EzT81l.com' but got %q", r.Server) + } + if r.Port != 1234 { + t.Errorf("expected port '1234' but got %d", r.Port) + } + if r.Path != "/this/is/the/path" { + t.Errorf("expected path '/this/is/the/path' but got %q", r.Path) + } + + if r.Account != "this-is-the-username" { + t.Errorf("expected account 'this-is-the-username' but got %q", r.Account) + } + if r.Label != "this is the label" { + t.Errorf("expected label 'this is the label' but got %q", r.Label) + } + if r.Comment != "this is the comment" { + t.Errorf("expected comment 'this is the comment' but got %q", r.Comment) } }