diff --git a/corefoundation.go b/corefoundation.go index 224f1ee..e5cc226 100644 --- a/corefoundation.go +++ b/corefoundation.go @@ -43,6 +43,12 @@ func Release(ref C.CFTypeRef) { C.CFRelease(ref) } +// releaseCFDictionary releases a CFDictionaryRef. Wrapper around a C function +// for testing (cgo is not supported directly in _test.go files). +func releaseCFDictionary(d C.CFDictionaryRef) { + Release(C.CFTypeRef(d)) +} + // BytesToCFData will return a CFDataRef and if non-nil, must be released with // Release(ref). func BytesToCFData(b []byte) (C.CFDataRef, error) { diff --git a/ios.go b/ios.go index 9332508..403c16f 100644 --- a/ios.go +++ b/ios.go @@ -20,3 +20,12 @@ var accessibleTypeRef = map[Accessible]C.CFTypeRef{ AccessibleAfterFirstUnlockThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly), AccessibleAccessibleAlwaysThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleAlwaysThisDeviceOnly), } + +// UseDataProtectionKeychainKey has no effect on iOS, where the data protection +// keychain is the only keychain. It is defined for cross-platform API parity +// and is always empty. +var UseDataProtectionKeychainKey string + +// SetUseDataProtectionKeychain is a no-op on iOS, where the data protection +// keychain is the only keychain. It is defined for cross-platform API parity. +func (*Item) SetUseDataProtectionKeychain(_ bool) {} diff --git a/keychain_test.go b/keychain_test.go new file mode 100644 index 0000000..26c2f7e --- /dev/null +++ b/keychain_test.go @@ -0,0 +1,73 @@ +//go:build darwin && !ios + +package keychain + +import ( + "testing" +) + +func TestUseDataProtectionKeychainKey(t *testing.T) { + if !dataProtectionKeychainAvailable() { + // On macOS < 10.15 the key is intentionally empty (see macos.go). + if UseDataProtectionKeychainKey != "" { + t.Fatalf("expected empty key on macOS < 10.15, got %q", UseDataProtectionKeychainKey) + } + t.Skip("kSecUseDataProtectionKeychain unavailable (macOS < 10.15)") + } + + // On macOS 10.15+ the kSecUseDataProtectionKeychain binding must resolve to + // a non-empty key. An empty key here would mean the cgo binding or the + // __builtin_available guard in macos.go is broken. + if UseDataProtectionKeychainKey == "" { + t.Fatal("UseDataProtectionKeychainKey is empty on macOS 10.15+; kSecUseDataProtectionKeychain binding failed") + } +} + +func TestSetUseDataProtectionKeychain(t *testing.T) { + if UseDataProtectionKeychainKey == "" { + t.Skip("kSecUseDataProtectionKeychain unavailable (macOS < 10.15)") + } + + item := NewItem() + if _, ok := item.attr[UseDataProtectionKeychainKey]; ok { + t.Fatal("expected no data protection attribute on a fresh item") + } + + item.SetUseDataProtectionKeychain(true) + v, ok := item.attr[UseDataProtectionKeychainKey] + if !ok { + t.Fatal("expected data protection attribute to be set") + } + if v != true { + t.Fatalf("expected attribute value true, got %v", v) + } + + item.SetUseDataProtectionKeychain(false) + v, ok = item.attr[UseDataProtectionKeychainKey] + if !ok { + t.Fatal("expected data protection attribute to remain set when false") + } + if v != false { + t.Fatalf("expected attribute value false, got %v", v) + } +} + +func TestUseDataProtectionKeychainConvertsToCFDictionary(t *testing.T) { + if UseDataProtectionKeychainKey == "" { + t.Skip("kSecUseDataProtectionKeychain unavailable (macOS < 10.15)") + } + + item := NewItem() + item.SetSecClass(SecClassGenericPassword) + item.SetService("TestUseDataProtectionKeychain") + item.SetAccount("test") + item.SetUseDataProtectionKeychain(true) + + // Exercises the bool -> CFBoolean marshalling path for the new key + // without touching the real keychain. + cfDict, err := ConvertMapToCFDictionary(item.attr) + if err != nil { + t.Fatalf("ConvertMapToCFDictionary failed: %v", err) + } + releaseCFDictionary(cfDict) +} diff --git a/macos.go b/macos.go index f2597ee..bfef6d8 100644 --- a/macos.go +++ b/macos.go @@ -7,6 +7,30 @@ package keychain #cgo LDFLAGS: -framework CoreFoundation -framework Security #include #include + +// goKeychainUseDataProtectionKeychainKey returns kSecUseDataProtectionKeychain +// when running on macOS 10.15 or later. That symbol is annotated +// API_AVAILABLE(macos(10.15)), so on older systems it is weak-imported and is +// NULL at runtime. The __builtin_available guard ensures we only read it when +// it actually exists; otherwise referencing it during package init would pass +// NULL to CFStringToString and crash the whole package. +static CFStringRef goKeychainUseDataProtectionKeychainKey(void) { + if (__builtin_available(macOS 10.15, *)) { + return kSecUseDataProtectionKeychain; + } + return NULL; +} + +// goKeychainDataProtectionKeychainAvailable reports whether the running macOS +// version provides the data protection keychain (macOS 10.15+). It checks the +// OS version directly rather than inferring it from whether the key resolved, +// so callers can distinguish "OS too old" from "binding broken". +static int goKeychainDataProtectionKeychainAvailable(void) { + if (__builtin_available(macOS 10.15, *)) { + return 1; + } + return 0; +} */ import "C" @@ -25,3 +49,35 @@ var ( // AccessibleWhenPasscodeSetThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly), } ) + +// UseDataProtectionKeychainKey is the attribute key for +// kSecUseDataProtectionKeychain. It is empty on macOS versions older than +// 10.15, where that key is unavailable. +var UseDataProtectionKeychainKey = resolveUseDataProtectionKeychainKey() + +func resolveUseDataProtectionKeychainKey() string { + ref := C.goKeychainUseDataProtectionKeychainKey() + if ref == 0 { + return "" + } + return CFStringToString(ref) +} + +// dataProtectionKeychainAvailable reports whether the running macOS version +// (10.15+) provides the kSecUseDataProtectionKeychain key. +func dataProtectionKeychainAvailable() bool { + return C.goKeychainDataProtectionKeychainAvailable() != 0 +} + +// SetUseDataProtectionKeychain controls whether the operation targets the data +// protection keychain (the iOS-style keychain) instead of the legacy +// file-based keychain. The two are separate stores, so this should be set +// consistently across the add, query, update, and delete calls for an item. +// Requires macOS 10.15+; on older versions the kSecUseDataProtectionKeychain +// key is unavailable and this is a no-op. +func (k *Item) SetUseDataProtectionKeychain(b bool) { + if UseDataProtectionKeychainKey == "" { + return + } + k.attr[UseDataProtectionKeychainKey] = b +}