blob: 25ab4493b3bc6ec9c3a015379786c101a6e00cdd [file] [log] [blame]
/*
* 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 android.net.IpConfiguration.IpAssignment;
import android.net.IpConfiguration.ProxySettings;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiConfiguration.Status;
import android.net.wifi.WifiEnterpriseConfig;
import android.net.wifi.WifiSsid;
import android.net.wifi.WpsInfo;
import android.net.wifi.WpsResult;
import android.os.FileObserver;
import android.os.Process;
import android.security.Credentials;
import android.security.KeyChain;
import android.security.KeyStore;
import android.text.TextUtils;
import android.util.ArraySet;
import android.util.LocalLog;
import android.util.Log;
import android.util.SparseArray;
import com.android.server.wifi.hotspot2.Utils;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* This class provides the API's to save/load/modify network configurations from a persistent
* config database.
* We use wpa_supplicant as our config database currently, but will be migrating to a different
* one sometime in the future.
* We use keystore for certificate/key management operations.
*
* NOTE: This class should only be used from WifiConfigManager!!!
*/
public class WifiConfigStore {
public static final String TAG = "WifiConfigStore";
// This is the only variable whose contents will not be interpreted by wpa_supplicant. We use it
// to store metadata that allows us to correlate a wpa_supplicant.conf entry with additional
// information about the same network stored in other files. The metadata is stored as a
// serialized JSON dictionary.
public static final String ID_STRING_VAR_NAME = "id_str";
public static final String ID_STRING_KEY_FQDN = "fqdn";
public static final String ID_STRING_KEY_CREATOR_UID = "creatorUid";
public static final String ID_STRING_KEY_CONFIG_KEY = "configKey";
public static final String SUPPLICANT_CONFIG_FILE = "/data/misc/wifi/wpa_supplicant.conf";
public static final String SUPPLICANT_CONFIG_FILE_BACKUP = SUPPLICANT_CONFIG_FILE + ".tmp";
// Value stored by supplicant to requirePMF
public static final int STORED_VALUE_FOR_REQUIRE_PMF = 2;
private static final boolean DBG = true;
private static boolean VDBG = false;
private final LocalLog mLocalLog;
private final WpaConfigFileObserver mFileObserver;
private final WifiNative mWifiNative;
private final KeyStore mKeyStore;
private final boolean mShowNetworks;
private final HashSet<String> mBssidBlacklist = new HashSet<String>();
private final BackupManagerProxy mBackupManagerProxy;
WifiConfigStore(WifiNative wifiNative, KeyStore keyStore, LocalLog localLog,
boolean showNetworks, boolean verboseDebug) {
mWifiNative = wifiNative;
mKeyStore = keyStore;
mShowNetworks = showNetworks;
mBackupManagerProxy = new BackupManagerProxy();
if (mShowNetworks) {
mLocalLog = localLog;
mFileObserver = new WpaConfigFileObserver();
mFileObserver.startWatching();
} else {
mLocalLog = null;
mFileObserver = null;
}
VDBG = verboseDebug;
}
private static String removeDoubleQuotes(String string) {
int length = string.length();
if ((length > 1) && (string.charAt(0) == '"')
&& (string.charAt(length - 1) == '"')) {
return string.substring(1, length - 1);
}
return string;
}
/**
* Generate a string to be used as a key value by wpa_supplicant from
* 'set', within the set of strings from 'strings' for the variable concatenated.
* Also transform the internal string format that uses _ (for bewildering
* reasons) into a wpa_supplicant adjusted value, that uses - as a separator
* (most of the time at least...).
* @param set a bit set with a one for each corresponding string to be included from strings.
* @param strings the set of string literals to concatenate strinfs from.
* @return A wpa_supplicant formatted value.
*/
private static String makeString(BitSet set, String[] strings) {
return makeStringWithException(set, strings, null);
}
/**
* Same as makeString with an exclusion parameter.
* @param set a bit set with a one for each corresponding string to be included from strings.
* @param strings the set of string literals to concatenate strinfs from.
* @param exception literal string to be excluded from the _ to - transformation.
* @return A wpa_supplicant formatted value.
*/
private static String makeStringWithException(BitSet set, String[] strings, String exception) {
StringBuilder result = new StringBuilder();
/* Make sure all set bits are in [0, strings.length) to avoid
* going out of bounds on strings. (Shouldn't happen, but...) */
BitSet trimmedSet = set.get(0, strings.length);
List<String> valueSet = new ArrayList<>();
for (int bit = trimmedSet.nextSetBit(0);
bit >= 0;
bit = trimmedSet.nextSetBit(bit+1)) {
String currentName = strings[bit];
if (exception != null && currentName.equals(exception)) {
valueSet.add(currentName);
} else {
// Most wpa_supplicant strings use a dash whereas (for some bizarre
// reason) the strings are defined with underscore in the code...
valueSet.add(currentName.replace('_', '-'));
}
}
return TextUtils.join(" ", valueSet);
}
/*
* Convert string to Hexadecimal before passing to wifi native layer
* In native function "doCommand()" have trouble in converting Unicode character string to UTF8
* conversion to hex is required because SSIDs can have space characters in them;
* and that can confuses the supplicant because it uses space charaters as delimiters
*/
private static String encodeSSID(String str) {
return Utils.toHex(removeDoubleQuotes(str).getBytes(StandardCharsets.UTF_8));
}
// Certificate and private key management for EnterpriseConfig
private static boolean needsKeyStore(WifiEnterpriseConfig config) {
return (!(config.getClientCertificate() == null && config.getCaCertificate() == null));
}
private static boolean isHardwareBackedKey(PrivateKey key) {
return KeyChain.isBoundKeyAlgorithm(key.getAlgorithm());
}
private static boolean hasHardwareBackedKey(Certificate certificate) {
return KeyChain.isBoundKeyAlgorithm(certificate.getPublicKey().getAlgorithm());
}
private static boolean needsSoftwareBackedKeyStore(WifiEnterpriseConfig config) {
java.lang.String client = config.getClientCertificateAlias();
if (!TextUtils.isEmpty(client)) {
// a valid client certificate is configured
// BUGBUG: keyStore.get() never returns certBytes; because it is not
// taking WIFI_UID as a parameter. It always looks for certificate
// with SYSTEM_UID, and never finds any Wifi certificates. Assuming that
// all certificates need software keystore until we get the get() API
// fixed.
return true;
}
return false;
}
private int lookupString(String string, String[] strings) {
int size = strings.length;
string = string.replace('-', '_');
for (int i = 0; i < size; i++) {
if (string.equals(strings[i])) {
return i;
}
}
loge("Failed to look-up a string: " + string);
return -1;
}
private void readNetworkBitsetVariable(int netId, BitSet variable, String varName,
String[] strings) {
String value = mWifiNative.getNetworkVariable(netId, varName);
if (!TextUtils.isEmpty(value)) {
variable.clear();
String[] vals = value.split(" ");
for (String val : vals) {
int index = lookupString(val, strings);
if (0 <= index) {
variable.set(index);
}
}
}
}
/**
* Read the variables from the supplicant daemon that are needed to
* fill in the WifiConfiguration object.
*
* @param config the {@link WifiConfiguration} object to be filled in.
*/
public void readNetworkVariables(WifiConfiguration config) {
if (config == null) {
return;
}
if (VDBG) localLog("readNetworkVariables: " + config.networkId);
int netId = config.networkId;
if (netId < 0) {
return;
}
/*
* TODO: maybe should have a native method that takes an array of
* variable names and returns an array of values. But we'd still
* be doing a round trip to the supplicant daemon for each variable.
*/
String value;
value = mWifiNative.getNetworkVariable(netId, WifiConfiguration.ssidVarName);
if (!TextUtils.isEmpty(value)) {
if (value.charAt(0) != '"') {
config.SSID = "\"" + WifiSsid.createFromHex(value).toString() + "\"";
//TODO: convert a hex string that is not UTF-8 decodable to a P-formatted
//supplicant string
} else {
config.SSID = value;
}
} else {
config.SSID = null;
}
value = mWifiNative.getNetworkVariable(netId, WifiConfiguration.bssidVarName);
if (!TextUtils.isEmpty(value)) {
config.getNetworkSelectionStatus().setNetworkSelectionBSSID(value);
} else {
config.getNetworkSelectionStatus().setNetworkSelectionBSSID(null);
}
value = mWifiNative.getNetworkVariable(netId, WifiConfiguration.priorityVarName);
config.priority = -1;
if (!TextUtils.isEmpty(value)) {
try {
config.priority = Integer.parseInt(value);
} catch (NumberFormatException ignore) {
}
}
value = mWifiNative.getNetworkVariable(netId, WifiConfiguration.hiddenSSIDVarName);
config.hiddenSSID = false;
if (!TextUtils.isEmpty(value)) {
try {
config.hiddenSSID = Integer.parseInt(value) != 0;
} catch (NumberFormatException ignore) {
}
}
value = mWifiNative.getNetworkVariable(netId, WifiConfiguration.pmfVarName);
config.requirePMF = false;
if (!TextUtils.isEmpty(value)) {
try {
config.requirePMF = Integer.parseInt(value) == STORED_VALUE_FOR_REQUIRE_PMF;
} catch (NumberFormatException ignore) {
}
}
value = mWifiNative.getNetworkVariable(netId, WifiConfiguration.wepTxKeyIdxVarName);
config.wepTxKeyIndex = -1;
if (!TextUtils.isEmpty(value)) {
try {
config.wepTxKeyIndex = Integer.parseInt(value);
} catch (NumberFormatException ignore) {
}
}
for (int i = 0; i < 4; i++) {
value = mWifiNative.getNetworkVariable(netId,
WifiConfiguration.wepKeyVarNames[i]);
if (!TextUtils.isEmpty(value)) {
config.wepKeys[i] = value;
} else {
config.wepKeys[i] = null;
}
}
value = mWifiNative.getNetworkVariable(netId, WifiConfiguration.pskVarName);
if (!TextUtils.isEmpty(value)) {
config.preSharedKey = value;
} else {
config.preSharedKey = null;
}
readNetworkBitsetVariable(config.networkId, config.allowedProtocols,
WifiConfiguration.Protocol.varName, WifiConfiguration.Protocol.strings);
readNetworkBitsetVariable(config.networkId, config.allowedKeyManagement,
WifiConfiguration.KeyMgmt.varName, WifiConfiguration.KeyMgmt.strings);
readNetworkBitsetVariable(config.networkId, config.allowedAuthAlgorithms,
WifiConfiguration.AuthAlgorithm.varName, WifiConfiguration.AuthAlgorithm.strings);
readNetworkBitsetVariable(config.networkId, config.allowedPairwiseCiphers,
WifiConfiguration.PairwiseCipher.varName, WifiConfiguration.PairwiseCipher.strings);
readNetworkBitsetVariable(config.networkId, config.allowedGroupCiphers,
WifiConfiguration.GroupCipher.varName, WifiConfiguration.GroupCipher.strings);
if (config.enterpriseConfig == null) {
config.enterpriseConfig = new WifiEnterpriseConfig();
}
config.enterpriseConfig.loadFromSupplicant(new SupplicantLoader(netId));
}
/**
* Load all the configured networks from wpa_supplicant.
*
* @param configs Map of configuration key to configuration objects corresponding to all
* the networks.
* @param networkExtras Map of extra configuration parameters stored in wpa_supplicant.conf
* @return Max priority of all the configs.
*/
public int loadNetworks(Map<String, WifiConfiguration> configs,
SparseArray<Map<String, String>> networkExtras) {
int lastPriority = 0;
int last_id = -1;
boolean done = false;
while (!done) {
String listStr = mWifiNative.listNetworks(last_id);
if (listStr == null) {
return lastPriority;
}
String[] lines = listStr.split("\n");
if (mShowNetworks) {
localLog("loadNetworks: ");
for (String net : lines) {
localLog(net);
}
}
// Skip the first line, which is a header
for (int i = 1; i < lines.length; i++) {
String[] result = lines[i].split("\t");
// network-id | ssid | bssid | flags
WifiConfiguration config = new WifiConfiguration();
try {
config.networkId = Integer.parseInt(result[0]);
last_id = config.networkId;
} catch (NumberFormatException e) {
loge("Failed to read network-id '" + result[0] + "'");
continue;
}
// Ignore the supplicant status, start all networks disabled.
config.status = WifiConfiguration.Status.DISABLED;
readNetworkVariables(config);
// Parse the serialized JSON dictionary in ID_STRING_VAR_NAME once and cache the
// result for efficiency.
Map<String, String> extras = mWifiNative.getNetworkExtra(config.networkId,
ID_STRING_VAR_NAME);
if (extras == null) {
extras = new HashMap<String, String>();
// If ID_STRING_VAR_NAME did not contain a dictionary, assume that it contains
// just a quoted FQDN. This is the legacy format that was used in Marshmallow.
final String fqdn = Utils.unquote(mWifiNative.getNetworkVariable(
config.networkId, ID_STRING_VAR_NAME));
if (fqdn != null) {
extras.put(ID_STRING_KEY_FQDN, fqdn);
config.FQDN = fqdn;
// Mark the configuration as a Hotspot 2.0 network.
config.providerFriendlyName = "";
}
}
networkExtras.put(config.networkId, extras);
if (config.priority > lastPriority) {
lastPriority = config.priority;
}
config.setIpAssignment(IpAssignment.DHCP);
config.setProxySettings(ProxySettings.NONE);
if (!WifiServiceImpl.isValid(config)) {
if (mShowNetworks) {
localLog("Ignoring network " + config.networkId + " because configuration "
+ "loaded from wpa_supplicant.conf is not valid.");
}
continue;
}
// The configKey is explicitly stored in wpa_supplicant.conf, because config does
// not contain sufficient information to compute it at this point.
String configKey = extras.get(ID_STRING_KEY_CONFIG_KEY);
if (configKey == null) {
// Handle the legacy case where the configKey is not stored in
// wpa_supplicant.conf but can be computed straight away.
// Force an update of this legacy network configuration by writing
// the configKey for this network into wpa_supplicant.conf.
configKey = config.configKey();
saveNetworkMetadata(config);
}
final WifiConfiguration duplicateConfig = configs.put(configKey, config);
if (duplicateConfig != null) {
// The network is already known. Overwrite the duplicate entry.
if (mShowNetworks) {
localLog("Replacing duplicate network " + duplicateConfig.networkId
+ " with " + config.networkId + ".");
}
// This can happen after the user manually connected to an AP and tried to use
// WPS to connect the AP later. In this case, the supplicant will create a new
// network for the AP although there is an existing network already.
mWifiNative.removeNetwork(duplicateConfig.networkId);
}
}
done = (lines.length == 1);
}
return lastPriority;
}
/**
* Install keys for given enterprise network.
*
* @param existingConfig Existing config corresponding to the network already stored in our
* database. This maybe null if it's a new network.
* @param config Config corresponding to the network.
* @return true if successful, false otherwise.
*/
private boolean installKeys(WifiEnterpriseConfig existingConfig, WifiEnterpriseConfig config,
String name) {
boolean ret = true;
String privKeyName = Credentials.USER_PRIVATE_KEY + name;
String userCertName = Credentials.USER_CERTIFICATE + name;
if (config.getClientCertificate() != null) {
byte[] privKeyData = config.getClientPrivateKey().getEncoded();
if (DBG) {
if (isHardwareBackedKey(config.getClientPrivateKey())) {
Log.d(TAG, "importing keys " + name + " in hardware backed store");
} else {
Log.d(TAG, "importing keys " + name + " in software backed store");
}
}
ret = mKeyStore.importKey(privKeyName, privKeyData, Process.WIFI_UID,
KeyStore.FLAG_NONE);
if (!ret) {
return ret;
}
ret = putCertInKeyStore(userCertName, config.getClientCertificate());
if (!ret) {
// Remove private key installed
mKeyStore.delete(privKeyName, Process.WIFI_UID);
return ret;
}
}
X509Certificate[] caCertificates = config.getCaCertificates();
Set<String> oldCaCertificatesToRemove = new ArraySet<String>();
if (existingConfig != null && existingConfig.getCaCertificateAliases() != null) {
oldCaCertificatesToRemove.addAll(
Arrays.asList(existingConfig.getCaCertificateAliases()));
}
List<String> caCertificateAliases = null;
if (caCertificates != null) {
caCertificateAliases = new ArrayList<String>();
for (int i = 0; i < caCertificates.length; i++) {
String alias = caCertificates.length == 1 ? name
: String.format("%s_%d", name, i);
oldCaCertificatesToRemove.remove(alias);
ret = putCertInKeyStore(Credentials.CA_CERTIFICATE + alias, caCertificates[i]);
if (!ret) {
// Remove client key+cert
if (config.getClientCertificate() != null) {
mKeyStore.delete(privKeyName, Process.WIFI_UID);
mKeyStore.delete(userCertName, Process.WIFI_UID);
}
// Remove added CA certs.
for (String addedAlias : caCertificateAliases) {
mKeyStore.delete(Credentials.CA_CERTIFICATE + addedAlias, Process.WIFI_UID);
}
return ret;
} else {
caCertificateAliases.add(alias);
}
}
}
// Remove old CA certs.
for (String oldAlias : oldCaCertificatesToRemove) {
mKeyStore.delete(Credentials.CA_CERTIFICATE + oldAlias, Process.WIFI_UID);
}
// Set alias names
if (config.getClientCertificate() != null) {
config.setClientCertificateAlias(name);
config.resetClientKeyEntry();
}
if (caCertificates != null) {
config.setCaCertificateAliases(
caCertificateAliases.toArray(new String[caCertificateAliases.size()]));
config.resetCaCertificate();
}
return ret;
}
private boolean putCertInKeyStore(String name, Certificate cert) {
try {
byte[] certData = Credentials.convertToPem(cert);
if (DBG) Log.d(TAG, "putting certificate " + name + " in keystore");
return mKeyStore.put(name, certData, Process.WIFI_UID, KeyStore.FLAG_NONE);
} catch (IOException e1) {
return false;
} catch (CertificateException e2) {
return false;
}
}
/**
* Remove enterprise keys from the network config.
*
* @param config Config corresponding to the network.
*/
private void removeKeys(WifiEnterpriseConfig config) {
String client = config.getClientCertificateAlias();
// a valid client certificate is configured
if (!TextUtils.isEmpty(client)) {
if (DBG) Log.d(TAG, "removing client private key and user cert");
mKeyStore.delete(Credentials.USER_PRIVATE_KEY + client, Process.WIFI_UID);
mKeyStore.delete(Credentials.USER_CERTIFICATE + client, Process.WIFI_UID);
}
String[] aliases = config.getCaCertificateAliases();
// a valid ca certificate is configured
if (aliases != null) {
for (String ca : aliases) {
if (!TextUtils.isEmpty(ca)) {
if (DBG) Log.d(TAG, "removing CA cert: " + ca);
mKeyStore.delete(Credentials.CA_CERTIFICATE + ca, Process.WIFI_UID);
}
}
}
}
/**
* Update the network metadata info stored in wpa_supplicant network extra field.
* @param config Config corresponding to the network.
* @return true if successful, false otherwise.
*/
public boolean saveNetworkMetadata(WifiConfiguration config) {
final Map<String, String> metadata = new HashMap<String, String>();
if (config.isPasspoint()) {
metadata.put(ID_STRING_KEY_FQDN, config.FQDN);
}
metadata.put(ID_STRING_KEY_CONFIG_KEY, config.configKey());
metadata.put(ID_STRING_KEY_CREATOR_UID, Integer.toString(config.creatorUid));
if (!mWifiNative.setNetworkExtra(config.networkId, ID_STRING_VAR_NAME, metadata)) {
loge("failed to set id_str: " + metadata.toString());
return false;
}
return true;
}
/**
* Save an entire network configuration to wpa_supplicant.
*
* @param config Config corresponding to the network.
* @param netId Net Id of the network.
* @return true if successful, false otherwise.
*/
private boolean saveNetwork(WifiConfiguration config, int netId) {
if (config == null) {
return false;
}
if (VDBG) localLog("saveNetwork: " + netId);
if (config.SSID != null && !mWifiNative.setNetworkVariable(
netId,
WifiConfiguration.ssidVarName,
encodeSSID(config.SSID))) {
loge("failed to set SSID: " + config.SSID);
return false;
}
if (!saveNetworkMetadata(config)) {
return false;
}
//set selected BSSID to supplicant
if (config.getNetworkSelectionStatus().getNetworkSelectionBSSID() != null) {
String bssid = config.getNetworkSelectionStatus().getNetworkSelectionBSSID();
if (!mWifiNative.setNetworkVariable(netId, WifiConfiguration.bssidVarName, bssid)) {
loge("failed to set BSSID: " + bssid);
return false;
}
}
String allowedKeyManagementString =
makeString(config.allowedKeyManagement, WifiConfiguration.KeyMgmt.strings);
if (config.allowedKeyManagement.cardinality() != 0 && !mWifiNative.setNetworkVariable(
netId,
WifiConfiguration.KeyMgmt.varName,
allowedKeyManagementString)) {
loge("failed to set key_mgmt: " + allowedKeyManagementString);
return false;
}
String allowedProtocolsString =
makeString(config.allowedProtocols, WifiConfiguration.Protocol.strings);
if (config.allowedProtocols.cardinality() != 0 && !mWifiNative.setNetworkVariable(
netId,
WifiConfiguration.Protocol.varName,
allowedProtocolsString)) {
loge("failed to set proto: " + allowedProtocolsString);
return false;
}
String allowedAuthAlgorithmsString =
makeString(config.allowedAuthAlgorithms,
WifiConfiguration.AuthAlgorithm.strings);
if (config.allowedAuthAlgorithms.cardinality() != 0 && !mWifiNative.setNetworkVariable(
netId,
WifiConfiguration.AuthAlgorithm.varName,
allowedAuthAlgorithmsString)) {
loge("failed to set auth_alg: " + allowedAuthAlgorithmsString);
return false;
}
String allowedPairwiseCiphersString = makeString(config.allowedPairwiseCiphers,
WifiConfiguration.PairwiseCipher.strings);
if (config.allowedPairwiseCiphers.cardinality() != 0 && !mWifiNative.setNetworkVariable(
netId,
WifiConfiguration.PairwiseCipher.varName,
allowedPairwiseCiphersString)) {
loge("failed to set pairwise: " + allowedPairwiseCiphersString);
return false;
}
// Make sure that the string "GTK_NOT_USED" is /not/ transformed - wpa_supplicant
// uses this literal value and not the 'dashed' version.
String allowedGroupCiphersString =
makeStringWithException(config.allowedGroupCiphers,
WifiConfiguration.GroupCipher.strings,
WifiConfiguration.GroupCipher
.strings[WifiConfiguration.GroupCipher.GTK_NOT_USED]);
if (config.allowedGroupCiphers.cardinality() != 0 && !mWifiNative.setNetworkVariable(
netId,
WifiConfiguration.GroupCipher.varName,
allowedGroupCiphersString)) {
loge("failed to set group: " + allowedGroupCiphersString);
return false;
}
// Prevent client screw-up by passing in a WifiConfiguration we gave it
// by preventing "*" as a key.
if (config.preSharedKey != null && !config.preSharedKey.equals("*")
&& !mWifiNative.setNetworkVariable(
netId,
WifiConfiguration.pskVarName,
config.preSharedKey)) {
loge("failed to set psk");
return false;
}
boolean hasSetKey = false;
if (config.wepKeys != null) {
for (int i = 0; i < config.wepKeys.length; i++) {
// Prevent client screw-up by passing in a WifiConfiguration we gave it
// by preventing "*" as a key.
if (config.wepKeys[i] != null && !config.wepKeys[i].equals("*")) {
if (!mWifiNative.setNetworkVariable(
netId,
WifiConfiguration.wepKeyVarNames[i],
config.wepKeys[i])) {
loge("failed to set wep_key" + i + ": " + config.wepKeys[i]);
return false;
}
hasSetKey = true;
}
}
}
if (hasSetKey) {
if (!mWifiNative.setNetworkVariable(
netId,
WifiConfiguration.wepTxKeyIdxVarName,
Integer.toString(config.wepTxKeyIndex))) {
loge("failed to set wep_tx_keyidx: " + config.wepTxKeyIndex);
return false;
}
}
if (!mWifiNative.setNetworkVariable(
netId,
WifiConfiguration.priorityVarName,
Integer.toString(config.priority))) {
loge(config.SSID + ": failed to set priority: " + config.priority);
return false;
}
if (config.hiddenSSID && !mWifiNative.setNetworkVariable(
netId,
WifiConfiguration.hiddenSSIDVarName,
Integer.toString(config.hiddenSSID ? 1 : 0))) {
loge(config.SSID + ": failed to set hiddenSSID: " + config.hiddenSSID);
return false;
}
if (config.requirePMF && !mWifiNative.setNetworkVariable(
netId,
WifiConfiguration.pmfVarName,
Integer.toString(STORED_VALUE_FOR_REQUIRE_PMF))) {
loge(config.SSID + ": failed to set requirePMF: " + config.requirePMF);
return false;
}
if (config.updateIdentifier != null && !mWifiNative.setNetworkVariable(
netId,
WifiConfiguration.updateIdentiferVarName,
config.updateIdentifier)) {
loge(config.SSID + ": failed to set updateIdentifier: " + config.updateIdentifier);
return false;
}
return true;
}
/**
* Update/Install keys for given enterprise network.
*
* @param config Config corresponding to the network.
* @param existingConfig Existing config corresponding to the network already stored in our
* database. This maybe null if it's a new network.
* @return true if successful, false otherwise.
*/
private boolean updateNetworkKeys(WifiConfiguration config, WifiConfiguration existingConfig) {
WifiEnterpriseConfig enterpriseConfig = config.enterpriseConfig;
if (needsKeyStore(enterpriseConfig)) {
try {
/* config passed may include only fields being updated.
* In order to generate the key id, fetch uninitialized
* fields from the currently tracked configuration
*/
String keyId = config.getKeyIdForCredentials(existingConfig);
if (!installKeys(existingConfig != null
? existingConfig.enterpriseConfig : null, enterpriseConfig, keyId)) {
loge(config.SSID + ": failed to install keys");
return false;
}
} catch (IllegalStateException e) {
loge(config.SSID + " invalid config for key installation: " + e.getMessage());
return false;
}
}
if (!enterpriseConfig.saveToSupplicant(
new SupplicantSaver(config.networkId, config.SSID))) {
removeKeys(enterpriseConfig);
return false;
}
return true;
}
/**
* Add or update a network configuration to wpa_supplicant.
*
* @param config Config corresponding to the network.
* @param existingConfig Existing config corresponding to the network saved in our database.
* @return true if successful, false otherwise.
*/
public boolean addOrUpdateNetwork(WifiConfiguration config, WifiConfiguration existingConfig) {
if (config == null) {
return false;
}
if (VDBG) localLog("addOrUpdateNetwork: " + config.networkId);
int netId = config.networkId;
boolean newNetwork = false;
/*
* If the supplied networkId is INVALID_NETWORK_ID, we create a new empty
* network configuration. Otherwise, the networkId should
* refer to an existing configuration.
*/
if (netId == WifiConfiguration.INVALID_NETWORK_ID) {
newNetwork = true;
netId = mWifiNative.addNetwork();
if (netId < 0) {
loge("Failed to add a network!");
return false;
} else {
logi("addOrUpdateNetwork created netId=" + netId);
}
// Save the new network ID to the config
config.networkId = netId;
}
if (!saveNetwork(config, netId)) {
if (newNetwork) {
mWifiNative.removeNetwork(netId);
loge("Failed to set a network variable, removed network: " + netId);
}
return false;
}
if (config.enterpriseConfig != null
&& config.enterpriseConfig.getEapMethod() != WifiEnterpriseConfig.Eap.NONE) {
return updateNetworkKeys(config, existingConfig);
}
// Stage the backup of the SettingsProvider package which backs this up
mBackupManagerProxy.notifyDataChanged();
return true;
}
/**
* Remove the specified network and save config
*
* @param config Config corresponding to the network.
* @return {@code true} if it succeeds, {@code false} otherwise
*/
public boolean removeNetwork(WifiConfiguration config) {
if (config == null) {
return false;
}
if (VDBG) localLog("removeNetwork: " + config.networkId);
if (!mWifiNative.removeNetwork(config.networkId)) {
loge("Remove network in wpa_supplicant failed on " + config.networkId);
return false;
}
// Remove any associated keys
if (config.enterpriseConfig != null) {
removeKeys(config.enterpriseConfig);
}
// Stage the backup of the SettingsProvider package which backs this up
mBackupManagerProxy.notifyDataChanged();
return true;
}
/**
* Select a network in wpa_supplicant.
*
* @param config Config corresponding to the network.
* @return true if successful, false otherwise.
*/
public boolean selectNetwork(WifiConfiguration config, Collection<WifiConfiguration> configs) {
if (config == null) {
return false;
}
if (VDBG) localLog("selectNetwork: " + config.networkId);
if (!mWifiNative.selectNetwork(config.networkId)) {
loge("Select network in wpa_supplicant failed on " + config.networkId);
return false;
}
config.status = Status.ENABLED;
markAllNetworksDisabledExcept(config.networkId, configs);
return true;
}
/**
* Disable a network in wpa_supplicant.
*
* @param config Config corresponding to the network.
* @return true if successful, false otherwise.
*/
boolean disableNetwork(WifiConfiguration config) {
if (config == null) {
return false;
}
if (VDBG) localLog("disableNetwork: " + config.networkId);
if (!mWifiNative.disableNetwork(config.networkId)) {
loge("Disable network in wpa_supplicant failed on " + config.networkId);
return false;
}
config.status = Status.DISABLED;
return true;
}
/**
* Set priority for a network in wpa_supplicant.
*
* @param config Config corresponding to the network.
* @return true if successful, false otherwise.
*/
public boolean setNetworkPriority(WifiConfiguration config, int priority) {
if (config == null) {
return false;
}
if (VDBG) localLog("setNetworkPriority: " + config.networkId);
if (!mWifiNative.setNetworkVariable(config.networkId,
WifiConfiguration.priorityVarName, Integer.toString(priority))) {
loge("Set priority of network in wpa_supplicant failed on " + config.networkId);
return false;
}
config.priority = priority;
return true;
}
/**
* Set SSID for a network in wpa_supplicant.
*
* @param config Config corresponding to the network.
* @return true if successful, false otherwise.
*/
public boolean setNetworkSSID(WifiConfiguration config, String ssid) {
if (config == null) {
return false;
}
if (VDBG) localLog("setNetworkSSID: " + config.networkId);
if (!mWifiNative.setNetworkVariable(config.networkId, WifiConfiguration.ssidVarName,
encodeSSID(ssid))) {
loge("Set SSID of network in wpa_supplicant failed on " + config.networkId);
return false;
}
config.SSID = ssid;
return true;
}
/**
* Set BSSID for a network in wpa_supplicant from network selection.
*
* @param config Config corresponding to the network.
* @param bssid BSSID to be set.
* @return true if successful, false otherwise.
*/
public boolean setNetworkBSSID(WifiConfiguration config, String bssid) {
// Sanity check the config is valid
if (config == null
|| (config.networkId == WifiConfiguration.INVALID_NETWORK_ID
&& config.SSID == null)) {
return false;
}
if (VDBG) localLog("setNetworkBSSID: " + config.networkId);
if (!mWifiNative.setNetworkVariable(config.networkId, WifiConfiguration.bssidVarName,
bssid)) {
loge("Set BSSID of network in wpa_supplicant failed on " + config.networkId);
return false;
}
config.getNetworkSelectionStatus().setNetworkSelectionBSSID(bssid);
return true;
}
/**
* Enable/Disable HS20 parameter in wpa_supplicant.
*
* @param enable Enable/Disable the parameter.
*/
public void enableHS20(boolean enable) {
mWifiNative.setHs20(enable);
}
/**
* Disables all the networks in the provided list in wpa_supplicant.
*
* @param configs Collection of configs which needs to be enabled.
* @return true if successful, false otherwise.
*/
public boolean disableAllNetworks(Collection<WifiConfiguration> configs) {
if (VDBG) localLog("disableAllNetworks");
boolean networkDisabled = false;
for (WifiConfiguration enabled : configs) {
if (disableNetwork(enabled)) {
networkDisabled = true;
}
}
saveConfig();
return networkDisabled;
}
/**
* Save the current configuration to wpa_supplicant.conf.
*/
public boolean saveConfig() {
return mWifiNative.saveConfig();
}
/**
* Read network variables from wpa_supplicant.conf.
*
* @param key The parameter to be parsed.
* @return Map of corresponding configKey to the value of the param requested.
*/
public Map<String, String> readNetworkVariablesFromSupplicantFile(String key) {
Map<String, String> result = new HashMap<>();
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader(SUPPLICANT_CONFIG_FILE));
result = readNetworkVariablesFromReader(reader, key);
} catch (FileNotFoundException e) {
if (VDBG) loge("Could not open " + SUPPLICANT_CONFIG_FILE + ", " + e);
} catch (IOException e) {
if (VDBG) loge("Could not read " + SUPPLICANT_CONFIG_FILE + ", " + e);
} finally {
try {
if (reader != null) {
reader.close();
}
} catch (IOException e) {
if (VDBG) {
loge("Could not close reader for " + SUPPLICANT_CONFIG_FILE + ", " + e);
}
}
}
return result;
}
/**
* Read network variables from a given reader. This method is separate from
* readNetworkVariablesFromSupplicantFile() for testing.
*
* @param reader The reader to read the network variables from.
* @param key The parameter to be parsed.
* @return Map of corresponding configKey to the value of the param requested.
*/
public Map<String, String> readNetworkVariablesFromReader(BufferedReader reader, String key)
throws IOException {
Map<String, String> result = new HashMap<>();
if (VDBG) localLog("readNetworkVariablesFromReader key=" + key);
boolean found = false;
String configKey = null;
String value = null;
for (String line = reader.readLine(); line != null; line = reader.readLine()) {
if (line.matches("[ \\t]*network=\\{")) {
found = true;
configKey = null;
value = null;
} else if (line.matches("[ \\t]*\\}")) {
found = false;
configKey = null;
value = null;
}
if (found) {
String trimmedLine = line.trim();
if (trimmedLine.startsWith(ID_STRING_VAR_NAME + "=")) {
try {
// Trim the quotes wrapping the id_str value.
final String encodedExtras = trimmedLine.substring(
8, trimmedLine.length() -1);
final JSONObject json =
new JSONObject(URLDecoder.decode(encodedExtras, "UTF-8"));
if (json.has(WifiConfigStore.ID_STRING_KEY_CONFIG_KEY)) {
final Object configKeyFromJson =
json.get(WifiConfigStore.ID_STRING_KEY_CONFIG_KEY);
if (configKeyFromJson instanceof String) {
configKey = (String) configKeyFromJson;
}
}
} catch (JSONException e) {
if (VDBG) {
loge("Could not get "+ WifiConfigStore.ID_STRING_KEY_CONFIG_KEY
+ ", " + e);
}
}
}
if (trimmedLine.startsWith(key + "=")) {
value = trimmedLine.substring(key.length() + 1);
}
if (configKey != null && value != null) {
result.put(configKey, value);
}
}
}
return result;
}
/**
* Checks if the network is a sim config.
*
* @param config Config corresponding to the network.
* @return true if it is a sim config, false otherwise.
*/
public boolean isSimConfig(WifiConfiguration config) {
if (config == null) {
return false;
}
if (config.enterpriseConfig == null) {
return false;
}
int method = config.enterpriseConfig.getEapMethod();
return (method == WifiEnterpriseConfig.Eap.SIM
|| method == WifiEnterpriseConfig.Eap.AKA
|| method == WifiEnterpriseConfig.Eap.AKA_PRIME);
}
/**
* Resets all sim networks from the provided network list.
*
* @param configs List of all the networks.
*/
public void resetSimNetworks(Collection<WifiConfiguration> configs) {
if (VDBG) localLog("resetSimNetworks");
for (WifiConfiguration config : configs) {
if (isSimConfig(config)) {
/* This configuration may have cached Pseudonym IDs; lets remove them */
mWifiNative.setNetworkVariable(config.networkId, "identity", "NULL");
mWifiNative.setNetworkVariable(config.networkId, "anonymous_identity", "NULL");
}
}
}
/**
* Clear BSSID blacklist in wpa_supplicant.
*/
public void clearBssidBlacklist() {
if (VDBG) localLog("clearBlacklist");
mBssidBlacklist.clear();
mWifiNative.clearBlacklist();
mWifiNative.setBssidBlacklist(null);
}
/**
* Add a BSSID to the blacklist.
*
* @param bssid bssid to be added.
*/
public void blackListBssid(String bssid) {
if (bssid == null) {
return;
}
if (VDBG) localLog("blackListBssid: " + bssid);
mBssidBlacklist.add(bssid);
// Blacklist at wpa_supplicant
mWifiNative.addToBlacklist(bssid);
// Blacklist at firmware
String[] list = mBssidBlacklist.toArray(new String[mBssidBlacklist.size()]);
mWifiNative.setBssidBlacklist(list);
}
/**
* Checks if the provided bssid is blacklisted or not.
*
* @param bssid bssid to be checked.
* @return true if present, false otherwise.
*/
public boolean isBssidBlacklisted(String bssid) {
return mBssidBlacklist.contains(bssid);
}
/* Mark all networks except specified netId as disabled */
private void markAllNetworksDisabledExcept(int netId, Collection<WifiConfiguration> configs) {
for (WifiConfiguration config : configs) {
if (config != null && config.networkId != netId) {
if (config.status != Status.DISABLED) {
config.status = Status.DISABLED;
}
}
}
}
private void markAllNetworksDisabled(Collection<WifiConfiguration> configs) {
markAllNetworksDisabledExcept(WifiConfiguration.INVALID_NETWORK_ID, configs);
}
/**
* Start WPS pin method configuration with pin obtained
* from the access point
*
* @param config WPS configuration
* @return Wps result containing status and pin
*/
public WpsResult startWpsWithPinFromAccessPoint(WpsInfo config,
Collection<WifiConfiguration> configs) {
WpsResult result = new WpsResult();
if (mWifiNative.startWpsRegistrar(config.BSSID, config.pin)) {
/* WPS leaves all networks disabled */
markAllNetworksDisabled(configs);
result.status = WpsResult.Status.SUCCESS;
} else {
loge("Failed to start WPS pin method configuration");
result.status = WpsResult.Status.FAILURE;
}
return result;
}
/**
* Start WPS pin method configuration with obtained
* from the device
*
* @return WpsResult indicating status and pin
*/
public WpsResult startWpsWithPinFromDevice(WpsInfo config,
Collection<WifiConfiguration> configs) {
WpsResult result = new WpsResult();
result.pin = mWifiNative.startWpsPinDisplay(config.BSSID);
/* WPS leaves all networks disabled */
if (!TextUtils.isEmpty(result.pin)) {
markAllNetworksDisabled(configs);
result.status = WpsResult.Status.SUCCESS;
} else {
loge("Failed to start WPS pin method configuration");
result.status = WpsResult.Status.FAILURE;
}
return result;
}
/**
* Start WPS push button configuration
*
* @param config WPS configuration
* @return WpsResult indicating status and pin
*/
public WpsResult startWpsPbc(WpsInfo config,
Collection<WifiConfiguration> configs) {
WpsResult result = new WpsResult();
if (mWifiNative.startWpsPbc(config.BSSID)) {
/* WPS leaves all networks disabled */
markAllNetworksDisabled(configs);
result.status = WpsResult.Status.SUCCESS;
} else {
loge("Failed to start WPS push button configuration");
result.status = WpsResult.Status.FAILURE;
}
return result;
}
protected void logd(String s) {
Log.d(TAG, s);
}
protected void logi(String s) {
Log.i(TAG, s);
}
protected void loge(String s) {
loge(s, false);
}
protected void loge(String s, boolean stack) {
if (stack) {
Log.e(TAG, s + " stack:" + Thread.currentThread().getStackTrace()[2].getMethodName()
+ " - " + Thread.currentThread().getStackTrace()[3].getMethodName()
+ " - " + Thread.currentThread().getStackTrace()[4].getMethodName()
+ " - " + Thread.currentThread().getStackTrace()[5].getMethodName());
} else {
Log.e(TAG, s);
}
}
protected void log(String s) {
Log.d(TAG, s);
}
private void localLog(String s) {
if (mLocalLog != null) {
mLocalLog.log(TAG + ": " + s);
}
}
private void localLogAndLogcat(String s) {
localLog(s);
Log.d(TAG, s);
}
private class SupplicantSaver implements WifiEnterpriseConfig.SupplicantSaver {
private final int mNetId;
private final String mSetterSSID;
SupplicantSaver(int netId, String setterSSID) {
mNetId = netId;
mSetterSSID = setterSSID;
}
@Override
public boolean saveValue(String key, String value) {
if (key.equals(WifiEnterpriseConfig.PASSWORD_KEY)
&& value != null && value.equals("*")) {
// No need to try to set an obfuscated password, which will fail
return true;
}
if (key.equals(WifiEnterpriseConfig.REALM_KEY)
|| key.equals(WifiEnterpriseConfig.PLMN_KEY)) {
// No need to save realm or PLMN in supplicant
return true;
}
// TODO: We need a way to clear values in wpa_supplicant as opposed to
// mapping unset values to empty strings.
if (value == null) {
value = "\"\"";
}
if (!mWifiNative.setNetworkVariable(mNetId, key, value)) {
loge(mSetterSSID + ": failed to set " + key + ": " + value);
return false;
}
return true;
}
}
private class SupplicantLoader implements WifiEnterpriseConfig.SupplicantLoader {
private final int mNetId;
SupplicantLoader(int netId) {
mNetId = netId;
}
@Override
public String loadValue(String key) {
String value = mWifiNative.getNetworkVariable(mNetId, key);
if (!TextUtils.isEmpty(value)) {
if (!enterpriseConfigKeyShouldBeQuoted(key)) {
value = removeDoubleQuotes(value);
}
return value;
} else {
return null;
}
}
/**
* Returns true if a particular config key needs to be quoted when passed to the supplicant.
*/
private boolean enterpriseConfigKeyShouldBeQuoted(String key) {
switch (key) {
case WifiEnterpriseConfig.EAP_KEY:
case WifiEnterpriseConfig.ENGINE_KEY:
return false;
default:
return true;
}
}
}
// TODO(rpius): Remove this.
private class WpaConfigFileObserver extends FileObserver {
WpaConfigFileObserver() {
super(SUPPLICANT_CONFIG_FILE, CLOSE_WRITE);
}
@Override
public void onEvent(int event, String path) {
if (event == CLOSE_WRITE) {
File file = new File(SUPPLICANT_CONFIG_FILE);
if (VDBG) localLog("wpa_supplicant.conf changed; new size = " + file.length());
}
}
}
}