1 /*
2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package android.net.wifi;
17 
18 import android.annotation.FlaggedApi;
19 import android.annotation.NonNull;
20 import android.annotation.SystemApi;
21 import android.text.TextUtils;
22 import android.util.Log;
23 
24 import androidx.annotation.VisibleForTesting;
25 
26 import com.android.wifi.flags.Flags;
27 
28 import java.util.ArrayList;
29 import java.util.Arrays;
30 import java.util.List;
31 import java.util.regex.Pattern;
32 
33 /**
34  * Supports to parse 2 types of Wifi Uri
35  *
36  * <p>1. Standard Wi-Fi Easy Connect (DPP) bootstrapping information or 2. ZXing reader library's
37  * Wi-Fi Network config format described in
38  * https://github.com/zxing/zxing/wiki/Barcode-Contents#wi-fi-network-config-android-ios-11
39  *
40  * <p>ZXing reader library's Wi-Fi Network config format example:
41  *
42  * <p>WIFI:T:WPA;S:mynetwork;P:mypass;;
43  *
44  * <p>parameter Example Description T WPA Authentication type; can be WEP, WPA, SAE or 'nopass' for
45  * no password. Or, omit for no password. S mynetwork Network SSID. Required. Enclose in double
46  * quotes if it is an ASCII name, but could be interpreted as hex (i.e. "ABCD") P mypass Password,
47  * ignored if T is "nopass" (in which case it may be omitted). Enclose in double quotes if it is an
48  * ASCII name, but could be interpreted as hex (i.e. "ABCD") H true Optional. True if the network
49  * SSID is hidden.
50  * @hide
51  */
52 @FlaggedApi(Flags.FLAG_ANDROID_V_WIFI_API)
53 @SystemApi
54 public class WifiUriParser {
55     static final String TAG = "WifiUriParser";
56     static final String SCHEME_DPP = "DPP";
57     static final String SCHEME_ZXING_WIFI_NETWORK_CONFIG = "WIFI";
58     static final String PREFIX_DPP = "DPP:";
59     static final String PREFIX_ZXING_WIFI_NETWORK_CONFIG = "WIFI:";
60 
61     static final String PREFIX_DPP_PUBLIC_KEY = "K:";
62     static final String PREFIX_DPP_INFORMATION = "I:";
63 
64     static final String PREFIX_ZXING_SECURITY = "T:";
65     static final String PREFIX_ZXING_SSID = "S:";
66     static final String PREFIX_ZXING_PASSWORD = "P:";
67     static final String PREFIX_ZXING_HIDDEN_SSID = "H:";
68     static final String PREFIX_ZXING_TRANSITION_DISABLE = "R:";
69 
70     static final String DELIMITER_QR_CODE = ";";
71 
72     // Ignores password if security is SECURITY_NO_PASSWORD or absent
73     static final String SECURITY_NO_PASSWORD = "nopass"; // open network or OWE
74     static final String SECURITY_WEP = "WEP";
75     static final String SECURITY_WPA_PSK = "WPA";
76     static final String SECURITY_SAE = "SAE";
77 
78     private static final String SECURITY_ADB = "ADB";
79 
WifiUriParser()80     private WifiUriParser() {}
81 
82     /**
83      * Returns parsed result from given uri.
84      *
85      * @param uri URI of the configuration that was obtained out of band(QR code scanning, BLE).
86      * @throws IllegalArgumentException when parse failed.
87      */
88     @FlaggedApi(Flags.FLAG_ANDROID_V_WIFI_API)
89     @NonNull
parseUri(@onNull String uri)90     public static UriParserResults parseUri(@NonNull String uri) {
91         if (TextUtils.isEmpty(uri)) {
92             throw new IllegalArgumentException("Empty Wifi Uri");
93         }
94 
95         if (uri.startsWith(PREFIX_DPP)) {
96             return parseWifiDppUri(uri);
97         } else if (uri.startsWith(PREFIX_ZXING_WIFI_NETWORK_CONFIG)) {
98             return parseZxingWifiUriParser(uri);
99         } else {
100             throw new IllegalArgumentException("Unsupport scheme (Not start with "
101                     + PREFIX_DPP + "/" + PREFIX_ZXING_WIFI_NETWORK_CONFIG + ")");
102         }
103     }
104 
105     /** Parses Wi-Fi Easy Connect (DPP) Wifi Uri string */
parseWifiDppUri(String uri)106     private static UriParserResults parseWifiDppUri(String uri) throws IllegalArgumentException {
107         List<String> keyValueList = getKeyValueList(uri, PREFIX_DPP, DELIMITER_QR_CODE);
108 
109         String publicKey = getValueOrNull(keyValueList, PREFIX_DPP_PUBLIC_KEY);
110         if (TextUtils.isEmpty(publicKey)) {
111             throw new IllegalArgumentException("Invalid format, publicKey is empty");
112         }
113 
114         String information = getValueOrNull(keyValueList, PREFIX_DPP_INFORMATION);
115 
116         return new UriParserResults(UriParserResults.URI_SCHEME_DPP, publicKey, information, null);
117     }
118 
119     /** Parses ZXing reader library's Wi-Fi Network config format */
parseZxingWifiUriParser(String uri)120     private static UriParserResults parseZxingWifiUriParser(String uri)
121             throws IllegalArgumentException {
122         List<String> keyValueList =
123                 getKeyValueList(uri, PREFIX_ZXING_WIFI_NETWORK_CONFIG, DELIMITER_QR_CODE);
124         WifiConfiguration config = null;
125         String security = getValueOrNull(keyValueList, PREFIX_ZXING_SECURITY);
126         String ssid = getValueOrNull(keyValueList, PREFIX_ZXING_SSID);
127         String password = getValueOrNull(keyValueList, PREFIX_ZXING_PASSWORD);
128         String hiddenSsidString = getValueOrNull(keyValueList, PREFIX_ZXING_HIDDEN_SSID);
129         String transitionDisabledValue = getValueOrNull(keyValueList,
130                 PREFIX_ZXING_TRANSITION_DISABLE);
131         boolean hiddenSsid = "true".equalsIgnoreCase(hiddenSsidString);
132         boolean isTransitionDisabled = "1".equalsIgnoreCase(transitionDisabledValue);
133 
134         // "\", ";", "," and ":" are escaped with a backslash "\", should remove at first
135         security = removeBackSlash(security);
136         ssid = removeBackSlash(ssid);
137         password = removeBackSlash(password);
138         if (isValidConfig(security, ssid, password)) {
139             config = generatetWifiConfiguration(
140                         security, ssid, password, hiddenSsid, WifiConfiguration.INVALID_NETWORK_ID,
141                         isTransitionDisabled);
142         }
143 
144         if (config == null) {
145             throw new IllegalArgumentException("Invalid format, can't generate WifiConfiguration");
146         }
147         return new UriParserResults(UriParserResults.URI_SCHEME_ZXING_WIFI_NETWORK_CONFIG,
148                 null, null, config);
149     }
150 
151     /**
152      * Splits key/value pairs from uri
153      *
154      * @param uri the Wifi Uri raw string
155      * @param prefixUri the string before all key/value pairs in uri
156      * @param delimiter the string to split key/value pairs, can't contain a backslash
157      * @return a list contains string of key/value (e.g. K:key1)
158      */
getKeyValueList(String uri, String prefixUri, String delimiter)159     private static List<String> getKeyValueList(String uri, String prefixUri, String delimiter) {
160         String keyValueString = uri.substring(prefixUri.length());
161 
162         // Should not treat \delimiter as a delimiter
163         String regex = "(?<!\\\\)" + Pattern.quote(delimiter);
164 
165         return Arrays.asList(keyValueString.split(regex));
166     }
167 
getValueOrNull(List<String> keyValueList, String prefix)168     private static String getValueOrNull(List<String> keyValueList, String prefix) {
169         for (String keyValue : keyValueList) {
170             String strippedKeyValue = keyValue.stripLeading();
171             if (strippedKeyValue.startsWith(prefix)) {
172                 return strippedKeyValue.substring(prefix.length());
173             }
174         }
175 
176         return null;
177     }
178 
179     @VisibleForTesting
removeBackSlash(String input)180     static String removeBackSlash(String input) {
181         if (input == null) {
182             return null;
183         }
184 
185         StringBuilder sb = new StringBuilder();
186         boolean backSlash = false;
187         for (int i = 0; i < input.length(); i++) {
188             char ch = input.charAt(i);
189             if (ch != '\\') {
190                 sb.append(ch);
191                 backSlash = false;
192             } else {
193                 if (backSlash) {
194                     sb.append(ch);
195                     backSlash = false;
196                     continue;
197                 }
198 
199                 backSlash = true;
200             }
201         }
202 
203         return sb.toString();
204     }
205 
addQuotationIfNeeded(String input)206     private static String addQuotationIfNeeded(String input) {
207         if (TextUtils.isEmpty(input)) {
208             return "";
209         }
210 
211         if (input.length() >= 2 && input.startsWith("\"") && input.endsWith("\"")) {
212             return input;
213         }
214 
215         StringBuilder sb = new StringBuilder();
216         sb.append("\"").append(input).append("\"");
217         return sb.toString();
218     }
219 
isValidConfig(String security, String ssid, String preSharedKey)220     private static boolean isValidConfig(String security, String ssid, String preSharedKey) {
221         if (!TextUtils.isEmpty(security) && !SECURITY_NO_PASSWORD.equals(security)) {
222             if (TextUtils.isEmpty(preSharedKey)) {
223                 return false;
224             }
225         }
226 
227         if (TextUtils.isEmpty(ssid)) {
228             return false;
229         }
230 
231         return true;
232     }
233 
234     /**
235      * This is a simplified method from {@code WifiConfigController.getConfig()}
236      *
237      * @return WifiConfiguration from parsing result
238      */
generatetWifiConfiguration( String security, String ssid, String preSharedKey, boolean hiddenSsid, int networkId, boolean isTransitionDisabled)239     private static WifiConfiguration generatetWifiConfiguration(
240             String security, String ssid, String preSharedKey, boolean hiddenSsid, int networkId,
241             boolean isTransitionDisabled) {
242         final WifiConfiguration wifiConfiguration = new WifiConfiguration();
243         wifiConfiguration.SSID = addQuotationIfNeeded(ssid);
244         wifiConfiguration.hiddenSSID = hiddenSsid;
245         wifiConfiguration.networkId = networkId;
246 
247         if (TextUtils.isEmpty(security) || SECURITY_NO_PASSWORD.equals(security)) {
248             wifiConfiguration.setSecurityParams(
249                     Arrays.asList(
250                             SecurityParams.createSecurityParamsBySecurityType(
251                                     WifiConfiguration.SECURITY_TYPE_OPEN),
252                             SecurityParams.createSecurityParamsBySecurityType(
253                                     WifiConfiguration.SECURITY_TYPE_OWE)));
254             return wifiConfiguration;
255         }
256 
257         if (security.startsWith(SECURITY_WEP)) {
258             wifiConfiguration.setSecurityParams(WifiConfiguration.SECURITY_TYPE_WEP);
259 
260             // WEP-40, WEP-104, and 256-bit WEP (WEP-232?)
261             final int length = preSharedKey.length();
262             if ((length == 10 || length == 26 || length == 58)
263                     && preSharedKey.matches("[0-9A-Fa-f]*")) {
264                 wifiConfiguration.wepKeys[0] = preSharedKey;
265             } else {
266                 wifiConfiguration.wepKeys[0] = addQuotationIfNeeded(preSharedKey);
267             }
268         } else if (security.startsWith(SECURITY_WPA_PSK)) {
269             List<SecurityParams> securityParamsList = new ArrayList<>();
270             SecurityParams scannedSecurityParam = SecurityParams.createSecurityParamsBySecurityType(
271                             WifiConfiguration.SECURITY_TYPE_PSK);
272             securityParamsList.add(scannedSecurityParam);
273             if (isTransitionDisabled) {
274                 scannedSecurityParam.setEnabled(false);
275                 securityParamsList.add(
276                         SecurityParams.createSecurityParamsBySecurityType(
277                                 WifiConfiguration.SECURITY_TYPE_SAE));
278             }
279             wifiConfiguration.setSecurityParams(securityParamsList);
280 
281             if (preSharedKey.matches("[0-9A-Fa-f]{64}")) {
282                 wifiConfiguration.preSharedKey = preSharedKey;
283             } else {
284                 wifiConfiguration.preSharedKey = addQuotationIfNeeded(preSharedKey);
285             }
286         } else if (security.startsWith(SECURITY_SAE)) {
287             wifiConfiguration.setSecurityParams(WifiConfiguration.SECURITY_TYPE_SAE);
288             if (preSharedKey.length() != 0) {
289                 wifiConfiguration.preSharedKey = addQuotationIfNeeded(preSharedKey);
290             }
291         } else if (security.startsWith(SECURITY_ADB)) {
292             Log.i(TAG, "Specific security key: ADB");
293             if (preSharedKey.length() != 0) {
294                 wifiConfiguration.preSharedKey = addQuotationIfNeeded(preSharedKey);
295             }
296         } else {
297             throw new IllegalArgumentException("Unsupported security");
298         }
299 
300         return wifiConfiguration;
301     }
302 }
303