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
76 changes: 76 additions & 0 deletions ios.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ package keychain
*/
import "C"

import "fmt"

var AccessibleKey = attrKey(C.CFTypeRef(C.kSecAttrAccessible))
var accessibleTypeRef = map[Accessible]C.CFTypeRef{
AccessibleWhenUnlocked: C.CFTypeRef(C.kSecAttrAccessibleWhenUnlocked),
Expand All @@ -21,3 +23,77 @@ var accessibleTypeRef = map[Accessible]C.CFTypeRef{
AccessibleAfterFirstUnlockThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly),
AccessibleAccessibleAlwaysThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleAlwaysThisDeviceOnly),
}

// AccessControlKey is key for kSecAttrAccessControl
var AccessControlKey = attrKey(C.CFTypeRef(C.kSecAttrAccessControl))

// AccessControlFlag is a bitmask for SecAccessControlCreateFlags.
type AccessControlFlag = C.SecAccessControlCreateFlags

// Access control flag constants for use with SetAccessControl.
var (
// AccessControlUserPresence constrains access with either biometry or passcode.
AccessControlUserPresence = AccessControlFlag(C.kSecAccessControlUserPresence)
// AccessControlBiometryAny constrains access with Touch ID for any enrolled finger.
AccessControlBiometryAny = AccessControlFlag(C.kSecAccessControlBiometryAny)
// AccessControlBiometryCurrentSet constrains access with Touch ID for currently enrolled fingers.
AccessControlBiometryCurrentSet = AccessControlFlag(C.kSecAccessControlBiometryCurrentSet)
// AccessControlDevicePasscode constrains access with the device passcode.
AccessControlDevicePasscode = AccessControlFlag(C.kSecAccessControlDevicePasscode)
// AccessControlWatch constrains access with Apple Watch.
AccessControlWatch = AccessControlFlag(C.kSecAccessControlWatch)
// AccessControlOr allows satisfying any single constraint.
AccessControlOr = AccessControlFlag(C.kSecAccessControlOr)
// AccessControlAnd requires satisfying all constraints.
AccessControlAnd = AccessControlFlag(C.kSecAccessControlAnd)
// AccessControlPrivateKeyUsage enables using a private key for signing/decryption operations.
AccessControlPrivateKeyUsage = AccessControlFlag(C.kSecAccessControlPrivateKeyUsage)
// AccessControlApplicationPassword requires an application-provided password for access.
AccessControlApplicationPassword = AccessControlFlag(C.kSecAccessControlApplicationPassword)
)

// SetAccessControl sets the access control for the item with a protection level and flags.
// This replaces SetAccessible when you need biometric protection.
// Protection should be one of the Accessible constants (e.g., AccessibleWhenPasscodeSetThisDeviceOnly).
// Flags should be a bitmask of AccessControlFlag values (e.g., AccessControlBiometryCurrentSet).
//
// Note: kSecAttrAccessControl and kSecAttrAccessible are mutually exclusive.
// This method removes any previously set kSecAttrAccessible value.
func (k *Item) SetAccessControl(accessible Accessible, flags AccessControlFlag) error {
protection, ok := accessibleTypeRef[accessible]
if !ok {
return fmt.Errorf("invalid accessible value: %d", accessible)
}

var cerr C.CFErrorRef
access := C.SecAccessControlCreateWithFlags(
C.kCFAllocatorDefault,
C.CFTypeRef(protection),
C.SecAccessControlCreateFlags(flags),
&cerr,
)
if access == 0 {
if cerr != 0 {
defer C.CFRelease(C.CFTypeRef(cerr))
return fmt.Errorf("SecAccessControlCreateWithFlags failed: %d", C.CFErrorGetCode(cerr))
}
return fmt.Errorf("SecAccessControlCreateWithFlags failed with unknown error")
}

// kSecAttrAccessControl and kSecAttrAccessible are mutually exclusive.
delete(k.attr, AccessibleKey)

// Release any previous SecAccessControlRef to avoid leaking CF objects.
if old, exists := k.attr[AccessControlKey]; exists {
C.CFRelease(C.CFTypeRef(old.(C.CFTypeRef)))
}

k.attr[AccessControlKey] = C.CFTypeRef(access)
return nil
}

// SetUseDataProtectionKeychain is a no-op on iOS since iOS always uses
// the data protection keychain.
func (k *Item) SetUseDataProtectionKeychain(b bool) {
// iOS always uses data protection keychain; this is a no-op for API compatibility.
}
84 changes: 81 additions & 3 deletions macos.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,96 @@ package keychain
*/
import "C"

import "fmt"

// AccessibleKey is key for kSecAttrAccessible
var (
AccessibleKey = attrKey(C.CFTypeRef(C.kSecAttrAccessible))
accessibleTypeRef = map[Accessible]C.CFTypeRef{
AccessibleWhenUnlocked: C.CFTypeRef(C.kSecAttrAccessibleWhenUnlocked),
AccessibleAfterFirstUnlock: C.CFTypeRef(C.kSecAttrAccessibleAfterFirstUnlock),
AccessibleAlways: C.CFTypeRef(C.kSecAttrAccessibleAlways),
AccessibleWhenPasscodeSetThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly),
AccessibleWhenUnlockedThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleWhenUnlockedThisDeviceOnly),
AccessibleAfterFirstUnlockThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly),
AccessibleAccessibleAlwaysThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleAlwaysThisDeviceOnly),

