Introducing SwiftSecurity: Type-safe Swift APIs for Apple Security framework

SwiftSecurity

SwiftSecurity is a modern Swift API for Apple Security framework (Keychain API, SharedWebCredentials API, etc).

Why should I use this one?

  • Supports every Keychain item class (Generic & Internet Password, Key, Certificate and Identity). The most popular solutions are designed only for GenericPassword, and in rare cases for other types. That could be handy for Enterprise use-cases.
  • Prevents creation of an incorrect set of attributes for items.
  • Follows recommendations from Quinn β€œThe Eskimo!”.
  • Compatible with CryptoKit and SwiftUI.
  • Free of deprecated calls and legacy implementations (min. target set to iOS 14.0).
  • Unified API across Apple systems (iOS, macOS, watchOS, tvOS, visionOS). Uses useDataProtectionKeychain key by default (for macOS).

It is built with safety and convenience in mind, yet provides progressive disclosure style for further customizations.

Here's an brief overview of what you can do:

// Keychain (with default Access Group)
let keychain = Keychain.default

// Shared Keychain (through App Group)
let keychain = Keychain(accessGroup: .keychainGroup(teamID: "J42EP42PB2", nameID: "com.example.app"))

// Store secret
try keychain.store("8e9c0a7f", query: .credential(for: "OpenAI"))

// Store secret with biometry protection
try keychain.store("8e9c0a7f", query: .credential(for: "OpenAI"),
    accessPolicy: AccessPolicy(.whenUnlocked, options: .userPresence) // Requires biometry/passcode authentication
)

// Store asymmetric NIST keys from CryptoKit 
let privateKey = P256.KeyAgreement.PrivateKey()
try keychain.store(privateKey, query: .privateKey(tag: "Alice"))

// Retrieve secret
let token: String? = try keychain.retrieve(.credential(for: "OpenAI"))

// Retrieve secret with pre-evaluated LAContext 
let token: String? = try keychain.retrieve(.credential(for: "OpenAI"), authenticationContext: LAContext())

// Retrieve attributes
if let info = try keychain.info(for: .credential(for: "OpenAI")) {
    // Creation date
    print(info.creationDate)
    // Comment
    print(info.comment)
}

// Retrieve data and persistent reference for secret
let value = try keychain.retrieve([.data, .persistentReference], query: .privateKey(tag: "Alice"))
if case let .dictionary(info) = value {
    // Data
    info.data
    // Persistent Reference (suitable for `NEVPNProtocol`)
    info.persistentReference
}

// Remove secret
try keychain.remove(.credential(for: "OpenAI"))

// Customize queries (with type-checking)
var query = SecItemQuery<GenericPassword>()
query.synchronizable = true         // βœ… Common
query.label = "OpenAI Access Token" // βœ… Common
query.service = "OpenAI"            // βœ… Only for GenericPassword
query.keySizeInBits = 2048          // ❌ Only for `SecKey`, so not accessible

// Generate data with 20 uniformly distributed random bytes
let randomData = try SecureRandomDataGenerator(count: 20).next()

// Error Handling
do {
    let token: String? = try keychain.store("8e9c0a7f", query: .credential(for: "OpenAI"))
} catch {
    switch error as? SwiftSecurityError {
    case .duplicateItem:
        // handle duplicate
    default:
        // unhandled
    }
}

As you can see, access to nearly every low-level parameter is available.

14 Likes