Skip to content

Latest commit

 

History

History
103 lines (75 loc) · 4.92 KB

File metadata and controls

103 lines (75 loc) · 4.92 KB

Apple Keychain

BondryApple provides the Apple-specific database-key boundary for bondry-store-sqlcipher. It is a separate Swift product so the portable Rust core does not depend on Security.framework.

Protection Policy

The provider stores one 256-bit random value as a generic-password item with these properties:

  • Data Protection Keychain is selected explicitly.
  • kSecAttrAccessibleWhenUnlockedThisDeviceOnly prevents access while locked and prevents migration to another device.
  • Keychain synchronization is disabled explicitly.
  • Random bytes come from SecRandomCopyBytes and every status is checked.
  • The service and account identify the item but are not treated as secrets.

The public API supports loading a key and atomically creating one when absent. It does not expose deletion or regeneration. An invalid stored key fails without replacement, and simultaneous creators converge on the item that Keychain accepted first.

Host Integration

Add the BondryApple product, then create one stable service and account pair for each encrypted database:

import BondryApple

let configuration = try KeychainDatabaseKeyConfiguration(
  service: "com.example.application.automation",
  account: "database-key"
)
let key = try KeychainDatabaseKeyProvider(configuration: configuration).loadOrCreate()

Use the separate Bondry product to pass the key directly into the native runtime:

import Bondry

let runtime = try BondryRuntime.open(at: databaseURL, key: key)
try runtime.checkHealth()

let client = try runtime.createClient(named: "Local AI Client")
_ = try runtime.addGrant(
  principalID: client.id,
  adapterID: "rest",
  capabilityID: "battery.status"
)
let issued = try runtime.issueToken(for: client.id, label: "Primary")
let principal = try runtime.authenticate(token: issued)
let tokenForDeliberateCopy = issued.copySecret()

Applications can register protocol-neutral async capabilities and dispatch with the issued token without creating a Swift String copy of its secret:

try runtime.registerCapability(
  BondryCapability(
    id: "battery.status",
    summary: "Read battery status",
    effect: .readOnly
  )
) { invocation in
  try await batteryStatusJSON(for: invocation.inputJSON)
}

let output = try await runtime.dispatch(
  adapterID: "rest",
  token: issued,
  capabilityID: "battery.status",
  inputJSON: Data("{}".utf8)
)

Capability inputs and outputs must be JSON and are limited to 1 MiB each. Throw BondryCapabilityHandlerError.failed(code:) only with a stable, non-sensitive code intended for audit and adapter responses. Other Swift errors are mapped to handler_failed without exposing their descriptions.

The wrapper passes the bytes only for the duration of the open call. The host must not write them to defaults, logs, crash metadata, environment variables, or a file next to the database.

DatabaseKeyMaterial redacts its normal and debug descriptions. Swift's value semantics can still leave transient copies in process memory, so the provider does not claim memory protection after a successful read.

BondryIssuedToken retains the one-time bearer token in its caller-owned C record, redacts debug output, and clears the record when the last Swift value sharing it is released. Prefer withUnsafeSecretBytes when the consumer accepts bytes. copySecret() is an explicit convenience for UI and protocol APIs that require a String; Swift cannot guarantee clearing copies created by that method.

On macOS, a Data Protection Keychain caller must be code signed with an application-identifier entitlement. Xcode normally supplies it for an application target. A bare Swift Package test process does not have that entitlement and returns missingKeychainEntitlement.

Leave accessGroup unset for app-private storage. Set it only when multiple signed targets deliberately share the database key, and add the matching Keychain Sharing capability to every target. An unentitled access group fails closed.

Verification

The regular test suite uses an in-memory Keychain boundary and never touches the user's Keychain:

swift test --package-path apple

The signed integration probe requires XcodeGen, a valid Apple development signing identity, the Rust static library, and the caller's team identifier:

apple/scripts/build-rust-macos.sh
cd apple/IntegrationTests/KeychainProbe
DEVELOPMENT_TEAM=YOUR_TEAM_ID xcodegen generate
xcodebuild -quiet -project KeychainProbe.xcodeproj \
  -scheme KeychainProbe -configuration Debug \
  -derivedDataPath DerivedData build
DerivedData/Build/Products/Debug/KeychainProbe.app/Contents/MacOS/KeychainProbe

The probe uses a random service name, opens a real encrypted runtime through Bondry, exercises client and token administration plus async capability dispatch through the Swift API, verifies that its file has no plaintext SQLite header, and removes both the Keychain item and database before exiting. Generated projects and build products are ignored by Git.