/* * Copyright (C) 2016 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 static android.net.wifi.WifiManager.ALL_ZEROS_MAC_ADDRESS; import static com.android.server.wifi.util.NativeUtil.addEnclosingQuotes; import android.annotation.SuppressLint; import android.net.IpConfiguration; import android.net.MacAddress; import android.net.StaticIpConfiguration; import android.net.wifi.SecurityParams; import android.net.wifi.WifiConfiguration; import android.net.wifi.WifiEnterpriseConfig; import android.net.wifi.WifiManager; import android.net.wifi.WifiNetworkSpecifier; import android.net.wifi.WifiScanner; import android.net.wifi.WifiSsid; import android.os.PatternMatcher; import android.text.TextUtils; import android.util.Log; import android.util.Pair; import com.android.internal.annotations.VisibleForTesting; import com.android.modules.utils.build.SdkLevel; import com.android.server.wifi.util.NativeUtil; import java.nio.charset.StandardCharsets; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Arrays; import java.util.BitSet; import java.util.Comparator; import java.util.List; import java.util.Objects; /** * WifiConfiguration utility for any {@link android.net.wifi.WifiConfiguration} related operations. * Currently contains: * > Helper method to check if the WifiConfiguration object is visible to the provided users. * > Helper methods to identify the encryption of a WifiConfiguration object. */ public class WifiConfigurationUtil { private static final String TAG = "WifiConfigurationUtil"; /** * Constants used for validating external config objects. */ private static final int ENCLOSING_QUOTES_LEN = 2; private static final int SSID_PLAINTEXT_MAX_LEN = 32 + ENCLOSING_QUOTES_LEN; private static final int SSID_HEX_MAX_LEN = 64; private static final int PSK_ASCII_MIN_LEN = 8 + ENCLOSING_QUOTES_LEN; private static final int SAE_ASCII_MIN_LEN = 1 + ENCLOSING_QUOTES_LEN; private static final int PSK_SAE_ASCII_MAX_LEN = 63 + ENCLOSING_QUOTES_LEN; private static final int PSK_SAE_HEX_LEN = 64; private static final int WEP104_KEY_BYTES_LEN = 13; private static final int WEP40_KEY_BYTES_LEN = 5; @VisibleForTesting public static final String PASSWORD_MASK = "*"; private static final String MATCH_EMPTY_SSID_PATTERN_PATH = ""; private static final Pair MATCH_NONE_BSSID_PATTERN = new Pair<>(MacAddress.BROADCAST_ADDRESS, MacAddress.BROADCAST_ADDRESS); private static final Pair MATCH_ALL_BSSID_PATTERN = new Pair<>(ALL_ZEROS_MAC_ADDRESS, ALL_ZEROS_MAC_ADDRESS); private static final String SYSTEM_CA_STORE_PATH = "/system/etc/security/cacerts"; /** * Checks if the provided |wepKeys| array contains any non-null value; */ public static boolean hasAnyValidWepKey(String[] wepKeys) { for (int i = 0; i < wepKeys.length; i++) { if (wepKeys[i] != null) { return true; } } return false; } /** * Helper method to check if the provided |config| corresponds to a PSK network or not. */ public static boolean isConfigForPskNetwork(WifiConfiguration config) { return config.isSecurityType(WifiConfiguration.SECURITY_TYPE_PSK); } /** * Helper method to check if the provided |config| corresponds to a WAPI PSK network or not. */ public static boolean isConfigForWapiPskNetwork(WifiConfiguration config) { return config.isSecurityType(WifiConfiguration.SECURITY_TYPE_WAPI_PSK); } /** * Helper method to check if the provided |config| corresponds to a WAPI CERT network or not. */ public static boolean isConfigForWapiCertNetwork(WifiConfiguration config) { return config.isSecurityType(WifiConfiguration.SECURITY_TYPE_WAPI_CERT); } /** * Helper method to check if the provided |config| corresponds to an SAE network or not. */ public static boolean isConfigForSaeNetwork(WifiConfiguration config) { return config.isSecurityType(WifiConfiguration.SECURITY_TYPE_SAE); } /** * Helper method to check if the provided |config| corresponds to an OWE network or not. */ public static boolean isConfigForOweNetwork(WifiConfiguration config) { return config.isSecurityType(WifiConfiguration.SECURITY_TYPE_OWE); } /** * Helper method to check if the provided |config| corresponds to a EAP network or not. * * Attention: This method returns true only for WiFi configuration with traditional EAP methods. * It returns false for passpoint WiFi configuration. Please consider to use * isConfigForEnterpriseNetwork() if necessary. */ public static boolean isConfigForEapNetwork(WifiConfiguration config) { return config.isSecurityType(WifiConfiguration.SECURITY_TYPE_EAP); } /** * Helper method to check if the provided |config| corresponds to an enterprise network or not. */ public static boolean isConfigForEnterpriseNetwork(WifiConfiguration config) { return config.getDefaultSecurityParams().isEnterpriseSecurityType(); } /** * Helper method to check if the provided |config| corresponds to * a WPA3 Enterprise network or not. */ public static boolean isConfigForWpa3EnterpriseNetwork(WifiConfiguration config) { return config.isSecurityType(WifiConfiguration.SECURITY_TYPE_EAP_WPA3_ENTERPRISE); } /** * Helper method to check if the provided |config| corresponds to a EAP Suite-B network or not. */ public static boolean isConfigForWpa3Enterprise192BitNetwork(WifiConfiguration config) { return config.isSecurityType(WifiConfiguration.SECURITY_TYPE_EAP_WPA3_ENTERPRISE_192_BIT); } /** * Helper method to check if the provided |config| corresponds to a DPP network or not. */ public static boolean isConfigForDppNetwork(WifiConfiguration config) { return config.isSecurityType(WifiConfiguration.SECURITY_TYPE_DPP); } /** * Helper method to check if the provided |config| corresponds to a WEP network or not. */ public static boolean isConfigForWepNetwork(WifiConfiguration config) { return config.isSecurityType(WifiConfiguration.SECURITY_TYPE_WEP); } /** * Helper method to check if the provided |config| corresponds to a Passpoint network or not. */ public static boolean isConfigForPasspoint(WifiConfiguration config) { return config.isSecurityType(WifiConfiguration.SECURITY_TYPE_PASSPOINT_R1_R2) || config.isSecurityType(WifiConfiguration.SECURITY_TYPE_PASSPOINT_R3); } /** * Helper method to check if the provided |config| corresponds to an open or enhanced * open network, or not. */ public static boolean isConfigForOpenNetwork(WifiConfiguration config) { return (!(isConfigForWepNetwork(config) || isConfigForPskNetwork(config) || isConfigForWapiPskNetwork(config) || isConfigForWapiCertNetwork(config) || isConfigForEapNetwork(config) || isConfigForSaeNetwork(config) || isConfigForWpa3Enterprise192BitNetwork(config) || isConfigForPasspoint(config))); } /** * Compare existing and new WifiConfiguration objects after a network update and return if * IP parameters have changed or not. * * @param existingConfig Existing WifiConfiguration object corresponding to the network. * @param newConfig New WifiConfiguration object corresponding to the network. * @return true if IP parameters have changed, false otherwise. */ public static boolean hasIpChanged(WifiConfiguration existingConfig, WifiConfiguration newConfig) { if (existingConfig.getIpAssignment() != newConfig.getIpAssignment()) { return true; } if (newConfig.getIpAssignment() == IpConfiguration.IpAssignment.STATIC) { return !Objects.equals(existingConfig.getStaticIpConfiguration(), newConfig.getStaticIpConfiguration()); } return false; } /** * Compare existing and new WifiConfiguration objects after a network update and return if * proxy parameters have changed or not. * * @param existingConfig Existing WifiConfiguration object corresponding to the network. * @param newConfig New WifiConfiguration object corresponding to the network. * @return true if proxy parameters have changed, false if no existing config and proxy settings * are NONE, false otherwise. */ public static boolean hasProxyChanged(WifiConfiguration existingConfig, WifiConfiguration newConfig) { if (existingConfig == null) { return newConfig.getProxySettings() != IpConfiguration.ProxySettings.NONE; } if (newConfig.getProxySettings() != existingConfig.getProxySettings()) { return true; } return !Objects.equals(existingConfig.getHttpProxy(), newConfig.getHttpProxy()); } /** * Compare existing and new WifiConfiguration objects after a network update and return if * Repeater Enabled flag has changed or not. In case the there is no existing WifiConfiguration, * checks if Repeater Enabled flag has changed from the default value of false. * * @param existingConfig Existing WifiConfiguration object corresponding to the network. * @param newConfig New WifiConfiguration object corresponding to the network. * @return true if RepeaterEnabled flag has changed, or if there is no existing config, and * the flag is set to true, false otherwise. */ public static boolean hasRepeaterEnabledChanged(WifiConfiguration existingConfig, WifiConfiguration newConfig) { if (existingConfig == null) { return newConfig.isRepeaterEnabled(); } return (newConfig.isRepeaterEnabled() != existingConfig.isRepeaterEnabled()); } /** * Compare existing and new WifiConfiguration objects after a network update and return if * MAC randomization setting has changed or not. * @param existingConfig Existing WifiConfiguration object corresponding to the network. * @param newConfig New WifiConfiguration object corresponding to the network. * @return true if MAC randomization setting changed or the existing configuration is * null and the newConfig is setting macRandomizationSetting to the default value. */ public static boolean hasMacRandomizationSettingsChanged(WifiConfiguration existingConfig, WifiConfiguration newConfig) { if (existingConfig == null) { return newConfig.macRandomizationSetting != WifiConfiguration.RANDOMIZATION_AUTO; } return newConfig.macRandomizationSetting != existingConfig.macRandomizationSetting; } /** * Compare existing and new WifiConfiguration objects after a network update and return if * DHCP hostname setting has changed or not. * @param existingConfig Existing WifiConfiguration object corresponding to the network. * @param newConfig New WifiConfiguration object corresponding to the network. * @return true if DHCP hostname setting changed or the existing configuration is * null and the newConfig is setting the DHCP hostname setting to the default value. */ public static boolean hasSendDhcpHostnameEnabledChanged(WifiConfiguration existingConfig, WifiConfiguration newConfig) { if (existingConfig == null) { return !newConfig.isSendDhcpHostnameEnabled(); } return newConfig.isSendDhcpHostnameEnabled() != existingConfig.isSendDhcpHostnameEnabled(); } /** * Compare existing and new WifiEnterpriseConfig objects after a network update and return if * credential parameters have changed or not. * * @param existingEnterpriseConfig Existing WifiConfiguration object corresponding to the * network. * @param newEnterpriseConfig New WifiConfiguration object corresponding to the network. * @return true if credentials have changed, false otherwise. */ @VisibleForTesting public static boolean hasEnterpriseConfigChanged(WifiEnterpriseConfig existingEnterpriseConfig, WifiEnterpriseConfig newEnterpriseConfig) { if (existingEnterpriseConfig != null && newEnterpriseConfig != null) { if (existingEnterpriseConfig.getEapMethod() != newEnterpriseConfig.getEapMethod()) { return true; } if (existingEnterpriseConfig.isAuthenticationSimBased()) { // On Pre-T devices consider it as a credential change so that the network // configuration is reloaded in wpa_supplicant during reconnection. This is to // ensure that the updated anonymous identity is sent to wpa_supplicant. On newer // releases the anonymous identity is updated immediately after connection // completion event. if (!SdkLevel.isAtLeastT() && !TextUtils.equals(existingEnterpriseConfig.getAnonymousIdentity(), newEnterpriseConfig.getAnonymousIdentity())) { return true; } return false; } if (existingEnterpriseConfig.getPhase2Method() != newEnterpriseConfig.getPhase2Method()) { return true; } if (!TextUtils.equals(existingEnterpriseConfig.getIdentity(), newEnterpriseConfig.getIdentity())) { return true; } if (!TextUtils.equals(existingEnterpriseConfig.getAnonymousIdentity(), newEnterpriseConfig.getAnonymousIdentity())) { return true; } if (!TextUtils.equals(existingEnterpriseConfig.getPassword(), newEnterpriseConfig.getPassword())) { return true; } X509Certificate[] existingCaCerts = existingEnterpriseConfig.getCaCertificates(); X509Certificate[] newCaCerts = newEnterpriseConfig.getCaCertificates(); if (!Arrays.equals(existingCaCerts, newCaCerts)) { return true; } if (!Arrays.equals(newEnterpriseConfig.getCaCertificateAliases(), existingEnterpriseConfig.getCaCertificateAliases())) { return true; } if (!TextUtils.equals(newEnterpriseConfig.getClientCertificateAlias(), existingEnterpriseConfig.getClientCertificateAlias())) { return true; } if (!TextUtils.equals(newEnterpriseConfig.getAltSubjectMatch(), existingEnterpriseConfig.getAltSubjectMatch())) { return true; } if (!TextUtils.equals(newEnterpriseConfig.getWapiCertSuite(), existingEnterpriseConfig.getWapiCertSuite())) { return true; } if (newEnterpriseConfig.getOcsp() != existingEnterpriseConfig.getOcsp()) { return true; } if (!TextUtils.equals(newEnterpriseConfig.getDomainSuffixMatch(), existingEnterpriseConfig.getDomainSuffixMatch())) { return true; } if (newEnterpriseConfig.getMinimumTlsVersion() != existingEnterpriseConfig.getMinimumTlsVersion()) { return true; } } else { // One of the configs may have an enterpriseConfig if (existingEnterpriseConfig != null || newEnterpriseConfig != null) { return true; } } return false; } /** * Compare existing and new WifiConfiguration objects after a network update and return if * credential parameters have changed or not. * * @param existingConfig Existing WifiConfiguration object corresponding to the network. * @param newConfig New WifiConfiguration object corresponding to the network. * @return true if credentials have changed, false otherwise. */ public static boolean hasCredentialChanged(WifiConfiguration existingConfig, WifiConfiguration newConfig) { if (!Objects.equals(existingConfig.allowedKeyManagement, newConfig.allowedKeyManagement)) { return true; } if (!Objects.equals(existingConfig.allowedProtocols, newConfig.allowedProtocols)) { return true; } if (!Objects.equals(existingConfig.allowedAuthAlgorithms, newConfig.allowedAuthAlgorithms)) { return true; } if (!Objects.equals(existingConfig.allowedPairwiseCiphers, newConfig.allowedPairwiseCiphers)) { return true; } if (!Objects.equals(existingConfig.allowedGroupCiphers, newConfig.allowedGroupCiphers)) { return true; } if (!Objects.equals(existingConfig.allowedGroupManagementCiphers, newConfig.allowedGroupManagementCiphers)) { return true; } if (!Objects.equals(existingConfig.allowedSuiteBCiphers, newConfig.allowedSuiteBCiphers)) { return true; } if (!existingConfig.getSecurityParamsList().equals(newConfig.getSecurityParamsList())) { return true; } if (!Objects.equals(existingConfig.preSharedKey, newConfig.preSharedKey)) { return true; } if (!Arrays.equals(existingConfig.wepKeys, newConfig.wepKeys)) { return true; } if (existingConfig.wepTxKeyIndex != newConfig.wepTxKeyIndex) { return true; } if (existingConfig.hiddenSSID != newConfig.hiddenSSID) { return true; } if (existingConfig.requirePmf != newConfig.requirePmf) { return true; } if (existingConfig.carrierId != newConfig.carrierId) { return true; } if (hasEnterpriseConfigChanged(existingConfig.enterpriseConfig, newConfig.enterpriseConfig)) { return true; } return false; } private static boolean validateSsid(String ssid, boolean isAdd) { if (isAdd) { if (ssid == null) { Log.e(TAG, "validateSsid : null string"); return false; } } else { if (ssid == null) { // This is an update, so the SSID can be null if that is not being changed. return true; } } if (ssid.isEmpty()) { Log.e(TAG, "validateSsid failed: empty string"); return false; } if (ssid.startsWith("\"")) { if (ssid.length() > SSID_PLAINTEXT_MAX_LEN) { Log.e(TAG, "validateSsid failed: plaintext ssid " + ssid + " longer than 32 chars"); return false; } } else { if (ssid.length() > SSID_HEX_MAX_LEN) { Log.e(TAG, "validateSsid failed: hex ssid " + ssid + " longer than 32 bytes"); return false; } } WifiSsid wifiSsid; try { wifiSsid = WifiSsid.fromString(ssid); } catch (IllegalArgumentException e) { Log.e(TAG, "validateSsid failed: malformed string: " + ssid); return false; } int ssidLength = wifiSsid.getBytes().length; if (ssidLength == 0) { Log.e(TAG, "validateSsid failed: ssid 0 length!"); return false; } return true; } private static boolean validateBssid(MacAddress bssid) { if (bssid == null) return true; if (bssid.getAddressType() != MacAddress.TYPE_UNICAST) { Log.e(TAG, "validateBssid failed: invalid bssid"); return false; } return true; } private static boolean validateBssid(String bssid) { if (bssid == null) return true; if (bssid.isEmpty()) { Log.e(TAG, "validateBssid failed: empty string"); return false; } // Allow reset of bssid with "any". if (bssid.equals(ClientModeImpl.SUPPLICANT_BSSID_ANY)) return true; MacAddress bssidMacAddress; try { bssidMacAddress = MacAddress.fromString(bssid); } catch (IllegalArgumentException e) { Log.e(TAG, "validateBssid failed: malformed string: " + bssid); return false; } if (!validateBssid(bssidMacAddress)) { return false; } return true; } private static boolean validatePassword(String password, boolean isAdd, boolean isSae, boolean isWapi) { if (isAdd) { if (password == null) { Log.e(TAG, "validatePassword: null string"); return false; } } else { if (password == null) { // This is an update, so the psk can be null if that is not being changed. return true; } else if (password.equals(PASSWORD_MASK)) { // This is an update, so the app might have returned back the masked password, let // it thru. WifiConfigManager will handle it. return true; } } if (password.isEmpty()) { Log.e(TAG, "validatePassword failed: empty string"); return false; } if (password.startsWith("\"")) { // ASCII PSK string byte[] passwordBytes = password.getBytes(StandardCharsets.US_ASCII); int targetMinLength; if (isSae) { targetMinLength = SAE_ASCII_MIN_LEN; } else { targetMinLength = PSK_ASCII_MIN_LEN; } if (passwordBytes.length < targetMinLength) { Log.e(TAG, "validatePassword failed: ASCII string size too small: " + passwordBytes.length); return false; } if (passwordBytes.length > PSK_SAE_ASCII_MAX_LEN) { Log.e(TAG, "validatePassword failed: ASCII string size too large: " + passwordBytes.length); return false; } } else { // HEX PSK string if (isWapi) { // Protect system against malicious actors injecting arbitrarily large passwords. if (password.length() > 100) { Log.e(TAG, "validatePassword failed: WAPI hex string too long: " + password.length()); return false; } } else if (password.length() != PSK_SAE_HEX_LEN) { Log.e(TAG, "validatePassword failed: hex string size mismatch: " + password.length()); return false; } } try { NativeUtil.hexOrQuotedStringToBytes(password); } catch (IllegalArgumentException e) { Log.e(TAG, "validatePassword failed: malformed string: " + password); return false; } return true; } private static boolean validateWepKeys(String[] wepKeys, int wepTxKeyIndex, boolean isAdd) { if (isAdd) { if (wepKeys == null) { Log.e(TAG, "validateWepKeys: null string"); return false; } } else { if (wepKeys == null) { // This is an update, so the psk can be null if that is not being changed. return true; } else { boolean allMaskedKeys = true; for (int i = 0; i < wepKeys.length; i++) { if (wepKeys[i] != null && !TextUtils.equals(wepKeys[i], PASSWORD_MASK)) { allMaskedKeys = false; } } if (allMaskedKeys) { // This is an update, so the app might have returned back the masked password, // let it thru. WifiConfigManager will handle it. return true; } } } for (int i = 0; i < wepKeys.length; i++) { if (wepKeys[i] != null) { try { ArrayList wepKeyBytes = NativeUtil.hexOrQuotedStringToBytes(wepKeys[i]); if (wepKeyBytes.size() != WEP40_KEY_BYTES_LEN && wepKeyBytes.size() != WEP104_KEY_BYTES_LEN) { Log.e(TAG, "validateWepKeys: invalid wep key length " + wepKeys[i].length() + " at index " + i); return false; } } catch (IllegalArgumentException e) { Log.e(TAG, "validateWepKeys: invalid wep key at index " + i, e); return false; } } } if (wepTxKeyIndex >= wepKeys.length) { Log.e(TAG, "validateWepKeys: invalid wep tx key index " + wepTxKeyIndex + " wepKeys len: " + wepKeys.length); return false; } return true; } private static boolean validateBitSet(BitSet bitSet, int validValuesLength) { if (bitSet == null) return false; BitSet clonedBitset = (BitSet) bitSet.clone(); clonedBitset.clear(0, validValuesLength); return clonedBitset.isEmpty(); } private static boolean validateBitSets(WifiConfiguration config) { // 1. Check |allowedKeyManagement|. if (!validateBitSet(config.allowedKeyManagement, WifiConfiguration.KeyMgmt.strings.length)) { Log.e(TAG, "validateBitsets failed: invalid allowedKeyManagement bitset " + config.allowedKeyManagement); return false; } // 2. Check |allowedProtocols|. if (!validateBitSet(config.allowedProtocols, WifiConfiguration.Protocol.strings.length)) { Log.e(TAG, "validateBitsets failed: invalid allowedProtocols bitset " + config.allowedProtocols); return false; } // 3. Check |allowedAuthAlgorithms|. if (!validateBitSet(config.allowedAuthAlgorithms, WifiConfiguration.AuthAlgorithm.strings.length)) { Log.e(TAG, "validateBitsets failed: invalid allowedAuthAlgorithms bitset " + config.allowedAuthAlgorithms); return false; } // 4. Check |allowedGroupCiphers|. if (!validateBitSet(config.allowedGroupCiphers, WifiConfiguration.GroupCipher.strings.length)) { Log.e(TAG, "validateBitsets failed: invalid allowedGroupCiphers bitset " + config.allowedGroupCiphers); return false; } // 5. Check |allowedPairwiseCiphers|. if (!validateBitSet(config.allowedPairwiseCiphers, WifiConfiguration.PairwiseCipher.strings.length)) { Log.e(TAG, "validateBitsets failed: invalid allowedPairwiseCiphers bitset " + config.allowedPairwiseCiphers); return false; } return true; } private static boolean validateKeyMgmt(BitSet keyMgmnt) { if (keyMgmnt.cardinality() > 1) { if (keyMgmnt.cardinality() > 3) { Log.e(TAG, "validateKeyMgmt failed: cardinality > 3"); return false; } if (!keyMgmnt.get(WifiConfiguration.KeyMgmt.WPA_EAP)) { Log.e(TAG, "validateKeyMgmt failed: not WPA_EAP"); return false; } if (!keyMgmnt.get(WifiConfiguration.KeyMgmt.IEEE8021X)) { Log.e(TAG, "validateKeyMgmt failed: not 8021X"); return false; } // SUITE-B keymgmt must be WPA_EAP + IEEE8021X + SUITE_B_192. if (keyMgmnt.cardinality() == 3 && !(keyMgmnt.get(WifiConfiguration.KeyMgmt.WPA_EAP) && keyMgmnt.get(WifiConfiguration.KeyMgmt.IEEE8021X) && keyMgmnt.get(WifiConfiguration.KeyMgmt.SUITE_B_192))) { Log.e(TAG, "validateKeyMgmt failed: not SUITE_B_192"); return false; } } // There should be at least one keymgmt. if (keyMgmnt.cardinality() == 0) { Log.e(TAG, "validateKeyMgmt failed: cardinality = 0"); return false; } return true; } private static boolean validateIpConfiguration(IpConfiguration ipConfig) { if (ipConfig == null) { Log.e(TAG, "validateIpConfiguration failed: null IpConfiguration"); return false; } if (ipConfig.getIpAssignment() == IpConfiguration.IpAssignment.STATIC) { StaticIpConfiguration staticIpConfig = ipConfig.getStaticIpConfiguration(); if (staticIpConfig == null) { Log.e(TAG, "validateIpConfiguration failed: null StaticIpConfiguration"); return false; } if (staticIpConfig.getIpAddress() == null) { Log.e(TAG, "validateIpConfiguration failed: null static ip Address"); return false; } } return true; } /** * Enums to specify if the provided config is being validated for add or update. */ public static final boolean VALIDATE_FOR_ADD = true; public static final boolean VALIDATE_FOR_UPDATE = false; /** * Validate the configuration received from an external application. * * This method checks for the following parameters: * 1. {@link WifiConfiguration#SSID} * 2. {@link WifiConfiguration#BSSID} * 3. {@link WifiConfiguration#preSharedKey} * 4. {@link WifiConfiguration#allowedKeyManagement} * 5. {@link WifiConfiguration#allowedProtocols} * 6. {@link WifiConfiguration#allowedAuthAlgorithms} * 7. {@link WifiConfiguration#allowedGroupCiphers} * 8. {@link WifiConfiguration#allowedPairwiseCiphers} * 9. {@link WifiConfiguration#getIpConfiguration()} * * @param config {@link WifiConfiguration} received from an external application. * @param supportedFeatureSet bitmask for supported features using {@code WIFI_FEATURE_} * @param isAdd {@link #VALIDATE_FOR_ADD} to indicate a network config received for an add, * {@link #VALIDATE_FOR_UPDATE} for a network config received for an update. * These 2 cases need to be handled differently because the config received for an * update could contain only the fields that are being changed. * @return true if the parameters are valid, false otherwise. */ public static boolean validate(WifiConfiguration config, long supportedFeatureSet, boolean isAdd) { if (!validateSsid(config.SSID, isAdd)) { return false; } if (!validateBssid(config.BSSID)) { return false; } if (!validateBitSets(config)) { return false; } if (!validateKeyMgmt(config.allowedKeyManagement)) { return false; } if (config.isSecurityType(WifiConfiguration.SECURITY_TYPE_WEP) && config.wepKeys != null && !validateWepKeys(config.wepKeys, config.wepTxKeyIndex, isAdd)) { return false; } if (config.isSecurityType(WifiConfiguration.SECURITY_TYPE_PSK) && !validatePassword(config.preSharedKey, isAdd, false, false)) { return false; } if (config.isSecurityType(WifiConfiguration.SECURITY_TYPE_SAE) && !validatePassword(config.preSharedKey, isAdd, true, false)) { return false; } if (config.isSecurityType(WifiConfiguration.SECURITY_TYPE_WAPI_PSK) && !validatePassword(config.preSharedKey, isAdd, false, true)) { return false; } if (config.isSecurityType(WifiConfiguration.SECURITY_TYPE_DPP) && (supportedFeatureSet & WifiManager.WIFI_FEATURE_DPP_AKM) == 0) { Log.e(TAG, "DPP AKM is not supported"); return false; } if (!validateEnterpriseConfig(config, isAdd)) { return false; } // b/153435438: Added to deal with badly formed WifiConfiguration from apps. if (config.preSharedKey != null && !config.needsPreSharedKey()) { Log.e(TAG, "preSharedKey set with an invalid KeyMgmt, resetting KeyMgmt to WPA_PSK"); config.setSecurityParams(WifiConfiguration.SECURITY_TYPE_PSK); } if (!validateIpConfiguration(config.getIpConfiguration())) { return false; } // TBD: Validate some enterprise params as well in the future here. return true; } private static boolean validateBssidPattern( Pair bssidPatternMatcher) { if (bssidPatternMatcher == null) return true; MacAddress baseAddress = bssidPatternMatcher.first; MacAddress mask = bssidPatternMatcher.second; if (baseAddress.getAddressType() != MacAddress.TYPE_UNICAST) { Log.e(TAG, "validateBssidPatternMatcher failed : invalid base address: " + baseAddress); return false; } if (mask.equals(ALL_ZEROS_MAC_ADDRESS) && !baseAddress.equals(ALL_ZEROS_MAC_ADDRESS)) { Log.e(TAG, "validateBssidPatternMatcher failed : invalid mask/base: " + mask + "/" + baseAddress); return false; } return true; } // TODO(b/113878056): Some of this is duplicated in {@link WifiNetworkConfigBuilder}. // Merge them somehow?. private static boolean isValidNetworkSpecifier(WifiNetworkSpecifier specifier) { PatternMatcher ssidPatternMatcher = specifier.ssidPatternMatcher; Pair bssidPatternMatcher = specifier.bssidPatternMatcher; if (ssidPatternMatcher == null || bssidPatternMatcher == null) { return false; } if (ssidPatternMatcher.getPath() == null || bssidPatternMatcher.first == null || bssidPatternMatcher.second == null) { return false; } return true; } private static boolean isMatchNoneNetworkSpecifier(WifiNetworkSpecifier specifier) { PatternMatcher ssidPatternMatcher = specifier.ssidPatternMatcher; Pair bssidPatternMatcher = specifier.bssidPatternMatcher; if (ssidPatternMatcher.getType() != PatternMatcher.PATTERN_PREFIX && ssidPatternMatcher.getPath().equals(MATCH_EMPTY_SSID_PATTERN_PATH)) { return true; } if (bssidPatternMatcher.equals(MATCH_NONE_BSSID_PATTERN)) { return true; } return false; } /** * Check if the network specifier matches all networks. * @param specifier The network specifier * @return true if it matches all networks. */ public static boolean isMatchAllNetworkSpecifier(WifiNetworkSpecifier specifier) { PatternMatcher ssidPatternMatcher = specifier.ssidPatternMatcher; Pair bssidPatternMatcher = specifier.bssidPatternMatcher; if (ssidPatternMatcher.match(MATCH_EMPTY_SSID_PATTERN_PATH) && bssidPatternMatcher.equals(MATCH_ALL_BSSID_PATTERN)) { return true; } return false; } // TODO: b/177434707 calls inside same module are safe @SuppressLint("NewApi") private static int getBand(WifiNetworkSpecifier s) { return s.getBand(); } /** * Validate the configuration received from an external application inside * {@link WifiNetworkSpecifier}. * * This method checks for the following parameters: * 1. {@link WifiNetworkSpecifier#ssidPatternMatcher} * 2. {@link WifiNetworkSpecifier#bssidPatternMatcher} * 3. {@link WifiNetworkSpecifier#getBand()} * 4. {@link WifiConfiguration#SSID} * 5. {@link WifiConfiguration#BSSID} * 6. {@link WifiConfiguration#preSharedKey} * 7. {@link WifiConfiguration#allowedKeyManagement} * 8. {@link WifiConfiguration#allowedProtocols} * 9. {@link WifiConfiguration#allowedAuthAlgorithms} * 10. {@link WifiConfiguration#allowedGroupCiphers} * 11. {@link WifiConfiguration#allowedPairwiseCiphers} * 12. {@link WifiConfiguration#getIpConfiguration()} * * @param specifier Instance of {@link WifiNetworkSpecifier}. * @param maxChannelsAllowed The max number allowed to set in a WifiNetworkSpecifier * @return true if the parameters are valid, false otherwise. */ public static boolean validateNetworkSpecifier(WifiNetworkSpecifier specifier, int maxChannelsAllowed) { if (!isValidNetworkSpecifier(specifier)) { Log.e(TAG, "validateNetworkSpecifier failed : invalid network specifier"); return false; } if (isMatchNoneNetworkSpecifier(specifier)) { Log.e(TAG, "validateNetworkSpecifier failed : match-none specifier"); return false; } if (isMatchAllNetworkSpecifier(specifier)) { Log.e(TAG, "validateNetworkSpecifier failed : match-all specifier"); return false; } if (!WifiNetworkSpecifier.validateBand(getBand(specifier))) { return false; } if (specifier.getPreferredChannelFrequenciesMhz().length > maxChannelsAllowed) { return false; } WifiConfiguration config = specifier.wifiConfiguration; if (specifier.ssidPatternMatcher.getType() == PatternMatcher.PATTERN_LITERAL) { // For literal SSID matches, the value should satisfy SSID requirements. // WifiConfiguration.SSID needs quotes around ASCII SSID. if (!validateSsid(addEnclosingQuotes(specifier.ssidPatternMatcher.getPath()), true)) { return false; } } else { if (config.hiddenSSID) { Log.e(TAG, "validateNetworkSpecifier failed : ssid pattern not supported " + "for hidden networks"); return false; } } if (Objects.equals(specifier.bssidPatternMatcher.second, MacAddress.BROADCAST_ADDRESS)) { // For literal BSSID matches, the value should satisfy MAC address requirements. if (!validateBssid(specifier.bssidPatternMatcher.first)) { return false; } } else { if (!validateBssidPattern(specifier.bssidPatternMatcher)) { return false; } } if (!validateBitSets(config)) { return false; } if (!validateKeyMgmt(config.allowedKeyManagement)) { return false; } if (config.isSecurityType(WifiConfiguration.SECURITY_TYPE_PSK) && !validatePassword(config.preSharedKey, true, false, false)) { return false; } if (config.isSecurityType(WifiConfiguration.SECURITY_TYPE_SAE) && !validatePassword(config.preSharedKey, true, true, false)) { return false; } // TBD: Validate some enterprise params as well in the future here. return true; } /** * Check if the provided two networks are the same. * Note: This does not check if network selection BSSID's are the same. * * @param config Configuration corresponding to a network. * @param config1 Configuration corresponding to another network. * * @return true if |config| and |config1| are the same network. * false otherwise. */ public static boolean isSameNetwork(WifiConfiguration config, WifiConfiguration config1) { if (config == null && config1 == null) { return true; } if (config == null || config1 == null) { return false; } if (config.networkId != config1.networkId) { return false; } if (!Objects.equals(config.SSID, config1.SSID)) { return false; } if (!Objects.equals(config.getNetworkSelectionStatus().getCandidateSecurityParams(), config1.getNetworkSelectionStatus().getCandidateSecurityParams())) { return false; } if (WifiConfigurationUtil.hasCredentialChanged(config, config1)) { return false; } if (config.isWifi7Enabled() != config1.isWifi7Enabled()) { return false; } return true; } /** * Create a PnoNetwork object from the provided WifiConfiguration. * * @param config Configuration corresponding to the network. * @return PnoNetwork object corresponding to the network. */ public static WifiScanner.PnoSettings.PnoNetwork createPnoNetwork( WifiConfiguration config) { WifiScanner.PnoSettings.PnoNetwork pnoNetwork = new WifiScanner.PnoSettings.PnoNetwork(config.SSID); if (config.hiddenSSID) { pnoNetwork.flags |= WifiScanner.PnoSettings.PnoNetwork.FLAG_DIRECTED_SCAN; } pnoNetwork.flags |= WifiScanner.PnoSettings.PnoNetwork.FLAG_A_BAND; pnoNetwork.flags |= WifiScanner.PnoSettings.PnoNetwork.FLAG_G_BAND; if (config.isSecurityType(WifiConfiguration.SECURITY_TYPE_PSK)) { pnoNetwork.authBitField |= WifiScanner.PnoSettings.PnoNetwork.AUTH_CODE_PSK; } else if (config.isSecurityType(WifiConfiguration.SECURITY_TYPE_EAP)) { pnoNetwork.authBitField |= WifiScanner.PnoSettings.PnoNetwork.AUTH_CODE_EAPOL; } else { pnoNetwork.authBitField |= WifiScanner.PnoSettings.PnoNetwork.AUTH_CODE_OPEN; } return pnoNetwork; } private static void addOpenUpgradableSecurityTypeIfNecessary(WifiConfiguration config) { if (!config.isSecurityType(WifiConfiguration.SECURITY_TYPE_OPEN)) return; if (config.isSecurityType(WifiConfiguration.SECURITY_TYPE_OWE)) return; Log.d(TAG, "Add upgradable OWE configuration."); SecurityParams oweParams = SecurityParams.createSecurityParamsBySecurityType( WifiConfiguration.SECURITY_TYPE_OWE); oweParams.setIsAddedByAutoUpgrade(true); config.addSecurityParams(oweParams); } private static void addPskUpgradableSecurityTypeIfNecessary(WifiConfiguration config) { if (!config.isSecurityType(WifiConfiguration.SECURITY_TYPE_PSK)) return; if (config.isSecurityType(WifiConfiguration.SECURITY_TYPE_SAE)) return; Log.d(TAG, "Add upgradable SAE configuration."); SecurityParams saeParams = SecurityParams.createSecurityParamsBySecurityType( WifiConfiguration.SECURITY_TYPE_SAE); saeParams.setIsAddedByAutoUpgrade(true); config.addSecurityParams(saeParams); } private static void addEapUpgradableSecurityTypeIfNecessary(WifiConfiguration config) { if (!config.isSecurityType(WifiConfiguration.SECURITY_TYPE_EAP)) return; if (config.isSecurityType(WifiConfiguration.SECURITY_TYPE_EAP_WPA3_ENTERPRISE)) return; Log.d(TAG, "Add upgradable Enterprise configuration."); SecurityParams wpa3EnterpriseParams = SecurityParams.createSecurityParamsBySecurityType( WifiConfiguration.SECURITY_TYPE_EAP_WPA3_ENTERPRISE); wpa3EnterpriseParams.setIsAddedByAutoUpgrade(true); config.addSecurityParams(wpa3EnterpriseParams); } /** * Add upgradable securit type to the given wifi configuration. * * @param config the wifi configuration to be checked. */ public static boolean addUpgradableSecurityTypeIfNecessary(WifiConfiguration config) { try { addOpenUpgradableSecurityTypeIfNecessary(config); addPskUpgradableSecurityTypeIfNecessary(config); addEapUpgradableSecurityTypeIfNecessary(config); } catch (IllegalArgumentException e) { Log.e(TAG, "Failed to add upgradable security type"); return false; } return true; } /** * For a upgradable type which is added by the auto-upgrade mechenism, it is only * matched when corresponding auto-upgrade features are enabled. */ private static boolean shouldOmitAutoUpgradeParams(SecurityParams params) { if (!params.isAddedByAutoUpgrade()) return false; WifiGlobals wifiGlobals = WifiInjector.getInstance().getWifiGlobals(); if (params.isSecurityType(WifiConfiguration.SECURITY_TYPE_SAE)) { return !wifiGlobals.isWpa3SaeUpgradeEnabled(); } if (params.isSecurityType(WifiConfiguration.SECURITY_TYPE_OWE)) { return !wifiGlobals.isOweUpgradeEnabled(); } return false; } private static boolean isSecurityParamsSupported(SecurityParams params) { final long wifiFeatures = WifiInjector.getInstance() .getActiveModeWarden().getPrimaryClientModeManager() .getSupportedFeatures(); switch (params.getSecurityType()) { case WifiConfiguration.SECURITY_TYPE_SAE: return 0 != (wifiFeatures & WifiManager.WIFI_FEATURE_WPA3_SAE); case WifiConfiguration.SECURITY_TYPE_OWE: return 0 != (wifiFeatures & WifiManager.WIFI_FEATURE_OWE); } return true; } /** * Check the security params is valid or not. * @param params the requesting security params. * @return true if it's valid; otherwise false. */ public static boolean isSecurityParamsValid(SecurityParams params) { if (!params.isEnabled()) return false; if (!isSecurityParamsSupported(params)) return false; return true; } /** * General WifiConfiguration list sorting algorithm: * 1, Place the fully enabled networks first. * 2. Next place all the temporarily disabled networks. * 3. Place the permanently disabled networks last (Permanently disabled networks are removed * before WifiConfigManager uses this comparator today!). * * Among the networks with the same status, sort them in the order determined by the return of * {@link #compareNetworksWithSameStatus(WifiConfiguration, WifiConfiguration)} method * implementation. */ public abstract static class WifiConfigurationComparator implements Comparator { private static final int ENABLED_NETWORK_SCORE = 3; private static final int TEMPORARY_DISABLED_NETWORK_SCORE = 2; private static final int PERMANENTLY_DISABLED_NETWORK_SCORE = 1; @Override public int compare(WifiConfiguration a, WifiConfiguration b) { int configAScore = getNetworkStatusScore(a); int configBScore = getNetworkStatusScore(b); if (configAScore == configBScore) { return compareNetworksWithSameStatus(a, b); } else { return Integer.compare(configBScore, configAScore); } } // This needs to be implemented by the connected/disconnected PNO list comparator. abstract int compareNetworksWithSameStatus(WifiConfiguration a, WifiConfiguration b); /** * Returns an integer representing a score for each configuration. The scores are assigned * based on the status of the configuration. The scores are assigned according to the order: * Fully enabled network > Temporarily disabled network > Permanently disabled network. */ private int getNetworkStatusScore(WifiConfiguration config) { if (config.getNetworkSelectionStatus().isNetworkEnabled()) { return ENABLED_NETWORK_SCORE; } else if (config.getNetworkSelectionStatus().isNetworkTemporaryDisabled()) { return TEMPORARY_DISABLED_NETWORK_SCORE; } else { return PERMANENTLY_DISABLED_NETWORK_SCORE; } } } /** * Convert multi-type configurations to a list of configurations with a single security type, * where a configuration with multiple security configurations will be converted to multiple * Wi-Fi configurations with a single security type. * For R or older releases, Settings/WifiTrackerLib does not handle multiple configurations * with the same SSID and will result in duplicate saved networks. As a result, just return * the merged configs directly. * * @param configs the list of multi-type configurations. * @param ignoreDisabledType indicates whether or not disabled types should be ignored. * @return a list of Wi-Fi configurations with a single security type, * that may contain multiple configurations with the same network ID. */ public static List convertMultiTypeConfigsToLegacyConfigs( List configs, boolean ignoreDisabledType) { if (!SdkLevel.isAtLeastS()) { return configs; } List legacyConfigs = new ArrayList<>(); for (WifiConfiguration config : configs) { for (SecurityParams params: config.getSecurityParamsList()) { if (ignoreDisabledType && !params.isEnabled()) continue; if (shouldOmitAutoUpgradeParams(params)) continue; WifiConfiguration legacyConfig = new WifiConfiguration(config); legacyConfig.setSecurityParams(params); if (!params.isEnabled()) { legacyConfig.getNetworkSelectionStatus().setNetworkSelectionStatus( WifiConfiguration.NetworkSelectionStatus .NETWORK_SELECTION_PERMANENTLY_DISABLED); legacyConfig.getNetworkSelectionStatus().setNetworkSelectionDisableReason( WifiConfiguration.NetworkSelectionStatus .DISABLED_TRANSITION_DISABLE_INDICATION); legacyConfig.getNetworkSelectionStatus().setDisableReasonCounter( WifiConfiguration.NetworkSelectionStatus .DISABLED_TRANSITION_DISABLE_INDICATION, 1); } legacyConfigs.add(legacyConfig); } } return legacyConfigs; } private static boolean validateEnterpriseConfig(WifiConfiguration config, boolean isAdd) { if ((config.isSecurityType(WifiConfiguration.SECURITY_TYPE_EAP) || config.isSecurityType(WifiConfiguration.SECURITY_TYPE_EAP_WPA3_ENTERPRISE)) && !config.isEnterprise()) { return false; } if (config.isSecurityType(WifiConfiguration.SECURITY_TYPE_EAP_WPA3_ENTERPRISE_192_BIT) && (!config.isEnterprise() || config.enterpriseConfig.getEapMethod() != WifiEnterpriseConfig.Eap.TLS)) { return false; } if (config.isEnterprise()) { if (config.enterpriseConfig.getEapMethod() == WifiEnterpriseConfig.Eap.PEAP || config.enterpriseConfig.getEapMethod() == WifiEnterpriseConfig.Eap.TTLS) { int phase2Method = config.enterpriseConfig.getPhase2Method(); if (phase2Method == WifiEnterpriseConfig.Phase2.MSCHAP || phase2Method == WifiEnterpriseConfig.Phase2.MSCHAPV2 || phase2Method == WifiEnterpriseConfig.Phase2.PAP || phase2Method == WifiEnterpriseConfig.Phase2.GTC) { // Check the password on add only. When updating, the password may not be // available and it appears as "(Unchanged)" in Settings if ((isAdd && TextUtils.isEmpty(config.enterpriseConfig.getPassword())) || TextUtils.isEmpty(config.enterpriseConfig.getIdentity())) { Log.e(TAG, "Enterprise network without an identity or a password set"); return false; } } } } return true; } /** * Indicate that this configuration could be linked. * * @param config the configuartion to be checked. * @return true if it's linkable; otherwise false. */ public static boolean isConfigLinkable(WifiConfiguration config) { WifiGlobals wifiGlobals = WifiInjector.getInstance().getWifiGlobals(); if (config.isSecurityType(WifiConfiguration.SECURITY_TYPE_PSK) && config.getSecurityParams(WifiConfiguration.SECURITY_TYPE_PSK) .isEnabled()) { return true; } // If SAE offload is supported, link SAE type also. if (wifiGlobals.isWpa3SaeUpgradeOffloadEnabled()) { if (config.isSecurityType(WifiConfiguration.SECURITY_TYPE_SAE) && config.getSecurityParams(WifiConfiguration.SECURITY_TYPE_SAE) .isEnabled()) { return true; } } return false; } /** * Get the system trust store path which can be used when setting the CA path of an Enterprise * Wi-Fi connection {@link WifiEnterpriseConfig#setCaPath(String)} * * @return The system trust store path */ public static String getSystemTrustStorePath() { return SYSTEM_CA_STORE_PATH; } }