Skip to content

Commit 94347bc

Browse files
darwin: implement keyring using purego
Use purego to call CoreFoundation and Security frameworks directly without CGo. Fixes #110 Signed-off-by: Alexander Yastrebov <yastrebov.alex@gmail.com>
1 parent 5c6f7e0 commit 94347bc

File tree

9 files changed

+292
-191
lines changed

9 files changed

+292
-191
lines changed

README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,7 @@ keyring instead of having the user type it on every invocation.
2121

2222
#### OS X
2323

24-
The OS X implementation depends on the `/usr/bin/security` binary for
25-
interfacing with the OS X keychain. It should be available by default.
24+
The OS X implementation uses [purego](https://pkg.go.dev/github.com/ebitengine/purego) to call Security Framework API.
2625

2726
#### Linux and *BSD
2827

go.mod

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,7 @@ require (
77
github.com/godbus/dbus/v5 v5.1.0
88
)
99

10-
require golang.org/x/sys v0.26.0 // indirect
10+
require (
11+
github.com/ebitengine/purego v0.10.0 // indirect
12+
golang.org/x/sys v0.26.0 // indirect
13+
)

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0=
22
github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8=
33
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
4+
github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=
5+
github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
46
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
57
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
68
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=

internal/darwin/framework.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
//go:build darwin
2+
3+
package darwin
4+
5+
import (
6+
"unsafe"
7+
8+
"github.com/ebitengine/purego"
9+
)
10+
11+
const (
12+
kCFStringEncodingUTF8 = 0x08000100
13+
kCFAllocatorDefault = 0
14+
)
15+
16+
type osStatus int32
17+
18+
const (
19+
errSecSuccess osStatus = 0 // No error.
20+
errSecDuplicateItem osStatus = -25299 // The specified item already exists in the keychain.
21+
errSecItemNotFound osStatus = -25300 // The specified item could not be found in the keychain.
22+
)
23+
24+
type _CFRange struct {
25+
location int64
26+
length int64
27+
}
28+
29+
var (
30+
kCFTypeDictionaryKeyCallBacks uintptr
31+
kCFTypeDictionaryValueCallBacks uintptr
32+
kCFBooleanTrue uintptr
33+
)
34+
35+
var (
36+
kSecClass uintptr
37+
kSecClassGenericPassword uintptr
38+
kSecAttrService uintptr
39+
kSecAttrAccount uintptr
40+
kSecValueData uintptr
41+
kSecReturnData uintptr
42+
kSecMatchLimit uintptr
43+
kSecMatchLimitAll uintptr
44+
)
45+
46+
var (
47+
CFDictionaryCreate func(allocator uintptr, keys, values *uintptr, numValues int64, keyCallBacks, valueCallBacks uintptr) uintptr
48+
CFStringCreateWithCString func(allocator uintptr, cStr string, encoding uint32) uintptr
49+
CFDataCreate func(alloc uintptr, bytes []byte, length int64) uintptr
50+
CFDataGetLength func(theData uintptr) int64
51+
CFDataGetBytes func(theData uintptr, theRange _CFRange, buffer []byte)
52+
CFRelease func(cf uintptr)
53+
)
54+
55+
var (
56+
SecItemCopyMatching func(query uintptr, result *uintptr) osStatus
57+
SecItemAdd func(query uintptr, result uintptr) osStatus
58+
SecItemUpdate func(query uintptr, attributesToUpdate uintptr) osStatus
59+
SecItemDelete func(query uintptr) osStatus
60+
)
61+
62+
func init() {
63+
cfLib := must(purego.Dlopen("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation", purego.RTLD_NOW|purego.RTLD_GLOBAL))
64+
65+
kCFTypeDictionaryKeyCallBacks = must(purego.Dlsym(cfLib, "kCFTypeDictionaryKeyCallBacks"))
66+
kCFTypeDictionaryValueCallBacks = must(purego.Dlsym(cfLib, "kCFTypeDictionaryValueCallBacks"))
67+
kCFBooleanTrue = deref(must(purego.Dlsym(cfLib, "kCFBooleanTrue")))
68+
69+
purego.RegisterLibFunc(&CFDictionaryCreate, cfLib, "CFDictionaryCreate")
70+
purego.RegisterLibFunc(&CFStringCreateWithCString, cfLib, "CFStringCreateWithCString")
71+
purego.RegisterLibFunc(&CFDataCreate, cfLib, "CFDataCreate")
72+
purego.RegisterLibFunc(&CFDataGetLength, cfLib, "CFDataGetLength")
73+
purego.RegisterLibFunc(&CFDataGetBytes, cfLib, "CFDataGetBytes")
74+
purego.RegisterLibFunc(&CFRelease, cfLib, "CFRelease")
75+
76+
secLib := must(purego.Dlopen("/System/Library/Frameworks/Security.framework/Security", purego.RTLD_NOW|purego.RTLD_GLOBAL))
77+
78+
kSecClass = deref(must(purego.Dlsym(secLib, "kSecClass")))
79+
kSecClassGenericPassword = deref(must(purego.Dlsym(secLib, "kSecClassGenericPassword")))
80+
kSecAttrService = deref(must(purego.Dlsym(secLib, "kSecAttrService")))
81+
kSecAttrAccount = deref(must(purego.Dlsym(secLib, "kSecAttrAccount")))
82+
kSecValueData = deref(must(purego.Dlsym(secLib, "kSecValueData")))
83+
kSecReturnData = deref(must(purego.Dlsym(secLib, "kSecReturnData")))
84+
kSecMatchLimit = deref(must(purego.Dlsym(secLib, "kSecMatchLimit")))
85+
kSecMatchLimitAll = deref(must(purego.Dlsym(secLib, "kSecMatchLimitAll")))
86+
87+
purego.RegisterLibFunc(&SecItemCopyMatching, secLib, "SecItemCopyMatching")
88+
purego.RegisterLibFunc(&SecItemAdd, secLib, "SecItemAdd")
89+
purego.RegisterLibFunc(&SecItemUpdate, secLib, "SecItemUpdate")
90+
purego.RegisterLibFunc(&SecItemDelete, secLib, "SecItemDelete")
91+
}
92+
93+
func deref(ptr uintptr) uintptr {
94+
// We take the address and then dereference it to trick
95+
// go vet from creating a possible miss-use of unsafe.Pointer
96+
// See https://github.com/golang/go/issues/41205
97+
return **(**uintptr)(unsafe.Pointer(&ptr))
98+
}
99+
100+
func must[T any](v T, err error) T {
101+
if err != nil {
102+
panic(err)
103+
}
104+
return v
105+
}

internal/darwin/keychain.go

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
//go:build darwin
2+
3+
package darwin
4+
5+
import (
6+
"errors"
7+
"fmt"
8+
)
9+
10+
var ErrNotFound = errors.New("secret not found in keyring")
11+
12+
func FindGenericPassword(service, username string) (string, error) {
13+
cfService := CFStringCreateWithCString(kCFAllocatorDefault, service, kCFStringEncodingUTF8)
14+
defer CFRelease(cfService)
15+
16+
cfAccount := CFStringCreateWithCString(kCFAllocatorDefault, username, kCFStringEncodingUTF8)
17+
defer CFRelease(cfAccount)
18+
19+
keys := []uintptr{
20+
kSecClass,
21+
kSecAttrService,
22+
kSecAttrAccount,
23+
kSecReturnData,
24+
}
25+
values := []uintptr{
26+
kSecClassGenericPassword,
27+
cfService,
28+
cfAccount,
29+
kCFBooleanTrue,
30+
}
31+
32+
query := CFDictionaryCreate(
33+
kCFAllocatorDefault,
34+
&keys[0], &values[0], int64(len(keys)),
35+
kCFTypeDictionaryKeyCallBacks,
36+
kCFTypeDictionaryValueCallBacks,
37+
)
38+
defer CFRelease(query)
39+
40+
var data uintptr
41+
st := SecItemCopyMatching(query, &data)
42+
if st == errSecItemNotFound {
43+
return "", ErrNotFound
44+
} else if st != errSecSuccess {
45+
return "", fmt.Errorf("error SecItemCopyMatching: %d", st)
46+
}
47+
defer CFRelease(data)
48+
49+
length := CFDataGetLength(data)
50+
if length < 0 {
51+
return "", fmt.Errorf("error CFDataGetLength: %d", length)
52+
}
53+
54+
buffer := make([]byte, length)
55+
CFDataGetBytes(data, _CFRange{0, length}, buffer)
56+
57+
return string(buffer), nil
58+
}
59+
60+
func AddGenericPassword(service, username, password string) error {
61+
cfService := CFStringCreateWithCString(kCFAllocatorDefault, service, kCFStringEncodingUTF8)
62+
defer CFRelease(cfService)
63+
64+
cfAccount := CFStringCreateWithCString(kCFAllocatorDefault, username, kCFStringEncodingUTF8)
65+
defer CFRelease(cfAccount)
66+
67+
cfPasswordData := CFDataCreate(kCFAllocatorDefault, []byte(password), int64(len(password)))
68+
defer CFRelease(cfPasswordData)
69+
70+
keys := []uintptr{
71+
kSecClass,
72+
kSecAttrService,
73+
kSecAttrAccount,
74+
kSecValueData,
75+
}
76+
values := []uintptr{
77+
kSecClassGenericPassword,
78+
cfService,
79+
cfAccount,
80+
cfPasswordData,
81+
}
82+
83+
query := CFDictionaryCreate(
84+
kCFAllocatorDefault,
85+
&keys[0], &values[0], int64(len(keys)),
86+
kCFTypeDictionaryKeyCallBacks,
87+
kCFTypeDictionaryValueCallBacks,
88+
)
89+
defer CFRelease(query)
90+
91+
sa := SecItemAdd(query, 0)
92+
if sa == errSecDuplicateItem {
93+
su := SecItemUpdate(query, query)
94+
if su != errSecSuccess {
95+
return fmt.Errorf("error SecItemUpdate: %d", su)
96+
}
97+
} else if sa != errSecSuccess {
98+
return fmt.Errorf("error SecItemAdd: %d", sa)
99+
}
100+
return nil
101+
}
102+
103+
func DeleteGenericPassword(service, username string) error {
104+
cfService := CFStringCreateWithCString(kCFAllocatorDefault, service, kCFStringEncodingUTF8)
105+
defer CFRelease(cfService)
106+
107+
cfAccount := CFStringCreateWithCString(kCFAllocatorDefault, username, kCFStringEncodingUTF8)
108+
defer CFRelease(cfAccount)
109+
110+
keys := []uintptr{
111+
kSecClass,
112+
kSecAttrService,
113+
kSecAttrAccount,
114+
}
115+
values := []uintptr{
116+
kSecClassGenericPassword,
117+
cfService,
118+
cfAccount,
119+
}
120+
121+
query := CFDictionaryCreate(
122+
kCFAllocatorDefault,
123+
&keys[0], &values[0], int64(len(keys)),
124+
kCFTypeDictionaryKeyCallBacks,
125+
kCFTypeDictionaryValueCallBacks,
126+
)
127+
defer CFRelease(query)
128+
129+
st := SecItemDelete(query)
130+
if st == errSecItemNotFound {
131+
return ErrNotFound
132+
} else if st != errSecSuccess {
133+
return fmt.Errorf("error SecItemDelete: %d", st)
134+
}
135+
return nil
136+
}
137+
138+
func DeleteGenericPasswords(service string) error {
139+
cfService := CFStringCreateWithCString(kCFAllocatorDefault, service, kCFStringEncodingUTF8)
140+
defer CFRelease(cfService)
141+
142+
keys := []uintptr{
143+
kSecClass,
144+
kSecAttrService,
145+
kSecMatchLimit,
146+
}
147+
values := []uintptr{
148+
kSecClassGenericPassword,
149+
cfService,
150+
kSecMatchLimitAll,
151+
}
152+
153+
query := CFDictionaryCreate(
154+
kCFAllocatorDefault,
155+
&keys[0], &values[0], int64(len(keys)),
156+
kCFTypeDictionaryKeyCallBacks,
157+
kCFTypeDictionaryValueCallBacks,
158+
)
159+
defer CFRelease(query)
160+
161+
st := SecItemDelete(query)
162+
if st == errSecItemNotFound {
163+
return nil
164+
} else if st != errSecSuccess {
165+
return fmt.Errorf("error SecItemDelete: %d", st)
166+
}
167+
return nil
168+
}

internal/shellescape/LICENSE

Lines changed: 0 additions & 21 deletions
This file was deleted.

internal/shellescape/shellescape.go

Lines changed: 0 additions & 39 deletions
This file was deleted.

0 commit comments

Comments
 (0)