import Foundation import Security /// Wo die App ihre Einstellungen und ihr Geheimnis hinlegt. /// /// Zwei getrennte Orte, mit Absicht: Die Server-Adresse ist keine geheime /// Angabe und liegt in den Voreinstellungen. Die Gerätemarke ist ein /// Vollzugang und gehört in den Schlüsselbund – dort überlebt sie kein /// unverschlüsseltes Backup und keine Gerätesicherung ohne Passcode. enum Ablage { /// Setze das hier auf deine App-Gruppe, sobald es Erweiterungen gibt /// (`group.`). Solange `nil`, benutzt die App ihren /// eigenen Bereich – das genügt, bis Teilen-Menü und Kurzbefehle dazukommen. static let gruppe: String? = nil /// Dieselbe Gruppe für den Schlüsselbund. Erst nötig, wenn eine /// Erweiterung an dieselbe Marke muss. static let schluesselbundGruppe: String? = nil private static var voreinstellungen: UserDefaults { if let gruppe, let geteilt = UserDefaults(suiteName: gruppe) { return geteilt } return .standard } private enum Schluessel { static let serverAdresse = "certain.serverAdresse" static let marke = "certain.marke" static let benutzername = "certain.benutzername" } // MARK: - Server-Adresse static var serverAdresse: URL? { get { guard let text = voreinstellungen.string(forKey: Schluessel.serverAdresse) else { return nil } return URL(string: text) } set { if let neue = newValue { voreinstellungen.set(neue.absoluteString, forKey: Schluessel.serverAdresse) } else { voreinstellungen.removeObject(forKey: Schluessel.serverAdresse) } } } /// Nur zur Anzeige („angemeldet als …"), nicht zur Prüfung. static var benutzername: String? { get { voreinstellungen.string(forKey: Schluessel.benutzername) } set { if let neuer = newValue { voreinstellungen.set(neuer, forKey: Schluessel.benutzername) } else { voreinstellungen.removeObject(forKey: Schluessel.benutzername) } } } // MARK: - Gerätemarke im Schlüsselbund static var marke: String? { get { schluesselbundLesen(Schluessel.marke) } set { if let neue = newValue { schluesselbundSchreiben(Schluessel.marke, neue) } else { schluesselbundLoeschen(Schluessel.marke) } } } /// Alles vergessen – beim Abmelden und wenn der Server die Marke ablehnt. static func raeumeAuf() { marke = nil benutzername = nil serverAdresse = nil Zwischenspeicher.leere() } // MARK: - Schlüsselbund, schmucklos private static func grundlage(_ konto: String) -> [String: Any] { var abfrage: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: "com.certain.app", kSecAttrAccount as String: konto, ] if let schluesselbundGruppe { abfrage[kSecAttrAccessGroup as String] = schluesselbundGruppe } return abfrage } private static func schluesselbundLesen(_ konto: String) -> String? { var abfrage = grundlage(konto) abfrage[kSecReturnData as String] = true abfrage[kSecMatchLimit as String] = kSecMatchLimitOne var ergebnis: CFTypeRef? let status = SecItemCopyMatching(abfrage as CFDictionary, &ergebnis) guard status == errSecSuccess, let daten = ergebnis as? Data else { return nil } return String(data: daten, encoding: .utf8) } private static func schluesselbundSchreiben(_ konto: String, _ wert: String) { let abfrage = grundlage(konto) let inhalt: [String: Any] = [ kSecValueData as String: Data(wert.utf8), // Nicht in ein Backup und nicht auf ein anderes Gerät: Die Marke // gehoert genau diesem Telefon. kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly, ] let status = SecItemUpdate(abfrage as CFDictionary, inhalt as CFDictionary) if status == errSecItemNotFound { var neu = abfrage neu.merge(inhalt) { _, zweites in zweites } SecItemAdd(neu as CFDictionary, nil) } } private static func schluesselbundLoeschen(_ konto: String) { SecItemDelete(grundlage(konto) as CFDictionary) } }