import CoreWLAN import CryptoKit import Foundation import LocalAuthentication import Security private let keychainService = "NODEDC Mission Core Host Wi-Fi Profiles" private let credentialMaterialKeychainService = "NODEDC Mission Core Device Credential Materials" private struct HostWifiRequest: Decodable { let action: String let profileID: String let ssid: String? let password: String? let scanTimeoutSeconds: Double? let credentialSourceID: String? let interfaceName: String? let continuityKeyHex: String? enum CodingKeys: String, CodingKey { case action case profileID = "profile_id" case ssid case password case scanTimeoutSeconds = "scan_timeout_seconds" case credentialSourceID = "credential_source_id" case interfaceName = "interface_name" case continuityKeyHex = "continuity_key_hex" } } private struct StoredProfile: Codable { let schemaVersion: Int let ssid: String let password: String let credentialSource: String? enum CodingKeys: String, CodingKey { case schemaVersion = "schema_version" case ssid case password case credentialSource = "credential_source" } } private struct StoredCredentialMaterial: Codable { let schemaVersion: Int let password: String enum CodingKeys: String, CodingKey { case schemaVersion = "schema_version" case password } } private struct HostWifiResponse: Encodable { let ok: Bool let adapter: String? let alreadyAssociated: Bool? let stored: Bool? let found: Bool? let profileAvailable: Bool? let profileEnrolled: Bool? let scanAttemptCount: Int? let scanElapsedMilliseconds: Int? let credentialSource: String? let wifiInterface: Bool? let associationIdentity: String? let associationEvidence: String? let reasonCode: String? enum CodingKeys: String, CodingKey { case ok case adapter case alreadyAssociated = "already_associated" case stored case found case profileAvailable = "profile_available" case profileEnrolled = "profile_enrolled" case scanAttemptCount = "scan_attempt_count" case scanElapsedMilliseconds = "scan_elapsed_ms" case credentialSource = "credential_source" case wifiInterface = "wifi_interface" case associationIdentity = "association_identity" case associationEvidence = "association_evidence" case reasonCode = "reason_code" } } private func emit( ok: Bool, adapter: String? = nil, alreadyAssociated: Bool? = nil, stored: Bool? = nil, found: Bool? = nil, profileAvailable: Bool? = nil, profileEnrolled: Bool? = nil, scanAttemptCount: Int? = nil, scanElapsedMilliseconds: Int? = nil, credentialSource: String? = nil, wifiInterface: Bool? = nil, associationIdentity: String? = nil, associationEvidence: String? = nil, reasonCode: String? = nil, exitCode: Int32 ) -> Never { let response = HostWifiResponse( ok: ok, adapter: adapter, alreadyAssociated: alreadyAssociated, stored: stored, found: found, profileAvailable: profileAvailable, profileEnrolled: profileEnrolled, scanAttemptCount: scanAttemptCount, scanElapsedMilliseconds: scanElapsedMilliseconds, credentialSource: credentialSource, wifiInterface: wifiInterface, associationIdentity: associationIdentity, associationEvidence: associationEvidence, reasonCode: reasonCode ) if let data = try? JSONEncoder().encode(response) { FileHandle.standardOutput.write(data) } exit(exitCode) } private func profileIsValid(_ profile: StoredProfile) -> Bool { guard profile.schemaVersion == 1, let ssidData = profile.ssid.data(using: .utf8), (1 ... 32).contains(ssidData.count), let passwordData = profile.password.data(using: .utf8), (1 ... 64).contains(passwordData.count) else { return false } return true } private func credentialMaterialIsValid(_ material: StoredCredentialMaterial) -> Bool { guard material.schemaVersion == 1, let passwordData = material.password.data(using: .utf8), (8 ... 63).contains(passwordData.count) else { return false } return true } private func keychainQuery(service: String, account: String) -> [String: Any] { return [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: account, ] } private func profileKeychainQuery(profileID: String) -> [String: Any] { return keychainQuery(service: keychainService, account: profileID) } private func materialKeychainQuery(sourceID: String) -> [String: Any] { return keychainQuery(service: credentialMaterialKeychainService, account: sourceID) } private func nonInteractiveAuthenticationContext() -> LAContext { let context = LAContext() context.interactionNotAllowed = true return context } private func keychainItemExists(service: String, account: String) throws -> Bool { var query = keychainQuery(service: service, account: account) query[kSecReturnAttributes as String] = true query[kSecMatchLimit as String] = kSecMatchLimitOne // Preflight is deliberately non-interactive. Authorization prompts belong // only to an explicit enrollment/migration step, never to a K1 network // mutation that has already been admitted by the browser. query[kSecUseAuthenticationContext as String] = nonInteractiveAuthenticationContext() var item: CFTypeRef? let status = SecItemCopyMatching(query as CFDictionary, &item) if status == errSecSuccess { return true } if status == errSecItemNotFound { return false } throw NSError(domain: "HostWifiKeychainMetadata", code: Int(status)) } private func keychainReasonCode(_ error: Error, missing: String) -> String { let status = OSStatus((error as NSError).code) switch status { case errSecItemNotFound: return missing case errSecInteractionNotAllowed: return "keychain-authorization-required" case errSecAuthFailed: return "keychain-authorization-denied" case errSecUserCanceled: return "keychain-authorization-cancelled" default: return "keychain-access-failed" } } private func coreWLANReasonCode(_ error: Error) -> String { let nsError = error as NSError guard nsError.domain == CWErrorDomain else { return "corewlan-error" } // Stable CWErr values from Apple's CoreWLANTypes contract. Export only a // reviewed failure class; NSError descriptions may contain host details. switch nsError.code { case -3930: // kCWOperationNotPermittedErr return "corewlan-authorization-denied" case -3905, -3925: // kCWTimeoutErr, kCWSupplicantTimeoutErr return "host-wifi-operation-timeout" default: return "corewlan-error" } } private func loadProfile( profileID: String, interactionAllowed: Bool = true ) throws -> StoredProfile { var query = profileKeychainQuery(profileID: profileID) query[kSecReturnData as String] = true query[kSecMatchLimit as String] = kSecMatchLimitOne if !interactionAllowed { query[kSecUseAuthenticationContext as String] = nonInteractiveAuthenticationContext() } var item: CFTypeRef? let status = SecItemCopyMatching(query as CFDictionary, &item) guard status == errSecSuccess, let data = item as? Data else { throw NSError(domain: "HostWifiProfile", code: Int(status)) } let profile = try JSONDecoder().decode(StoredProfile.self, from: data) guard profileIsValid(profile) else { throw NSError(domain: "HostWifiProfile", code: Int(errSecDecode)) } return profile } private func storeProfile(profileID: String, profile: StoredProfile) throws { guard profileIsValid(profile) else { throw NSError(domain: "HostWifiProfile", code: Int(errSecParam)) } let data = try JSONEncoder().encode(profile) let query = profileKeychainQuery(profileID: profileID) let attributes = [ kSecValueData as String: data, kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly, ] as [String: Any] let updateStatus = SecItemUpdate(query as CFDictionary, attributes as CFDictionary) if updateStatus == errSecSuccess { return } guard updateStatus == errSecItemNotFound else { throw NSError(domain: "HostWifiProfile", code: Int(updateStatus)) } var newItem = query attributes.forEach { key, value in newItem[key] = value } let addStatus = SecItemAdd(newItem as CFDictionary, nil) guard addStatus == errSecSuccess else { throw NSError(domain: "HostWifiProfile", code: Int(addStatus)) } } private func loadCredentialMaterial( sourceID: String, interactionAllowed: Bool = true ) throws -> StoredCredentialMaterial { var query = materialKeychainQuery(sourceID: sourceID) query[kSecReturnData as String] = true query[kSecMatchLimit as String] = kSecMatchLimitOne if !interactionAllowed { query[kSecUseAuthenticationContext as String] = nonInteractiveAuthenticationContext() } var item: CFTypeRef? let status = SecItemCopyMatching(query as CFDictionary, &item) guard status == errSecSuccess, let data = item as? Data else { throw NSError(domain: "HostWifiCredentialMaterial", code: Int(status)) } let material = try JSONDecoder().decode(StoredCredentialMaterial.self, from: data) guard credentialMaterialIsValid(material) else { throw NSError(domain: "HostWifiCredentialMaterial", code: Int(errSecDecode)) } return material } private func storeCredentialMaterial( sourceID: String, material: StoredCredentialMaterial ) throws { guard credentialMaterialIsValid(material) else { throw NSError(domain: "HostWifiCredentialMaterial", code: Int(errSecParam)) } let data = try JSONEncoder().encode(material) let query = materialKeychainQuery(sourceID: sourceID) let attributes = [ kSecValueData as String: data, kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly, ] as [String: Any] let updateStatus = SecItemUpdate(query as CFDictionary, attributes as CFDictionary) if updateStatus == errSecSuccess { return } guard updateStatus == errSecItemNotFound else { throw NSError(domain: "HostWifiCredentialMaterial", code: Int(updateStatus)) } var newItem = query attributes.forEach { key, value in newItem[key] = value } let addStatus = SecItemAdd(newItem as CFDictionary, nil) guard addStatus == errSecSuccess else { throw NSError(domain: "HostWifiCredentialMaterial", code: Int(addStatus)) } } private struct TargetedScanResult { let network: CWNetwork? let attemptCount: Int let elapsedMilliseconds: Int } private func scanForExpectedNetwork( interface: CWInterface, ssid: String, ssidData: Data, timeoutSeconds: Double ) throws -> TargetedScanResult { let started = ProcessInfo.processInfo.systemUptime var attemptCount = 0 while true { attemptCount += 1 let networks = try interface.scanForNetworks(withSSID: ssidData) if let network = networks.first(where: { $0.ssid == ssid }) { return TargetedScanResult( network: network, attemptCount: attemptCount, elapsedMilliseconds: max( 0, Int((ProcessInfo.processInfo.systemUptime - started) * 1_000) ) ) } let elapsed = ProcessInfo.processInfo.systemUptime - started let remaining = timeoutSeconds - elapsed if remaining <= 0 { return TargetedScanResult( network: nil, attemptCount: attemptCount, elapsedMilliseconds: max(0, Int(elapsed * 1_000)) ) } Thread.sleep(forTimeInterval: min(0.75, remaining)) } } private func decodeContinuityKey(_ value: String) -> Data? { let bytes = Array(value.utf8) guard bytes.count == 64 else { return nil } func nibble(_ byte: UInt8) -> UInt8? { switch byte { case 48 ... 57: return byte - 48 case 97 ... 102: return byte - 87 default: return nil } } var decoded = Data(capacity: 32) for offset in stride(from: 0, to: bytes.count, by: 2) { guard let high = nibble(bytes[offset]), let low = nibble(bytes[offset + 1]) else { return nil } decoded.append((high << 4) | low) } return decoded } private func appendLengthPrefixed(_ value: String, to material: inout Data) { let data = Data(value.utf8) var length = UInt32(data.count).bigEndian withUnsafeBytes(of: &length) { bytes in material.append(contentsOf: bytes) } material.append(data) } private func associationIdentity( continuityKey: Data, interfaceName: String, bssid: String ) -> String { // BSSID is the association identity. SSID visibility is permission- and // timing-dependent on macOS, so folding it into this token would rotate a // healthy binding when the same AP alternates between `ssid+bssid` and // `bssid-only` evidence. var material = Data("mission-core/host-wifi-association/v2".utf8) appendLengthPrefixed(interfaceName, to: &material) appendLengthPrefixed(bssid.lowercased(), to: &material) let digest = HMAC.authenticationCode( for: material, using: SymmetricKey(data: continuityKey) ) return digest.map { String(format: "%02x", $0) }.joined() } private let input = FileHandle.standardInput.readDataToEndOfFile() guard input.count <= 2048 else { emit(ok: false, reasonCode: "request-too-large", exitCode: 1) } do { let request = try JSONDecoder().decode(HostWifiRequest.self, from: input) guard (1 ... 128).contains(request.profileID.count), request.profileID.allSatisfy({ $0.isASCII && ($0.isLetter || $0.isNumber || ".-_".contains($0)) }) else { emit(ok: false, reasonCode: "profile-id-invalid", exitCode: 1) } if request.action == "inspect-association" { guard let interfaceName = request.interfaceName, (1 ... 32).contains(interfaceName.count), interfaceName.allSatisfy({ $0.isASCII && ($0.isLetter || $0.isNumber || ".-_".contains($0)) }), let continuityKeyHex = request.continuityKeyHex, let continuityKey = decodeContinuityKey(continuityKeyHex) else { emit(ok: false, reasonCode: "association-inspection-invalid", exitCode: 1) } guard let interface = CWWiFiClient.shared().interface(withName: interfaceName) else { emit( ok: true, adapter: "CoreWLAN", wifiInterface: false, associationIdentity: associationIdentity( continuityKey: continuityKey, interfaceName: interfaceName, bssid: "not-wifi-interface" ), associationEvidence: "not-wifi", exitCode: 0 ) } guard interface.powerOn(), interface.serviceActive() else { emit( ok: true, adapter: "CoreWLAN", wifiInterface: true, associationEvidence: "unavailable", reasonCode: "wifi-interface-inactive", exitCode: 0 ) } let currentSSID = interface.ssid()?.trimmingCharacters(in: .whitespacesAndNewlines) let currentBSSID = interface.bssid()?.trimmingCharacters(in: .whitespacesAndNewlines) guard let currentBSSID, !currentBSSID.isEmpty else { // SSID alone is not an exact association identity: two APs may use // the same network name. Returning no digest forces the Python // caller to rotate its fail-closed continuity token. emit( ok: true, adapter: "CoreWLAN", wifiInterface: true, associationEvidence: "unavailable", reasonCode: "association-identity-unavailable", exitCode: 0 ) } emit( ok: true, adapter: "CoreWLAN", wifiInterface: true, associationIdentity: associationIdentity( continuityKey: continuityKey, interfaceName: interfaceName, bssid: currentBSSID ), associationEvidence: ( currentSSID == nil || currentSSID?.isEmpty == true ? "bssid-only" : "ssid+bssid" ), exitCode: 0 ) } if request.action == "store-profile" { guard let ssid = request.ssid, let password = request.password else { emit(ok: false, reasonCode: "credential-missing", exitCode: 1) } try storeProfile( profileID: request.profileID, profile: StoredProfile( schemaVersion: 1, ssid: ssid, password: password, credentialSource: nil ) ) emit(ok: true, adapter: "macOS Keychain", stored: true, exitCode: 0) } if request.action == "store-credential-material" { guard let password = request.password else { emit(ok: false, reasonCode: "credential-missing", exitCode: 1) } try storeCredentialMaterial( sourceID: request.profileID, material: StoredCredentialMaterial(schemaVersion: 1, password: password) ) emit(ok: true, adapter: "macOS Keychain", stored: true, exitCode: 0) } if request.action == "check-credential-material" { do { let available = try keychainItemExists( service: credentialMaterialKeychainService, account: request.profileID ) emit( ok: true, adapter: "macOS Keychain", profileAvailable: available, credentialSource: available ? "exact-firmware-profile" : nil, exitCode: 0 ) } catch { emit( ok: false, adapter: "macOS Keychain", profileAvailable: false, reasonCode: keychainReasonCode( error, missing: "credential-source-unavailable" ), exitCode: 1 ) } } if request.action == "ensure-profile" { guard let ssid = request.ssid, let ssidData = ssid.data(using: .utf8), (1 ... 32).contains(ssidData.count), let sourceID = request.credentialSourceID, (1 ... 128).contains(sourceID.count), sourceID.allSatisfy({ $0.isASCII && ($0.isLetter || $0.isNumber || ".-_".contains($0)) }) else { emit(ok: false, reasonCode: "credential-source-invalid", exitCode: 1) } do { let profileAvailable = try keychainItemExists( service: keychainService, account: request.profileID ) if profileAvailable { let profile = try loadProfile( profileID: request.profileID, interactionAllowed: false ) guard profile.ssid == ssid else { emit( ok: false, adapter: "macOS Keychain", profileAvailable: false, profileEnrolled: false, reasonCode: "profile-ssid-mismatch", exitCode: 1 ) } guard profile.credentialSource == "exact-firmware-profile" else { emit( ok: false, adapter: "macOS Keychain", profileAvailable: false, profileEnrolled: false, reasonCode: "profile-credential-source-mismatch", exitCode: 1 ) } emit( ok: true, adapter: "macOS Keychain", profileAvailable: true, profileEnrolled: false, credentialSource: "exact-firmware-profile", exitCode: 0 ) } } catch { emit( ok: false, adapter: "macOS Keychain", profileAvailable: false, profileEnrolled: false, reasonCode: keychainReasonCode(error, missing: "profile-unavailable"), exitCode: 1 ) } let material: StoredCredentialMaterial do { material = try loadCredentialMaterial( sourceID: sourceID, interactionAllowed: false ) } catch { emit( ok: false, adapter: "macOS Keychain", profileAvailable: false, profileEnrolled: false, reasonCode: keychainReasonCode( error, missing: "credential-source-unavailable" ), exitCode: 1 ) } do { try storeProfile( profileID: request.profileID, profile: StoredProfile( schemaVersion: 1, ssid: ssid, password: material.password, credentialSource: "exact-firmware-profile" ) ) emit( ok: true, adapter: "macOS Keychain", profileAvailable: true, profileEnrolled: true, credentialSource: "exact-firmware-profile", exitCode: 0 ) } catch { emit( ok: false, adapter: "macOS Keychain", profileAvailable: false, profileEnrolled: false, reasonCode: keychainReasonCode(error, missing: "profile-unavailable"), exitCode: 1 ) } } if request.action == "check-profile" { guard let expectedSSID = request.ssid, let expectedSSIDData = expectedSSID.data(using: .utf8), (1 ... 32).contains(expectedSSIDData.count) else { emit(ok: false, reasonCode: "ssid-invalid", exitCode: 1) } do { let available = try keychainItemExists( service: keychainService, account: request.profileID ) if !available { emit( ok: true, adapter: "macOS Keychain", profileAvailable: false, exitCode: 0 ) } let profile = try loadProfile( profileID: request.profileID, interactionAllowed: false ) guard profile.ssid == expectedSSID else { emit( ok: false, adapter: "macOS Keychain", profileAvailable: false, reasonCode: "profile-ssid-mismatch", exitCode: 1 ) } guard profile.credentialSource == "exact-firmware-profile" else { emit( ok: false, adapter: "macOS Keychain", profileAvailable: false, reasonCode: "profile-credential-source-mismatch", exitCode: 1 ) } emit( ok: true, adapter: "macOS Keychain", profileAvailable: true, credentialSource: "exact-firmware-profile", exitCode: 0 ) } catch { emit( ok: false, adapter: "macOS Keychain", profileAvailable: false, reasonCode: keychainReasonCode(error, missing: "profile-unavailable"), exitCode: 1 ) } } guard request.action == "associate" || request.action == "associate-ephemeral" || request.action == "scan-profile" else { emit(ok: false, reasonCode: "action-unsupported", exitCode: 1) } guard let interface = CWWiFiClient.shared().interface() else { emit(ok: false, reasonCode: "wifi-interface-unavailable", exitCode: 1) } let scanTimeoutSeconds = request.scanTimeoutSeconds ?? 0 guard scanTimeoutSeconds >= 0, scanTimeoutSeconds <= 60 else { emit(ok: false, reasonCode: "scan-timeout-invalid", exitCode: 1) } if request.action == "scan-profile" { let profile: StoredProfile do { profile = try loadProfile(profileID: request.profileID) } catch { emit(ok: false, reasonCode: "profile-unavailable", exitCode: 1) } let ssidData = profile.ssid.data(using: .utf8)! let scan = try scanForExpectedNetwork( interface: interface, ssid: profile.ssid, ssidData: ssidData, timeoutSeconds: scanTimeoutSeconds ) emit( ok: true, adapter: "CoreWLAN", found: scan.network != nil, scanAttemptCount: scan.attemptCount, scanElapsedMilliseconds: scan.elapsedMilliseconds, exitCode: 0 ) } guard request.action == "associate" || request.action == "associate-ephemeral" else { emit(ok: false, reasonCode: "action-unsupported", exitCode: 1) } guard let expectedSSID = request.ssid, let expectedSSIDData = expectedSSID.data(using: .utf8), (1 ... 32).contains(expectedSSIDData.count) else { emit(ok: false, reasonCode: "ssid-invalid", exitCode: 1) } let scan = try scanForExpectedNetwork( interface: interface, ssid: expectedSSID, ssidData: expectedSSIDData, timeoutSeconds: scanTimeoutSeconds ) guard let network = scan.network else { emit( ok: false, scanAttemptCount: scan.attemptCount, scanElapsedMilliseconds: scan.elapsedMilliseconds, reasonCode: "network-not-found", exitCode: 1 ) } let credentialSource: String let associationPassword: String if request.action == "associate-ephemeral" { guard let password = request.password, let passwordData = password.data(using: .utf8), (1 ... 64).contains(passwordData.count) else { emit( ok: false, scanAttemptCount: scan.attemptCount, scanElapsedMilliseconds: scan.elapsedMilliseconds, reasonCode: "credential-missing", exitCode: 1 ) } credentialSource = "operation-memory" associationPassword = password } else { let profile: StoredProfile do { // The AP write has already happened. A prepared-host Quick action must // never trigger a Keychain authorization sheet at this stage. profile = try loadProfile( profileID: request.profileID, interactionAllowed: false ) guard profile.credentialSource == "exact-firmware-profile" else { emit( ok: false, scanAttemptCount: scan.attemptCount, scanElapsedMilliseconds: scan.elapsedMilliseconds, reasonCode: "profile-credential-source-mismatch", exitCode: 1 ) } credentialSource = "exact-firmware-profile" } catch { emit( ok: false, scanAttemptCount: scan.attemptCount, scanElapsedMilliseconds: scan.elapsedMilliseconds, reasonCode: keychainReasonCode(error, missing: "profile-unavailable"), exitCode: 1 ) } guard profile.ssid == expectedSSID else { emit( ok: false, scanAttemptCount: scan.attemptCount, scanElapsedMilliseconds: scan.elapsedMilliseconds, reasonCode: "profile-ssid-mismatch", exitCode: 1 ) } associationPassword = profile.password } if interface.ssid() == expectedSSID { emit( ok: true, adapter: "CoreWLAN", alreadyAssociated: true, profileEnrolled: false, scanAttemptCount: scan.attemptCount, scanElapsedMilliseconds: scan.elapsedMilliseconds, credentialSource: credentialSource, exitCode: 0 ) } try interface.associate(to: network, password: associationPassword) // CoreWLAN's synchronous association call throws on failure. Reading the // current SSID again would require Location authorization on recent macOS // versions and could turn a successful association into a false negative. emit( ok: true, adapter: "CoreWLAN", alreadyAssociated: false, profileEnrolled: false, scanAttemptCount: scan.attemptCount, scanElapsedMilliseconds: scan.elapsedMilliseconds, credentialSource: credentialSource, exitCode: 0 ) } catch { emit(ok: false, reasonCode: coreWLANReasonCode(error), exitCode: 1) }