// Only available in 10.10
// AccessibleWhenPasscodeSetThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly),
}
)

// AccessControlKey is key for kSecAttrAccessControl
var AccessControlKey = attrKey(C.CFTypeRef(C.kSecAttrAccessControl))

// UseDataProtectionKeychainKey is key for kSecUseDataProtectionKeychain
var UseDataProtectionKeychainKey = attrKey(C.CFTypeRef(C.kSecUseDataProtectionKeychain))

// AccessControlFlag is a bitmask for SecAccessControlCreateFlags.
type AccessControlFlag = C.SecAccessControlCreateFlags

// Access control flag constants for use with SetAccessControl.
var (
// AccessControlUserPresence constrains access with either biometry or passcode.
AccessControlUserPresence = AccessControlFlag(C.kSecAccessControlUserPresence)
// AccessControlBiometryAny constrains access with Touch ID for any enrolled finger.
AccessControlBiometryAny = AccessControlFlag(C.kSecAccessControlBiometryAny)
// AccessControlBiometryCurrentSet constrains access with Touch ID for currently enrolled fingers.
AccessControlBiometryCurrentSet = AccessControlFlag(C.kSecAccessControlBiometryCurrentSet)
// AccessControlDevicePasscode constrains access with the device passcode.
AccessControlDevicePasscode = AccessControlFlag(C.kSecAccessControlDevicePasscode)
// AccessControlWatch constrains access with Apple Watch.
AccessControlWatch = AccessControlFlag(C.kSecAccessControlWatch)
// AccessControlOr allows satisfying any single constraint.
AccessControlOr = AccessControlFlag(C.kSecAccessControlOr)
// AccessControlAnd requires satisfying all constraints.
AccessControlAnd = AccessControlFlag(C.kSecAccessControlAnd)
// AccessControlPrivateKeyUsage enables using a private key for signing/decryption operations.
AccessControlPrivateKeyUsage = AccessControlFlag(C.kSecAccessControlPrivateKeyUsage)
// AccessControlApplicationPassword requires an application-provided password for access.
AccessControlApplicationPassword = AccessControlFlag(C.kSecAccessControlApplicationPassword)
)

// SetAccessControl sets the access control for the item with a protection level and flags.
// This replaces SetAccessible when you need biometric protection.
// Protection should be one of the Accessible constants (e.g., AccessibleWhenPasscodeSetThisDeviceOnly).
// Flags should be a bitmask of AccessControlFlag values (e.g., AccessControlBiometryCurrentSet).
//
// Note: kSecAttrAccessControl and kSecAttrAccessible are mutually exclusive.
// This method removes any previously set kSecAttrAccessible value.
func (k *Item) SetAccessControl(accessible Accessible, flags AccessControlFlag) error {
protection, ok := accessibleTypeRef[accessible]
if !ok {
return fmt.Errorf("invalid accessible value: %d", accessible)
}

var cerr C.CFErrorRef
access := C.SecAccessControlCreateWithFlags(
C.kCFAllocatorDefault,
C.CFTypeRef(protection),
C.SecAccessControlCreateFlags(flags),
&cerr,
)
if access == 0 {
if cerr != 0 {
defer C.CFRelease(C.CFTypeRef(cerr))
return fmt.Errorf("SecAccessControlCreateWithFlags failed: %d", C.CFErrorGetCode(cerr))
}
return fmt.Errorf("SecAccessControlCreateWithFlags failed with unknown error")
}

// kSecAttrAccessControl and kSecAttrAccessible are mutually exclusive.
delete(k.attr, AccessibleKey)

// Release any previous SecAccessControlRef to avoid leaking CF objects.
if old, exists := k.attr[AccessControlKey]; exists {
C.CFRelease(C.CFTypeRef(old.(C.CFTypeRef)))
}

k.attr[AccessControlKey] = C.CFTypeRef(access)
return nil
}

// SetUseDataProtectionKeychain forces use of the modern data protection keychain
// instead of the legacy file-based keychain on macOS.
// This is required when using SecAccessControl with biometry.
func (k *Item) SetUseDataProtectionKeychain(b bool) {
k.attr[UseDataProtectionKeychainKey] = b
}
124 changes: 124 additions & 0 deletions macos_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ package keychain

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestUpdateItem(t *testing.T) {
Expand Down Expand Up @@ -127,3 +130,124 @@ func TestInternetPassword(t *testing.T) {
t.Errorf("expected comment 'this is the comment' but got %q", r.Comment)
}
}

