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          */
notifyANQPResponse( ScanDetail scanDetail, Map<Constants.ANQPElementType, ANQPElement> anqpElements)63         void notifyANQPResponse(
64                 ScanDetail scanDetail,
65                 Map<Constants.ANQPElementType, ANQPElement> anqpElements);
66 
67         /**
68          * Notify failure.
69          * @param bssid
70          */
notifyIconFailed(long bssid)71         void notifyIconFailed(long bssid);
72     }
73 
isAnqpAttribute(String line)74     public static boolean isAnqpAttribute(String line) {
75         int split = line.indexOf('=');
76         return split >= 0 && sWpsNames.containsKey(line.substring(0, split));
77     }
78 
SupplicantBridge(WifiNative supplicantHook, SupplicantBridgeCallbacks callbacks)79     public SupplicantBridge(WifiNative supplicantHook, SupplicantBridgeCallbacks callbacks) {
80         mSupplicantHook = supplicantHook;
81         mCallbacks = callbacks;
82     }
83 
parseANQPLines(List<String> lines)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 
startANQP(ScanDetail scanDetail, List<Constants.ANQPElementType> elements)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 
doIconQuery(long bssid, String fileName)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 
retrieveIcon(IconEvent iconEvent)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 
notifyANQPDone(Long bssid, boolean success)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 
escapeSSID(NetworkDetail networkDetail)206     private static String escapeSSID(NetworkDetail networkDetail) {
207         return escapeString(networkDetail.getSSID(), networkDetail.isSSID_UTF8());
208     }
209 
escapeString(String s, boolean utf8)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      */
buildWPSQueryRequest(NetworkDetail networkDetail, List<Constants.ANQPElementType> querySet)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 
getWPSNetCommands(String netID, NetworkDetail networkDetail, Credential credential)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 
parseWPSData(String bssInfo)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 
buildElement(String text)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 
mapEAPMethodName(EAP.EAPMethodID eapMethodID)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 
unescapeSSID(String ssid)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 
CharIterator(String s)438         private CharIterator(String s) {
439             mString = s;
440         }
441 
hasNext()442         private boolean hasNext() {
443             return mPosition < mString.length();
444         }
445 
next()446         private char next() {
447             return mString.charAt(mPosition++);
448         }
449 
hasDoubleHex()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 
nextDoubleHex()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 
main(String[] args)484     public static void main(String[] args) {
485         for (String string : TestStrings) {
486             System.out.println(unescapeSSID(string));
487         }
488     }
489 }
490