diff --git a/Calendarr iOS.xcodeproj/project.pbxproj b/Calendarr iOS.xcodeproj/project.pbxproj index ef2eb75..7b0fb67 100644 --- a/Calendarr iOS.xcodeproj/project.pbxproj +++ b/Calendarr iOS.xcodeproj/project.pbxproj @@ -499,7 +499,7 @@ INFOPLIST_KEY_CFBundleName = Calendarr; INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; - INFOPLIST_KEY_NSAppTransportSecurity_NSAllowsArbitraryLoads = YES; + INFOPLIST_KEY_NSAppTransportSecurity_NSAllowsLocalNetworking = YES; INFOPLIST_KEY_NSHumanReadableCopyright = "© 2026 Scarriffleservices"; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; @@ -543,7 +543,7 @@ INFOPLIST_KEY_CFBundleName = Calendarr; INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; - INFOPLIST_KEY_NSAppTransportSecurity_NSAllowsArbitraryLoads = YES; + INFOPLIST_KEY_NSAppTransportSecurity_NSAllowsLocalNetworking = YES; INFOPLIST_KEY_NSHumanReadableCopyright = "© 2026 Scarriffleservices"; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; diff --git a/Calendarr iOS/CalendarrApp.swift b/Calendarr iOS/CalendarrApp.swift index e8c9a6b..fe39d08 100644 --- a/Calendarr iOS/CalendarrApp.swift +++ b/Calendarr iOS/CalendarrApp.swift @@ -1,4 +1,5 @@ import SwiftUI +import Security @main struct CalendarrApp: App { @@ -24,7 +25,13 @@ class AppState { init() { serverURL = UserDefaults.standard.string(forKey: "serverURL") ?? "" - authToken = UserDefaults.standard.string(forKey: "authToken") ?? "" + // Migrate a token previously kept in UserDefaults into the Keychain once, + // so existing logins survive the change without re-authenticating. + if let legacy = UserDefaults.standard.string(forKey: "authToken"), !legacy.isEmpty { + Keychain.set(legacy, for: "authToken") + UserDefaults.standard.removeObject(forKey: "authToken") + } + authToken = Keychain.get("authToken") ?? "" username = UserDefaults.standard.string(forKey: "username") ?? "" isAdmin = UserDefaults.standard.bool(forKey: "isAdmin") } @@ -39,7 +46,7 @@ class AppState { authToken = token username = user isAdmin = admin - UserDefaults.standard.set(token, forKey: "authToken") + Keychain.set(token, for: "authToken") // secret → Keychain, not UserDefaults UserDefaults.standard.set(user, forKey: "username") UserDefaults.standard.set(admin, forKey: "isAdmin") } @@ -48,7 +55,8 @@ class AppState { authToken = "" username = "" isAdmin = false - UserDefaults.standard.removeObject(forKey: "authToken") + Keychain.set(nil, for: "authToken") + UserDefaults.standard.removeObject(forKey: "authToken") // clear any legacy copy UserDefaults.standard.removeObject(forKey: "username") UserDefaults.standard.removeObject(forKey: "isAdmin") } @@ -59,3 +67,37 @@ class AppState { UserDefaults.standard.removeObject(forKey: "serverURL") } } + +/// Minimal Keychain wrapper for secrets (the auth bearer token). Values are +/// stored as generic passwords, accessible after first unlock. +enum Keychain { + private static let service = "Calendarr" + + static func set(_ value: String?, for key: String) { + let base: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: service, + kSecAttrAccount as String: key, + ] + SecItemDelete(base as CFDictionary) + guard let value, let data = value.data(using: .utf8) else { return } + var add = base + add[kSecValueData as String] = data + add[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock + SecItemAdd(add as CFDictionary, nil) + } + + static func get(_ key: String) -> String? { + 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 out: AnyObject? + guard SecItemCopyMatching(query as CFDictionary, &out) == errSecSuccess, + let data = out as? Data else { return nil } + return String(data: data, encoding: .utf8) + } +}