ronaldoussoren / pyobjc

The Python <-> Objective-C Bridge with bindings for macOS frameworks
https://pyobjc.readthedocs.io
522 stars 45 forks source link

CoreWLAN returning None for SSID/BSSID #600

Open phenotypic opened 3 months ago

phenotypic commented 3 months ago

Hi there! In my project, WiFiCrackPy, several people have opened issues stating that CoreWLAN is returning None for the SSIDs and/or BSSIDs of networks (see here).

I am aware that an issue was opened in 2022 (#484) where a similar problem was occurring, and I see that this was resolved by granting Python location services permissions.

What is strange is that I do not experience this issue, but people with the same macOS versions and Python versions as me do. They have confirmed they have granted Python location services permissions. I am using Python 3.12.2 installed via Homebrew, macOS 14.4.1, and PyObjC 10.2.

Someone has noticed that the issue does not occur when running the script with the system-native version of Python, but it is not clear why there is an inconsistency with the Homebrew-installed version of Python.

As a note, there is a post in the Apple Developer forums where people have highlighted the same issue and underscored the inconsistency across the same macOS/Python/PyObjC versions.

Here is an excerpt of my WiFiCrackPy script which contains just the location authorisation and network scanning sections. As mentioned, this works fine on my Mac but you can see examples from other people in the issue I linked above where the SSIDs and BSSIDs are shown as None:

import re, time, CoreWLAN, CoreLocation
from prettytable import PrettyTable

# Initialise CoreLocation
print('\nObtaining authorisation for location services (required for WiFi scanning)...\n')
location_manager = CoreLocation.CLLocationManager.alloc().init()
location_manager.startUpdatingLocation()

# Wait for location services to be authorised
max_wait = 60
for i in range(1, max_wait):
    authorization_status = location_manager.authorizationStatus()
    if authorization_status == 3 or authorization_status == 4:
        print('Received authorisation, continuing...\n')
        break
    if i == max_wait-1:
        exit('Unable to obtain authorisation, exiting...\n')
    time.sleep(1)

# Get the default WiFi interface
cwlan_interface = CoreWLAN.CWInterface.interface()

# Scan for networks
print('Scanning for networks...\n')
scan_result, _ = cwlan_interface.scanForNetworksWithName_error_(None, None)

# Parse scan results and display in a table
table = PrettyTable(['Number', 'Name', 'BSSID', 'RSSI', 'Channel', 'Security'])

# Iterate through each network in the scan results
for i, net in enumerate(scan_result):
    # Extract security type from the CWNetwork description
    security = re.search(r'security=(.*?)(,|$)', str(net)).group(1)

    # Add the network to the table
    table.add_row([i + 1, net.ssid(), net.bssid(), net.rssiValue(), net.channel(), security])

# Display the table
print(table)

It has been difficult for me to troubleshoot the issue as I have not experienced the problem.

I recently tried to manually load the frameworks using objc rather than importing CoreWLAN and CoreLocation directly (see here), but that does not seem to have fixed the issue.

Thank you in advance for any help or guidance you can give!

ronaldoussoren commented 2 months ago

I can see this behaviour on my main machine:

Obtaining authorisation for location services (required for WiFi scanning)...

Scanning for networks...

+--------+------+-------+------+---------+--------------------+
| Number | Name | BSSID | RSSI | Channel |      Security      |
+--------+------+-------+------+---------+--------------------+
|   1    | None |  None | -85  |    36   |   WPA2 Personal    |
|   2    | None |  None | -68  |    1    |   WPA2 Personal    |
|   3    | None |  None | -85  |    6    | WPA2/WPA3 Personal |
|   4    | None |  None | -89  |    40   |   WPA2 Personal    |
|   5    | None |  None | -90  |    44   |   WPA2 Personal    |
|   6    | None |  None | -90  |    40   |   WPA2 Personal    |
|   7    | None |  None | -78  |    8    |        Open        |
|   8    | None |  None | -84  |    36   |   WPA2 Personal    |
|   9    | None |  None | -70  |    4    | WPA/WPA2 Personal  |
|   10   | None |  None | -63  |    40   | WPA2/WPA3 Personal |
|   11   | None |  None | -85  |    36   |   WPA2 Personal    |
|   12   | None |  None | -55  |   108   | WPA2/WPA3 Personal |
|   13   | None |  None | -91  |    36   |   WPA2 Personal    |
|   14   | None |  None | -64  |    7    | WPA2/WPA3 Personal |
|   15   | None |  None | -80  |    44   |   WPA2 Personal    |
+--------+------+-------+------+---------+--------------------+

Python 3.12.3, PyObjC 10.2, macOS 14.4.1 on an M1 laptop.

One thing I noticed: On the first try it took a long time to obtain authorisation, no problems from the second run onwards. This is in a virtualenv.

ronaldoussoren commented 2 months ago

I get the same behaviour with an ObjC program, but... note how I disabled the location authorisation check which was needed because the loop always failed:

#import <Foundation/Foundation.h>
#import <CoreWLAN/CoreWLAN.h>
#import <CoreLocation/CoreLocation.h>

#include <time.h>

int main(void)
{
    // Initialise CoreLocation
    NSLog(@"Obtaining authorisation for location services (required for WiFi scanning)...");
    CLLocationManager* location_manager = [[CLLocationManager alloc] init];
    [location_manager startUpdatingLocation];

#if 0
    // Wait for location services to be authorised
    const int max_wait = 10;
    for(int i = 0; i < max_wait; i++) {
        CLAuthorizationStatus authorization_status = location_manager.authorizationStatus;
        NSLog(@"status %ld", (long)authorization_status);
        switch (authorization_status) {
        case kCLAuthorizationStatusAuthorized:
        //case kCLAuthorizationStatusAuthorizedAlways:
        //case kCLAuthorizationStatusAuthorizedWhenInUse:
            NSLog(@"Received authorization, continuing...");
            break;
        case kCLAuthorizationStatusRestricted:
        case kCLAuthorizationStatusDenied:
            NSLog(@"No authorization, aborting...");
            return 1;
        default:
            if (i == max_wait - 1) {
                NSLog(@"Unable to obtain authorisation, exiting...");
                return 1;
            }
        }

        sleep(1);
    }
#endif

    // Get the default WiFi interface
    CWInterface* cwlan_interface = [CWInterface interface];

    // Scan for networks
    NSLog(@"Scanning for networks...");
    NSError* error;

    NSSet<CWNetwork*>* scan_result = [cwlan_interface scanForNetworksWithName:nil error:&error];
    if (scan_result == nil) {
        NSLog(@"scan failed %@", error);
        return 1;
    }

    for (CWNetwork* net in scan_result) {
        NSLog(@"net = %@", net);
    }
        /* 
    # Iterate through each network in the scan results
    for i, net in enumerate(scan_result):
        # Extract security type from the CWNetwork description
        security = re.search(r'security=(.*?)(,|$)', str(net)).group(1)

        # Add the network to the table
        table.add_row([i + 1, net.ssid(), net.bssid(), net.rssiValue(), net.channel(), security])

    # Display the table
    print(table)
    */
    return 0;
    }

Noteworthy: the +[CWInterface interface] API is deprecated as of macOS 10.10:

repro.m:43:49: warning: 'interface' is deprecated: first deprecated in macOS 10.10 - Use -[CWWiFiClient interface] instead [-Wdeprecated-declarations]
    CWInterface* cwlan_interface = [CWInterface interface];

That's a red herring though, the modern way to do the same way gives the same result:

    CWInterface* cwlan_interface = [[CWWiFiClient sharedWiFiClient] interface];

UPDATE: The failure happens both with a command-line tool and when running the code in an app bundle with the required Info.plist keys.

ronaldoussoren commented 2 months ago

I cannot reproduce this after all, just noticed that I disabled the check in the python version as well and had disabled location services for python. After reenabling the code and location services I got the expected result with names and BSSIDs (not sharing the output for obvious reasons).

import re, time, CoreWLAN, CoreLocation, Foundation
from prettytable import PrettyTable

# Initialise CoreLocation
print('\nObtaining authorisation for location services (required for WiFi scanning)...\n')
location_manager = CoreLocation.CLLocationManager.alloc().init()
#location_manager.requestWhenInUseAuthorization()

location_manager.startUpdatingLocation()

loop = Foundation.NSRunLoop.currentRunLoop()

# Wait for location services to be authorised
max_wait = 10
for i in range(1, max_wait):
    authorization_status = location_manager.authorizationStatus()
    print(f"{authorization_status=}")
    if authorization_status == 3 or authorization_status == 4:
        print('Received authorisation, continuing...\n')
        break
    #loop.runUntilDate_(Foundation.NSDate.dateWithTimeIntervalSinceNow_(1))
    time.sleep(1)
else:
    exit('Unable to obtain authorisation, exiting...\n')
# Get the default WiFi interface
cwlan_interface = CoreWLAN.CWInterface.interface()

# Scan for networks
print('Scanning for networks...\n')
scan_result, _ = cwlan_interface.scanForNetworksWithName_error_(None, None)

# Parse scan results and display in a table
table = PrettyTable(['Number', 'Name', 'BSSID', 'RSSI', 'Channel', 'Security'])

# Iterate through each network in the scan results
for i, net in enumerate(scan_result):
    # Extract security type from the CWNetwork description
    security = re.search(r'security=(.*?)(,|$)', str(net)).group(1)

    # Add the network to the table
    table.add_row([i + 1, net.ssid(), net.bssid(), net.rssiValue(), net.channel(), security])

# Display the table
print(table)
phenotypic commented 1 month ago

Thank you for the response!

Just to confirm, you were able to get the code to function as expected simply by disabling and re-enabling Python's location services permission in System Settings?

Either way, it's strange that your code ran in the first instance with the correct permissions, but returned 'None' for the SSID and BSSID. Indeed, CoreLocation will have had to return 1 or 2 for the authorization_status for the code to have proceeded with network scanning.

Why did CoreWLAN fail to return the correct SSID/BSSID values despite having correct permissions? And why did disabling/re-enabling Python's location services remedy this issue for you? Is there some additional requirement past location services authorisation for CoreWLAN to return the expected SSID/BSSID values?

waknin commented 1 month ago

Same error! Location permitted!

image

MBP M1 Pro Sonoma 14.4.1 python installed via homebrew 3.12.2

gabrielmaldi commented 3 weeks ago

I'm not getting the BSSID using Apple's Python executable: /usr/bin/python3 WiFiCrackPy.py

macOS Sonoma 14.5 (23F79) Location permission granted (before grating the permission, I wasn't getting the Name either)

image image
ronaldoussoren commented 1 week ago

I get the same result here. I have two virtual environments using Python 3.9 and the same version of PyObjC ("pip install pyobjc"):

  1. /usr/bin/python3: Does not work, BSSID column contains "None"
  2. Python.org installer (3.9.10): Works, BSSID contains a correct value.

There are various differences between the two python installations, the most relevant differences are likely:

a. The deployment target is different (the python.org installer targets 10.9, the version of Python included with Xcode targets a newer release b. Code signing differences

The latter seems to be the most relevant, an old build of python without code signing that I have lying around also fails to collect the information.

ronaldoussoren commented 1 week ago

I haven't tried doing a fresh build yet, I might do that later. Likewise for testing the Python.org installation when strip the code signing from that.