blob: 4e756d4e9cd8dafac2886802c770ed72e7c64dca [file] [log] [blame]
/*
* 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;
}
}