303 lines
12 KiB
Java
303 lines
12 KiB
Java
/*
|
|
* Copyright (C) 2024 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 android.net.wifi;
|
|
|
|
import android.annotation.FlaggedApi;
|
|
import android.annotation.NonNull;
|
|
import android.annotation.SystemApi;
|
|
import android.text.TextUtils;
|
|
import android.util.Log;
|
|
|
|
import androidx.annotation.VisibleForTesting;
|
|
|
|
import com.android.wifi.flags.Flags;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.List;
|
|
import java.util.regex.Pattern;
|
|
|
|
/**
|
|
* Supports to parse 2 types of Wifi Uri
|
|
*
|
|
* <p>1. Standard Wi-Fi Easy Connect (DPP) bootstrapping information or 2. ZXing reader library's
|
|
* Wi-Fi Network config format described in
|
|
* https://github.com/zxing/zxing/wiki/Barcode-Contents#wi-fi-network-config-android-ios-11
|
|
*
|
|
* <p>ZXing reader library's Wi-Fi Network config format example:
|
|
*
|
|
* <p>WIFI:T:WPA;S:mynetwork;P:mypass;;
|
|
*
|
|
* <p>parameter Example Description T WPA Authentication type; can be WEP, WPA, SAE or 'nopass' for
|
|
* no password. Or, omit for no password. S mynetwork Network SSID. Required. Enclose in double
|
|
* quotes if it is an ASCII name, but could be interpreted as hex (i.e. "ABCD") P mypass Password,
|
|
* ignored if T is "nopass" (in which case it may be omitted). Enclose in double quotes if it is an
|
|
* ASCII name, but could be interpreted as hex (i.e. "ABCD") H true Optional. True if the network
|
|
* SSID is hidden.
|
|
* @hide
|
|
*/
|
|
@FlaggedApi(Flags.FLAG_ANDROID_V_WIFI_API)
|
|
@SystemApi
|
|
public class WifiUriParser {
|
|
static final String TAG = "WifiUriParser";
|
|
static final String SCHEME_DPP = "DPP";
|
|
static final String SCHEME_ZXING_WIFI_NETWORK_CONFIG = "WIFI";
|
|
static final String PREFIX_DPP = "DPP:";
|
|
static final String PREFIX_ZXING_WIFI_NETWORK_CONFIG = "WIFI:";
|
|
|
|
static final String PREFIX_DPP_PUBLIC_KEY = "K:";
|
|
static final String PREFIX_DPP_INFORMATION = "I:";
|
|
|
|
static final String PREFIX_ZXING_SECURITY = "T:";
|
|
static final String PREFIX_ZXING_SSID = "S:";
|
|
static final String PREFIX_ZXING_PASSWORD = "P:";
|
|
static final String PREFIX_ZXING_HIDDEN_SSID = "H:";
|
|
static final String PREFIX_ZXING_TRANSITION_DISABLE = "R:";
|
|
|
|
static final String DELIMITER_QR_CODE = ";";
|
|
|
|
// Ignores password if security is SECURITY_NO_PASSWORD or absent
|
|
static final String SECURITY_NO_PASSWORD = "nopass"; // open network or OWE
|
|
static final String SECURITY_WEP = "WEP";
|
|
static final String SECURITY_WPA_PSK = "WPA";
|
|
static final String SECURITY_SAE = "SAE";
|
|
|
|
private static final String SECURITY_ADB = "ADB";
|
|
|
|
private WifiUriParser() {}
|
|
|
|
/**
|
|
* Returns parsed result from given uri.
|
|
*
|
|
* @param uri URI of the configuration that was obtained out of band(QR code scanning, BLE).
|
|
* @throws IllegalArgumentException when parse failed.
|
|
*/
|
|
@FlaggedApi(Flags.FLAG_ANDROID_V_WIFI_API)
|
|
@NonNull
|
|
public static UriParserResults parseUri(@NonNull String uri) {
|
|
if (TextUtils.isEmpty(uri)) {
|
|
throw new IllegalArgumentException("Empty Wifi Uri");
|
|
}
|
|
|
|
if (uri.startsWith(PREFIX_DPP)) {
|
|
return parseWifiDppUri(uri);
|
|
} else if (uri.startsWith(PREFIX_ZXING_WIFI_NETWORK_CONFIG)) {
|
|
return parseZxingWifiUriParser(uri);
|
|
} else {
|
|
throw new IllegalArgumentException("Unsupport scheme (Not start with "
|
|
+ PREFIX_DPP + "/" + PREFIX_ZXING_WIFI_NETWORK_CONFIG + ")");
|
|
}
|
|
}
|
|
|
|
/** Parses Wi-Fi Easy Connect (DPP) Wifi Uri string */
|
|
private static UriParserResults parseWifiDppUri(String uri) throws IllegalArgumentException {
|
|
List<String> keyValueList = getKeyValueList(uri, PREFIX_DPP, DELIMITER_QR_CODE);
|
|
|
|
String publicKey = getValueOrNull(keyValueList, PREFIX_DPP_PUBLIC_KEY);
|
|
if (TextUtils.isEmpty(publicKey)) {
|
|
throw new IllegalArgumentException("Invalid format, publicKey is empty");
|
|
}
|
|
|
|
String information = getValueOrNull(keyValueList, PREFIX_DPP_INFORMATION);
|
|
|
|
return new UriParserResults(UriParserResults.URI_SCHEME_DPP, publicKey, information, null);
|
|
}
|
|
|
|
/** Parses ZXing reader library's Wi-Fi Network config format */
|
|
private static UriParserResults parseZxingWifiUriParser(String uri)
|
|
throws IllegalArgumentException {
|
|
List<String> keyValueList =
|
|
getKeyValueList(uri, PREFIX_ZXING_WIFI_NETWORK_CONFIG, DELIMITER_QR_CODE);
|
|
WifiConfiguration config = null;
|
|
String security = getValueOrNull(keyValueList, PREFIX_ZXING_SECURITY);
|
|
String ssid = getValueOrNull(keyValueList, PREFIX_ZXING_SSID);
|
|
String password = getValueOrNull(keyValueList, PREFIX_ZXING_PASSWORD);
|
|
String hiddenSsidString = getValueOrNull(keyValueList, PREFIX_ZXING_HIDDEN_SSID);
|
|
String transitionDisabledValue = getValueOrNull(keyValueList,
|
|
PREFIX_ZXING_TRANSITION_DISABLE);
|
|
boolean hiddenSsid = "true".equalsIgnoreCase(hiddenSsidString);
|
|
boolean isTransitionDisabled = "1".equalsIgnoreCase(transitionDisabledValue);
|
|
|
|
// "\", ";", "," and ":" are escaped with a backslash "\", should remove at first
|
|
security = removeBackSlash(security);
|
|
ssid = removeBackSlash(ssid);
|
|
password = removeBackSlash(password);
|
|
if (isValidConfig(security, ssid, password)) {
|
|
config = generatetWifiConfiguration(
|
|
security, ssid, password, hiddenSsid, WifiConfiguration.INVALID_NETWORK_ID,
|
|
isTransitionDisabled);
|
|
}
|
|
|
|
if (config == null) {
|
|
throw new IllegalArgumentException("Invalid format, can't generate WifiConfiguration");
|
|
}
|
|
return new UriParserResults(UriParserResults.URI_SCHEME_ZXING_WIFI_NETWORK_CONFIG,
|
|
null, null, config);
|
|
}
|
|
|
|
/**
|
|
* Splits key/value pairs from uri
|
|
*
|
|
* @param uri the Wifi Uri raw string
|
|
* @param prefixUri the string before all key/value pairs in uri
|
|
* @param delimiter the string to split key/value pairs, can't contain a backslash
|
|
* @return a list contains string of key/value (e.g. K:key1)
|
|
*/
|
|
private static List<String> getKeyValueList(String uri, String prefixUri, String delimiter) {
|
|
String keyValueString = uri.substring(prefixUri.length());
|
|
|
|
// Should not treat \delimiter as a delimiter
|
|
String regex = "(?<!\\\\)" + Pattern.quote(delimiter);
|
|
|
|
return Arrays.asList(keyValueString.split(regex));
|
|
}
|
|
|
|
private static String getValueOrNull(List<String> keyValueList, String prefix) {
|
|
for (String keyValue : keyValueList) {
|
|
String strippedKeyValue = keyValue.stripLeading();
|
|
if (strippedKeyValue.startsWith(prefix)) {
|
|
return strippedKeyValue.substring(prefix.length());
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
@VisibleForTesting
|
|
static String removeBackSlash(String input) {
|
|
if (input == null) {
|
|
return null;
|
|
}
|
|
|
|
StringBuilder sb = new StringBuilder();
|
|
boolean backSlash = false;
|
|
for (int i = 0; i < input.length(); i++) {
|
|
char ch = input.charAt(i);
|
|
if (ch != '\\') {
|
|
sb.append(ch);
|
|
backSlash = false;
|
|
} else {
|
|
if (backSlash) {
|
|
sb.append(ch);
|
|
backSlash = false;
|
|
continue;
|
|
}
|
|
|
|
backSlash = true;
|
|
}
|
|
}
|
|
|
|
return sb.toString();
|
|
}
|
|
|
|
private static String addQuotationIfNeeded(String input) {
|
|
if (TextUtils.isEmpty(input)) {
|
|
return "";
|
|
}
|
|
|
|
if (input.length() >= 2 && input.startsWith("\"") && input.endsWith("\"")) {
|
|
return input;
|
|
}
|
|
|
|
StringBuilder sb = new StringBuilder();
|
|
sb.append("\"").append(input).append("\"");
|
|
return sb.toString();
|
|
}
|
|
|
|
private static boolean isValidConfig(String security, String ssid, String preSharedKey) {
|
|
if (!TextUtils.isEmpty(security) && !SECURITY_NO_PASSWORD.equals(security)) {
|
|
if (TextUtils.isEmpty(preSharedKey)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (TextUtils.isEmpty(ssid)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* This is a simplified method from {@code WifiConfigController.getConfig()}
|
|
*
|
|
* @return WifiConfiguration from parsing result
|
|
*/
|
|
private static WifiConfiguration generatetWifiConfiguration(
|
|
String security, String ssid, String preSharedKey, boolean hiddenSsid, int networkId,
|
|
boolean isTransitionDisabled) {
|
|
final WifiConfiguration wifiConfiguration = new WifiConfiguration();
|
|
wifiConfiguration.SSID = addQuotationIfNeeded(ssid);
|
|
wifiConfiguration.hiddenSSID = hiddenSsid;
|
|
wifiConfiguration.networkId = networkId;
|
|
|
|
if (TextUtils.isEmpty(security) || SECURITY_NO_PASSWORD.equals(security)) {
|
|
wifiConfiguration.setSecurityParams(
|
|
Arrays.asList(
|
|
SecurityParams.createSecurityParamsBySecurityType(
|
|
WifiConfiguration.SECURITY_TYPE_OPEN),
|
|
SecurityParams.createSecurityParamsBySecurityType(
|
|
WifiConfiguration.SECURITY_TYPE_OWE)));
|
|
return wifiConfiguration;
|
|
}
|
|
|
|
if (security.startsWith(SECURITY_WEP)) {
|
|
wifiConfiguration.setSecurityParams(WifiConfiguration.SECURITY_TYPE_WEP);
|
|
|
|
// WEP-40, WEP-104, and 256-bit WEP (WEP-232?)
|
|
final int length = preSharedKey.length();
|
|
if ((length == 10 || length == 26 || length == 58)
|
|
&& preSharedKey.matches("[0-9A-Fa-f]*")) {
|
|
wifiConfiguration.wepKeys[0] = preSharedKey;
|
|
} else {
|
|
wifiConfiguration.wepKeys[0] = addQuotationIfNeeded(preSharedKey);
|
|
}
|
|
} else if (security.startsWith(SECURITY_WPA_PSK)) {
|
|
List<SecurityParams> securityParamsList = new ArrayList<>();
|
|
SecurityParams scannedSecurityParam = SecurityParams.createSecurityParamsBySecurityType(
|
|
WifiConfiguration.SECURITY_TYPE_PSK);
|
|
securityParamsList.add(scannedSecurityParam);
|
|
if (isTransitionDisabled) {
|
|
scannedSecurityParam.setEnabled(false);
|
|
securityParamsList.add(
|
|
SecurityParams.createSecurityParamsBySecurityType(
|
|
WifiConfiguration.SECURITY_TYPE_SAE));
|
|
}
|
|
wifiConfiguration.setSecurityParams(securityParamsList);
|
|
|
|
if (preSharedKey.matches("[0-9A-Fa-f]{64}")) {
|
|
wifiConfiguration.preSharedKey = preSharedKey;
|
|
} else {
|
|
wifiConfiguration.preSharedKey = addQuotationIfNeeded(preSharedKey);
|
|
}
|
|
} else if (security.startsWith(SECURITY_SAE)) {
|
|
wifiConfiguration.setSecurityParams(WifiConfiguration.SECURITY_TYPE_SAE);
|
|
if (preSharedKey.length() != 0) {
|
|
wifiConfiguration.preSharedKey = addQuotationIfNeeded(preSharedKey);
|
|
}
|
|
} else if (security.startsWith(SECURITY_ADB)) {
|
|
Log.i(TAG, "Specific security key: ADB");
|
|
if (preSharedKey.length() != 0) {
|
|
wifiConfiguration.preSharedKey = addQuotationIfNeeded(preSharedKey);
|
|
}
|
|
} else {
|
|
throw new IllegalArgumentException("Unsupported security");
|
|
}
|
|
|
|
return wifiConfiguration;
|
|
}
|
|
}
|