Skip to content
Merged
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
5 changes: 5 additions & 0 deletions lib/page/_shared/components/wifi_ui.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
7 changes: 3 additions & 4 deletions lib/page/wifi_settings/models/wifi_network_ui_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
3 changes: 2 additions & 1 deletion lib/page/wifi_settings/models/wifi_settings_settings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
24 changes: 16 additions & 8 deletions lib/page/wifi_settings/services/usp_wifi_settings_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
[
Expand All @@ -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,
)
Expand Down Expand Up @@ -627,17 +634,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';
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Expand All @@ -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);
});
});
}
220 changes: 220 additions & 0 deletions test/page/wifi_settings/services/usp_wifi_settings_service_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<String?> 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',
);
});
});

// -------------------------------------------------------------------------
Expand Down Expand Up @@ -1321,4 +1382,163 @@ 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 `<ap>Security.ModeEnabled`, or null if the
/// param was never sent.
Future<String?> 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],
);
// 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: 'WPA3-Personal-Transition',
supportedSecurityModes: [
'None',
'WPA2-Personal',
'WPA3-Personal',
'OWE'
],
);

final original = WifiSettingsSettings(
networks: [
makeNetwork(band: band, accessPointInstancePath: ap)
.copyWith(securityMode: 'WPA3-Personal-Transition')
],
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 {
// 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: 'WPA2-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',
);
});
});
}
Loading