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
6 changes: 6 additions & 0 deletions corefoundation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
9 changes: 9 additions & 0 deletions ios.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {}
73 changes: 73 additions & 0 deletions keychain_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
56 changes: 56 additions & 0 deletions macos.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,30 @@ package keychain
#cgo LDFLAGS: -framework CoreFoundation -framework Security
#include <CoreFoundation/CoreFoundation.h>
#include <Security/Security.h>

// 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"

Expand All @@ -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
}