import CryptoKit 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" static let pin = "certain.pin" } // 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) } } } // MARK: - Bildschirmsperre /// Die PIN selbst wird nie abgelegt, nur ein gesalzener SHA-256 davon. /// Das Salz liegt daneben, beides im Schlüsselbund als `salz:hash` in Hex. /// /// Die PIN verdeckt die Oberfläche, sie verschlüsselt nicht die /// Gerätemarke – wer das Telefon entsperrt hat und es auslesen kann, /// käme an die Marke. Gegen Mitlesen über die Schulter und gegen ein /// kurz liegengelassenes Telefon hilft sie, gegen Forensik nicht. static var hatPIN: Bool { schluesselbundLesen(Schluessel.pin) != nil } static func setzePIN(_ pin: String) { var salz = Data(count: 16) _ = salz.withUnsafeMutableBytes { SecRandomCopyBytes(kSecRandomDefault, 16, $0.baseAddress!) } schluesselbundSchreiben(Schluessel.pin, "\(hex(salz)):\(hex(verhashe(pin, salz: salz)))") } static func stimmtPIN(_ pin: String) -> Bool { guard let abgelegt = schluesselbundLesen(Schluessel.pin) else { return false } let teile = abgelegt.split(separator: ":", maxSplits: 1) guard teile.count == 2, let salz = daten(hex: String(teile[0])) else { return false } return hex(verhashe(pin, salz: salz)) == String(teile[1]) } static func loeschePIN() { schluesselbundLoeschen(Schluessel.pin) } private static func verhashe(_ pin: String, salz: Data) -> Data { Data(SHA256.hash(data: salz + Data(pin.utf8))) } private static func hex(_ daten: Data) -> String { daten.map { String(format: "%02x", $0) }.joined() } private static func daten(hex text: String) -> Data? { guard text.count % 2 == 0 else { return nil } var roh = Data(); var rest = Substring(text) while !rest.isEmpty { guard let wert = UInt8(rest.prefix(2), radix: 16) else { return nil } roh.append(wert); rest = rest.dropFirst(2) } return roh } /// Alles vergessen – beim Abmelden und wenn der Server die Marke ablehnt. static func raeumeAuf() { marke = nil loeschePIN() 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) } }