Skip to main content

Keychain Integration

This page’s content is being updated
This guide covers securely storing Privacy Boost session data using iOS Keychain.

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

  1. Use kSecAttrAccessibleWhenUnlockedThisDeviceOnly - Data only accessible when device is unlocked
  2. Enable biometrics - Add Face ID/Touch ID for sensitive operations
  3. Don’t store in UserDefaults - UserDefaults is not encrypted
  4. Clear on logout - Delete Keychain items when user logs out
  5. 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