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