From b478016f1f3d4bef405e7d6860aa5b600d623df3 Mon Sep 17 00:00:00 2001 From: Andrey Zhuchkov Date: Sat, 6 Jun 2026 15:39:02 +0400 Subject: [PATCH] feat: expose Wi-Fi information elements Add raw CoreWLAN informationElementData to scan results and carry the IE blob in v2 scan responses while preserving v1 scan decoding for older helper frames. Update the scan example to summarize RSN auth, unicast, and group suites from information elements, with targeted tests for TLV parsing and malformed data handling. Verified with: - go test ./examples/scan - go test . -run 'Test(ReadHeader|DecodeScanResponse)' - go test -short ./... - make swift-test with the project Swift test target override --- README.md | 1 + docs/getting-started.md | 1 + examples/scan/main.go | 134 +++++++++++++++++++++++++-- examples/scan/main_test.go | 97 +++++++++++++++++++ macwifi.go | 31 ++++--- protocol.go | 55 ++++++++--- protocol_test.go | 107 ++++++++++++++------- scanner/Sources/WifiScanner.swift | 35 ++++--- scanner/Tests/WifiScannerTests.swift | 35 ++++++- 9 files changed, 410 insertions(+), 86 deletions(-) create mode 100644 examples/scan/main_test.go diff --git a/README.md b/README.md index 603ef8b..6f62a7a 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,7 @@ Available data: | `ChannelWidth` | Channel bandwidth in MHz. | | `Security` | Open, WEP, WPA, WPA2, WPA3, enterprise, OWE, or unknown. | | `PHYMode` | 802.11 mode when available. | +| `InformationElements` | Raw 802.11 information element TLV bytes, when macOS reports them. | | `Current` | Whether the Mac is connected to this network now. | | `Saved` | Whether the SSID is in the Mac's preferred networks list. | | `Password` | Always empty from `Scan`; use `Password(ctx, ssid)` when needed. | diff --git a/docs/getting-started.md b/docs/getting-started.md index 7ae64ec..cf164e1 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -114,6 +114,7 @@ exposes that a Go developer is likely to want: | `ChannelWidth` | Channel bandwidth in MHz. | | `Security` | Open, WEP, WPA, WPA2, WPA3, enterprise, OWE, or unknown. | | `PHYMode` | 802.11 mode when available. | +| `InformationElements` | Raw 802.11 information element TLV bytes, when macOS reports them. | | `Current` | Whether the Mac is connected to this network now. | | `Saved` | Whether the SSID is in the preferred-networks list. | | `Password` | Always `""` from `Scan`; use `Password(ctx, ssid)`. | diff --git a/examples/scan/main.go b/examples/scan/main.go index 3abaeb0..c3786fd 100644 --- a/examples/scan/main.go +++ b/examples/scan/main.go @@ -6,9 +6,11 @@ package main import ( "context" + "encoding/binary" "flag" "fmt" "os" + "strings" "text/tabwriter" "github.com/jaisonerick/macwifi" @@ -43,18 +45,130 @@ func main() { } tw := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) - fmt.Fprintln(tw, "SSID\tRSSI\tCH\tBAND\tWIDTH\tSEC\tBSSID\tFLAGS") + fmt.Fprintln(tw, "SSID\tRSSI\tCH\tBAND\tWIDTH\tSEC\tRSN (auth/unicast/group)\tBSSID\tFLAGS") for _, n := range nets { - flags := "" - if n.Current { - flags += "C" - } - if n.Saved { - flags += "S" - } - fmt.Fprintf(tw, "%s\t%d\t%d\t%s\t%d\t%s\t%s\t%s\n", + ies := parseInformationElements(n.InformationElements) + fmt.Fprintf(tw, "%s\t%d\t%d\t%s\t%d\t%s\t%s\t%s\t%s\n", n.SSID, n.RSSI, n.Channel, n.ChannelBand, n.ChannelWidth, - n.Security, n.BSSID, flags) + n.Security, rsnSummary(ies), n.BSSID, networkFlags(n)) } tw.Flush() } + +func networkFlags(n macwifi.Network) string { + flags := "" + if n.Current { + flags += "C" + } + if n.Saved { + flags += "S" + } + return flags +} + +func rsnSummary(ies map[byte][][]byte) string { + for _, payload := range ies[48] { + if summary := parseRSN(payload); summary != "" { + return summary + } + } + return "-" +} + +func parseRSN(data []byte) string { + if len(data) < 8 { + return "" + } + group := suiteName(data[2:6], cipherSuites) + offset := 6 + + pairwise, next, ok := parseSuiteList(data, offset, cipherSuites) + if !ok { + return "" + } + akms, next, ok := parseSuiteList(data, next, akmSuites) + if !ok { + return "" + } + + return fmt.Sprintf("%s/%s/%s", + strings.Join(akms, "+"), strings.Join(pairwise, "+"), group) +} + +func parseSuiteList(data []byte, offset int, names map[byte]string) ([]string, int, bool) { + if offset > len(data) { + return nil, 0, false + } + if len(data[offset:]) < 2 { + return nil, 0, false + } + count := int(binary.LittleEndian.Uint16(data[offset : offset+2])) + offset += 2 + if len(data[offset:]) < count*4 { + return nil, 0, false + } + suites := make([]string, 0, count) + for i := 0; i < count; i++ { + suites = append(suites, suiteName(data[offset:offset+4], names)) + offset += 4 + } + return suites, offset, true +} + +func suiteName(suite []byte, names map[byte]string) string { + if len(suite) != 4 { + return "?" + } + if suite[0] == 0x00 && suite[1] == 0x0f && suite[2] == 0xac { + if name, ok := names[suite[3]]; ok { + return name + } + } + return fmt.Sprintf("%02x-%02x-%02x:%d", suite[0], suite[1], suite[2], suite[3]) +} + +func parseInformationElements(data []byte) map[byte][][]byte { + ies := make(map[byte][][]byte) + for i := 0; i+2 <= len(data); { + id := data[i] + n := int(data[i+1]) + i += 2 + if i+n > len(data) { + break + } + payload := data[i : i+n] + i += n + ies[id] = append(ies[id], payload) + } + return ies +} + +var cipherSuites = map[byte]string{ + 0: "USE-GROUP", + 1: "WEP40", + 2: "TKIP", + 4: "CCMP", + 5: "WEP104", + 6: "BIP-CMAC", + 8: "GCMP", + 9: "GCMP-256", + 10: "CCMP-256", + 11: "BIP-GMAC", + 12: "BIP-GMAC-256", + 13: "BIP-CMAC-256", +} + +var akmSuites = map[byte]string{ + 1: "802.1X", + 2: "PSK", + 3: "FT-802.1X", + 4: "FT-PSK", + 5: "802.1X-SHA256", + 6: "PSK-SHA256", + 8: "SAE", + 9: "FT-SAE", + 11: "802.1X-SUITE-B", + 12: "802.1X-SUITE-B-192", + 13: "FT-802.1X-SHA384", + 18: "OWE", +} diff --git a/examples/scan/main_test.go b/examples/scan/main_test.go new file mode 100644 index 0000000..f711d6e --- /dev/null +++ b/examples/scan/main_test.go @@ -0,0 +1,97 @@ +package main + +import "testing" + +func TestParseInformationElements(t *testing.T) { + tests := []struct { + name string + data []byte + wantIDs map[byte]int + }{ + {name: "empty", wantIDs: map[byte]int{}}, + {name: "truncated header", data: []byte{48}, wantIDs: map[byte]int{}}, + {name: "truncated payload", data: []byte{48, 2, 1}, wantIDs: map[byte]int{}}, + {name: "complete elements", data: []byte{ + 3, 1, 6, + 48, 2, 1, 0, + 221, 4, 0x00, 0x50, 0xF2, 0x01, + }, wantIDs: map[byte]int{3: 1, 48: 1, 221: 1}}, + {name: "complete before truncated tail", data: []byte{ + 48, 1, 0, + 61, 4, 1, + }, wantIDs: map[byte]int{48: 1}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := parseInformationElements(tt.data) + if len(got) != len(tt.wantIDs) { + t.Fatalf("len(parseInformationElements()) = %d, want %d", len(got), len(tt.wantIDs)) + } + for id, want := range tt.wantIDs { + if len(got[id]) != want { + t.Fatalf("len(ies[%d]) = %d, want %d", id, len(got[id]), want) + } + } + }) + } +} + +func TestParseInformationElementsIndexesPayloadsByID(t *testing.T) { + data := []byte{ + 61, 2, 6, 1, + 48, 2, 1, 0, + 61, 2, 6, 3, + } + + got := parseInformationElements(data) + if len(got[61]) != 2 { + t.Fatalf("len(ies[61]) = %d, want 2", len(got[61])) + } + if len(got[48]) != 1 { + t.Fatalf("len(ies[48]) = %d, want 1", len(got[48])) + } + if &got[61][0][0] != &data[2] { + t.Fatal("ies[61][0] does not reference the original data payload") + } + if got[61][1][1] != 3 { + t.Fatalf("ies[61][1][1] = %d, want 3", got[61][1][1]) + } +} + +func TestRSNSummary(t *testing.T) { + ies := parseInformationElements([]byte{ + 48, 20, + 1, 0, // version + 0x00, 0x0f, 0xac, 4, // group CCMP + 1, 0, 0x00, 0x0f, 0xac, 4, // pairwise CCMP + 1, 0, 0x00, 0x0f, 0xac, 2, // AKM PSK + 0xc0, 0x00, // MFP capable + required + }) + + got := rsnSummary(ies) + want := "PSK/CCMP/CCMP" + if got != want { + t.Fatalf("rsnSummary() = %q, want %q", got, want) + } +} + +func TestRSNSummaryHandlesMissingAndMalformedData(t *testing.T) { + tests := []struct { + name string + data []byte + want string + }{ + {name: "missing", want: "-"}, + {name: "truncated", data: []byte{48, 2, 1, 0}, want: "-"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := rsnSummary(parseInformationElements(tt.data)) + if got != tt.want { + t.Fatalf("rsnSummary() = %q, want %q", got, tt.want) + } + }) + } +} diff --git a/macwifi.go b/macwifi.go index 998d644..4d81987 100644 --- a/macwifi.go +++ b/macwifi.go @@ -102,18 +102,19 @@ func (s Security) String() string { // Network is one WiFi network observed by the scanner. type Network struct { - SSID string - BSSID string // six-octet MAC, lower-case colon-separated; empty if unavailable - RSSI int // signal strength, dBm - Noise int // noise floor, dBm - Channel int // channel number - ChannelBand Band // 2.4/5/6 GHz - ChannelWidth int // bandwidth in MHz (20/40/80/160) - Security Security - PHYMode string // "802.11ax" etc. - Password string // always "" from Scan; use Password() separately - Current bool // connected right now - Saved bool // in the preferred-networks list + SSID string + BSSID string // six-octet MAC, lower-case colon-separated; empty if unavailable + RSSI int // signal strength, dBm + Noise int // noise floor, dBm + Channel int // channel number + ChannelBand Band // 2.4/5/6 GHz + ChannelWidth int // bandwidth in MHz (20/40/80/160) + Security Security + PHYMode string // "802.11ax" etc. + InformationElements []byte // raw CoreWLAN informationElementData TLV bytes + Password string // always "" from Scan; use Password() separately + Current bool // connected right now + Saved bool // in the preferred-networks list } // Client is a live session against the scanner helper. One helper process @@ -189,14 +190,14 @@ func (c *Client) Scan(ctx context.Context) ([]Network, error) { if err := writeScanRequest(c.conn); err != nil { return nil, fmt.Errorf("macwifi: write scan request: %w", err) } - msgType, err := readHeader(c.conn) + version, msgType, err := readHeader(c.conn) if err != nil { return nil, fmt.Errorf("macwifi: read scan response: %w", err) } if msgType != msgTypeScanResponse { return nil, fmt.Errorf("macwifi: expected scan_response, got 0x%02x", msgType) } - return decodeScanResponse(c.conn) + return decodeScanResponse(c.conn, version) } // Password requests a saved WiFi password for ssid. Returns "" (no error) @@ -229,7 +230,7 @@ func (c *Client) Password(ctx context.Context, ssid string, opts ...PasswordOpti if err := writePasswordRequest(c.conn, ssid); err != nil { return "", fmt.Errorf("macwifi: write password request: %w", err) } - msgType, err := readHeader(c.conn) + _, msgType, err := readHeader(c.conn) if err != nil { return "", fmt.Errorf("macwifi: read password response: %w", err) } diff --git a/protocol.go b/protocol.go index 79e2fa9..cf242ec 100644 --- a/protocol.go +++ b/protocol.go @@ -9,7 +9,7 @@ package macwifi // // Frame header (every message): // -// "MWIF" | u8 version=1 | u8 msgType | body +// "MWIF" | u8 version | u8 msgType | body // // Body layout depends on msgType: // @@ -19,6 +19,10 @@ package macwifi // 0x01 scan_response u16 errLen | errMsg | u32 count | networks // 0x02 password_response u16 errLen | errMsg | u16 pwdLen | pwd // +// Protocol v2 extends each scan_response network with: +// +// u32 informationElementLen | informationElementBytes +// // All multi-byte integers little-endian. Strings are UTF-8. import ( @@ -29,8 +33,9 @@ import ( ) const ( - protocolMagic = "MWIF" - protocolVersion = 1 + protocolMagic = "MWIF" + protocolVersion = 2 + minProtocolVersion = 1 msgTypeScanResponse = 0x01 msgTypePasswordResponse = 0x02 @@ -71,25 +76,24 @@ func writeCloseRequest(w io.Writer) error { // readHeader reads the "MWIF"|version|msgType prefix. The caller then // dispatches on msgType to parse the body. -func readHeader(r io.Reader) (msgType uint8, err error) { +func readHeader(r io.Reader) (version, msgType uint8, err error) { var magic [4]byte if _, err := io.ReadFull(r, magic[:]); err != nil { - return 0, fmt.Errorf("read magic: %w", err) + return 0, 0, fmt.Errorf("read magic: %w", err) } if string(magic[:]) != protocolMagic { - return 0, fmt.Errorf("bad magic: got %q, want %q", magic, protocolMagic) + return 0, 0, fmt.Errorf("bad magic: got %q, want %q", magic, protocolMagic) } - var version uint8 if err := readInt(r, &version); err != nil { - return 0, err + return 0, 0, err } - if version != protocolVersion { - return 0, fmt.Errorf("unsupported protocol version %d (want %d)", version, protocolVersion) + if version < minProtocolVersion || version > protocolVersion { + return 0, 0, fmt.Errorf("unsupported protocol version %d (want %d-%d)", version, minProtocolVersion, protocolVersion) } if err := readInt(r, &msgType); err != nil { - return 0, err + return 0, 0, err } - return msgType, nil + return version, msgType, nil } // readError reads the leading u16 errLen | errMsg from a response body. @@ -110,7 +114,7 @@ func readError(r io.Reader) error { } // decodeScanResponse reads a scan-response body (header already consumed). -func decodeScanResponse(r io.Reader) ([]Network, error) { +func decodeScanResponse(r io.Reader, version uint8) ([]Network, error) { if err := readError(r); err != nil { return nil, err } @@ -120,7 +124,7 @@ func decodeScanResponse(r io.Reader) ([]Network, error) { } nets := make([]Network, 0, count) for i := uint32(0); i < count; i++ { - n, err := decodeNetwork(r) + n, err := decodeNetwork(r, version) if err != nil { return nil, fmt.Errorf("network %d: %w", i, err) } @@ -137,7 +141,7 @@ func decodePasswordResponse(r io.Reader) (string, error) { return readString16(r) } -func decodeNetwork(r io.Reader) (Network, error) { +func decodeNetwork(r io.Reader, version uint8) (Network, error) { var n Network ssid, err := readString16(r) @@ -207,6 +211,14 @@ func decodeNetwork(r io.Reader) (Network, error) { n.Current = flags&flagCurrent != 0 n.Saved = flags&flagSaved != 0 + if version >= 2 { + ies, err := readBytes32(r) + if err != nil { + return n, err + } + n.InformationElements = ies + } + return n, nil } @@ -225,6 +237,19 @@ func readBytes8(r io.Reader) ([]byte, error) { return buf, err } +func readBytes32(r io.Reader) ([]byte, error) { + var n uint32 + if err := readInt(r, &n); err != nil { + return nil, err + } + if n == 0 { + return nil, nil + } + buf := make([]byte, n) + _, err := io.ReadFull(r, buf) + return buf, err +} + func readString8(r io.Reader) (string, error) { b, err := readBytes8(r) return string(b), err diff --git a/protocol_test.go b/protocol_test.go index 2cc7a5a..983e729 100644 --- a/protocol_test.go +++ b/protocol_test.go @@ -78,7 +78,7 @@ func TestReadHeader(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := readHeader(bytes.NewReader(tt.frame)) + version, got, err := readHeader(bytes.NewReader(tt.frame)) if tt.wantErr != "" { if err == nil || !strings.Contains(err.Error(), tt.wantErr) { t.Fatalf("readHeader() error = %v, want containing %q", err, tt.wantErr) @@ -91,57 +91,84 @@ func TestReadHeader(t *testing.T) { if got != tt.want { t.Fatalf("readHeader() = 0x%02x, want 0x%02x", got, tt.want) } + if version != protocolVersion { + t.Fatalf("readHeader() version = %d, want %d", version, protocolVersion) + } }) } } -func TestDecodeScanResponse(t *testing.T) { +func TestDecodeScanResponseV2(t *testing.T) { var body bytes.Buffer writeString16(&body, "") writeUint32(&body, 1) writeNetwork(&body, Network{ - SSID: "Office WiFi", - BSSID: "aa:bb:cc:dd:ee:ff", - RSSI: -52, - Noise: -91, - Channel: 149, - ChannelBand: Band5GHz, - ChannelWidth: 80, - Security: SecurityWPA2Personal, - PHYMode: "802.11ax", - Password: "", - Current: true, - Saved: true, - }) + SSID: "Office WiFi", + BSSID: "aa:bb:cc:dd:ee:ff", + RSSI: -52, + Noise: -91, + Channel: 149, + ChannelBand: Band5GHz, + ChannelWidth: 80, + Security: SecurityWPA2Personal, + PHYMode: "802.11ax", + InformationElements: []byte{0x30, 0x02, 0x01, 0x00}, + Password: "", + Current: true, + Saved: true, + }, 2) - got, err := decodeScanResponse(&body) + got, err := decodeScanResponse(&body, 2) if err != nil { t.Fatal(err) } want := []Network{{ - SSID: "Office WiFi", - BSSID: "aa:bb:cc:dd:ee:ff", - RSSI: -52, - Noise: -91, - Channel: 149, - ChannelBand: Band5GHz, - ChannelWidth: 80, - Security: SecurityWPA2Personal, - PHYMode: "802.11ax", - Password: "", - Current: true, - Saved: true, + SSID: "Office WiFi", + BSSID: "aa:bb:cc:dd:ee:ff", + RSSI: -52, + Noise: -91, + Channel: 149, + ChannelBand: Band5GHz, + ChannelWidth: 80, + Security: SecurityWPA2Personal, + PHYMode: "802.11ax", + InformationElements: []byte{0x30, 0x02, 0x01, 0x00}, + Password: "", + Current: true, + Saved: true, }} if !reflect.DeepEqual(got, want) { t.Fatalf("decodeScanResponse() = %#v, want %#v", got, want) } } +func TestDecodeScanResponseV1LeavesInformationElementsEmpty(t *testing.T) { + var body bytes.Buffer + writeString16(&body, "") + writeUint32(&body, 1) + writeNetwork(&body, Network{ + SSID: "Cafe", + RSSI: -42, + Security: SecurityWPA2Personal, + }, 1) + + got, err := decodeScanResponse(&body, 1) + if err != nil { + t.Fatal(err) + } + if len(got) != 1 { + t.Fatalf("decodeScanResponse() returned %d networks, want 1", len(got)) + } + if got[0].InformationElements != nil { + t.Fatalf("InformationElements = %x, want nil for v1 response", got[0].InformationElements) + } +} + func TestDecodeScanResponseReturnsRemoteError(t *testing.T) { var body bytes.Buffer writeString16(&body, "Location Services denied.") - got, err := decodeScanResponse(&body) + got, err := decodeScanResponse(&body, protocolVersion) if err == nil || err.Error() != "Location Services denied." { t.Fatalf("decodeScanResponse() error = %v, want remote error", err) } @@ -156,7 +183,21 @@ func TestDecodeScanResponseIdentifiesTruncatedNetwork(t *testing.T) { writeUint32(&body, 1) writeString16(&body, "Office WiFi") - _, err := decodeScanResponse(&body) + _, err := decodeScanResponse(&body, protocolVersion) + if err == nil || !strings.Contains(err.Error(), "network 0") { + t.Fatalf("decodeScanResponse() error = %v, want network index", err) + } +} + +func TestDecodeScanResponseIdentifiesTruncatedInformationElements(t *testing.T) { + var body bytes.Buffer + writeString16(&body, "") + writeUint32(&body, 1) + writeNetwork(&body, Network{SSID: "Office WiFi"}, 1) + writeUint32(&body, 4) + body.WriteByte(0x30) + + _, err := decodeScanResponse(&body, 2) if err == nil || !strings.Contains(err.Error(), "network 0") { t.Fatalf("decodeScanResponse() error = %v, want network index", err) } @@ -220,7 +261,7 @@ func TestFormatMAC(t *testing.T) { } } -func writeNetwork(w *bytes.Buffer, n Network) { +func writeNetwork(w *bytes.Buffer, n Network, version uint8) { writeString16(w, n.SSID) writeBytes8(w, []byte{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}) writeInt16(w, int16(n.RSSI)) @@ -239,6 +280,10 @@ func writeNetwork(w *bytes.Buffer, n Network) { flags |= flagSaved } w.WriteByte(flags) + if version >= 2 { + writeUint32(w, uint32(len(n.InformationElements))) + w.Write(n.InformationElements) + } } func writeBytes8(w *bytes.Buffer, b []byte) { diff --git a/scanner/Sources/WifiScanner.swift b/scanner/Sources/WifiScanner.swift index f58d911..a5b5a1e 100644 --- a/scanner/Sources/WifiScanner.swift +++ b/scanner/Sources/WifiScanner.swift @@ -36,6 +36,9 @@ struct BinaryWriter { let b = Array(s.utf8); precondition(b.count <= 0xFFFF) putU16LE(UInt16(b.count)); data.append(contentsOf: b) } + mutating func putBytes32(_ b: Data) { + putU32LE(UInt32(b.count)); data.append(b) + } mutating func putBytes8(_ b: Data) { precondition(b.count <= 0xFF); putU8(UInt8(b.count)); data.append(b) } @@ -129,6 +132,7 @@ struct ScannedNetwork { var channelWidth: UInt16 var security: UInt8 var phyMode: String + var informationElements: Data var password: String var current: Bool var saved: Bool @@ -208,7 +212,9 @@ func runScan() throws -> [ScannedNetwork] { band: ch.map { mapBand($0.channelBand) } ?? 0, channelWidth: ch.map { mapWidth($0.channelWidth) } ?? 0, security: detectSecurity(n), - phyMode: "", password: "", + phyMode: "", + informationElements: n.informationElementData ?? Data(), + password: "", current: currentSSID == ssid, saved: saved.contains(ssid) )) } @@ -216,7 +222,7 @@ func runScan() throws -> [ScannedNetwork] { out.append(ScannedNetwork( ssid: ssid, bssid: Data(), rssi: 0, noise: 0, channel: 0, band: 0, channelWidth: 0, - security: 0, phyMode: "", password: "", + security: 0, phyMode: "", informationElements: Data(), password: "", current: currentSSID == ssid, saved: true )) } @@ -251,9 +257,9 @@ func keychainPassword(ssid: String) -> (String, String) { // ─────────────────────────── encoders ────────────────────────────────────── -func encodeScanResponse(networks: [ScannedNetwork], error: String) -> Data { +func encodeScanResponse(networks: [ScannedNetwork], error: String, protocolVersion: UInt8 = 2) -> Data { var w = BinaryWriter() - w.putMagic("MWIF"); w.putU8(1); w.putU8(0x01) + w.putMagic("MWIF"); w.putU8(protocolVersion); w.putU8(0x01) w.putString16(error) w.putU32LE(UInt32(networks.count)) for n in networks { @@ -265,13 +271,16 @@ func encodeScanResponse(networks: [ScannedNetwork], error: String) -> Data { var f: UInt8 = 0 if n.current { f |= 0x01 }; if n.saved { f |= 0x02 } w.putU8(f) + if protocolVersion >= 2 { + w.putBytes32(n.informationElements) + } } return w.data } -func encodePasswordResponse(password: String, error: String) -> Data { +func encodePasswordResponse(password: String, error: String, protocolVersion: UInt8 = 2) -> Data { var w = BinaryWriter() - w.putMagic("MWIF"); w.putU8(1); w.putU8(0x02) + w.putMagic("MWIF"); w.putU8(protocolVersion); w.putU8(0x02) w.putString16(error) w.putString16(password) return w.data @@ -295,7 +304,7 @@ func connectLoopback(port: UInt16) -> Int32? { return fd } -func handleScan(fd: Int32) { +func handleScan(fd: Int32, protocolVersion: UInt8) { let nets: [ScannedNetwork] let err: String do { @@ -306,13 +315,13 @@ func handleScan(fd: Int32) { } catch { nets = []; err = "\(error)" } - _ = writeAll(fd, encodeScanResponse(networks: nets, error: err)) + _ = writeAll(fd, encodeScanResponse(networks: nets, error: err, protocolVersion: protocolVersion)) } -func handlePassword(fd: Int32) -> Bool { +func handlePassword(fd: Int32, protocolVersion: UInt8) -> Bool { guard let ssid = readString16(fd) else { return false } let (pw, err) = keychainPassword(ssid: ssid) - return writeAll(fd, encodePasswordResponse(password: pw, error: err)) + return writeAll(fd, encodePasswordResponse(password: pw, error: err, protocolVersion: protocolVersion)) } func runService(port: UInt16) { @@ -325,14 +334,14 @@ func runService(port: UInt16) { loop: while true { // Expect a header: 4 magic + 1 version + 1 msgType guard let magic = readExact(fd, 4), String(data: magic, encoding: .ascii) == "MWIF" else { break } - guard let version = readU8(fd), version == 1 else { break } + guard let version = readU8(fd), version == 1 || version == 2 else { break } guard let msgType = readU8(fd) else { break } switch msgType { case 0x10: // scan_request - handleScan(fd: fd) + handleScan(fd: fd, protocolVersion: version) case 0x11: // password_request - if !handlePassword(fd: fd) { break loop } + if !handlePassword(fd: fd, protocolVersion: version) { break loop } case 0x1F: // close_request break loop default: diff --git a/scanner/Tests/WifiScannerTests.swift b/scanner/Tests/WifiScannerTests.swift index b3ea3ff..a69305d 100644 --- a/scanner/Tests/WifiScannerTests.swift +++ b/scanner/Tests/WifiScannerTests.swift @@ -114,7 +114,7 @@ func testEncodePasswordResponseMatchesWireFormat() throws { Array(response), [ 0x4D, 0x57, 0x49, 0x46, - 0x01, + 0x02, 0x02, 0x06, 0x00, 0x64, 0x65, 0x6E, 0x69, 0x65, 0x64, 0x06, 0x00, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, @@ -134,6 +134,7 @@ func testEncodeScanResponseMatchesWireFormat() throws { channelWidth: 80, security: 4, phyMode: "ax", + informationElements: Data([0x30, 0x02, 0x01, 0x00]), password: "pw", current: true, saved: true @@ -145,7 +146,7 @@ func testEncodeScanResponseMatchesWireFormat() throws { Array(response), [ 0x4D, 0x57, 0x49, 0x46, - 0x01, + 0x02, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, @@ -160,11 +161,40 @@ func testEncodeScanResponseMatchesWireFormat() throws { 0x02, 0x61, 0x78, 0x02, 0x00, 0x70, 0x77, 0x03, + 0x04, 0x00, 0x00, 0x00, + 0x30, 0x02, 0x01, 0x00, ], "scan response should match the Go protocol wire format" ) } +func testEncodeV1ScanResponseOmitsInformationElements() throws { + let network = ScannedNetwork( + ssid: "Cafe", + bssid: Data(), + rssi: -42, + noise: 0, + channel: 1, + band: 1, + channelWidth: 20, + security: 4, + phyMode: "", + informationElements: Data([0x30, 0x02, 0x01, 0x00]), + password: "", + current: false, + saved: false + ) + + let response = encodeScanResponse(networks: [network], error: "", protocolVersion: 1) + + try expectEqual(response[4], 0x01, "v1 scan response should use protocol version 1") + try expectEqual( + Array(response.suffix(1)), + [0x00], + "v1 scan response should end at flags and omit information elements" + ) +} + func makePipe() throws -> (read: Int32, write: Int32) { var fds: [Int32] = [0, 0] try expectEqual(Darwin.pipe(&fds), 0, "pipe() should succeed") @@ -182,6 +212,7 @@ struct WifiScannerTestRunner { ("parseBSSID rejects invalid values", testParseBSSIDRejectsMissingInvalidOrShortValues), ("encodePasswordResponse matches wire format", testEncodePasswordResponseMatchesWireFormat), ("encodeScanResponse matches wire format", testEncodeScanResponseMatchesWireFormat), + ("encode v1 scan response omits information elements", testEncodeV1ScanResponseOmitsInformationElements), ] var failures = 0