1 /*
2  * Copyright (C) 2018 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 
17 package com.android.settings.wifi.dpp;
18 
19 import android.net.wifi.WifiConfiguration;
20 import android.text.TextUtils;
21 
22 import androidx.annotation.VisibleForTesting;
23 
24 import java.util.Arrays;
25 import java.util.List;
26 import java.util.regex.Pattern;
27 
28 /**
29  * Supports to parse 2 types of QR code
30  *
31  *     1. Standard Wi-Fi DPP bootstrapping information or
32  *     2. ZXing reader library's Wi-Fi Network config format described in
33  *     https://github.com/zxing/zxing/wiki/Barcode-Contents#wi-fi-network-config-android-ios-11
34  *
35  * ZXing reader library's Wi-Fi Network config format example:
36  *
37  *     WIFI:T:WPA;S:mynetwork;P:mypass;;
38  *
39  * parameter Example    Description
40  * T         WPA        Authentication type; can be WEP or WPA, or 'nopass' for no password. Or,
41  *                      omit for no password.
42  * S         mynetwork  Network SSID. Required. Enclose in double quotes if it is an ASCII name,
43  *                      but could be interpreted as hex (i.e. "ABCD")
44  * P         mypass     Password, ignored if T is "nopass" (in which case it may be omitted).
45  *                      Enclose in double quotes if it is an ASCII name, but could be interpreted as
46  *                      hex (i.e. "ABCD")
47  * H         true       Optional. True if the network SSID is hidden.
48  *
49  */
50 public class WifiQrCode {
51     public static final String SCHEME_DPP = "DPP";
52     public static final String SCHEME_ZXING_WIFI_NETWORK_CONFIG = "WIFI";
53     public static final String PREFIX_DPP = "DPP:";
54     public static final String PREFIX_ZXING_WIFI_NETWORK_CONFIG = "WIFI:";
55 
56     public static final String PREFIX_DPP_PUBLIC_KEY = "K:";
57     public static final String PREFIX_DPP_INFORMATION = "I:";
58 
59     public static final String PREFIX_ZXING_SECURITY = "T:";
60     public static final String PREFIX_ZXING_SSID = "S:";
61     public static final String PREFIX_ZXING_PASSWORD = "P:";
62     public static final String PREFIX_ZXING_HIDDEN_SSID = "H:";
63 
64     public static final String DELIMITER_QR_CODE = ";";
65 
66     // Ignores password if security is SECURITY_NO_PASSWORD or absent
67     public static final String SECURITY_NO_PASSWORD = "nopass"; //open network or OWE
68     public static final String SECURITY_WEP = "WEP";
69     public static final String SECURITY_WPA_PSK = "WPA";
70     public static final String SECURITY_SAE = "SAE";
71     // Adb QR code pairing is in the following format:
72     // WIFI:T:ADB;S:myname;P:mypass;;
73     public static final String SECURITY_ADB = "ADB";
74 
75     private String mQrCode;
76 
77     /**
78      * SCHEME_DPP for standard Wi-Fi device provision protocol; SCHEME_ZXING_WIFI_NETWORK_CONFIG
79      * for ZXing reader library' Wi-Fi Network config format
80      */
81     private String mScheme;
82 
83     // Data from parsed Wi-Fi DPP QR code
84     private String mPublicKey;
85     private String mInformation;
86 
87     // Data from parsed ZXing reader library's Wi-Fi Network config format
88     private WifiNetworkConfig mWifiNetworkConfig;
89 
WifiQrCode(String qrCode)90     public WifiQrCode(String qrCode) throws IllegalArgumentException {
91         if (TextUtils.isEmpty(qrCode)) {
92             throw new IllegalArgumentException("Empty QR code");
93         }
94 
95         mQrCode = qrCode;
96 
97         if (qrCode.startsWith(PREFIX_DPP)) {
98             mScheme = SCHEME_DPP;
99             parseWifiDppQrCode(qrCode);
100         } else if (qrCode.startsWith(PREFIX_ZXING_WIFI_NETWORK_CONFIG)) {
101             mScheme = SCHEME_ZXING_WIFI_NETWORK_CONFIG;
102             parseZxingWifiQrCode(qrCode);
103         } else {
104             throw new IllegalArgumentException("Invalid scheme");
105         }
106     }
107 
108     /** Parses Wi-Fi DPP QR code string */
parseWifiDppQrCode(String qrCode)109     private void parseWifiDppQrCode(String qrCode) throws IllegalArgumentException {
110         List keyValueList = getKeyValueList(qrCode, PREFIX_DPP, DELIMITER_QR_CODE);
111 
112         String publicKey = getValueOrNull(keyValueList, PREFIX_DPP_PUBLIC_KEY);
113         if (TextUtils.isEmpty(publicKey)) {
114             throw new IllegalArgumentException("Invalid format");
115         }
116         mPublicKey = publicKey;
117 
118         mInformation = getValueOrNull(keyValueList, PREFIX_DPP_INFORMATION);
119     }
120 
121     /** Parses ZXing reader library's Wi-Fi Network config format */
parseZxingWifiQrCode(String qrCode)122     private void parseZxingWifiQrCode(String qrCode) throws IllegalArgumentException {
123         List keyValueList = getKeyValueList(qrCode, PREFIX_ZXING_WIFI_NETWORK_CONFIG,
124                 DELIMITER_QR_CODE);
125 
126         String security = getValueOrNull(keyValueList, PREFIX_ZXING_SECURITY);
127         String ssid = getValueOrNull(keyValueList, PREFIX_ZXING_SSID);
128         String password = getValueOrNull(keyValueList, PREFIX_ZXING_PASSWORD);
129         String hiddenSsidString = getValueOrNull(keyValueList, PREFIX_ZXING_HIDDEN_SSID);
130 
131         boolean hiddenSsid = "true".equalsIgnoreCase(hiddenSsidString);
132 
133         //"\", ";", "," and ":" are escaped with a backslash "\", should remove at first
134         security = removeBackSlash(security);
135         ssid = removeBackSlash(ssid);
136         password = removeBackSlash(password);
137 
138         mWifiNetworkConfig = WifiNetworkConfig.getValidConfigOrNull(security, ssid, password,
139                 hiddenSsid, WifiConfiguration.INVALID_NETWORK_ID, /* isHotspot */ false);
140 
141         if (mWifiNetworkConfig == null) {
142             throw new IllegalArgumentException("Invalid format");
143         }
144     }
145 
146     /**
147      * Splits key/value pairs from qrCode
148      *
149      * @param qrCode the QR code raw string
150      * @param prefixQrCode the string before all key/value pairs in qrCode
151      * @param delimiter the string to split key/value pairs, can't contain a backslash
152      * @return a list contains string of key/value (e.g. K:key1)
153      */
getKeyValueList(String qrCode, String prefixQrCode, String delimiter)154     private List<String> getKeyValueList(String qrCode, String prefixQrCode,
155                 String delimiter) {
156         String keyValueString = qrCode.substring(prefixQrCode.length());
157 
158         // Should not treat \delimiter as a delimiter
159         String regex = "(?<!\\\\)" + Pattern.quote(delimiter);
160 
161         List<String> result = Arrays.asList(keyValueString.split(regex));
162         return result;
163     }
164 
getValueOrNull(List<String> keyValueList, String prefix)165     private String getValueOrNull(List<String> keyValueList, String prefix) {
166         for (String keyValue : keyValueList) {
167             if (keyValue.startsWith(prefix)) {
168                 return  keyValue.substring(prefix.length());
169             }
170         }
171 
172         return null;
173     }
174 
175     @VisibleForTesting
removeBackSlash(String input)176     String removeBackSlash(String input) {
177         if (input == null) {
178             return null;
179         }
180 
181         StringBuilder sb = new StringBuilder();
182         boolean backSlash = false;
183         for (char ch : input.toCharArray()) {
184             if (ch != '\\') {
185                 sb.append(ch);
186                 backSlash = false;
187             } else {
188                 if (backSlash) {
189                     sb.append(ch);
190                     backSlash = false;
191                     continue;
192                 }
193 
194                 backSlash = true;
195             }
196         }
197 
198         return sb.toString();
199     }
200 
getQrCode()201     public String getQrCode() {
202         return mQrCode;
203     }
204 
205     /**
206      * Uses to check type of QR code
207      *
208      * SCHEME_DPP for standard Wi-Fi device provision protocol; SCHEME_ZXING_WIFI_NETWORK_CONFIG
209      * for ZXing reader library' Wi-Fi Network config format
210      */
getScheme()211     public String getScheme() {
212         return mScheme;
213     }
214 
215     /** Available when {@code getScheme()} returns SCHEME_DPP */
216     @VisibleForTesting
getPublicKey()217     String getPublicKey() {
218         return mPublicKey;
219     }
220 
221     /** May be available when {@code getScheme()} returns SCHEME_DPP */
getInformation()222     public String getInformation() {
223         return mInformation;
224     }
225 
226     /** Available when {@code getScheme()} returns SCHEME_ZXING_WIFI_NETWORK_CONFIG */
getWifiNetworkConfig()227     public WifiNetworkConfig getWifiNetworkConfig() {
228         if (mWifiNetworkConfig == null) {
229             return null;
230         }
231 
232         return new WifiNetworkConfig(mWifiNetworkConfig);
233     }
234 
getValidWifiDppQrCodeOrNull(String qrCode)235     public static WifiQrCode getValidWifiDppQrCodeOrNull(String qrCode) {
236         WifiQrCode wifiQrCode;
237         try {
238             wifiQrCode = new WifiQrCode(qrCode);
239         } catch(IllegalArgumentException e) {
240             return null;
241         }
242 
243         if (SCHEME_DPP.equals(wifiQrCode.getScheme())) {
244             return wifiQrCode;
245         }
246 
247         return null;
248     }
249 }
250