import Foundation import Security class KeychainHelper { static let shared = KeychainHelper() private init() {} private static let accessGroup = "8J69P655GN.de.ilyashallak.readeck" @discardableResult func saveToken(_ token: String) -> Bool { saveString(token, forKey: "readeck_token") } func loadToken() -> String? { loadString(forKey: "readeck_token") } @discardableResult func saveEndpoint(_ endpoint: String) -> Bool { saveString(endpoint, forKey: "readeck_endpoint") } func loadEndpoint() -> String? { loadString(forKey: "readeck_endpoint") } @discardableResult func saveUsername(_ username: String) -> Bool { saveString(username, forKey: "readeck_username") } func loadUsername() -> String? { loadString(forKey: "readeck_username") } @discardableResult func savePassword(_ password: String) -> Bool { saveString(password, forKey: "readeck_password") } func loadPassword() -> String? { loadString(forKey: "readeck_password") } // MARK: - OAuth Token Storage // Note: OAuth is only available in the main app target, not in URLShare extension @discardableResult func saveOAuthToken(_ token: OAuthToken) -> Bool { guard let data = try? JSONEncoder().encode(token), let jsonString = String(data: data, encoding: .utf8) else { return false } return saveString(jsonString, forKey: "readeck_oauth_token") } func loadOAuthToken() -> OAuthToken? { guard let jsonString = loadString(forKey: "readeck_oauth_token"), let data = jsonString.data(using: .utf8), let token = try? JSONDecoder().decode(OAuthToken.self, from: data) else { return nil } return token } @discardableResult func saveAuthMethod(_ method: AuthenticationMethod) -> Bool { saveString(method.rawValue, forKey: "readeck_auth_method") } func loadAuthMethod() -> AuthenticationMethod? { guard let rawValue = loadString(forKey: "readeck_auth_method") else { return nil } return AuthenticationMethod(rawValue: rawValue) } @discardableResult func clearCredentials() -> Bool { let tokenCleared = saveString("", forKey: "readeck_token") let endpointCleared = saveString("", forKey: "readeck_endpoint") let usernameCleared = saveString("", forKey: "readeck_username") let passwordCleared = saveString("", forKey: "readeck_password") let oauthTokenCleared = saveString("", forKey: "readeck_oauth_token") let authMethodCleared = saveString("", forKey: "readeck_auth_method") return tokenCleared && endpointCleared && usernameCleared && passwordCleared && oauthTokenCleared && authMethodCleared } // MARK: - Private generic helpers @discardableResult private func saveString(_ value: String, forKey key: String) -> Bool { guard let data = value.data(using: .utf8) else { return false } let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrAccount as String: key, kSecValueData as String: data, kSecAttrAccessGroup as String: KeychainHelper.accessGroup ] SecItemDelete(query as CFDictionary) let status = SecItemAdd(query as CFDictionary, nil) return status == errSecSuccess } private func loadString(forKey key: String) -> String? { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrAccount as String: key, kSecAttrAccessGroup as String: KeychainHelper.accessGroup, kSecReturnData as String: true, kSecMatchLimit as String: kSecMatchLimitOne ] var result: AnyObject? let status = SecItemCopyMatching(query as CFDictionary, &result) if status == errSecSuccess, let data = result as? Data { return String(data: data, encoding: .utf8) } return nil } }