Dev.to Security πŸ” Cybersecurity πŸ‘ 0 πŸ“– 3 min read

SecureStorageKit: a dependency-free Swift package for Keychain, Face ID and Secure Enclave storage

Every iOS app I've worked on in the last decade ended up with its own hand-rolled Keychain wrapper. Most of them had the same bugs: errSecDuplicateItem not handled, queries that didn't match the attributes used to save,

Every iOS app I've worked on in the last decade ended up with its own hand-rolled Keychain wrapper. Most of them had the same bugs: errSecDuplicateItem not handled, queries that didn't match the attributes used to save, background reads failing with errSecInteractionNotAllowed and logging the user out, and tokens quietly ending up in print statements.

So I packaged the patterns I keep rewriting into SecureStorageKit: a free, open-source Swift package for storing secrets on iOS, iPadOS and macOS. It's serverless, built only on Apple frameworks (Keychain, LocalAuthentication, CryptoKit and the Secure Enclave), and has no third-party dependencies.

What's inside

Area Features
Keychain Save, read, update and delete Strings, Data and Codable values. Scoped clear, accessibility options, access groups, iCloud sync, typed errors, migration from legacy Keychain wrappers.
Biometrics Item-level Face ID / Touch ID / passcode protection, biometryCurrentSet, availability checks and lockout.
Crypto AES-256-GCM in a versioned envelope, HKDF, key manager (create, rotate, retire), recoverable key rotation engine, streaming encrypted file vault, Secure Enclave signing and sealing.
Accounts Token store with refresh, per-account isolation and logout cleanup; multi-account vault.
Ops Redacted logging, audit log, in-memory mock, versioned Codable migrations, password-protected backup and restore.

Requirements: iOS 15+, macOS 12+, Mac Catalyst 15+, Swift 6 toolchain (Xcode 16 or later).

Install

In Xcode: File β–Έ Add Package Dependencies… and paste the repo URL. Or in Package.swift:

.package(url: "https://github.com/eeshuu/SecureStorageKit.git", from: "1.0.1"),
// …
.target(name: "App", dependencies: [
    .product(name: "SecureStorageKit", package: "SecureStorageKit"),
]),

Quick start

import SecureStorageKit

let storage = SecureStorage(service: "my-app")

try await storage.save("secret", forKey: "access_token")
let token: String? = try await storage.retrieve("access_token", as: String.self)
try await storage.delete("access_token")
try await storage.clearAll() // only this namespace, nothing else

// Protected by the Keychain item itself: no Face ID, no data.
try await storage.save("private", forKey: "vault", protection: .biometryCurrentSet)
let value: String? = try await storage.retrieve("vault", as: String.self,
                                                authenticationReason: "Unlock secure data")

retrieve returns nil when the key doesn't exist, and every failure is a typed StorageError.

A few things I'm proud of

Tokens that refresh themselves, once. Concurrent callers share a single refresh, and token values print as <redacted>.

let tokens = SecureTokenStore(service: "my-app", refresher: AuthAPI())
let bearer = try await tokens.validAccessToken(for: "alice") // refreshes once, even if called concurrently
try await tokens.logout(account: "alice") // tokens + session secrets

Encryption with real key rotation. Old data keeps decrypting while new data uses the new key; retire the old one when nothing needs it.

let keys = KeyManager(storage: SecureStorage(service: "my-app", namespace: "keys"))
let sealed = try await keys.encrypt(data, associatedData: Data(recordID.utf8))
try await keys.rotate()            // new data uses default#2; default#1 still decrypts
try await keys.retire(version: 1)  // destroys default#1 once nothing uses it

Secure Enclave keys that never leave the chip.

let se = SecureEnclaveManager(storage: storage.namespaced("se"))
let publicKey = try await se.createSigningKey(tag: "device", protection: .biometryCurrentSet)
let signature = try await se.sign(challenge, tag: "device", prompt: .init(reason: "Approve sign-in"))

Testable without a device. An in-memory Keychain lets you simulate Face ID cancel, a new fingerprint, or a locked device in unit tests.

let mock = InMemoryKeychain()
let storage = SecureStorage(configuration: .init(service: "test"), backend: mock)
mock.authenticationResult = .failure(.userCancelled)
mock.simulateBiometryEnrollmentChange()
mock.isDeviceLocked = true

Easy migration. Import existing items from KeychainAccess, SwiftKeychainWrapper or raw SecItem code with MigrationManager.

The repo also ships a SwiftUI demo app (Keychain, Biometrics, Encryption and Tokens tabs), 66 unit tests, a full API reference and a written threat model.

An honest note

SecureStorageKit has a full test suite but has not had an independent security audit yet. The threat model in Docs/ThreatModel.md spells out what is and isn't protected. If you work in security, reviews, issues and PRs are very welcome.

Want the background?

If you'd like the reasoning behind these choices (accessibility classes, ThisDeviceOnly, biometry binding, Secure Enclave keys), I wrote a long guide: iOS Application Security: SSL Pinning, Keychain, API Encryption and App Attest.

github.com/eeshuu/SecureStorageKit: a star helps other iOS devs find it. MIT licensed.

My other Swift packages

  • AppLoggerKit: privacy-first logging with OSLog, JSON Lines, redaction and export
  • DesignSystemKit: one design system for SwiftUI and UIKit, with runtime white-label theming
πŸ“° Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.