/* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server.wifi; import android.annotation.NonNull; import android.content.Context; import android.content.IntentFilter; import android.net.MacAddress; import android.net.wifi.SoftApConfiguration; import android.net.wifi.SoftApConfiguration.BandType; import android.os.Handler; import android.os.Process; import android.text.TextUtils; import android.util.Log; import android.util.SparseIntArray; import com.android.internal.annotations.VisibleForTesting; import com.android.modules.utils.build.SdkLevel; import com.android.net.module.util.MacAddressUtils; import com.android.server.wifi.util.ApConfigUtil; import com.android.wifi.resources.R; import java.nio.charset.CharsetEncoder; import java.nio.charset.StandardCharsets; import java.security.SecureRandom; import java.util.ArrayList; import java.util.Objects; import java.util.Random; import javax.annotation.Nullable; /** * Provides API for reading/writing soft access point configuration. */ public class WifiApConfigStore { // Intent when user has interacted with the softap settings change notification public static final String ACTION_HOTSPOT_CONFIG_USER_TAPPED_CONTENT = "com.android.server.wifi.WifiApConfigStoreUtil.HOTSPOT_CONFIG_USER_TAPPED_CONTENT"; private static final String TAG = "WifiApConfigStore"; private static final int RAND_SSID_INT_MIN = 1000; private static final int RAND_SSID_INT_MAX = 9999; @VisibleForTesting static final int SSID_MIN_LEN = 1; @VisibleForTesting static final int SSID_MAX_LEN = 32; @VisibleForTesting static final int PSK_MIN_LEN = 8; @VisibleForTesting static final int PSK_MAX_LEN = 63; private SoftApConfiguration mPersistentWifiApConfig = null; private final Context mContext; private final Handler mHandler; private final WifiMetrics mWifiMetrics; private final BackupManagerProxy mBackupManagerProxy; private final MacAddressUtil mMacAddressUtil; private final WifiConfigManager mWifiConfigManager; private final ActiveModeWarden mActiveModeWarden; private boolean mHasNewDataToSerialize = false; private boolean mForceApChannel = false; private int mForcedApBand; private int mForcedApChannel; /** * Module to interact with the wifi config store. */ private class SoftApStoreDataSource implements SoftApStoreData.DataSource { public SoftApConfiguration toSerialize() { mHasNewDataToSerialize = false; return mPersistentWifiApConfig; } public void fromDeserialized(SoftApConfiguration config) { mPersistentWifiApConfig = new SoftApConfiguration.Builder(config).build(); } public void reset() { mPersistentWifiApConfig = null; } public boolean hasNewDataToSerialize() { return mHasNewDataToSerialize; } } WifiApConfigStore(Context context, WifiInjector wifiInjector, Handler handler, BackupManagerProxy backupManagerProxy, WifiConfigStore wifiConfigStore, WifiConfigManager wifiConfigManager, ActiveModeWarden activeModeWarden, WifiMetrics wifiMetrics) { mContext = context; mHandler = handler; mBackupManagerProxy = backupManagerProxy; mWifiConfigManager = wifiConfigManager; mActiveModeWarden = activeModeWarden; mWifiMetrics = wifiMetrics; // Register store data listener wifiConfigStore.registerStoreData( wifiInjector.makeSoftApStoreData(new SoftApStoreDataSource())); IntentFilter filter = new IntentFilter(); filter.addAction(ACTION_HOTSPOT_CONFIG_USER_TAPPED_CONTENT); mMacAddressUtil = wifiInjector.getMacAddressUtil(); } /** * Return the current soft access point configuration. */ public synchronized SoftApConfiguration getApConfiguration() { if (mPersistentWifiApConfig == null) { /* Use default configuration. */ Log.d(TAG, "Fallback to use default AP configuration"); persistConfigAndTriggerBackupManagerProxy(getDefaultApConfiguration()); } SoftApConfiguration sanitizedPersistentconfig = sanitizePersistentApConfig(mPersistentWifiApConfig); if (!Objects.equals(mPersistentWifiApConfig, sanitizedPersistentconfig)) { Log.d(TAG, "persisted config was converted, need to resave it"); persistConfigAndTriggerBackupManagerProxy(sanitizedPersistentconfig); } if (mForceApChannel) { Log.d(TAG, "getApConfiguration: Band force to " + mForcedApBand + ", and channel force to " + mForcedApChannel); return mForcedApChannel == 0 ? new SoftApConfiguration.Builder(mPersistentWifiApConfig) .setBand(mForcedApBand).build() : new SoftApConfiguration.Builder(mPersistentWifiApConfig) .setChannel(mForcedApChannel, mForcedApBand).build(); } return mPersistentWifiApConfig; } /** * Update the current soft access point configuration. * Restore to default AP configuration if null is provided. * This can be invoked under context of binder threads (WifiManager.setWifiApConfiguration) * and the main Wifi thread (CMD_START_AP). */ public synchronized void setApConfiguration(SoftApConfiguration config) { if (config == null) { config = getDefaultApConfiguration(); } else { config = sanitizePersistentApConfig(config); } persistConfigAndTriggerBackupManagerProxy( new SoftApConfiguration.Builder(config).setUserConfiguration(true).build()); } /** * Returns SoftApConfiguration in which some parameters might be upgrade to supported default * configuration. */ public SoftApConfiguration upgradeSoftApConfiguration(@NonNull SoftApConfiguration config) { SoftApConfiguration.Builder configBuilder = new SoftApConfiguration.Builder(config); if (SdkLevel.isAtLeastS() && ApConfigUtil.isBridgedModeSupported(mContext) && config.getBands().length == 1) { int[] dual_bands = new int[] { SoftApConfiguration.BAND_2GHZ, SoftApConfiguration.BAND_2GHZ | SoftApConfiguration.BAND_5GHZ}; if (SdkLevel.isAtLeastS()) { configBuilder.setBands(dual_bands); } Log.i(TAG, "Device support bridged AP, upgrade band setting to bridged configuration"); } return configBuilder.build(); } /** * Returns SoftApConfiguration in which some parameters might be reset to supported default * config since it depends on UI or HW. * * MaxNumberOfClients and isClientControlByUserEnabled will need HAL support client force * disconnect, and Band setting (5g/6g) need HW support. * * HiddenSsid, Channel, ShutdownTimeoutMillis and AutoShutdownEnabled are features * which need UI(Setting) support. * * SAE/SAE-Transition need hardware support, reset to secured WPA2 security type when device * doesn't support it. * * Check band(s) setting to make sure all of the band(s) are supported. * - If previous bands configuration is bridged mode. Reset to 2.4G when device doesn't support * it. */ public SoftApConfiguration resetToDefaultForUnsupportedConfig( @NonNull SoftApConfiguration config) { SoftApConfiguration.Builder configBuilder = new SoftApConfiguration.Builder(config); if ((!ApConfigUtil.isClientForceDisconnectSupported(mContext) || mContext.getResources().getBoolean( R.bool.config_wifiSoftapResetUserControlConfig)) && (config.isClientControlByUserEnabled() || config.getBlockedClientList().size() != 0)) { configBuilder.setClientControlByUserEnabled(false); configBuilder.setBlockedClientList(new ArrayList<>()); Log.i(TAG, "Reset ClientControlByUser to false due to device doesn't support"); } if ((!ApConfigUtil.isClientForceDisconnectSupported(mContext) || mContext.getResources().getBoolean( R.bool.config_wifiSoftapResetMaxClientSettingConfig)) && config.getMaxNumberOfClients() != 0) { configBuilder.setMaxNumberOfClients(0); Log.i(TAG, "Reset MaxNumberOfClients to 0 due to device doesn't support"); } if (!ApConfigUtil.isWpa3SaeSupported(mContext) && (config.getSecurityType() == SoftApConfiguration.SECURITY_TYPE_WPA3_SAE || config.getSecurityType() == SoftApConfiguration.SECURITY_TYPE_WPA3_SAE_TRANSITION)) { configBuilder.setPassphrase(generatePassword(), SoftApConfiguration.SECURITY_TYPE_WPA2_PSK); Log.i(TAG, "Device doesn't support WPA3-SAE, reset config to WPA2"); } if (mContext.getResources().getBoolean(R.bool.config_wifiSoftapResetChannelConfig) && config.getChannel() != 0) { // The device might not support customize channel or forced channel might not // work in some countries. Need to reset it. configBuilder.setBand(ApConfigUtil.append24GToBandIf24GSupported( config.getBand(), mContext)); Log.i(TAG, "Reset SAP channel configuration"); } if (SdkLevel.isAtLeastS() && config.getBands().length > 1) { if (!ApConfigUtil.isBridgedModeSupported(mContext) || !isBandsSupported(config.getBands(), mContext)) { int newSingleApBand = 0; for (int targetBand : config.getBands()) { int availableBand = ApConfigUtil.removeUnsupportedBands( mContext, targetBand); newSingleApBand |= availableBand; } newSingleApBand = ApConfigUtil.append24GToBandIf24GSupported( newSingleApBand, mContext); configBuilder.setBand(newSingleApBand); Log.i(TAG, "An unsupported band setting for the bridged mode, force to " + newSingleApBand); } } else { // Single band case, check and remove unsupported band. int newBand = ApConfigUtil.removeUnsupportedBands(mContext, config.getBand()); if (newBand != config.getBand()) { newBand = ApConfigUtil.append24GToBandIf24GSupported(newBand, mContext); Log.i(TAG, "Reset band from " + config.getBand() + " to " + newBand); configBuilder.setBand(newBand); } } if (mContext.getResources().getBoolean(R.bool.config_wifiSoftapResetHiddenConfig) && config.isHiddenSsid()) { configBuilder.setHiddenSsid(false); Log.i(TAG, "Reset SAP Hidden Network configuration"); } if (mContext.getResources().getBoolean( R.bool.config_wifiSoftapResetAutoShutdownTimerConfig) && config.getShutdownTimeoutMillis() != 0) { configBuilder.setShutdownTimeoutMillis(0); Log.i(TAG, "Reset SAP auto shutdown configuration"); } if (!ApConfigUtil.isApMacRandomizationSupported(mContext)) { if (SdkLevel.isAtLeastS()) { configBuilder.setMacRandomizationSetting(SoftApConfiguration.RANDOMIZATION_NONE); Log.i(TAG, "Force set SAP MAC randomization to NONE when not supported"); } } mWifiMetrics.noteSoftApConfigReset(config, configBuilder.build()); return configBuilder.build(); } private SoftApConfiguration sanitizePersistentApConfig(SoftApConfiguration config) { SoftApConfiguration.Builder convertedConfigBuilder = new SoftApConfiguration.Builder(config); int[] bands = config.getBands(); // The bands length should always 1 in R. Adding SdkLevel.isAtLeastS for lint check only. if (bands.length > 1 && SdkLevel.isAtLeastS()) { // Consider 2.4G instance may be shutdown, i.e. only left 5G instance. If the 5G // configuration is 5G band only, it will cause that driver can't switch channel from // 5G to 2.4G when coexistence happene. Always append 2.4G into band configuration to // allow driver handle coexistence case after 2.4G instance shutdown. SparseIntArray newChannels = new SparseIntArray(); for (int i = 0; i < bands.length; i++) { int channel = config.getChannels().valueAt(i); if (channel == 0 && (bands[i] & SoftApConfiguration.BAND_2GHZ) == 0 && ApConfigUtil.isBandSupported(bands[i], mContext)) { newChannels.put(ApConfigUtil.append24GToBandIf24GSupported(bands[i], mContext), 0); } else { newChannels.put(bands[i], channel); } } convertedConfigBuilder.setChannels(newChannels); } else if (config.getChannel() == 0 && (bands[0] & SoftApConfiguration.BAND_2GHZ) == 0) { // some countries are unable to support 5GHz only operation, always allow for 2GHz when // config doesn't force channel if (ApConfigUtil.isBandSupported(bands[0], mContext)) { Log.i(TAG, "Supplied ap config band without 2.4G, add allowing for 2.4GHz"); convertedConfigBuilder.setBand( ApConfigUtil.append24GToBandIf24GSupported(bands[0], mContext)); } } return convertedConfigBuilder.build(); } private void persistConfigAndTriggerBackupManagerProxy(SoftApConfiguration config) { mPersistentWifiApConfig = config; mHasNewDataToSerialize = true; mWifiConfigManager.saveToStore(true); mBackupManagerProxy.notifyDataChanged(); } /** * Generate a default WPA3 SAE transition (if supported) or WPA2 based * configuration with a random password. * We are changing the Wifi Ap configuration storage from secure settings to a * flat file accessible only by the system. A WPA2 based default configuration * will keep the device secure after the update. */ private SoftApConfiguration getDefaultApConfiguration() { SoftApConfiguration.Builder configBuilder = new SoftApConfiguration.Builder(); configBuilder.setBand(generateDefaultBand(mContext)); configBuilder.setSsid(mContext.getResources().getString( R.string.wifi_tether_configure_ssid_default) + "_" + getRandomIntForDefaultSsid()); if (ApConfigUtil.isWpa3SaeSupported(mContext)) { configBuilder.setPassphrase(generatePassword(), SoftApConfiguration.SECURITY_TYPE_WPA3_SAE_TRANSITION); } else { configBuilder.setPassphrase(generatePassword(), SoftApConfiguration.SECURITY_TYPE_WPA2_PSK); } // It is new overlay configuration, it should always false in R. Add SdkLevel.isAtLeastS for // lint check if (ApConfigUtil.isBridgedModeSupported(mContext)) { if (SdkLevel.isAtLeastS()) { int[] dual_bands = new int[] { SoftApConfiguration.BAND_2GHZ, SoftApConfiguration.BAND_2GHZ | SoftApConfiguration.BAND_5GHZ}; configBuilder.setBands(dual_bands); } } // Update default MAC randomization setting to NONE when feature doesn't support it. if (!ApConfigUtil.isApMacRandomizationSupported(mContext)) { if (SdkLevel.isAtLeastS()) { configBuilder.setMacRandomizationSetting(SoftApConfiguration.RANDOMIZATION_NONE); } } configBuilder.setUserConfiguration(false); return configBuilder.build(); } private static int getRandomIntForDefaultSsid() { Random random = new Random(); return random.nextInt((RAND_SSID_INT_MAX - RAND_SSID_INT_MIN) + 1) + RAND_SSID_INT_MIN; } private static String generateLohsSsid(Context context) { return context.getResources().getString( R.string.wifi_localhotspot_configure_ssid_default) + "_" + getRandomIntForDefaultSsid(); } /** * Generate a temporary WPA2 based configuration for use by the local only hotspot. * This config is not persisted and will not be stored by the WifiApConfigStore. */ public SoftApConfiguration generateLocalOnlyHotspotConfig(Context context, int apBand, @Nullable SoftApConfiguration customConfig) { SoftApConfiguration.Builder configBuilder; if (customConfig != null) { configBuilder = new SoftApConfiguration.Builder(customConfig); } else { configBuilder = new SoftApConfiguration.Builder(); // Default to disable the auto shutdown configBuilder.setAutoShutdownEnabled(false); } configBuilder.setBand(apBand); if (customConfig == null || customConfig.getSsid() == null) { configBuilder.setSsid(generateLohsSsid(context)); } if (customConfig == null) { if (ApConfigUtil.isWpa3SaeSupported(context)) { configBuilder.setPassphrase(generatePassword(), SoftApConfiguration.SECURITY_TYPE_WPA3_SAE_TRANSITION); } else { configBuilder.setPassphrase(generatePassword(), SoftApConfiguration.SECURITY_TYPE_WPA2_PSK); } } // Update default MAC randomization setting to NONE when feature doesn't support it or // It was disabled in tethered mode. if (!ApConfigUtil.isApMacRandomizationSupported(context) || (mPersistentWifiApConfig != null && mPersistentWifiApConfig.getMacRandomizationSettingInternal() == SoftApConfiguration.RANDOMIZATION_NONE)) { if (SdkLevel.isAtLeastS()) { configBuilder.setMacRandomizationSetting(SoftApConfiguration.RANDOMIZATION_NONE); } } return configBuilder.build(); } /** * @return a copy of the given SoftApConfig with the BSSID randomized, unless a custom BSSID is * already set. */ SoftApConfiguration randomizeBssidIfUnset(Context context, SoftApConfiguration config) { SoftApConfiguration.Builder configBuilder = new SoftApConfiguration.Builder(config); if (config.getBssid() == null && ApConfigUtil.isApMacRandomizationSupported(mContext)) { if (config.getMacRandomizationSettingInternal() == SoftApConfiguration.RANDOMIZATION_NONE) { return configBuilder.build(); } MacAddress macAddress = mMacAddressUtil.calculatePersistentMac(config.getSsid(), mMacAddressUtil.obtainMacRandHashFunctionForSap(Process.WIFI_UID)); if (macAddress == null) { Log.e(TAG, "Failed to calculate MAC from SSID. " + "Generating new random MAC instead."); macAddress = MacAddressUtils.createRandomUnicastAddress(); } configBuilder.setBssid(macAddress); } return configBuilder.build(); } /** * Verify provided SSID for existence, length and conversion to bytes * * @param ssid String ssid name * @return boolean indicating ssid met requirements */ private static boolean validateApConfigSsid(String ssid) { if (TextUtils.isEmpty(ssid)) { Log.d(TAG, "SSID for softap configuration must be set."); return false; } try { byte[] ssid_bytes = ssid.getBytes(StandardCharsets.UTF_8); if (ssid_bytes.length < SSID_MIN_LEN || ssid_bytes.length > SSID_MAX_LEN) { Log.d(TAG, "softap SSID is defined as UTF-8 and it must be at least " + SSID_MIN_LEN + " byte and not more than " + SSID_MAX_LEN + " bytes"); return false; } } catch (IllegalArgumentException e) { Log.e(TAG, "softap config SSID verification failed: malformed string " + ssid); return false; } return true; } /** * Verify provided preSharedKey in ap config for WPA2_PSK network meets requirements. */ private static boolean validateApConfigPreSharedKey(String preSharedKey) { if (preSharedKey.length() < PSK_MIN_LEN || preSharedKey.length() > PSK_MAX_LEN) { Log.d(TAG, "softap network password string size must be at least " + PSK_MIN_LEN + " and no more than " + PSK_MAX_LEN); return false; } try { preSharedKey.getBytes(StandardCharsets.UTF_8); } catch (IllegalArgumentException e) { Log.e(TAG, "softap network password verification failed: malformed string"); return false; } return true; } /** * Validate a SoftApConfiguration is properly configured for use by SoftApManager. * * This method checks the length of the SSID and for consistency between security settings (if * it requires a password, was one provided?). * * @param apConfig {@link SoftApConfiguration} to use for softap mode * @param isPrivileged indicate the caller can pass some fields check or not * @return boolean true if the provided config meets the minimum set of details, false * otherwise. */ static boolean validateApWifiConfiguration(@NonNull SoftApConfiguration apConfig, boolean isPrivileged, Context context) { // first check the SSID if (!validateApConfigSsid(apConfig.getSsid())) { // failed SSID verificiation checks return false; } // BSSID can be set if caller own permission:android.Manifest.permission.NETWORK_SETTINGS. if (apConfig.getBssid() != null && !isPrivileged) { Log.e(TAG, "Config BSSID needs NETWORK_SETTINGS permission"); return false; } String preSharedKey = apConfig.getPassphrase(); boolean hasPreSharedKey = !TextUtils.isEmpty(preSharedKey); int authType; try { authType = apConfig.getSecurityType(); } catch (IllegalStateException e) { Log.d(TAG, "Unable to get AuthType for softap config: " + e.getMessage()); return false; } if (authType == SoftApConfiguration.SECURITY_TYPE_OPEN) { // open networks should not have a password if (hasPreSharedKey) { Log.d(TAG, "open softap network should not have a password"); return false; } } else if (authType == SoftApConfiguration.SECURITY_TYPE_WPA2_PSK || authType == SoftApConfiguration.SECURITY_TYPE_WPA3_SAE_TRANSITION || authType == SoftApConfiguration.SECURITY_TYPE_WPA3_SAE) { // this is a config that should have a password - check that first if (!hasPreSharedKey) { Log.d(TAG, "softap network password must be set"); return false; } if (context.getResources().getBoolean( R.bool.config_wifiSoftapPassphraseAsciiEncodableCheck)) { final CharsetEncoder asciiEncoder = StandardCharsets.US_ASCII.newEncoder(); if (!asciiEncoder.canEncode(preSharedKey)) { Log.d(TAG, "passphrase not ASCII encodable"); return false; } } if (authType != SoftApConfiguration.SECURITY_TYPE_WPA3_SAE && !validateApConfigPreSharedKey(preSharedKey)) { // failed preSharedKey checks for WPA2 and WPA3 SAE Transition mode. return false; } } else { // this is not a supported security type Log.d(TAG, "softap configs must either be open or WPA2 PSK networks"); return false; } if (SdkLevel.isAtLeastS()) { if (!isBandsSupported(apConfig.getBands(), context)) { return false; } } else { if (!ApConfigUtil.isBandSupported(apConfig.getBand(), context)) { return false; } } return true; } private static String generatePassword() { // Characters that will be used for password generation. Some characters commonly known to // be confusing like 0 and O excluded from this list. final String allowed = "23456789abcdefghijkmnpqrstuvwxyz"; final int passLength = 15; StringBuilder sb = new StringBuilder(passLength); SecureRandom random = new SecureRandom(); for (int i = 0; i < passLength; i++) { sb.append(allowed.charAt(random.nextInt(allowed.length()))); } return sb.toString(); } /** * Generate default band base on supported band configuration. * * @param context The caller context used to get value from resource file. * @return A band which will be used for a default band in default configuration. */ public static @BandType int generateDefaultBand(Context context) { for (int band : SoftApConfiguration.BAND_TYPES) { if (ApConfigUtil.isBandSupported(band, context)) { return band; } } Log.e(TAG, "Invalid overlay configuration! No any band supported on SoftAp"); return SoftApConfiguration.BAND_2GHZ; } private static boolean isBandsSupported(@NonNull int[] apBands, Context context) { for (int band : apBands) { if (!ApConfigUtil.isBandSupported(band, context)) { return false; } } return true; } /** * Enable force-soft-AP-channel mode which takes effect when soft AP starts next time * * @param forcedApBand The forced band. * @param forcedApChannel The forced IEEE channel number or 0 when forced AP band only. */ public void enableForceSoftApBandOrChannel(@BandType int forcedApBand, int forcedApChannel) { mForceApChannel = true; mForcedApChannel = forcedApChannel; mForcedApBand = forcedApBand; } /** * Disable force-soft-AP-channel mode which take effect when soft AP starts next time */ public void disableForceSoftApBandOrChannel() { mForceApChannel = false; } }