From 05b92c6690e56a852f7c9d392e00d74982e45bd6 Mon Sep 17 00:00:00 2001 From: Jaren Date: Thu, 8 Oct 2026 12:30:56 +0800 Subject: [PATCH] feat(wifi_iot): support WPA2 and WPA3-Personal networks Add explicit WPA2 and WPA3-Personal security types so Android uses the matching passphrase configuration when requesting or registering networks. Recognize SAE scan results and retain PSK fallback for transition networks on devices without SAE support. Reject unsupported WPA3 connections. Handle the new security values on iOS and document platform limitations. Preserve the existing WPA behavior and boolean connection APIs. --- packages/wifi_iot/README.md | 20 ++++++++ .../com/alternadom/wifiiot/WifiIotPlugin.java | 47 ++++++++++++++++--- .../Sources/wifi_iot/WifiIotPlugin.swift | 2 +- packages/wifi_iot/lib/wifi_iot.dart | 10 ++-- 4 files changed, 68 insertions(+), 11 deletions(-) diff --git a/packages/wifi_iot/README.md b/packages/wifi_iot/README.md index 9b3da102..293b813b 100644 --- a/packages/wifi_iot/README.md +++ b/packages/wifi_iot/README.md @@ -48,6 +48,26 @@ Becareful, some commands as no effect on iOS because Apple don't let us to do wh Additional Wifi protocols on Android side like - Wifi Direct, Wifi Aware, etc are in active discussion at [#140](https://github.com/flutternetwork/WiFiFlutter/issues/140). Encourage you to engage if you want this features. +## WPA2 and WPA3-Personal + +Use `NetworkSecurity.WPA2` for password-protected WPA2 networks or +`NetworkSecurity.WPA3` for WPA3-Personal (SAE) networks. The existing +`NetworkSecurity.WPA` value keeps its WPA2 behavior. WPA3-Enterprise is not +supported by these password-based connection methods. + +```dart +final connected = await WiFiForIoTPlugin.connect( + 'My network', + password: 'My password', + security: NetworkSecurity.WPA3, +); +``` + +On Android, WPA3-Personal requires Android 10 or later and SAE support on the +device. `findAndConnect` falls back to WPA2 for WPA2/WPA3 transition networks +when the device does not support SAE. On iOS, WPA2 and WPA3 use the system's +password-based hotspot configuration. + ## Access Point | Description | Android | iOS | | :------------------------------------------------------------------------------------ | :--------------------: | :------------------: | diff --git a/packages/wifi_iot/android/src/main/java/com/alternadom/wifiiot/WifiIotPlugin.java b/packages/wifi_iot/android/src/main/java/com/alternadom/wifiiot/WifiIotPlugin.java index 455839e1..020e6e7f 100644 --- a/packages/wifi_iot/android/src/main/java/com/alternadom/wifiiot/WifiIotPlugin.java +++ b/packages/wifi_iot/android/src/main/java/com/alternadom/wifiiot/WifiIotPlugin.java @@ -925,6 +925,12 @@ private void registerWifiNetwork(final MethodCall poCall, final Result poResult) String security = poCall.argument("security"); Boolean isHidden = poCall.argument("is_hidden"); + if ("WPA3".equalsIgnoreCase(security) + && (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q || !moWiFi.isWpa3SaeSupported())) { + poResult.error("Error", "WPA3-Personal is not supported on this device", ""); + return; + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { final WifiNetworkSuggestion.Builder suggestedNet = new WifiNetworkSuggestion.Builder(); suggestedNet.setSsid(ssid); @@ -938,8 +944,11 @@ private void registerWifiNetwork(final MethodCall poCall, final Result poResult) suggestedNet.setBssid(macAddress); } - if (security != null && security.toUpperCase().equals("WPA")) { + if (security != null + && (security.toUpperCase().equals("WPA") || security.toUpperCase().equals("WPA2"))) { suggestedNet.setWpa2Passphrase(password); + } else if (security != null && security.toUpperCase().equals("WPA3")) { + suggestedNet.setWpa3Passphrase(password); } else if (security != null && security.toUpperCase().equals("WEP")) { // WEP is not supported poResult.error( @@ -1038,10 +1047,17 @@ public void run() { }.start(); } - private static String getSecurityType(ScanResult scanResult) { + private String getSecurityType(ScanResult scanResult) { String capabilities = scanResult.capabilities; - if (capabilities.contains("WPA") + if (capabilities.contains("SAE")) { + // Transition networks also advertise PSK; use it if SAE is unavailable. + if (capabilities.contains("PSK") + && (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q || !moWiFi.isWpa3SaeSupported())) { + return "WPA"; + } + return "WPA3"; + } else if (capabilities.contains("WPA") || capabilities.contains("WPA2") || capabilities.contains("WPA/WPA2 PSK")) { return "WPA"; @@ -1267,6 +1283,11 @@ private void connectTo( final Boolean isHidden, final Integer timeoutInSeconds) { final Handler handler = new Handler(Looper.getMainLooper()); + if ("WPA3".equalsIgnoreCase(security) + && (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q || !moWiFi.isWpa3SaeSupported())) { + handler.post(() -> poResult.success(false)); + return; + } if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { final boolean connected = connectToDeprecated(ssid, bssid, password, security, joinOnce, isHidden); @@ -1313,8 +1334,11 @@ public void run() { } // set password - if (security != null && security.toUpperCase().equals("WPA")) { + if (security != null + && (security.toUpperCase().equals("WPA") || security.toUpperCase().equals("WPA2"))) { builder.setWpa2Passphrase(password); + } else if (security != null && security.toUpperCase().equals("WPA3")) { + builder.setWpa3Passphrase(password); } // remove suggestions if already existing @@ -1363,8 +1387,11 @@ public void run() { } // set security - if (security != null && security.toUpperCase().equals("WPA")) { + if (security != null + && (security.toUpperCase().equals("WPA") || security.toUpperCase().equals("WPA2"))) { builder.setWpa2Passphrase(password); + } else if (security != null && security.toUpperCase().equals("WPA3")) { + builder.setWpa3Passphrase(password); } final NetworkRequest networkRequest = @@ -1467,8 +1494,7 @@ private android.net.wifi.WifiConfiguration generateConfiguration( if (security != null) security = security.toUpperCase(); else security = "NONE"; - if (security.toUpperCase().equals("WPA")) { - + if (security.equals("WPA") || security.equals("WPA2")) { /// appropriate ciper is need to set according to security type used, /// ifcase of not added it will not be able to connect conf.preSharedKey = "\"" + password + "\""; @@ -1489,6 +1515,13 @@ private android.net.wifi.WifiConfiguration generateConfiguration( conf.allowedProtocols.set(android.net.wifi.WifiConfiguration.Protocol.RSN); conf.allowedProtocols.set(android.net.wifi.WifiConfiguration.Protocol.WPA); + } else if (security.equals("WPA3")) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + conf.preSharedKey = "\"" + password + "\""; + conf.allowedKeyManagement.set(android.net.wifi.WifiConfiguration.KeyMgmt.SAE); + conf.allowedProtocols.set(android.net.wifi.WifiConfiguration.Protocol.RSN); + conf.status = android.net.wifi.WifiConfiguration.Status.ENABLED; + } } else if (security.equals("WEP")) { conf.wepKeys[0] = "\"" + password + "\""; conf.wepTxKeyIndex = 0; diff --git a/packages/wifi_iot/ios/wifi_iot/Sources/wifi_iot/WifiIotPlugin.swift b/packages/wifi_iot/ios/wifi_iot/Sources/wifi_iot/WifiIotPlugin.swift index 65073906..0db16dab 100755 --- a/packages/wifi_iot/ios/wifi_iot/Sources/wifi_iot/WifiIotPlugin.swift +++ b/packages/wifi_iot/ios/wifi_iot/Sources/wifi_iot/WifiIotPlugin.swift @@ -171,7 +171,7 @@ public class WifiIotPlugin: NSObject, FlutterPlugin { -> NEHotspotConfiguration { switch security?.uppercased() { - case "WPA": + case "WPA", "WPA2", "WPA3": return NEHotspotConfiguration.init(ssid: ssid, passphrase: passphrase!, isWEP: false) case "WEP": return NEHotspotConfiguration.init(ssid: ssid, passphrase: passphrase!, isWEP: true) diff --git a/packages/wifi_iot/lib/wifi_iot.dart b/packages/wifi_iot/lib/wifi_iot.dart index 0125e4a8..bc817a87 100755 --- a/packages/wifi_iot/lib/wifi_iot.dart +++ b/packages/wifi_iot/lib/wifi_iot.dart @@ -13,12 +13,14 @@ enum WIFI_AP_STATE { WIFI_AP_STATE_FAILED, } -enum NetworkSecurity { WPA, WEP, NONE } +enum NetworkSecurity { WPA, WEP, NONE, WPA2, WPA3 } const serializeNetworkSecurityMap = { NetworkSecurity.WPA: "WPA", NetworkSecurity.WEP: "WEP", NetworkSecurity.NONE: "NONE", + NetworkSecurity.WPA2: "WPA2", + NetworkSecurity.WPA3: "WPA3", }; const MethodChannel _channel = const MethodChannel('wifi_iot'); @@ -320,7 +322,8 @@ class WiFiForIoTPlugin { /// @param [security] The security type of the network. [NetworkSecurity.NONE] /// means no password is required. /// On Android, from version 10 (Q) onward, [NetworkSecurity.WEP] is no - /// longer supported. + /// longer supported. WPA3-Personal requires Android 10 (Q) or later and + /// a device that supports SAE. WPA remains an alias for WPA2. /// /// @param [joinOnce] If true, the network will be removed on exit. /// @@ -397,7 +400,8 @@ class WiFiForIoTPlugin { /// @param [security] The security type of the network. [NetworkSecurity.NONE] /// means no password is required. /// On Android, from version 10 (Q) onward, [NetworkSecurity.WEP] is no - /// longer supported. + /// longer supported. WPA3-Personal requires Android 10 (Q) or later and + /// a device that supports SAE. WPA remains an alias for WPA2. /// /// @param [isHidden] Whether the SSID is hidden (not broadcasted by the AP). ///