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