func TestSetAccessControl(t *testing.T) {
item := NewItem()
item.SetSecClass(SecClassGenericPassword)
item.SetService("TestAccessControl")
item.SetAccount("test-access-control")
item.SetLabel("TestSetAccessControl")
item.SetData([]byte("secret"))

// SetAccessControl should succeed with valid accessible + flags.
err := item.SetAccessControl(AccessibleWhenPasscodeSetThisDeviceOnly, AccessControlDevicePasscode)
require.NoError(t, err)

// kSecAttrAccessible should have been removed (mutually exclusive with kSecAttrAccessControl).
_, hasAccessible := item.attr[AccessibleKey]
assert.False(t, hasAccessible, "kSecAttrAccessible should be removed when kSecAttrAccessControl is set")

// kSecAttrAccessControl should be set.
_, hasAccessControl := item.attr[AccessControlKey]
assert.True(t, hasAccessControl, "kSecAttrAccessControl should be set")
}

func TestSetAccessControlRemovesAccessible(t *testing.T) {
item := NewItem()
item.SetSecClass(SecClassGenericPassword)

// First set accessible.
item.SetAccessible(AccessibleWhenUnlocked)
_, hasAccessible := item.attr[AccessibleKey]
assert.True(t, hasAccessible, "kSecAttrAccessible should be set after SetAccessible")

// Now set access control, which should remove accessible.
err := item.SetAccessControl(AccessibleWhenPasscodeSetThisDeviceOnly, AccessControlDevicePasscode)
require.NoError(t, err)

_, hasAccessible = item.attr[AccessibleKey]
assert.False(t, hasAccessible, "kSecAttrAccessible should be removed after SetAccessControl")
}

func TestSetAccessControlInvalidAccessible(t *testing.T) {
item := NewItem()
err := item.SetAccessControl(Accessible(999), AccessControlDevicePasscode)
assert.Error(t, err)
assert.Contains(t, err.Error(), "invalid accessible value")
}

func TestSetUseDataProtectionKeychain(t *testing.T) {
item := NewItem()
item.SetUseDataProtectionKeychain(true)
val, ok := item.attr[UseDataProtectionKeychainKey]
assert.True(t, ok)
assert.Equal(t, true, val)

item.SetUseDataProtectionKeychain(false)
val, ok = item.attr[UseDataProtectionKeychainKey]
assert.True(t, ok)
assert.Equal(t, false, val)
}

func TestAccessControlAddDeleteItem(t *testing.T) {
item := NewItem()
item.SetSecClass(SecClassGenericPassword)
item.SetService("TestAccessControlAddDelete")
item.SetAccount("test-ac-add-delete")
item.SetLabel("TestAccessControlAddDeleteItem")
item.SetData([]byte("biometry-secret"))
item.SetSynchronizable(SynchronizableNo)
item.SetUseDataProtectionKeychain(true)

err := item.SetAccessControl(AccessibleWhenPasscodeSetThisDeviceOnly, AccessControlDevicePasscode)
require.NoError(t, err)

defer func() {
dq := NewItem()
dq.SetSecClass(SecClassGenericPassword)
dq.SetService("TestAccessControlAddDelete")
dq.SetAccount("test-ac-add-delete")
dq.SetUseDataProtectionKeychain(true)
_ = DeleteItem(dq)
}()

err = AddItem(item)
if err != nil {
// -34018 (errSecMissingEntitlement) occurs when the test binary is not
// code-signed with keychain-access-groups entitlement, which is required
// for the data protection keychain. Skip in that case.
if err == Error(-34018) {
t.Skip("skipping: test binary not code-signed with keychain entitlements (errSecMissingEntitlement)")
}
t.Fatal(err)
}

// Query it back.
query := NewItem()
query.SetSecClass(SecClassGenericPassword)
query.SetService("TestAccessControlAddDelete")
query.SetAccount("test-ac-add-delete")
query.SetMatchLimit(MatchLimitOne)
query.SetReturnData(true)
query.SetUseDataProtectionKeychain(true)

results, err := QueryItem(query)
if err != nil {
t.Fatal(err)
}

require.Len(t, results, 1)
assert.Equal(t, []byte("biometry-secret"), results[0].Data)
}

func TestAccessControlFlagConstants(t *testing.T) {
// Verify that flag constants are non-zero (they are bitmask flags).
assert.NotZero(t, AccessControlUserPresence, "AccessControlUserPresence should be non-zero")
assert.NotZero(t, AccessControlBiometryAny, "AccessControlBiometryAny should be non-zero")
assert.NotZero(t, AccessControlBiometryCurrentSet, "AccessControlBiometryCurrentSet should be non-zero")
assert.NotZero(t, AccessControlDevicePasscode, "AccessControlDevicePasscode should be non-zero")
assert.NotZero(t, AccessControlOr, "AccessControlOr should be non-zero")
assert.NotZero(t, AccessControlAnd, "AccessControlAnd should be non-zero")
assert.NotZero(t, AccessControlPrivateKeyUsage, "AccessControlPrivateKeyUsage should be non-zero")
assert.NotZero(t, AccessControlApplicationPassword, "AccessControlApplicationPassword should be non-zero")
}