Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down
1 change: 1 addition & 0 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)`. |
Expand Down
134 changes: 124 additions & 10 deletions examples/scan/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ package main

import (
"context"
"encoding/binary"
"flag"
"fmt"
"os"
"strings"
"text/tabwriter"

"github.com/jaisonerick/macwifi"
Expand Down Expand Up @@ -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",
}
97 changes: 97 additions & 0 deletions examples/scan/main_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
31 changes: 16 additions & 15 deletions macwifi.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}
Expand Down
Loading
Loading