wip(k1): checkpoint connection recovery rewrite
Capture the current unreleased K1 connection, recovery, lifecycle, viewer, and test work as a single known-bad baseline for subsequent fixes.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import AppKit
|
||||
import CoreWLAN
|
||||
import CryptoKit
|
||||
import Foundation
|
||||
import LocalAuthentication
|
||||
import Security
|
||||
|
||||
private let keychainService = "NODEDC Mission Core Host Wi-Fi Profiles"
|
||||
@@ -14,6 +15,8 @@ private struct HostWifiRequest: Decodable {
|
||||
let password: String?
|
||||
let scanTimeoutSeconds: Double?
|
||||
let credentialSourceID: String?
|
||||
let interfaceName: String?
|
||||
let continuityKeyHex: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case action
|
||||
@@ -22,6 +25,8 @@ private struct HostWifiRequest: Decodable {
|
||||
case password
|
||||
case scanTimeoutSeconds = "scan_timeout_seconds"
|
||||
case credentialSourceID = "credential_source_id"
|
||||
case interfaceName = "interface_name"
|
||||
case continuityKeyHex = "continuity_key_hex"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +65,9 @@ private struct HostWifiResponse: Encodable {
|
||||
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 {
|
||||
@@ -73,6 +81,9 @@ private struct HostWifiResponse: Encodable {
|
||||
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"
|
||||
}
|
||||
}
|
||||
@@ -88,6 +99,9 @@ private func emit(
|
||||
scanAttemptCount: Int? = nil,
|
||||
scanElapsedMilliseconds: Int? = nil,
|
||||
credentialSource: String? = nil,
|
||||
wifiInterface: Bool? = nil,
|
||||
associationIdentity: String? = nil,
|
||||
associationEvidence: String? = nil,
|
||||
reasonCode: String? = nil,
|
||||
exitCode: Int32
|
||||
) -> Never {
|
||||
@@ -102,6 +116,9 @@ private func emit(
|
||||
scanAttemptCount: scanAttemptCount,
|
||||
scanElapsedMilliseconds: scanElapsedMilliseconds,
|
||||
credentialSource: credentialSource,
|
||||
wifiInterface: wifiInterface,
|
||||
associationIdentity: associationIdentity,
|
||||
associationEvidence: associationEvidence,
|
||||
reasonCode: reasonCode
|
||||
)
|
||||
if let data = try? JSONEncoder().encode(response) {
|
||||
@@ -148,10 +165,75 @@ private func materialKeychainQuery(sourceID: String) -> [String: Any] {
|
||||
return keychainQuery(service: credentialMaterialKeychainService, account: sourceID)
|
||||
}
|
||||
|
||||
private func loadProfile(profileID: String) throws -> StoredProfile {
|
||||
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)
|
||||
@@ -191,10 +273,16 @@ private func storeProfile(profileID: String, profile: StoredProfile) throws {
|
||||
}
|
||||
}
|
||||
|
||||
private func loadCredentialMaterial(sourceID: String) throws -> StoredCredentialMaterial {
|
||||
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)
|
||||
@@ -237,25 +325,6 @@ private func storeCredentialMaterial(
|
||||
}
|
||||
}
|
||||
|
||||
private func loadSystemWiFiProfile(ssid: String, ssidData: Data) -> StoredProfile? {
|
||||
var password: NSString?
|
||||
let status = CWKeychainFindWiFiPassword(
|
||||
CWKeychainDomain.user,
|
||||
ssidData,
|
||||
&password
|
||||
)
|
||||
guard status == errSecSuccess, let password else {
|
||||
return nil
|
||||
}
|
||||
let profile = StoredProfile(
|
||||
schemaVersion: 1,
|
||||
ssid: ssid,
|
||||
password: password as String,
|
||||
credentialSource: "system-wifi-keychain"
|
||||
)
|
||||
return profileIsValid(profile) ? profile : nil
|
||||
}
|
||||
|
||||
private struct TargetedScanResult {
|
||||
let network: CWNetwork?
|
||||
let attemptCount: Int
|
||||
@@ -298,26 +367,57 @@ private func scanForExpectedNetwork(
|
||||
}
|
||||
}
|
||||
|
||||
private func promptForDevicePassword(ssid: String) -> String? {
|
||||
let application = NSApplication.shared
|
||||
application.setActivationPolicy(.accessory)
|
||||
|
||||
let passwordField = NSSecureTextField(frame: NSRect(x: 0, y: 0, width: 360, height: 24))
|
||||
passwordField.placeholderString = "Пароль точки доступа K1"
|
||||
|
||||
let alert = NSAlert()
|
||||
alert.alertStyle = .informational
|
||||
alert.messageText = "Первое подключение к \(ssid)"
|
||||
alert.informativeText = "macOS не нашла локальный профиль этой точки доступа. Если credential вам неизвестен, нажмите «Отмена» и выполните авторизованный импорт device-профиля LixelGO. Введённое значение будет сохранено только в Keychain этого Mac и не попадёт в браузер, API, журнал или evidence Mission Core."
|
||||
alert.accessoryView = passwordField
|
||||
alert.addButton(withTitle: "Подключиться")
|
||||
alert.addButton(withTitle: "Отмена")
|
||||
|
||||
application.activate(ignoringOtherApps: true)
|
||||
guard alert.runModal() == .alertFirstButtonReturn else {
|
||||
private func decodeContinuityKey(_ value: String) -> Data? {
|
||||
let bytes = Array(value.utf8)
|
||||
guard bytes.count == 64 else {
|
||||
return nil
|
||||
}
|
||||
return passwordField.stringValue
|
||||
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<SHA256>.authenticationCode(
|
||||
for: material,
|
||||
using: SymmetricKey(data: continuityKey)
|
||||
)
|
||||
return digest.map { String(format: "%02x", $0) }.joined()
|
||||
}
|
||||
|
||||
private let input = FileHandle.standardInput.readDataToEndOfFile()
|
||||
@@ -335,6 +435,75 @@ do {
|
||||
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)
|
||||
@@ -364,20 +533,27 @@ do {
|
||||
|
||||
if request.action == "check-credential-material" {
|
||||
do {
|
||||
_ = try loadCredentialMaterial(sourceID: request.profileID)
|
||||
let available = try keychainItemExists(
|
||||
service: credentialMaterialKeychainService,
|
||||
account: request.profileID
|
||||
)
|
||||
emit(
|
||||
ok: true,
|
||||
adapter: "macOS Keychain",
|
||||
profileAvailable: true,
|
||||
credentialSource: "exact-firmware-profile",
|
||||
profileAvailable: available,
|
||||
credentialSource: available ? "exact-firmware-profile" : nil,
|
||||
exitCode: 0
|
||||
)
|
||||
} catch {
|
||||
emit(
|
||||
ok: true,
|
||||
ok: false,
|
||||
adapter: "macOS Keychain",
|
||||
profileAvailable: false,
|
||||
exitCode: 0
|
||||
reasonCode: keychainReasonCode(
|
||||
error,
|
||||
missing: "credential-source-unavailable"
|
||||
),
|
||||
exitCode: 1
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -395,55 +571,77 @@ do {
|
||||
emit(ok: false, reasonCode: "credential-source-invalid", exitCode: 1)
|
||||
}
|
||||
|
||||
let material: StoredCredentialMaterial
|
||||
do {
|
||||
material = try loadCredentialMaterial(sourceID: sourceID)
|
||||
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: true,
|
||||
ok: false,
|
||||
adapter: "macOS Keychain",
|
||||
profileAvailable: false,
|
||||
profileEnrolled: false,
|
||||
reasonCode: "credential-source-unavailable",
|
||||
exitCode: 0
|
||||
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 {
|
||||
let existing = try loadProfile(profileID: request.profileID)
|
||||
guard existing.ssid == ssid else {
|
||||
emit(
|
||||
ok: false,
|
||||
adapter: "macOS Keychain",
|
||||
profileAvailable: false,
|
||||
profileEnrolled: false,
|
||||
reasonCode: "profile-ssid-mismatch",
|
||||
exitCode: 1
|
||||
)
|
||||
}
|
||||
if existing.password == material.password,
|
||||
existing.credentialSource != "exact-firmware-profile" {
|
||||
try storeProfile(
|
||||
profileID: request.profileID,
|
||||
profile: StoredProfile(
|
||||
schemaVersion: 1,
|
||||
ssid: ssid,
|
||||
password: existing.password,
|
||||
credentialSource: "exact-firmware-profile"
|
||||
)
|
||||
)
|
||||
}
|
||||
emit(
|
||||
ok: true,
|
||||
adapter: "macOS Keychain",
|
||||
profileAvailable: true,
|
||||
profileEnrolled: false,
|
||||
credentialSource: existing.password == material.password
|
||||
? "exact-firmware-profile"
|
||||
: (existing.credentialSource ?? "mission-core-keychain"),
|
||||
exitCode: 0
|
||||
)
|
||||
} catch {
|
||||
try storeProfile(
|
||||
profileID: request.profileID,
|
||||
profile: StoredProfile(
|
||||
@@ -461,39 +659,82 @@ do {
|
||||
credentialSource: "exact-firmware-profile",
|
||||
exitCode: 0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if request.action == "check-profile" {
|
||||
let profile: StoredProfile
|
||||
do {
|
||||
profile = try loadProfile(profileID: request.profileID)
|
||||
} catch {
|
||||
emit(
|
||||
ok: true,
|
||||
adapter: "macOS Keychain",
|
||||
profileAvailable: false,
|
||||
exitCode: 0
|
||||
)
|
||||
}
|
||||
if let expectedSSID = request.ssid, profile.ssid != expectedSSID {
|
||||
emit(
|
||||
ok: false,
|
||||
adapter: "macOS Keychain",
|
||||
profileAvailable: false,
|
||||
reasonCode: "profile-ssid-mismatch",
|
||||
profileEnrolled: false,
|
||||
reasonCode: keychainReasonCode(error, missing: "profile-unavailable"),
|
||||
exitCode: 1
|
||||
)
|
||||
}
|
||||
emit(
|
||||
ok: true,
|
||||
adapter: "macOS Keychain",
|
||||
profileAvailable: true,
|
||||
exitCode: 0
|
||||
)
|
||||
}
|
||||
|
||||
guard request.action == "associate" || request.action == "scan-profile" else {
|
||||
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 {
|
||||
@@ -528,7 +769,7 @@ do {
|
||||
)
|
||||
}
|
||||
|
||||
guard request.action == "associate" else {
|
||||
guard request.action == "associate" || request.action == "associate-ephemeral" else {
|
||||
emit(ok: false, reasonCode: "action-unsupported", exitCode: 1)
|
||||
}
|
||||
guard let expectedSSID = request.ssid,
|
||||
@@ -554,83 +795,76 @@ do {
|
||||
)
|
||||
}
|
||||
|
||||
var profileEnrolled = false
|
||||
var profileNeedsStore = false
|
||||
let credentialSource: String
|
||||
let profile: StoredProfile
|
||||
do {
|
||||
profile = try loadProfile(profileID: request.profileID)
|
||||
credentialSource = profile.credentialSource ?? "mission-core-keychain"
|
||||
} catch {
|
||||
if let systemProfile = loadSystemWiFiProfile(
|
||||
ssid: expectedSSID,
|
||||
ssidData: expectedSSIDData
|
||||
) {
|
||||
profile = systemProfile
|
||||
credentialSource = "system-wifi-keychain"
|
||||
profileNeedsStore = true
|
||||
} else {
|
||||
guard let password = promptForDevicePassword(ssid: expectedSSID) else {
|
||||
emit(
|
||||
ok: false,
|
||||
scanAttemptCount: scan.attemptCount,
|
||||
scanElapsedMilliseconds: scan.elapsedMilliseconds,
|
||||
reasonCode: "credential-entry-cancelled",
|
||||
exitCode: 1
|
||||
)
|
||||
}
|
||||
guard let passwordData = password.data(using: .utf8),
|
||||
(1 ... 64).contains(passwordData.count)
|
||||
else {
|
||||
emit(
|
||||
ok: false,
|
||||
scanAttemptCount: scan.attemptCount,
|
||||
scanElapsedMilliseconds: scan.elapsedMilliseconds,
|
||||
reasonCode: "credential-invalid",
|
||||
exitCode: 1
|
||||
)
|
||||
}
|
||||
profile = StoredProfile(
|
||||
schemaVersion: 1,
|
||||
ssid: expectedSSID,
|
||||
password: password,
|
||||
credentialSource: "native-secure-prompt"
|
||||
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 = "native-secure-prompt"
|
||||
profileNeedsStore = true
|
||||
}
|
||||
}
|
||||
guard profile.ssid == expectedSSID else {
|
||||
emit(
|
||||
ok: false,
|
||||
scanAttemptCount: scan.attemptCount,
|
||||
scanElapsedMilliseconds: scan.elapsedMilliseconds,
|
||||
reasonCode: "profile-ssid-mismatch",
|
||||
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() == profile.ssid {
|
||||
if profileNeedsStore {
|
||||
try storeProfile(profileID: request.profileID, profile: profile)
|
||||
profileEnrolled = true
|
||||
}
|
||||
if interface.ssid() == expectedSSID {
|
||||
emit(
|
||||
ok: true,
|
||||
adapter: "CoreWLAN",
|
||||
alreadyAssociated: true,
|
||||
profileEnrolled: profileEnrolled,
|
||||
profileEnrolled: false,
|
||||
scanAttemptCount: scan.attemptCount,
|
||||
scanElapsedMilliseconds: scan.elapsedMilliseconds,
|
||||
credentialSource: credentialSource,
|
||||
exitCode: 0
|
||||
)
|
||||
}
|
||||
try interface.associate(to: network, password: profile.password)
|
||||
if profileNeedsStore {
|
||||
try storeProfile(profileID: request.profileID, profile: profile)
|
||||
profileEnrolled = true
|
||||
}
|
||||
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.
|
||||
@@ -638,12 +872,12 @@ do {
|
||||
ok: true,
|
||||
adapter: "CoreWLAN",
|
||||
alreadyAssociated: false,
|
||||
profileEnrolled: profileEnrolled,
|
||||
profileEnrolled: false,
|
||||
scanAttemptCount: scan.attemptCount,
|
||||
scanElapsedMilliseconds: scan.elapsedMilliseconds,
|
||||
credentialSource: credentialSource,
|
||||
exitCode: 0
|
||||
)
|
||||
} catch {
|
||||
emit(ok: false, reasonCode: "corewlan-error", exitCode: 1)
|
||||
emit(ok: false, reasonCode: coreWLANReasonCode(error), exitCode: 1)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user