import Foundation import Security /// Ablage für die Victron-Verschlüsselungsschlüssel. Die gehören nicht in die /// UserDefaults, deshalb Keychain. enum KeychainStore { private static let service = "de.fritob.VanControl.victronKeys" static func setKey(_ hex: String?, for deviceID: UUID) { let account = deviceID.uuidString var query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: account, ] SecItemDelete(query as CFDictionary) guard let hex, let data = hex.data(using: .utf8) else { return } query[kSecValueData as String] = data query[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock SecItemAdd(query as CFDictionary, nil) } static func key(for deviceID: UUID) -> String? { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: deviceID.uuidString, kSecReturnData as String: true, kSecMatchLimit as String: kSecMatchLimitOne, ] var result: AnyObject? guard SecItemCopyMatching(query as CFDictionary, &result) == errSecSuccess, let data = result as? Data else { return nil } return String(data: data, encoding: .utf8) } } extension String { /// Wandelt einen Hex-String in Bytes. Leerzeichen, Doppelpunkte und ein /// führendes "0x" werden ignoriert, damit sich der Schlüssel aus /// VictronConnect einfach einfügen lässt. var hexBytes: [UInt8]? { var cleaned = self.lowercased() .replacingOccurrences(of: "0x", with: "") .filter { $0.isHexDigit } guard !cleaned.isEmpty, cleaned.count % 2 == 0 else { return nil } var bytes: [UInt8] = [] bytes.reserveCapacity(cleaned.count / 2) while !cleaned.isEmpty { let pair = String(cleaned.prefix(2)) cleaned.removeFirst(2) guard let byte = UInt8(pair, radix: 16) else { return nil } bytes.append(byte) } return bytes } }