Keychain Integration
This page’s content is being updated
Why Keychain?
Session data includes sensitive cryptographic keys. Keychain provides:- Hardware-backed encryption
- Access control (biometrics, passcode)
- Secure enclave support
- Data protection across app updates
Basic Keychain Wrapper
Create a Keychain helper:Copy
import Foundation
import Security
enum KeychainError: Error {
case duplicateItem
case itemNotFound
case unexpectedStatus(OSStatus)
case invalidData
}
class KeychainManager {
static let shared = KeychainManager()
private let service = "com.yourapp.privacyboost"
private init() {}
// MARK: - Save
func save(_ data: Data, for key: String) throws {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: key,
kSecValueData as String: data,
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
]
let status = SecItemAdd(query as CFDictionary, nil)
if status == errSecDuplicateItem {
// Update existing item
let updateQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: key
]
let attributes: [String: Any] = [
kSecValueData as String: data
]
let updateStatus = SecItemUpdate(updateQuery as CFDictionary, attributes as CFDictionary)
guard updateStatus == errSecSuccess else {
throw KeychainError.unexpectedStatus(updateStatus)
}
} else if status != errSecSuccess {
throw KeychainError.unexpectedStatus(status)
}
}
// MARK: - Load
func load(for key: String) throws -> Data {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: key,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne
]
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
guard status == errSecSuccess else {
if status == errSecItemNotFound {
throw KeychainError.itemNotFound
}
throw KeychainError.unexpectedStatus(status)
}
guard let data = result as? Data else {
throw KeychainError.invalidData
}
return data
}
// MARK: - Delete
func delete(for key: String) throws {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: key
]
let status = SecItemDelete(query as CFDictionary)
guard status == errSecSuccess || status == errSecItemNotFound else {
throw KeychainError.unexpectedStatus(status)
}
}
// MARK: - Exists
func exists(for key: String) -> Bool {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: key,
kSecReturnData as String: false
]
let status = SecItemCopyMatching(query as CFDictionary, nil)
return status == errSecSuccess
}
}
Session Storage
Store and retrieve Privacy Boost sessions:Copy
import PrivacyBoost
class SessionStorage {
static let shared = SessionStorage()
private let keychain = KeychainManager.shared
private let sessionKey = "privacyboost.session"
private init() {}
// MARK: - Save Session
func saveSession(_ session: ExportedSession) throws {
let data = try encodeSession(session)
try keychain.save(data, for: sessionKey)
}
// MARK: - Load Session
func loadSession() throws -> ExportedSession {
let data = try keychain.load(for: sessionKey)
return try decodeSession(data)
}
// MARK: - Delete Session
func deleteSession() throws {
try keychain.delete(for: sessionKey)
}
// MARK: - Has Session
func hasSession() -> Bool {
return keychain.exists(for: sessionKey)
}
// MARK: - Encoding
private func encodeSession(_ session: ExportedSession) throws -> Data {
// Create a dictionary representation
let dict: [String: Any] = [
"walletPublicKeyX": session.walletPublicKeyX,
"walletPublicKeyY": session.walletPublicKeyY,
"viewingKey": session.viewingKey,
"viewingPublicKeyX": session.viewingPublicKeyX,
"viewingPublicKeyY": session.viewingPublicKeyY,
"nullifyingKey": session.nullifyingKey,
"nullifyingPublicKeyX": session.nullifyingPublicKeyX,
"nullifyingPublicKeyY": session.nullifyingPublicKeyY,
"mpk": session.mpk,
"jwt": session.jwt,
"jwtExpiry": session.jwtExpiry,
"walletAddress": session.walletAddress
]
return try JSONSerialization.data(withJSONObject: dict)
}
private func decodeSession(_ data: Data) throws -> ExportedSession {
guard let dict = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
throw KeychainError.invalidData
}
return ExportedSession(
walletPublicKeyX: dict["walletPublicKeyX"] as? String ?? "",
walletPublicKeyY: dict["walletPublicKeyY"] as? String ?? "",
viewingKey: dict["viewingKey"] as? String ?? "",
viewingPublicKeyX: dict["viewingPublicKeyX"] as? String ?? "",
viewingPublicKeyY: dict["viewingPublicKeyY"] as? String ?? "",
nullifyingKey: dict["nullifyingKey"] as? String ?? "",
nullifyingPublicKeyX: dict["nullifyingPublicKeyX"] as? String ?? "",
nullifyingPublicKeyY: dict["nullifyingPublicKeyY"] as? String ?? "",
mpk: dict["mpk"] as? String ?? "",
jwt: dict["jwt"] as? String ?? "",
jwtExpiry: dict["jwtExpiry"] as? UInt64 ?? 0,
walletAddress: dict["walletAddress"] as? String ?? ""
)
}
}
Biometric Protection
Add Face ID/Touch ID protection:Copy
import LocalAuthentication
class BiometricSessionStorage {
static let shared = BiometricSessionStorage()
private let keychain = KeychainManager.shared
private let sessionKey = "privacyboost.session.biometric"
private init() {}
// MARK: - Save with Biometrics
func saveSession(_ session: ExportedSession) throws {
let data = try encodeSession(session)
// Create access control with biometrics
var error: Unmanaged<CFError>?
guard let accessControl = SecAccessControlCreateWithFlags(
kCFAllocatorDefault,
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
.biometryCurrentSet,
&error
) else {
throw error!.takeRetainedValue() as Error
}
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "com.yourapp.privacyboost.biometric",
kSecAttrAccount as String: sessionKey,
kSecValueData as String: data,
kSecAttrAccessControl as String: accessControl
]
// Delete existing and add new
SecItemDelete(query as CFDictionary)
let status = SecItemAdd(query as CFDictionary, nil)
guard status == errSecSuccess else {
throw KeychainError.unexpectedStatus(status)
}
}
// MARK: - Load with Biometrics
func loadSession() async throws -> ExportedSession {
return try await withCheckedThrowingContinuation { continuation in
let context = LAContext()
context.localizedReason = "Access your Privacy Boost session"
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "com.yourapp.privacyboost.biometric",
kSecAttrAccount as String: sessionKey,
kSecReturnData as String: true,
kSecUseAuthenticationContext as String: context
]
DispatchQueue.global().async {
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
if status == errSecSuccess, let data = result as? Data {
do {
let session = try self.decodeSession(data)
continuation.resume(returning: session)
} catch {
continuation.resume(throwing: error)
}
} else {
continuation.resume(throwing: KeychainError.unexpectedStatus(status))
}
}
}
}
// MARK: - Check Biometrics Available
static var biometricsAvailable: Bool {
let context = LAContext()
var error: NSError?
return context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error)
}
static var biometricType: String {
let context = LAContext()
_ = context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil)
switch context.biometryType {
case .faceID:
return "Face ID"
case .touchID:
return "Touch ID"
case .opticID:
return "Optic ID"
default:
return "Biometrics"
}
}
// MARK: - Encoding (same as SessionStorage)
private func encodeSession(_ session: ExportedSession) throws -> Data {
let dict: [String: Any] = [
"walletPublicKeyX": session.walletPublicKeyX,
"walletPublicKeyY": session.walletPublicKeyY,
"viewingKey": session.viewingKey,
"viewingPublicKeyX": session.viewingPublicKeyX,
"viewingPublicKeyY": session.viewingPublicKeyY,
"nullifyingKey": session.nullifyingKey,
"nullifyingPublicKeyX": session.nullifyingPublicKeyX,
"nullifyingPublicKeyY": session.nullifyingPublicKeyY,
"mpk": session.mpk,
"jwt": session.jwt,
"jwtExpiry": session.jwtExpiry,
"walletAddress": session.walletAddress
]
return try JSONSerialization.data(withJSONObject: dict)
}
private func decodeSession(_ data: Data) throws -> ExportedSession {
guard let dict = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
throw KeychainError.invalidData
}
return ExportedSession(
walletPublicKeyX: dict["walletPublicKeyX"] as? String ?? "",
walletPublicKeyY: dict["walletPublicKeyY"] as? String ?? "",
viewingKey: dict["viewingKey"] as? String ?? "",
viewingPublicKeyX: dict["viewingPublicKeyX"] as? String ?? "",
viewingPublicKeyY: dict["viewingPublicKeyY"] as? String ?? "",
nullifyingKey: dict["nullifyingKey"] as? String ?? "",
nullifyingPublicKeyX: dict["nullifyingPublicKeyX"] as? String ?? "",
nullifyingPublicKeyY: dict["nullifyingPublicKeyY"] as? String ?? "",
mpk: dict["mpk"] as? String ?? "",
jwt: dict["jwt"] as? String ?? "",
jwtExpiry: dict["jwtExpiry"] as? UInt64 ?? 0,
walletAddress: dict["walletAddress"] as? String ?? ""
)
}
}
Usage in App
Integrate with your SDK manager:Copy
actor SDKManager {
// ... existing code ...
func restoreSession() async throws -> Bool {
// Try biometric first
if BiometricSessionStorage.biometricsAvailable {
do {
let session = try await BiometricSessionStorage.shared.loadSession()
return try sdk.importSession(session)
} catch {
// Fall through to regular storage
}
}
// Try regular storage
if SessionStorage.shared.hasSession() {
let session = try SessionStorage.shared.loadSession()
return try sdk.importSession(session)
}
return false
}
func saveCurrentSession(withBiometrics: Bool = false) throws {
guard let session = sdk.exportSession() else {
return
}
if withBiometrics && BiometricSessionStorage.biometricsAvailable {
try BiometricSessionStorage.shared.saveSession(session)
} else {
try SessionStorage.shared.saveSession(session)
}
}
func clearSession() throws {
try SessionStorage.shared.deleteSession()
}
}
Security Best Practices
- Use
kSecAttrAccessibleWhenUnlockedThisDeviceOnly- Data only accessible when device is unlocked - Enable biometrics - Add Face ID/Touch ID for sensitive operations
- Don’t store in UserDefaults - UserDefaults is not encrypted
- Clear on logout - Delete Keychain items when user logs out
- Handle errors gracefully - Don’t expose Keychain errors to users
Info.plist
Add Face ID usage description:Copy
<key>NSFaceIDUsageDescription</key>
<string>We use Face ID to secure your Privacy Boost session</string>
Next Steps
- API Reference - Complete API documentation
- SwiftUI Guide - Integrate with SwiftUI