Skip to content

Instantly share code, notes, and snippets.

@wesmatlock
Created October 28, 2025 14:29
Show Gist options
  • Select an option

  • Save wesmatlock/a993872547eabac02d23a319b8cb26bf to your computer and use it in GitHub Desktop.

Select an option

Save wesmatlock/a993872547eabac02d23a319b8cb26bf to your computer and use it in GitHub Desktop.
SecureStorageManager
import Foundation
import CryptoKit
import LocalAuthentication
public final class SecureStorageManager {
public static let shared = SecureStorageManager()
private let keychainService = "com.flightbooking.secure"
private let encryptionKey = SymmetricKey(size: .bits256)
private init() {}
public enum StorageError: Error {
case encryptionFailed
case decryptionFailed
case keychainError(OSStatus)
case biometricAuthenticationFailed
case dataConversionError
}
public func storeSecurely(_ data: Data,
for key: String,
requiresBiometric: Bool = false) throws {
let encryptedData = try encrypt(data)
var query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: keychainService,
kSecAttrAccount as String: key,
kSecValueData as String: encryptedData
]
if requiresBiometric {
let context = LAContext()
context.localizedReason = "Access secure data"
let access = SecAccessControlCreateWithFlags(
nil,
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
.biometryCurrentSet,
nil
)!
query[kSecAttrAccessControl as String] = access
query[kSecUseAuthenticationContext as String] = context
} else {
query[kSecAttrAccessible as String] = kSecAttrAccessibleWhenUnlockedThisDeviceOnly
}
SecItemDelete(query as CFDictionary)
let status = SecItemAdd(query as CFDictionary, nil)
if status != errSecSuccess {
throw StorageError.keychainError(status)
}
}
public func retrieveSecurely(for key: String,
requiresBiometric: Bool = false) throws -> Data {
var query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: keychainService,
kSecAttrAccount as String: key,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne
]
if requiresBiometric {
let context = LAContext()
context.localizedReason = "Access secure data"
query[kSecUseAuthenticationContext as String] = context
}
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
guard status == errSecSuccess,
let encryptedData = result as? Data else {
throw StorageError.keychainError(status)
}
return try decrypt(encryptedData)
}
public func deleteSecurely(for key: String) throws {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: keychainService,
kSecAttrAccount as String: key
]
let status = SecItemDelete(query as CFDictionary)
if status != errSecSuccess && status != errSecItemNotFound {
throw StorageError.keychainError(status)
}
}
private func encrypt(_ data: Data) throws -> Data {
do {
let sealedBox = try AES.GCM.seal(data, using: encryptionKey)
guard let encryptedData = sealedBox.combined else {
throw StorageError.encryptionFailed
}
return encryptedData
} catch {
throw StorageError.encryptionFailed
}
}
private func decrypt(_ encryptedData: Data) throws -> Data {
do {
let sealedBox = try AES.GCM.SealedBox(combined: encryptedData)
return try AES.GCM.open(sealedBox, using: encryptionKey)
} catch {
throw StorageError.decryptionFailed
}
}
public func storeSensitiveUserData(_ userData: UserData) throws {
let encoder = JSONEncoder()
let data = try encoder.encode(userData)
try storeSecurely(data, for: "user_data", requiresBiometric: true)
}
public func retrieveSensitiveUserData() throws -> UserData {
let data = try retrieveSecurely(for: "user_data", requiresBiometric: true)
let decoder = JSONDecoder()
return try decoder.decode(UserData.self, from: data)
}
}
public struct UserData: Codable {
let userId: String
let paymentMethods: [PaymentMethod]
let personalInfo: PersonalInfo
}
public struct PaymentMethod: Codable {
let id: String
let type: String
let lastFourDigits: String
let expiryDate: String
}
public struct PersonalInfo: Codable {
let fullName: String
let email: String
let phoneNumber: String
let dateOfBirth: String
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment