/*
* 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
*
*
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
*
*
ZXing reader library's Wi-Fi Network config format example:
*
*
WIFI:T:WPA;S:mynetwork;P:mypass;;
*
*
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 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 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 getKeyValueList(String uri, String prefixUri, String delimiter) {
String keyValueString = uri.substring(prefixUri.length());
// Should not treat \delimiter as a delimiter
String regex = "(? 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 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;
}
}