Contents

sinoru/swift-keychain-kit

KeychainKit is a Swift interface to Apple's Keychain Services. It replaces the

Table of Contents

Getting Started

Add the package to your Package.swift, and KeychainKit to the target that uses it:

dependencies: [
    .package(url: "https://github.com/sinoru/swift-keychain-kit.git", from: "1.0.0"),
]
.target(
    name: "MyTarget",
    dependencies: [
        .product(name: "KeychainKit", package: "swift-keychain-kit"),
    ]
),

Usage

Items, attributes, and queries

An Item belongs to one item class and carries Attributes plus a value: secret Data for a password, a reference for a key, certificate, or identity. Each attribute is a typed property that exists only for the classes it is valid for: service on a generic password, server and port on an internet password, label and accessGroup on anything. Using one on the wrong class does not compile.

var item = Item(server: "example.com", account: "alice", password: "s3cret", internetProtocol: .https)
item.label = "Example"
item.synchronizable = true

A Query names the attributes an item must have. The method you pass it to decides how many items you get and in what shape; nothing matching is nil or ``, not an error.

let query = Query(service: "com.example.app", account: "alice")

try keychain.fetchFirst(matching: query)             // Item?
try keychain.fetchFirstData(matching: query)         // Data?
try keychain.fetchFirstAttributes(matching: query)   // Attributes?, never prompts
try keychain.fetch(matching: Query(service: "com.example.app"))   // [Item]

try keychain.update(matching: query, with: changes)   // throws .itemNotFound
try keychain.delete(matching: query)                  // idempotent

Keys, certificates, and identities

A key is generated into the keychain, found by its attributes, and used through KeyReference. Only the private key is stored; the public key is derived from it.

var attributes = Attributes<CryptographicKey>()
attributes.applicationTag = Data("com.example.keys.signing".utf8)

let key = try keychain.generateKey(.ecSECPrimeRandom, sizeInBits: 256, attributes: attributes)

let signature = try key.signature(for: message, using: .ecdsaSignatureMessageX962SHA256)
try key.publicKey?.isValidSignature(signature, for: message, using: .ecdsaSignatureMessageX962SHA256)   // Bool

var query = Query<CryptographicKey>()
query.applicationTag = Data("com.example.keys.signing".utf8)
try keychain.fetchFirstReference(matching: query)   // KeyReference?

A Secure Enclave key takes neither a type nor a size, because the Secure Enclave works only with 256-bit NIST P curve keys, and always carries the privateKeyUsage flag.

let enclaveKey = try keychain.generateSecureEnclaveKey(
    applicationTag: Data("com.example.keys.enclave".utf8),
    constraints: .biometryAny,
)

Certificates are created from DER and stored by reference; an identity appears wherever the keychain holds a certificate and its private key.

if let certificate = CertificateReference(derRepresentation: der) {
    try keychain.add(Item<Certificate>(reference: certificate))
}

if let identity = try keychain.fetchFirstReference(matching: Query<Identity>()) {
    let privateKey = try identity.privateKey()
    let publicKey = try identity.certificate().publicKey
}

KeychainKit does not depend on CryptoKit or on a certificate library. A NIST key of CryptoKit or Swift Crypto converts through its X9.63 representation, and a certificate of Swift Certificates through DER.

Swift concurrency

Every operation has an async overload, selected automatically in an asynchronous context. Keychain Services blocks the calling thread while it talks to securityd, so the overloads are @concurrent and run on the global executor rather than the caller's actor. The same goes for the key operations that can wait on the daemon or the user. Keychain and the references are Sendable.

let item = try await keychain.fetchFirst(matching: query)

Protection

item.attributes.protection = .accessible(.whenPasscodeSetThisDeviceOnly)

let control = try AccessControl(accessibility: .whenUnlocked, flags: [.biometryCurrentSet, .or, .devicePasscode])
item.attributes.protection = .accessControl(control)

let query = Query(service: "com.example.bank", account: "alice")
let item = try keychain.fetchFirst(
    matching: query,
    authenticationContext: AuthenticationContext(context),   // an LAContext you configured
)

Sharing and storage

let shared = Keychain(accessGroup: .keychainGroup(teamID: "ABCDE12345", name: "com.example.shared"))

#if os(macOS)
let daemon = Keychain(storage: .fileBased())   // for code outside a user session
#endif

Design

  • The framework's semantics, not a new model. Primary keys, duplicate and not-found

statuses, synchronizable matching, and the result shape per combination of return keys are the framework's; the types only make them visible.

  • Typed everywhere. Attributes expose only the properties valid for their class; KeychainError

preserves the OSStatus and classifies the documented codes; every operation uses typed throws.

  • Strict memory safety. The package builds with strict memory safety, so the few calls

that need it carry an unsafe marker. Everything else is plain Swift over a Sendable value model.

  • No locking of its own. Keychain Services is thread-safe; the library does not

serialize calls. Coordinating user-facing prompts is the caller's concern.

  • Data protection by default. kSecUseDataProtectionKeychain is set on every call so

macOS behaves like the other platforms. The file-based keychain is opt-in and macOS only.

Platform Support

The package supports macOS 12, iOS 15, Mac Catalyst, tvOS 15, watchOS 9, and visionOS 1 or later. It is for Apple platforms alone: Keychain Services is part of the Security framework, and no other platform has one. A package that also builds elsewhere depends on it under a platform condition:

.product(
    name: "KeychainKit",
    package: "swift-keychain-kit",
    condition: .when(platforms: [.macOS, .macCatalyst, .iOS, .tvOS, .watchOS, .visionOS])
)

Building the package requires Swift 6.2 (Xcode 26) or later.

Testing

Tests never touch the user's keychain. The request-building and result-parsing halves of every operation are pure functions and are tested directly. The calls into the framework are tested on macOS against a temporary file-based keychain created in the temporary directory and deleted afterwards. Key operations run on keys that are never stored. Data-protection behaviour such as access groups and biometrics requires a signed host and is outside swift test; what the sources say about it was measured in a simulator through an entitled host app, and the Secure Enclave and application passwords, which the simulator does not enforce, still need a device.

swift test
swift test --sanitize=thread

Roadmap

Open work is tracked in the issues. Trust evaluation (SecTrust), certificate parsing, and passkeys are out of scope.

Contributing

Bug reports, feature ideas, and pull requests are welcome on GitHub.

Security

Report a vulnerability privately, as SECURITY.md describes, rather than in a public issue.

License

Apache License 2.0

Package Metadata

Repository: sinoru/swift-keychain-kit

Default branch: main

README: README.md