1 package com.android.server.wifi.hotspot2;
2 
3 import android.util.Log;
4 
5 import com.android.server.wifi.ScanDetail;
6 import com.android.server.wifi.WifiConfigStore;
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.BufferUnderflowException;
21 import java.nio.ByteBuffer;
22 import java.nio.ByteOrder;
23 import java.nio.CharBuffer;
24 import java.nio.charset.CharacterCodingException;
25 import java.nio.charset.StandardCharsets;
26 import java.util.ArrayList;
27 import java.util.HashMap;
28 import java.util.List;
29 import java.util.Map;
30 
31 public class SupplicantBridge {
32     private final WifiNative mSupplicantHook;
33     private final WifiConfigStore mConfigStore;
34     private final Map<Long, ScanDetail> mRequestMap = new HashMap<>();
35 
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 
isAnqpAttribute(String line)54     public static boolean isAnqpAttribute(String line) {
55         int split = line.indexOf('=');
56         return split >= 0 && sWpsNames.containsKey(line.substring(0, split));
57     }
58 
SupplicantBridge(WifiNative supplicantHook, WifiConfigStore configStore)59     public SupplicantBridge(WifiNative supplicantHook, WifiConfigStore configStore) {
60         mSupplicantHook = supplicantHook;
61         mConfigStore = configStore;
62     }
63 
parseANQPLines(List<String> lines)64     public static Map<Constants.ANQPElementType, ANQPElement> parseANQPLines(List<String> lines) {
65         if (lines == null) {
66             return null;
67         }
68         Map<Constants.ANQPElementType, ANQPElement> elements = new HashMap<>(lines.size());
69         for (String line : lines) {
70             try {
71                 ANQPElement element = buildElement(line);
72                 if (element != null) {
73                     elements.put(element.getID(), element);
74                 }
75             }
76             catch (ProtocolException pe) {
77                 Log.e(Utils.hs2LogTag(SupplicantBridge.class), "Failed to parse ANQP: " + pe);
78             }
79         }
80         return elements;
81     }
82 
startANQP(ScanDetail scanDetail)83     public void startANQP(ScanDetail scanDetail) {
84         String anqpGet = buildWPSQueryRequest(scanDetail.getNetworkDetail());
85         synchronized (mRequestMap) {
86             mRequestMap.put(scanDetail.getNetworkDetail().getBSSID(), scanDetail);
87         }
88         String result = mSupplicantHook.doCustomCommand(anqpGet);
89         if (result != null && result.startsWith("OK")) {
90             Log.d(Utils.hs2LogTag(getClass()), "ANQP initiated on " + scanDetail);
91         }
92         else {
93             Log.d(Utils.hs2LogTag(getClass()), "ANQP failed on " +
94                     scanDetail + ": " + result);
95         }
96     }
97 
notifyANQPDone(Long bssid, boolean success)98     public void notifyANQPDone(Long bssid, boolean success) {
99         ScanDetail scanDetail;
100         synchronized (mRequestMap) {
101             scanDetail = mRequestMap.remove(bssid);
102         }
103         if (scanDetail == null) {
104             Log.d(Utils.hs2LogTag(getClass()), String.format("Spurious %s ANQP response for %012x",
105                             success ? "successful" : "failed", bssid));
106             return;
107         }
108 
109         String bssData = mSupplicantHook.scanResult(scanDetail.getBSSIDString());
110         try {
111             Map<Constants.ANQPElementType, ANQPElement> elements = parseWPSData(bssData);
112             Log.d(Utils.hs2LogTag(getClass()), String.format("%s ANQP response for %012x: %s",
113                     success ? "successful" : "failed", bssid, elements));
114             mConfigStore.notifyANQPResponse(scanDetail, success ? elements : null);
115         }
116         catch (IOException ioe) {
117             Log.e(Utils.hs2LogTag(getClass()), "Failed to parse ANQP: " +
118                     ioe.toString() + ": " + bssData);
119         }
120         catch (RuntimeException rte) {
121             Log.e(Utils.hs2LogTag(getClass()), "Failed to parse ANQP: " +
122                     rte.toString() + ": " + bssData, rte);
123         }
124         mConfigStore.notifyANQPResponse(scanDetail, null);
125     }
126 
127     /*
128     public boolean addCredential(HomeSP homeSP, NetworkDetail networkDetail) {
129         Credential credential = homeSP.getCredential();
130         if (credential == null)
131             return false;
132 
133         String nwkID = null;
134         if (mLastSSID != null) {
135             String nwkList = mSupplicantHook.doCustomCommand("LIST_NETWORKS");
136 
137             BufferedReader reader = new BufferedReader(new StringReader(nwkList));
138             String line;
139             try {
140                 while ((line = reader.readLine()) != null) {
141                     String[] tokens = line.split("\\t");
142                     if (tokens.length < 2 || ! Utils.isDecimal(tokens[0])) {
143                         continue;
144                     }
145                     if (unescapeSSID(tokens[1]).equals(mLastSSID)) {
146                         nwkID = tokens[0];
147                         Log.d("HS2J", "Network " + tokens[0] +
148                                 " matches last SSID '" + mLastSSID + "'");
149                         break;
150                     }
151                 }
152             }
153             catch (IOException ioe) {
154                 //
155             }
156         }
157 
158         if (nwkID == null) {
159             nwkID = mSupplicantHook.doCustomCommand("ADD_NETWORK");
160             Log.d("HS2J", "add_network: '" + nwkID + "'");
161             if (! Utils.isDecimal(nwkID)) {
162                 return false;
163             }
164         }
165 
166         List<String> credCommand = getWPSNetCommands(nwkID, networkDetail, credential);
167         for (String command : credCommand) {
168             String status = mSupplicantHook.doCustomCommand(command);
169             Log.d("HS2J", "Status of '" + command + "': '" + status + "'");
170         }
171 
172         if (! networkDetail.getSSID().equals(mLastSSID)) {
173             mLastSSID = networkDetail.getSSID();
174             PrintWriter out = null;
175             try {
176                 out = new PrintWriter(new OutputStreamWriter(
177                         new FileOutputStream(mLastSSIDFile, false), StandardCharsets.UTF_8));
178                 out.println(mLastSSID);
179             } catch (IOException ioe) {
180             //
181             } finally {
182                 if (out != null) {
183                     out.close();
184                 }
185             }
186         }
187 
188         return true;
189     }
190     */
191 
escapeSSID(NetworkDetail networkDetail)192     private static String escapeSSID(NetworkDetail networkDetail) {
193         return escapeString(networkDetail.getSSID(), networkDetail.isSSID_UTF8());
194     }
195 
escapeString(String s, boolean utf8)196     private static String escapeString(String s, boolean utf8) {
197         boolean asciiOnly = true;
198         for (int n = 0; n < s.length(); n++) {
199             char ch = s.charAt(n);
200             if (ch > 127) {
201                 asciiOnly = false;
202                 break;
203             }
204         }
205 
206         if (asciiOnly) {
207             return '"' + s + '"';
208         }
209         else {
210             byte[] octets = s.getBytes(utf8 ? StandardCharsets.UTF_8 : StandardCharsets.ISO_8859_1);
211 
212             StringBuilder sb = new StringBuilder();
213             for (byte octet : octets) {
214                 sb.append(String.format("%02x", octet & Constants.BYTE_MASK));
215             }
216             return sb.toString();
217         }
218     }
219 
buildWPSQueryRequest(NetworkDetail networkDetail)220     private static String buildWPSQueryRequest(NetworkDetail networkDetail) {
221         StringBuilder sb = new StringBuilder();
222         sb.append("ANQP_GET ").append(networkDetail.getBSSIDString()).append(' ');
223 
224         boolean first = true;
225         for (Constants.ANQPElementType elementType : ANQPFactory.getBaseANQPSet()) {
226             if (networkDetail.getAnqpOICount() == 0 &&
227                     elementType == Constants.ANQPElementType.ANQPRoamingConsortium) {
228                 continue;
229             }
230             if (first) {
231                 first = false;
232             }
233             else {
234                 sb.append(',');
235             }
236             sb.append(Constants.getANQPElementID(elementType));
237         }
238         if (networkDetail.getHSRelease() != null) {
239             for (Constants.ANQPElementType elementType : ANQPFactory.getHS20ANQPSet()) {
240                 sb.append(",hs20:").append(Constants.getHS20ElementID(elementType));
241             }
242         }
243         return sb.toString();
244     }
245 
getWPSNetCommands(String netID, NetworkDetail networkDetail, Credential credential)246     private static List<String> getWPSNetCommands(String netID, NetworkDetail networkDetail,
247                                                  Credential credential) {
248 
249         List<String> commands = new ArrayList<String>();
250 
251         EAPMethod eapMethod = credential.getEAPMethod();
252         commands.add(String.format("SET_NETWORK %s key_mgmt WPA-EAP", netID));
253         commands.add(String.format("SET_NETWORK %s ssid %s", netID, escapeSSID(networkDetail)));
254         commands.add(String.format("SET_NETWORK %s bssid %s",
255                 netID, networkDetail.getBSSIDString()));
256         commands.add(String.format("SET_NETWORK %s eap %s",
257                 netID, mapEAPMethodName(eapMethod.getEAPMethodID())));
258 
259         AuthParam authParam = credential.getEAPMethod().getAuthParam();
260         if (authParam == null) {
261             return null;            // TLS or SIM/AKA
262         }
263         switch (authParam.getAuthInfoID()) {
264             case NonEAPInnerAuthType:
265             case InnerAuthEAPMethodType:
266                 commands.add(String.format("SET_NETWORK %s identity %s",
267                         netID, escapeString(credential.getUserName(), true)));
268                 commands.add(String.format("SET_NETWORK %s password %s",
269                         netID, escapeString(credential.getPassword(), true)));
270                 commands.add(String.format("SET_NETWORK %s anonymous_identity \"anonymous\"",
271                         netID));
272                 break;
273             default:                // !!! Needs work.
274                 return null;
275         }
276         commands.add(String.format("SET_NETWORK %s priority 0", netID));
277         commands.add(String.format("ENABLE_NETWORK %s", netID));
278         commands.add(String.format("SAVE_CONFIG"));
279         return commands;
280     }
281 
parseWPSData(String bssInfo)282     private static Map<Constants.ANQPElementType, ANQPElement> parseWPSData(String bssInfo)
283             throws IOException {
284         Map<Constants.ANQPElementType, ANQPElement> elements = new HashMap<>();
285         if (bssInfo == null) {
286             return elements;
287         }
288         BufferedReader lineReader = new BufferedReader(new StringReader(bssInfo));
289         String line;
290         while ((line=lineReader.readLine()) != null) {
291             ANQPElement element = buildElement(line);
292             if (element != null) {
293                 elements.put(element.getID(), element);
294             }
295         }
296         return elements;
297     }
298 
buildElement(String text)299     private static ANQPElement buildElement(String text) throws ProtocolException {
300         int separator = text.indexOf('=');
301         if (separator < 0) {
302             return null;
303         }
304 
305         String elementName = text.substring(0, separator);
306         Constants.ANQPElementType elementType = sWpsNames.get(elementName);
307         if (elementType == null) {
308             return null;
309         }
310 
311         byte[] payload;
312         try {
313             payload = Utils.hexToBytes(text.substring(separator + 1));
314         }
315         catch (NumberFormatException nfe) {
316             Log.e(Utils.hs2LogTag(SupplicantBridge.class), "Failed to parse hex string");
317             return null;
318         }
319         return Constants.getANQPElementID(elementType) != null ?
320                 ANQPFactory.buildElement(ByteBuffer.wrap(payload), elementType, payload.length) :
321                 ANQPFactory.buildHS20Element(elementType,
322                         ByteBuffer.wrap(payload).order(ByteOrder.LITTLE_ENDIAN));
323     }
324 
mapEAPMethodName(EAP.EAPMethodID eapMethodID)325     private static String mapEAPMethodName(EAP.EAPMethodID eapMethodID) {
326         switch (eapMethodID) {
327             case EAP_AKA:
328                 return "AKA";
329             case EAP_AKAPrim:
330                 return "AKA'";  // eap.c:1514
331             case EAP_SIM:
332                 return "SIM";
333             case EAP_TLS:
334                 return "TLS";
335             case EAP_TTLS:
336                 return "TTLS";
337             default:
338                 throw new IllegalArgumentException("No mapping for " + eapMethodID);
339         }
340     }
341 
342     private static final Map<Character,Integer> sMappings = new HashMap<Character, Integer>();
343 
344     static {
345         sMappings.put('\\', (int)'\\');
346         sMappings.put('"', (int)'"');
347         sMappings.put('e', 0x1b);
348         sMappings.put('n', (int)'\n');
349         sMappings.put('r', (int)'\n');
350         sMappings.put('t', (int)'\t');
351     }
352 
unescapeSSID(String ssid)353     public static String unescapeSSID(String ssid) {
354 
355         CharIterator chars = new CharIterator(ssid);
356         byte[] octets = new byte[ssid.length()];
357         int bo = 0;
358 
359         while (chars.hasNext()) {
360             char ch = chars.next();
361             if (ch != '\\' || ! chars.hasNext()) {
362                 octets[bo++] = (byte)ch;
363             }
364             else {
365                 char suffix = chars.next();
366                 Integer mapped = sMappings.get(suffix);
367                 if (mapped != null) {
368                     octets[bo++] = mapped.byteValue();
369                 }
370                 else if (suffix == 'x' && chars.hasDoubleHex()) {
371                     octets[bo++] = (byte)chars.nextDoubleHex();
372                 }
373                 else {
374                     octets[bo++] = '\\';
375                     octets[bo++] = (byte)suffix;
376                 }
377             }
378         }
379 
380         boolean asciiOnly = true;
381         for (byte b : octets) {
382             if ((b&0x80) != 0) {
383                 asciiOnly = false;
384                 break;
385             }
386         }
387         if (asciiOnly) {
388             return new String(octets, 0, bo, StandardCharsets.UTF_8);
389         } else {
390             try {
391                 // If UTF-8 decoding is successful it is almost certainly UTF-8
392                 CharBuffer cb = StandardCharsets.UTF_8.newDecoder().decode(
393                         ByteBuffer.wrap(octets, 0, bo));
394                 return cb.toString();
395             } catch (CharacterCodingException cce) {
396                 return new String(octets, 0, bo, StandardCharsets.ISO_8859_1);
397             }
398         }
399     }
400 
401     private static class CharIterator {
402         private final String mString;
403         private int mPosition;
404         private int mHex;
405 
CharIterator(String s)406         private CharIterator(String s) {
407             mString = s;
408         }
409 
hasNext()410         private boolean hasNext() {
411             return mPosition < mString.length();
412         }
413 
next()414         private char next() {
415             return mString.charAt(mPosition++);
416         }
417 
hasDoubleHex()418         private boolean hasDoubleHex() {
419             if (mString.length() - mPosition < 2) {
420                 return false;
421             }
422             int nh = Utils.fromHex(mString.charAt(mPosition), true);
423             if (nh < 0) {
424                 return false;
425             }
426             int nl = Utils.fromHex(mString.charAt(mPosition + 1), true);
427             if (nl < 0) {
428                 return false;
429             }
430             mPosition += 2;
431             mHex = (nh << 4) | nl;
432             return true;
433         }
434 
nextDoubleHex()435         private int nextDoubleHex() {
436             return mHex;
437         }
438     }
439 
440     private static final String[] TestStrings = {
441             "test-ssid",
442             "test\\nss\\tid",
443             "test\\x2d\\x5f\\nss\\tid",
444             "test\\x2d\\x5f\\nss\\tid\\\\",
445             "test\\x2d\\x5f\\nss\\tid\\n",
446             "test\\x2d\\x5f\\nss\\tid\\x4a",
447             "another\\",
448             "an\\other",
449             "another\\x2"
450     };
451 
main(String[] args)452     public static void main(String[] args) {
453         for (String string : TestStrings) {
454             System.out.println(unescapeSSID(string));
455         }
456     }
457 }
458