From 7e2370da3109c0cef760e096f691d98307a8d9b7 Mon Sep 17 00:00:00 2001 From: Hank Yu Date: Wed, 15 Jul 2026 22:35:15 +0800 Subject: [PATCH 1/3] fix(wifi): use OWE token for Enhanced Open security mode (#1073) The codebase used the string 'Enhanced-Open' for the Enhanced Open (OWE) security mode, but firmware only accepts the TR-181 token 'OWE' and rejects 'Enhanced-Open' as an invalid enumeration (verified on device). This produced two latent app-side bugs, independent of the firmware issue where OWE is accepted with success:true yet not committed: - 6 GHz open mode sent 'Enhanced-Open', which firmware rejects with "Out of range or invalid enumeration". - OWE networks were never recognized as open, so the UI spuriously required a password (WifiQuickSetupSettings.isPasswordRequired) and blocked save. Replace all three 'Enhanced-Open' occurrences with 'OWE': - WifiNetworkUIModel.isOpenSecurity - WifiQuickSetupSettings.isPasswordRequired - _securityModeFor6GHz (6 GHz override + comment) Fix the test fixture that asserted the old invalid token and add an isOpenSecurity test group covering None / empty / OWE / WPA variants. --- .../models/wifi_network_ui_model.dart | 7 ++-- .../models/wifi_settings_settings.dart | 3 +- .../services/usp_wifi_settings_service.dart | 9 ++-- .../usp_wifi_settings_state_test.dart | 41 ++++++++++++++++++- 4 files changed, 49 insertions(+), 11 deletions(-) diff --git a/lib/page/wifi_settings/models/wifi_network_ui_model.dart b/lib/page/wifi_settings/models/wifi_network_ui_model.dart index e9e06fb40..3ab9b0051 100644 --- a/lib/page/wifi_settings/models/wifi_network_ui_model.dart +++ b/lib/page/wifi_settings/models/wifi_network_ui_model.dart @@ -101,11 +101,10 @@ class WifiNetworkUIModel extends Equatable { return band.isNotEmpty ? band : 'Unknown'; } - /// True if this network uses an open (no password) security mode + /// True if this network uses an open (no password) security mode. + /// 'OWE' is the TR-181 token for Enhanced Open (firmware only accepts 'OWE'). bool get isOpenSecurity => - securityMode == 'None' || - securityMode.isEmpty || - securityMode == 'Enhanced-Open'; + securityMode == 'None' || securityMode.isEmpty || securityMode == 'OWE'; /// Channel display string ("Auto" if autoChannelEnable, else the channel number) String get channelDisplay => autoChannelEnable ? 'Auto' : channel.toString(); diff --git a/lib/page/wifi_settings/models/wifi_settings_settings.dart b/lib/page/wifi_settings/models/wifi_settings_settings.dart index 7ed6bacc7..1a0f634fb 100644 --- a/lib/page/wifi_settings/models/wifi_settings_settings.dart +++ b/lib/page/wifi_settings/models/wifi_settings_settings.dart @@ -52,7 +52,8 @@ class WifiQuickSetupSettings extends Equatable { final passwordChanged = original == null || password != original.password; final modeChanged = original == null || securityMode != original.securityMode; - final isOpen = securityMode == 'None' || securityMode == 'Enhanced-Open'; + // 'OWE' is the TR-181 token for Enhanced Open (firmware only accepts 'OWE'). + final isOpen = securityMode == 'None' || securityMode == 'OWE'; return (passwordChanged || modeChanged) && !isOpen; } diff --git a/lib/page/wifi_settings/services/usp_wifi_settings_service.dart b/lib/page/wifi_settings/services/usp_wifi_settings_service.dart index 83fe62bce..91c1c6ddb 100644 --- a/lib/page/wifi_settings/services/usp_wifi_settings_service.dart +++ b/lib/page/wifi_settings/services/usp_wifi_settings_service.dart @@ -627,17 +627,18 @@ class UspWifiSettingsService { /// Returns the effective security mode to apply to a given band. /// /// 6 GHz (Wi-Fi 6E) mandates WPA3: - /// - Open / Enhanced-Open selected → send "Enhanced-Open" - /// - Any other mode → send "WPA3-Personal" + /// - Open / OWE (Enhanced Open) selected → send "OWE" + /// - Any other mode → send "WPA3-Personal" /// + /// 'OWE' is the TR-181 token firmware accepts for Enhanced Open. /// All other bands: return [selectedMode] unchanged. String _securityModeFor6GHz({ required String band, required String selectedMode, }) { if (!band.contains('6')) return selectedMode; - const openModes = {'None', 'Enhanced-Open', ''}; - return openModes.contains(selectedMode) ? 'Enhanced-Open' : 'WPA3-Personal'; + const openModes = {'None', 'OWE', ''}; + return openModes.contains(selectedMode) ? 'OWE' : 'WPA3-Personal'; } } diff --git a/test/page/wifi_settings/providers/usp_wifi_settings_state_test.dart b/test/page/wifi_settings/providers/usp_wifi_settings_state_test.dart index 63121bdbe..202fb038b 100644 --- a/test/page/wifi_settings/providers/usp_wifi_settings_state_test.dart +++ b/test/page/wifi_settings/providers/usp_wifi_settings_state_test.dart @@ -534,8 +534,8 @@ void main() { enabled: true, ssid: 'OpenNet', password: '', - securityMode: 'Enhanced-Open', - supportedSecurityModes: ['None', 'Enhanced-Open'], + securityMode: 'OWE', + supportedSecurityModes: ['None', 'OWE'], ); expect(openPending.isPasswordRequired(null), isFalse); }); @@ -555,4 +555,41 @@ void main() { expect(updated.status.isSaving, isTrue); }); }); + + // ------------------------------------------------------------------------- + // WifiNetworkUIModel.isOpenSecurity + // + // 'OWE' is the TR-181 token for Enhanced Open — firmware advertises and + // accepts only 'OWE' (see issue #1073). It must be treated as an open + // (no-password) mode; 'Enhanced-Open' is not a valid firmware token. + // ------------------------------------------------------------------------- + + group('WifiNetworkUIModel.isOpenSecurity', () { + test('true for None', () { + final n = WifiSettingsTestData.createNetworkUIModel(securityMode: 'None'); + expect(n.isOpenSecurity, isTrue); + }); + + test('true for empty string', () { + final n = WifiSettingsTestData.createNetworkUIModel(securityMode: ''); + expect(n.isOpenSecurity, isTrue); + }); + + test('true for OWE (Enhanced Open)', () { + final n = WifiSettingsTestData.createNetworkUIModel(securityMode: 'OWE'); + expect(n.isOpenSecurity, isTrue); + }); + + test('false for WPA2-Personal', () { + final n = WifiSettingsTestData.createNetworkUIModel( + securityMode: 'WPA2-Personal'); + expect(n.isOpenSecurity, isFalse); + }); + + test('false for WPA3-Personal-Transition', () { + final n = WifiSettingsTestData.createNetworkUIModel( + securityMode: 'WPA3-Personal-Transition'); + expect(n.isOpenSecurity, isFalse); + }); + }); } From 19bc7f08d07f307cb118749bd0f44411350d1749 Mon Sep 17 00:00:00 2001 From: Hank Yu Date: Wed, 15 Jul 2026 22:45:48 +0800 Subject: [PATCH 2/3] test(wifi): cover 6 GHz OWE security override (#1073) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _securityModeFor6GHz had no direct test. Add a saveQuickSetup group asserting the exact ModeEnabled value sent to firmware: - 6 GHz + OWE / None (open) → 'OWE' - 6 GHz + WPA3-Personal → 'WPA3-Personal' - 2.4/5 GHz → selected mode written verbatim Guards against regressing to the invalid 'Enhanced-Open' token. --- .../usp_wifi_settings_service_test.dart | 151 ++++++++++++++++++ 1 file changed, 151 insertions(+) diff --git a/test/page/wifi_settings/services/usp_wifi_settings_service_test.dart b/test/page/wifi_settings/services/usp_wifi_settings_service_test.dart index eb166cffa..411063dce 100644 --- a/test/page/wifi_settings/services/usp_wifi_settings_service_test.dart +++ b/test/page/wifi_settings/services/usp_wifi_settings_service_test.dart @@ -1321,4 +1321,155 @@ void main() { expect(keys.any((k) => k.contains('Security.KeyPassphrase')), isFalse); }); }); + + // ------------------------------------------------------------------------- + // 6 GHz security override (_securityModeFor6GHz) — issue #1073 + // + // Wi-Fi 6E mandates WPA3, so on 6 GHz the service overrides the selected + // mode: open modes ('None' / 'OWE' / '') → 'OWE' (the TR-181 token firmware + // accepts for Enhanced Open), everything else → 'WPA3-Personal'. On other + // bands the selected mode is written verbatim. These tests assert the exact + // ModeEnabled value sent to firmware, guarding against a regression to the + // old invalid 'Enhanced-Open' token. + // ------------------------------------------------------------------------- + + group('saveQuickSetup — 6 GHz security override (#1073)', () { + late MockUspClient mockUsp; + late UspWifiSettingsService writeSvc; + + setUp(() { + mockUsp = MockUspClient(); + writeSvc = UspWifiSettingsService(mockUsp); + when(() => mockUsp.set(any(), allowPartial: any(named: 'allowPartial'))) + .thenAnswer((_) async => uspSuccess()); + }); + + WifiNetworkUIModel makeNetwork({ + required String band, + String ssidInstancePath = 'Device.WiFi.SSID.1.', + String accessPointInstancePath = 'Device.WiFi.AccessPoint.1.', + }) => + WifiNetworkUIModel( + ssidInstancePath: ssidInstancePath, + accessPointInstancePath: accessPointInstancePath, + radioInstancePath: 'Device.WiFi.Radio.1.', + ssid: 'Home', + enabled: true, + ssidAdvertisementEnabled: true, + supportedSecurityModes: const [ + 'None', + 'WPA2-Personal', + 'WPA3-Personal', + 'OWE' + ], + securityMode: 'WPA2-Personal', + keyPassphrase: '', + isGuest: false, + band: band, + channel: 6, + channelBandwidth: '20MHz', + autoChannelEnable: true, + possibleChannels: const [1, 6, 11], + operatingStandards: 'ax', + supportedStandards: 'ax', + ); + + /// Returns the value written to `Security.ModeEnabled`, or null if the + /// param was never sent. + Future capturedModeEnabled({ + required String band, + required String selectedMode, + String ap = 'Device.WiFi.AccessPoint.1.', + }) async { + final agg = WifiQuickSetupNetwork( + isGuest: false, + ssid: 'Home', + securityMode: 'WPA2-Personal', + keyPassphrase: '', + supportedSecurityModes: const [ + 'None', + 'WPA2-Personal', + 'WPA3-Personal', + 'OWE' + ], + ssidInstancePaths: const ['Device.WiFi.SSID.1.'], + apInstancePaths: [ap], + ); + const orig = WifiQuickSetupSettings( + isGuest: false, + enabled: true, + ssid: 'Home', + password: '', + securityMode: 'WPA2-Personal', + supportedSecurityModes: [ + 'None', + 'WPA2-Personal', + 'WPA3-Personal', + 'OWE' + ], + ); + + final original = WifiSettingsSettings( + networks: [makeNetwork(band: band, accessPointInstancePath: ap)], + quickSetupEnabled: true, + quickSetupMain: orig, + ); + final current = original.copyWith( + quickSetupMain: orig.copyWith(securityMode: selectedMode), + ); + final status = WifiSettingsStatus(quickSetupMainAggregate: agg); + + await writeSvc.saveQuickSetup( + original: original, + current: current, + status: status, + ); + + final captured = verify(() => mockUsp.set(captureAny(), + allowPartial: any(named: 'allowPartial'))).captured; + for (final arg in captured) { + if (arg is Map && arg.containsKey('${ap}Security.ModeEnabled')) { + return arg['${ap}Security.ModeEnabled'] as String?; + } + } + return null; + } + + test('6 GHz + OWE selected → sends OWE verbatim', () async { + expect( + await capturedModeEnabled(band: '6GHz', selectedMode: 'OWE'), + 'OWE', + ); + }); + + test('6 GHz + None (open) selected → normalized to OWE', () async { + expect( + await capturedModeEnabled(band: '6GHz', selectedMode: 'None'), + 'OWE', + ); + }); + + test('6 GHz + WPA2-Personal selected → forced to WPA3-Personal', () async { + expect( + await capturedModeEnabled(band: '6GHz', selectedMode: 'WPA3-Personal'), + 'WPA3-Personal', + ); + }); + + test('2.4 GHz + OWE selected → written verbatim (no 6 GHz override)', + () async { + expect( + await capturedModeEnabled(band: '2.4GHz', selectedMode: 'OWE'), + 'OWE', + ); + }); + + test('5 GHz + None selected → written verbatim (no 6 GHz override)', + () async { + expect( + await capturedModeEnabled(band: '5GHz', selectedMode: 'None'), + 'None', + ); + }); + }); } From 2aa7fe0a665cee11f7f9c184db2a6e831f2a5143 Mon Sep 17 00:00:00 2001 From: Hank Yu Date: Thu, 16 Jul 2026 11:22:01 +0800 Subject: [PATCH 3/3] fix(wifi): apply 6 GHz security override in saveAdvanced (#1073) saveAdvanced wrote curr.securityMode verbatim while saveQuickSetup routed it through _securityModeFor6GHz. The two save paths could therefore send different security modes to firmware, and on 6 GHz saveAdvanced could send a non-WPA3 (invalid) mode. - saveAdvanced: route securityModeEnabled through _securityModeFor6GHz (band from curr.band), matching saveQuickSetup. Still gated on securityChanged so enable-only toggles never re-write the mode. - wifiDisplayValue: add an 'OWE' case returning 'Enhanced Open' so the status card and selector show the Wi-Fi standard label instead of the raw token (like WPA2/WPA3-Personal, it is a technical term, not l10n'd). - tests: the 6 GHz "WPA2-Personal to WPA3-Personal" case passed 'WPA3-Personal' as input, an identity that would pass even without the override; pass 'WPA2-Personal' to exercise the non-open to WPA3 coercion. Add a saveAdvanced 6 GHz override group, and use a distinct baseline mode so the security diff always fires. --- lib/page/_shared/components/wifi_ui.dart | 5 ++ .../services/usp_wifi_settings_service.dart | 15 +++- .../usp_wifi_settings_service_test.dart | 75 ++++++++++++++++++- 3 files changed, 88 insertions(+), 7 deletions(-) diff --git a/lib/page/_shared/components/wifi_ui.dart b/lib/page/_shared/components/wifi_ui.dart index a106294c7..7db7f0337 100644 --- a/lib/page/_shared/components/wifi_ui.dart +++ b/lib/page/_shared/components/wifi_ui.dart @@ -105,6 +105,11 @@ String wifiDisplayValue(BuildContext context, String value) { return loc(context).none; case 'Mixed': return loc(context).mixed; + case 'OWE': + // Firmware reports Enhanced Open as the TR-181 token 'OWE'. Show the + // Wi-Fi standard label instead of the raw token; like WPA2/WPA3-Personal + // it is a technical term and is not localized. + return 'Enhanced Open'; default: return value; } diff --git a/lib/page/wifi_settings/services/usp_wifi_settings_service.dart b/lib/page/wifi_settings/services/usp_wifi_settings_service.dart index 91c1c6ddb..40373131a 100644 --- a/lib/page/wifi_settings/services/usp_wifi_settings_service.dart +++ b/lib/page/wifi_settings/services/usp_wifi_settings_service.dart @@ -421,6 +421,16 @@ class UspWifiSettingsService { orig.ssidAdvertisementEnabled != curr.ssidAdvertisementEnabled; if (ap != null && (enabledChanged || securityChanged || broadcastChanged)) { + // Apply the 6 GHz security override (Wi-Fi 6E mandates WPA3), the + // same way saveQuickSetup does, so both save paths write a + // firmware-valid mode on 6 GHz. Skip the override for enable-only + // toggles (securityChanged false) so the mode is never re-written. + final securityMode = securityChanged && curr.securityMode.isNotEmpty + ? _securityModeFor6GHz( + band: curr.band, + selectedMode: curr.securityMode, + ) + : null; final result = await WiFiAccessPoints.update( _usp, [ @@ -430,10 +440,7 @@ class UspWifiSettingsService { keyPassphrase: securityChanged && curr.keyPassphrase.isNotEmpty ? curr.keyPassphrase : null, - securityModeEnabled: - securityChanged && curr.securityMode.isNotEmpty - ? curr.securityMode - : null, + securityModeEnabled: securityMode, ssidAdvertisementEnabled: broadcastChanged ? curr.ssidAdvertisementEnabled : null, ) diff --git a/test/page/wifi_settings/services/usp_wifi_settings_service_test.dart b/test/page/wifi_settings/services/usp_wifi_settings_service_test.dart index 411063dce..783005950 100644 --- a/test/page/wifi_settings/services/usp_wifi_settings_service_test.dart +++ b/test/page/wifi_settings/services/usp_wifi_settings_service_test.dart @@ -1110,6 +1110,67 @@ void main() { isNot( contains('Device.WiFi.AccessPoint.1.SSIDAdvertisementEnabled'))); }); + + // ----------------------------------------------------------------------- + // 6 GHz security override (#1073, #1142) — saveAdvanced must apply the + // same _securityModeFor6GHz coercion as saveQuickSetup, so both save + // paths write a firmware-valid mode on 6 GHz. + // ----------------------------------------------------------------------- + + /// Returns the value written to AccessPoint.1 Security.ModeEnabled after a + /// mode change on the given band, or null if it was never sent. + Future capturedModeEnabled({ + required String band, + required String selectedMode, + }) async { + when(() => mockUsp.set(any(), allowPartial: any(named: 'allowPartial'))) + .thenAnswer((_) async => uspSuccess()); + // Baseline mode differs from every selectedMode under test so the + // security diff always fires (WPA2-in == WPA2-baseline would send nothing). + final original = [ + makeNetwork(band: band, securityMode: 'WPA3-Personal-Transition') + ]; + final current = [makeNetwork(band: band, securityMode: selectedMode)]; + + await writeSvc.saveAdvanced(original: original, current: current); + + final captured = verify(() => mockUsp.set(captureAny(), + allowPartial: any(named: 'allowPartial'))).captured; + const key = 'Device.WiFi.AccessPoint.1.Security.ModeEnabled'; + for (final arg in captured) { + if (arg is Map && arg.containsKey(key)) return arg[key] as String?; + } + return null; + } + + test('6 GHz + OWE selected → sends OWE verbatim', () async { + expect( + await capturedModeEnabled(band: '6GHz', selectedMode: 'OWE'), + 'OWE', + ); + }); + + test('6 GHz + None (open) selected → normalized to OWE', () async { + expect( + await capturedModeEnabled(band: '6GHz', selectedMode: 'None'), + 'OWE', + ); + }); + + test('6 GHz + WPA2-Personal selected → forced to WPA3-Personal', () async { + expect( + await capturedModeEnabled(band: '6GHz', selectedMode: 'WPA2-Personal'), + 'WPA3-Personal', + ); + }); + + test('5 GHz + None selected → written verbatim (no 6 GHz override)', + () async { + expect( + await capturedModeEnabled(band: '5GHz', selectedMode: 'None'), + 'None', + ); + }); }); // ------------------------------------------------------------------------- @@ -1395,12 +1456,15 @@ void main() { ssidInstancePaths: const ['Device.WiFi.SSID.1.'], apInstancePaths: [ap], ); + // Baseline mode differs from every selectedMode under test so the + // security diff always fires (otherwise WPA2-in == WPA2-baseline would + // be a no-op and nothing would be sent). const orig = WifiQuickSetupSettings( isGuest: false, enabled: true, ssid: 'Home', password: '', - securityMode: 'WPA2-Personal', + securityMode: 'WPA3-Personal-Transition', supportedSecurityModes: [ 'None', 'WPA2-Personal', @@ -1410,7 +1474,10 @@ void main() { ); final original = WifiSettingsSettings( - networks: [makeNetwork(band: band, accessPointInstancePath: ap)], + networks: [ + makeNetwork(band: band, accessPointInstancePath: ap) + .copyWith(securityMode: 'WPA3-Personal-Transition') + ], quickSetupEnabled: true, quickSetupMain: orig, ); @@ -1450,8 +1517,10 @@ void main() { }); test('6 GHz + WPA2-Personal selected → forced to WPA3-Personal', () async { + // Pass a non-WPA3 mode so this genuinely exercises the coercion branch + // (WPA3 input would pass even if the override were removed). expect( - await capturedModeEnabled(band: '6GHz', selectedMode: 'WPA3-Personal'), + await capturedModeEnabled(band: '6GHz', selectedMode: 'WPA2-Personal'), 'WPA3-Personal', ); });