Documentation Index
Fetch the complete documentation index at: https://docs.privacyboost.io/llms.txt
Use this file to discover all available pages before exploring further.
Session Storage
This guide covers securely storing and restoring Privacy Boost sessions using iOS Keychain, expanding on the persistence options in Key Management.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: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: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: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: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:<key>NSFaceIDUsageDescription</key>
<string>We use Face ID to secure your Privacy Boost session</string>
Next Steps
- API Reference - Complete API documentation
- iOS Getting Started - Integrate with SwiftUI