mirror of
https://github.com/fleetdm/fleet
synced 2026-05-23 08:58:41 +00:00
#13832 For macOS hosts, fleetd now stores and retrieves enroll secret from macOS keychain. - this feature must use the official signed and notarized version of fleetd - for contributors, this feature can disabled with either: - fleetctl package flag: --disable-keystore - fleetd runtime flag: --disable-keystore This feature does not cover the MDM usecase where enroll secret is stored in the MDM profile. This usecase will hopefully be worked on next sprint with the MDM team. For Windows hosts, fleetd now stores and retrieves enroll secret from Windows Credential Manager. # Checklist for submitter If some of the following don't apply, delete the relevant line. <!-- Note that API documentation changes are now addressed by the product design team. --> - [x] Changes file added for user-visible changes in `changes/` or `orbit/changes/`. See [Changes files](https://fleetdm.com/docs/contributing/committing-changes#changes-files) for more information. - [x] Added/updated tests - [x] Manual QA for all new/changed functionality - For Orbit and Fleet Desktop changes: - [x] Manual QA must be performed in the three main OSs, macOS, Windows and Linux. - [x] Auto-update manual QA, from released version of component to new version (see [tools/tuf/test](../tools/tuf/test/README.md)).
173 lines
5.3 KiB
Go
173 lines
5.3 KiB
Go
//go:build darwin && cgo
|
|
|
|
package keystore
|
|
|
|
/*
|
|
#cgo LDFLAGS: -framework CoreFoundation -framework Security
|
|
#include <CoreFoundation/CoreFoundation.h>
|
|
#include <Security/Security.h>
|
|
*/
|
|
import "C"
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
"unsafe"
|
|
)
|
|
|
|
const service = "com.fleetdm.fleetd.enroll.secret"
|
|
|
|
var serviceStringRef = stringToCFString(service)
|
|
var mu sync.Mutex
|
|
|
|
func Supported() bool {
|
|
return true
|
|
}
|
|
|
|
func Name() string {
|
|
return "default keychain"
|
|
}
|
|
|
|
// AddSecret will add a secret to the keychain. This secret can be retrieved by this application without any user authorization.
|
|
func AddSecret(secret string) error {
|
|
secret = strings.TrimSpace(secret)
|
|
if secret == "" {
|
|
return errors.New("secret cannot be empty")
|
|
}
|
|
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
|
|
query := C.CFDictionaryCreateMutable(
|
|
C.kCFAllocatorDefault,
|
|
0,
|
|
&C.kCFTypeDictionaryKeyCallBacks,
|
|
&C.kCFTypeDictionaryValueCallBacks,
|
|
)
|
|
defer C.CFRelease(C.CFTypeRef(query))
|
|
|
|
data := C.CFDataCreate(C.kCFAllocatorDefault, (*C.UInt8)(&[]byte(secret)[0]), C.CFIndex(len(secret)))
|
|
defer C.CFRelease(C.CFTypeRef(data))
|
|
|
|
C.CFDictionaryAddValue(query, unsafe.Pointer(C.kSecClass), unsafe.Pointer(C.kSecClassGenericPassword))
|
|
C.CFDictionaryAddValue(query, unsafe.Pointer(C.kSecAttrService), unsafe.Pointer(serviceStringRef))
|
|
C.CFDictionaryAddValue(query, unsafe.Pointer(C.kSecValueData), unsafe.Pointer(data))
|
|
|
|
status := C.SecItemAdd(C.CFDictionaryRef(query), nil)
|
|
if status != C.errSecSuccess {
|
|
return fmt.Errorf("failed to add %v to keychain: %v", service, status)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// UpdateSecret will update a secret in the keychain. This secret can be retrieved by this application without any user authorization.
|
|
func UpdateSecret(secret string) error {
|
|
secret = strings.TrimSpace(secret)
|
|
if secret == "" {
|
|
return errors.New("secret cannot be empty")
|
|
}
|
|
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
|
|
query := C.CFDictionaryCreateMutable(
|
|
C.kCFAllocatorDefault,
|
|
0,
|
|
&C.kCFTypeDictionaryKeyCallBacks,
|
|
&C.kCFTypeDictionaryValueCallBacks,
|
|
)
|
|
defer C.CFRelease(C.CFTypeRef(query))
|
|
|
|
C.CFDictionaryAddValue(query, unsafe.Pointer(C.kSecClass), unsafe.Pointer(C.kSecClassGenericPassword))
|
|
C.CFDictionaryAddValue(query, unsafe.Pointer(C.kSecAttrService), unsafe.Pointer(serviceStringRef))
|
|
|
|
update := C.CFDictionaryCreateMutable(
|
|
C.kCFAllocatorDefault,
|
|
0,
|
|
&C.kCFTypeDictionaryKeyCallBacks,
|
|
&C.kCFTypeDictionaryValueCallBacks,
|
|
)
|
|
defer C.CFRelease(C.CFTypeRef(update))
|
|
|
|
data := C.CFDataCreate(C.kCFAllocatorDefault, (*C.UInt8)(&[]byte(secret)[0]), C.CFIndex(len(secret)))
|
|
defer C.CFRelease(C.CFTypeRef(data))
|
|
C.CFDictionaryAddValue(update, unsafe.Pointer(C.kSecValueData), unsafe.Pointer(data))
|
|
|
|
status := C.SecItemUpdate(C.CFDictionaryRef(query), C.CFDictionaryRef(update))
|
|
if status != C.errSecSuccess {
|
|
return fmt.Errorf("failed to update %v in keychain: %v", service, status)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetSecret will retrieve a secret from the keychain. If secret doesn't exist, it will return "", nil.
|
|
// If the secret was added by user or another application,
|
|
// then this application needs to be authorized to retrieve the secret.
|
|
func GetSecret() (string, error) {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
|
|
var query C.CFMutableDictionaryRef
|
|
query = C.CFDictionaryCreateMutable(
|
|
C.kCFAllocatorDefault,
|
|
0,
|
|
&C.kCFTypeDictionaryKeyCallBacks,
|
|
&C.kCFTypeDictionaryValueCallBacks,
|
|
)
|
|
defer C.CFRelease(C.CFTypeRef(query))
|
|
|
|
C.CFDictionaryAddValue(query, unsafe.Pointer(C.kSecClass), unsafe.Pointer(C.kSecClassGenericPassword))
|
|
C.CFDictionaryAddValue(query, unsafe.Pointer(C.kSecReturnData), unsafe.Pointer(C.kCFBooleanTrue))
|
|
C.CFDictionaryAddValue(query, unsafe.Pointer(C.kSecMatchLimit), unsafe.Pointer(C.kSecMatchLimitOne))
|
|
C.CFDictionaryAddValue(query, unsafe.Pointer(C.kSecAttrLabel), unsafe.Pointer(serviceStringRef))
|
|
|
|
var data C.CFTypeRef
|
|
status := C.SecItemCopyMatching(C.CFDictionaryRef(query), &data)
|
|
if status != C.errSecSuccess {
|
|
if status == C.errSecItemNotFound {
|
|
return "", nil
|
|
}
|
|
return "", fmt.Errorf("failed to retrieve %v from keychain: %v", service, status)
|
|
}
|
|
defer C.CFRelease(data)
|
|
|
|
secret := C.CFDataGetBytePtr(C.CFDataRef(data))
|
|
return C.GoString((*C.char)(unsafe.Pointer(secret))), nil
|
|
}
|
|
|
|
// deleteSecret will delete a secret from the keychain.
|
|
// This function is only used by tests. It is here because usage of CGO in tests is not supported.
|
|
func deleteSecret() error {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
|
|
query := C.CFDictionaryCreateMutable(
|
|
C.kCFAllocatorDefault,
|
|
0,
|
|
&C.kCFTypeDictionaryKeyCallBacks,
|
|
&C.kCFTypeDictionaryValueCallBacks,
|
|
)
|
|
defer C.CFRelease(C.CFTypeRef(query))
|
|
|
|
C.CFDictionaryAddValue(query, unsafe.Pointer(C.kSecClass), unsafe.Pointer(C.kSecClassGenericPassword))
|
|
C.CFDictionaryAddValue(query, unsafe.Pointer(C.kSecAttrService), unsafe.Pointer(serviceStringRef))
|
|
|
|
status := C.SecItemDelete(C.CFDictionaryRef(query))
|
|
if status != C.errSecSuccess {
|
|
return fmt.Errorf("failed to delete %v from keychain: %v", service, status)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// stringToCFString will return a CFStringRef
|
|
func stringToCFString(s string) C.CFStringRef {
|
|
bytes := []byte(s)
|
|
ptr := (*C.UInt8)(&bytes[0])
|
|
return C.CFStringCreateWithBytes(C.kCFAllocatorDefault, ptr, C.CFIndex(len(bytes)), C.kCFStringEncodingUTF8, C.false)
|
|
}
|
|
|
|
// releaseCFString will release memory allocated for a CFStringRef
|
|
func releaseCFString(s C.CFStringRef) {
|
|
C.CFRelease(C.CFTypeRef(s))
|
|
}
|