1 package com.android.server.wifi.hotspot2; 2 3 import android.util.Base64; 4 import android.util.Log; 5 6 import com.android.server.wifi.ScanDetail; 7 import com.android.server.wifi.WifiNative; 8 import com.android.server.wifi.anqp.ANQPElement; 9 import com.android.server.wifi.anqp.ANQPFactory; 10 import com.android.server.wifi.anqp.Constants; 11 import com.android.server.wifi.anqp.eap.AuthParam; 12 import com.android.server.wifi.anqp.eap.EAP; 13 import com.android.server.wifi.anqp.eap.EAPMethod; 14 import com.android.server.wifi.hotspot2.pps.Credential; 15 16 import java.io.BufferedReader; 17 import java.io.IOException; 18 import java.io.StringReader; 19 import java.net.ProtocolException; 20 import java.nio.ByteBuffer; 21 import java.nio.ByteOrder; 22 import java.nio.CharBuffer; 23 import java.nio.charset.CharacterCodingException; 24 import java.nio.charset.StandardCharsets; 25 import java.util.ArrayList; 26 import java.util.HashMap; 27 import java.util.List; 28 import java.util.Map; 29 30 public class SupplicantBridge { 31 private final WifiNative mSupplicantHook; 32 private final SupplicantBridgeCallbacks mCallbacks; 33 private final Map<Long, ScanDetail> mRequestMap = new HashMap<>(); 34 35 private static final int IconChunkSize = 1400; // 2K*3/4 - overhead 36 private static final Map<String, Constants.ANQPElementType> sWpsNames = new HashMap<>(); 37 38 static { 39 sWpsNames.put("anqp_venue_name", Constants.ANQPElementType.ANQPVenueName); 40 sWpsNames.put("anqp_network_auth_type", Constants.ANQPElementType.ANQPNwkAuthType); 41 sWpsNames.put("anqp_roaming_consortium", Constants.ANQPElementType.ANQPRoamingConsortium); 42 sWpsNames.put("anqp_ip_addr_type_availability", 43 Constants.ANQPElementType.ANQPIPAddrAvailability); 44 sWpsNames.put("anqp_nai_realm", Constants.ANQPElementType.ANQPNAIRealm); 45 sWpsNames.put("anqp_3gpp", Constants.ANQPElementType.ANQP3GPPNetwork); 46 sWpsNames.put("anqp_domain_name", Constants.ANQPElementType.ANQPDomName); 47 sWpsNames.put("hs20_operator_friendly_name", Constants.ANQPElementType.HSFriendlyName); 48 sWpsNames.put("hs20_wan_metrics", Constants.ANQPElementType.HSWANMetrics); 49 sWpsNames.put("hs20_connection_capability", Constants.ANQPElementType.HSConnCapability); 50 sWpsNames.put("hs20_operating_class", Constants.ANQPElementType.HSOperatingclass); 51 sWpsNames.put("hs20_osu_providers_list", Constants.ANQPElementType.HSOSUProviders); 52 } 53 54 /** 55 * Interface to be implemented by the client to receive callbacks from SupplicantBridge. 56 */ 57 public interface SupplicantBridgeCallbacks { 58 /** 59 * Response from supplicant bridge for the initiated request. 60 * @param scanDetail 61 * @param anqpElements 62 */ 63 void notifyANQPResponse( 64 ScanDetail scanDetail, 65 Map<Constants.ANQPElementType, ANQPElement> anqpElements); 66 67 /** 68 * Notify failure. 69 * @param bssid 70 */ 71 void notifyIconFailed(long bssid); 72 } 73 74 public static boolean isAnqpAttribute(String line) { 75 int split = line.indexOf('='); 76 return split >= 0 && sWpsNames.containsKey(line.substring(0, split)); 77 } 78 79 public SupplicantBridge(WifiNative supplicantHook, SupplicantBridgeCallbacks callbacks) { 80 mSupplicantHook = supplicantHook; 81 mCallbacks = callbacks; 82 } 83 84 public static Map<Constants.ANQPElementType, ANQPElement> parseANQPLines(List<String> lines) { 85 if (lines == null) { 86 return null; 87 } 88 Map<Constants.ANQPElementType, ANQPElement> elements = new HashMap<>(lines.size()); 89 for (String line : lines) { 90 try { 91 ANQPElement element = buildElement(line); 92 if (element != null) { 93 elements.put(element.getID(), element); 94 } 95 } 96 catch (ProtocolException pe) { 97 Log.e(Utils.hs2LogTag(SupplicantBridge.class), "Failed to parse ANQP: " + pe); 98 } 99 } 100 return elements; 101 } 102 103 public boolean startANQP(ScanDetail scanDetail, List<Constants.ANQPElementType> elements) { 104 String anqpGet = buildWPSQueryRequest(scanDetail.getNetworkDetail(), elements); 105 if (anqpGet == null) { 106 return false; 107 } 108 synchronized (mRequestMap) { 109 mRequestMap.put(scanDetail.getNetworkDetail().getBSSID(), scanDetail); 110 } 111 String result = mSupplicantHook.doCustomSupplicantCommand(anqpGet); 112 if (result != null && result.startsWith("OK")) { 113 Log.d(Utils.hs2LogTag(getClass()), "ANQP initiated on " 114 + scanDetail + " (" + anqpGet + ")"); 115 return true; 116 } 117 else { 118 Log.d(Utils.hs2LogTag(getClass()), "ANQP failed on " + 119 scanDetail + ": " + result); 120 return false; 121 } 122 } 123 124 public boolean doIconQuery(long bssid, String fileName) { 125 String result = mSupplicantHook.doCustomSupplicantCommand("REQ_HS20_ICON " + 126 Utils.macToString(bssid) + " " + fileName); 127 return result != null && result.startsWith("OK"); 128 } 129 130 public byte[] retrieveIcon(IconEvent iconEvent) throws IOException { 131 byte[] iconData = new byte[iconEvent.getSize()]; 132 try { 133 int offset = 0; 134 while (offset < iconEvent.getSize()) { 135 int size = Math.min(iconEvent.getSize() - offset, IconChunkSize); 136 137 String command = String.format("GET_HS20_ICON %s %s %d %d", 138 Utils.macToString(iconEvent.getBSSID()), iconEvent.getFileName(), 139 offset, size); 140 Log.d(Utils.hs2LogTag(getClass()), "Issuing '" + command + "'"); 141 String response = mSupplicantHook.doCustomSupplicantCommand(command); 142 if (response == null) { 143 throw new IOException("No icon data returned"); 144 } 145 146 try { 147 byte[] fragment = Base64.decode(response, Base64.DEFAULT); 148 if (fragment.length == 0) { 149 throw new IOException("Null data for '" + command + "': " + response); 150 } 151 if (fragment.length + offset > iconData.length) { 152 throw new IOException("Icon chunk exceeds image size"); 153 } 154 System.arraycopy(fragment, 0, iconData, offset, fragment.length); 155 offset += fragment.length; 156 } catch (IllegalArgumentException iae) { 157 throw new IOException("Failed to parse response to '" + command 158 + "': " + response); 159 } 160 } 161 if (offset != iconEvent.getSize()) { 162 Log.w(Utils.hs2LogTag(getClass()), "Partial icon data: " + offset + 163 ", expected " + iconEvent.getSize()); 164 } 165 } 166 finally { 167 Log.d(Utils.hs2LogTag(getClass()), "Deleting icon for " + iconEvent); 168 String result = mSupplicantHook.doCustomSupplicantCommand("DEL_HS20_ICON " + 169 Utils.macToString(iconEvent.getBSSID()) + " " + iconEvent.getFileName()); 170 } 171 172 return iconData; 173 } 174 175 public void notifyANQPDone(Long bssid, boolean success) { 176 ScanDetail scanDetail; 177 synchronized (mRequestMap) { 178 scanDetail = mRequestMap.remove(bssid); 179 } 180 181 if (scanDetail == null) { 182 if (!success) { 183 mCallbacks.notifyIconFailed(bssid); 184 } 185 return; 186 } 187 188 String bssData = mSupplicantHook.scanResult(scanDetail.getBSSIDString()); 189 try { 190 Map<Constants.ANQPElementType, ANQPElement> elements = parseWPSData(bssData); 191 Log.d(Utils.hs2LogTag(getClass()), String.format("%s ANQP response for %012x: %s", 192 success ? "successful" : "failed", bssid, elements)); 193 mCallbacks.notifyANQPResponse(scanDetail, success ? elements : null); 194 } 195 catch (IOException ioe) { 196 Log.e(Utils.hs2LogTag(getClass()), "Failed to parse ANQP: " + 197 ioe.toString() + ": " + bssData); 198 } 199 catch (RuntimeException rte) { 200 Log.e(Utils.hs2LogTag(getClass()), "Failed to parse ANQP: " + 201 rte.toString() + ": " + bssData, rte); 202 } 203 mCallbacks.notifyANQPResponse(scanDetail, null); 204 } 205 206 private static String escapeSSID(NetworkDetail networkDetail) { 207 return escapeString(networkDetail.getSSID(), networkDetail.isSSID_UTF8()); 208 } 209 210 private static String escapeString(String s, boolean utf8) { 211 boolean asciiOnly = true; 212 for (int n = 0; n < s.length(); n++) { 213 char ch = s.charAt(n); 214 if (ch > 127) { 215 asciiOnly = false; 216 break; 217 } 218 } 219 220 if (asciiOnly) { 221 return '"' + s + '"'; 222 } 223 else { 224 byte[] octets = s.getBytes(utf8 ? StandardCharsets.UTF_8 : StandardCharsets.ISO_8859_1); 225 226 StringBuilder sb = new StringBuilder(); 227 for (byte octet : octets) { 228 sb.append(String.format("%02x", octet & Constants.BYTE_MASK)); 229 } 230 return sb.toString(); 231 } 232 } 233 234 /** 235 * Build a wpa_supplicant ANQP query command 236 * @param networkDetail The network to query. 237 * @param querySet elements to query 238 * @return A command string. 239 */ 240 private static String buildWPSQueryRequest(NetworkDetail networkDetail, 241 List<Constants.ANQPElementType> querySet) { 242 243 boolean baseANQPElements = Constants.hasBaseANQPElements(querySet); 244 StringBuilder sb = new StringBuilder(); 245 if (baseANQPElements) { 246 sb.append("ANQP_GET "); 247 } 248 else { 249 sb.append("HS20_ANQP_GET "); // ANQP_GET does not work for a sole hs20:8 (OSU) query 250 } 251 sb.append(networkDetail.getBSSIDString()).append(' '); 252 253 boolean first = true; 254 for (Constants.ANQPElementType elementType : querySet) { 255 if (first) { 256 first = false; 257 } 258 else { 259 sb.append(','); 260 } 261 262 Integer id = Constants.getANQPElementID(elementType); 263 if (id != null) { 264 sb.append(id); 265 } 266 else { 267 id = Constants.getHS20ElementID(elementType); 268 if (baseANQPElements) { 269 sb.append("hs20:"); 270 } 271 sb.append(id); 272 } 273 } 274 275 return sb.toString(); 276 } 277 278 private static List<String> getWPSNetCommands(String netID, NetworkDetail networkDetail, 279 Credential credential) { 280 281 List<String> commands = new ArrayList<String>(); 282 283 EAPMethod eapMethod = credential.getEAPMethod(); 284 commands.add(String.format("SET_NETWORK %s key_mgmt WPA-EAP", netID)); 285 commands.add(String.format("SET_NETWORK %s ssid %s", netID, escapeSSID(networkDetail))); 286 commands.add(String.format("SET_NETWORK %s bssid %s", 287 netID, networkDetail.getBSSIDString())); 288 commands.add(String.format("SET_NETWORK %s eap %s", 289 netID, mapEAPMethodName(eapMethod.getEAPMethodID()))); 290 291 AuthParam authParam = credential.getEAPMethod().getAuthParam(); 292 if (authParam == null) { 293 return null; // TLS or SIM/AKA 294 } 295 switch (authParam.getAuthInfoID()) { 296 case NonEAPInnerAuthType: 297 case InnerAuthEAPMethodType: 298 commands.add(String.format("SET_NETWORK %s identity %s", 299 netID, escapeString(credential.getUserName(), true))); 300 commands.add(String.format("SET_NETWORK %s password %s", 301 netID, escapeString(credential.getPassword(), true))); 302 commands.add(String.format("SET_NETWORK %s anonymous_identity \"anonymous\"", 303 netID)); 304 break; 305 default: // !!! Needs work. 306 return null; 307 } 308 commands.add(String.format("SET_NETWORK %s priority 0", netID)); 309 commands.add(String.format("ENABLE_NETWORK %s", netID)); 310 commands.add(String.format("SAVE_CONFIG")); 311 return commands; 312 } 313 314 private static Map<Constants.ANQPElementType, ANQPElement> parseWPSData(String bssInfo) 315 throws IOException { 316 Map<Constants.ANQPElementType, ANQPElement> elements = new HashMap<>(); 317 if (bssInfo == null) { 318 return elements; 319 } 320 BufferedReader lineReader = new BufferedReader(new StringReader(bssInfo)); 321 String line; 322 while ((line=lineReader.readLine()) != null) { 323 ANQPElement element = buildElement(line); 324 if (element != null) { 325 elements.put(element.getID(), element); 326 } 327 } 328 return elements; 329 } 330 331 private static ANQPElement buildElement(String text) throws ProtocolException { 332 int separator = text.indexOf('='); 333 if (separator < 0) { 334 return null; 335 } 336 337 String elementName = text.substring(0, separator); 338 Constants.ANQPElementType elementType = sWpsNames.get(elementName); 339 if (elementType == null) { 340 return null; 341 } 342 343 byte[] payload; 344 try { 345 payload = Utils.hexToBytes(text.substring(separator + 1)); 346 } 347 catch (NumberFormatException nfe) { 348 Log.e(Utils.hs2LogTag(SupplicantBridge.class), "Failed to parse hex string"); 349 return null; 350 } 351 return Constants.getANQPElementID(elementType) != null ? 352 ANQPFactory.buildElement(ByteBuffer.wrap(payload), elementType, payload.length) : 353 ANQPFactory.buildHS20Element(elementType, 354 ByteBuffer.wrap(payload).order(ByteOrder.LITTLE_ENDIAN)); 355 } 356 357 private static String mapEAPMethodName(EAP.EAPMethodID eapMethodID) { 358 switch (eapMethodID) { 359 case EAP_AKA: 360 return "AKA"; 361 case EAP_AKAPrim: 362 return "AKA'"; // eap.c:1514 363 case EAP_SIM: 364 return "SIM"; 365 case EAP_TLS: 366 return "TLS"; 367 case EAP_TTLS: 368 return "TTLS"; 369 default: 370 throw new IllegalArgumentException("No mapping for " + eapMethodID); 371 } 372 } 373 374 private static final Map<Character,Integer> sMappings = new HashMap<Character, Integer>(); 375 376 static { 377 sMappings.put('\\', (int)'\\'); 378 sMappings.put('"', (int)'"'); 379 sMappings.put('e', 0x1b); 380 sMappings.put('n', (int)'\n'); 381 sMappings.put('r', (int)'\n'); 382 sMappings.put('t', (int)'\t'); 383 } 384 385 public static String unescapeSSID(String ssid) { 386 387 CharIterator chars = new CharIterator(ssid); 388 byte[] octets = new byte[ssid.length()]; 389 int bo = 0; 390 391 while (chars.hasNext()) { 392 char ch = chars.next(); 393 if (ch != '\\' || ! chars.hasNext()) { 394 octets[bo++] = (byte)ch; 395 } 396 else { 397 char suffix = chars.next(); 398 Integer mapped = sMappings.get(suffix); 399 if (mapped != null) { 400 octets[bo++] = mapped.byteValue(); 401 } 402 else if (suffix == 'x' && chars.hasDoubleHex()) { 403 octets[bo++] = (byte)chars.nextDoubleHex(); 404 } 405 else { 406 octets[bo++] = '\\'; 407 octets[bo++] = (byte)suffix; 408 } 409 } 410 } 411 412 boolean asciiOnly = true; 413 for (byte b : octets) { 414 if ((b&0x80) != 0) { 415 asciiOnly = false; 416 break; 417 } 418 } 419 if (asciiOnly) { 420 return new String(octets, 0, bo, StandardCharsets.UTF_8); 421 } else { 422 try { 423 // If UTF-8 decoding is successful it is almost certainly UTF-8 424 CharBuffer cb = StandardCharsets.UTF_8.newDecoder().decode( 425 ByteBuffer.wrap(octets, 0, bo)); 426 return cb.toString(); 427 } catch (CharacterCodingException cce) { 428 return new String(octets, 0, bo, StandardCharsets.ISO_8859_1); 429 } 430 } 431 } 432 433 private static class CharIterator { 434 private final String mString; 435 private int mPosition; 436 private int mHex; 437 438 private CharIterator(String s) { 439 mString = s; 440 } 441 442 private boolean hasNext() { 443 return mPosition < mString.length(); 444 } 445 446 private char next() { 447 return mString.charAt(mPosition++); 448 } 449 450 private boolean hasDoubleHex() { 451 if (mString.length() - mPosition < 2) { 452 return false; 453 } 454 int nh = Utils.fromHex(mString.charAt(mPosition), true); 455 if (nh < 0) { 456 return false; 457 } 458 int nl = Utils.fromHex(mString.charAt(mPosition + 1), true); 459 if (nl < 0) { 460 return false; 461 } 462 mPosition += 2; 463 mHex = (nh << 4) | nl; 464 return true; 465 } 466 467 private int nextDoubleHex() { 468 return mHex; 469 } 470 } 471 472 private static final String[] TestStrings = { 473 "test-ssid", 474 "test\\nss\\tid", 475 "test\\x2d\\x5f\\nss\\tid", 476 "test\\x2d\\x5f\\nss\\tid\\\\", 477 "test\\x2d\\x5f\\nss\\tid\\n", 478 "test\\x2d\\x5f\\nss\\tid\\x4a", 479 "another\\", 480 "an\\other", 481 "another\\x2" 482 }; 483 484 public static void main(String[] args) { 485 for (String string : TestStrings) { 486 System.out.println(unescapeSSID(string)); 487 } 488 } 489 } 490