Created
October 28, 2025 14:29
-
-
Save wesmatlock/a993872547eabac02d23a319b8cb26bf to your computer and use it in GitHub Desktop.
SecureStorageManager
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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