1/*
2 * Copyright (C) 2010 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
17package com.android.server.wifi;
18
19import android.annotation.NonNull;
20import android.content.Context;
21import android.net.wifi.WifiConfiguration;
22import android.net.wifi.WifiConfiguration.KeyMgmt;
23import android.os.Environment;
24import android.text.TextUtils;
25import android.util.Log;
26
27import com.android.internal.R;
28import com.android.internal.annotations.VisibleForTesting;
29
30import java.io.BufferedInputStream;
31import java.io.BufferedOutputStream;
32import java.io.DataInputStream;
33import java.io.DataOutputStream;
34import java.io.FileInputStream;
35import java.io.FileOutputStream;
36import java.io.IOException;
37import java.nio.charset.StandardCharsets;
38import java.util.ArrayList;
39import java.util.Random;
40import java.util.UUID;
41
42/**
43 * Provides API for reading/writing soft access point configuration.
44 */
45public class WifiApConfigStore {
46
47    private static final String TAG = "WifiApConfigStore";
48
49    private static final String DEFAULT_AP_CONFIG_FILE =
50            Environment.getDataDirectory() + "/misc/wifi/softap.conf";
51
52    private static final int AP_CONFIG_FILE_VERSION = 2;
53
54    private static final int RAND_SSID_INT_MIN = 1000;
55    private static final int RAND_SSID_INT_MAX = 9999;
56
57    @VisibleForTesting
58    static final int SSID_MIN_LEN = 1;
59    @VisibleForTesting
60    static final int SSID_MAX_LEN = 32;
61    @VisibleForTesting
62    static final int PSK_MIN_LEN = 8;
63    @VisibleForTesting
64    static final int PSK_MAX_LEN = 63;
65
66    @VisibleForTesting
67    static final int AP_CHANNEL_DEFAULT = 0;
68
69    private WifiConfiguration mWifiApConfig = null;
70
71    private ArrayList<Integer> mAllowed2GChannel = null;
72
73    private final Context mContext;
74    private final String mApConfigFile;
75    private final BackupManagerProxy mBackupManagerProxy;
76    private boolean mRequiresApBandConversion = false;
77
78    WifiApConfigStore(Context context, BackupManagerProxy backupManagerProxy) {
79        this(context, backupManagerProxy, DEFAULT_AP_CONFIG_FILE);
80    }
81
82    WifiApConfigStore(Context context,
83                      BackupManagerProxy backupManagerProxy,
84                      String apConfigFile) {
85        mContext = context;
86        mBackupManagerProxy = backupManagerProxy;
87        mApConfigFile = apConfigFile;
88
89        String ap2GChannelListStr = mContext.getResources().getString(
90                R.string.config_wifi_framework_sap_2G_channel_list);
91        Log.d(TAG, "2G band allowed channels are:" + ap2GChannelListStr);
92
93        if (ap2GChannelListStr != null) {
94            mAllowed2GChannel = new ArrayList<Integer>();
95            String channelList[] = ap2GChannelListStr.split(",");
96            for (String tmp : channelList) {
97                mAllowed2GChannel.add(Integer.parseInt(tmp));
98            }
99        }
100
101        mRequiresApBandConversion = mContext.getResources().getBoolean(
102                R.bool.config_wifi_convert_apband_5ghz_to_any);
103
104        /* Load AP configuration from persistent storage. */
105        mWifiApConfig = loadApConfiguration(mApConfigFile);
106        if (mWifiApConfig == null) {
107            /* Use default configuration. */
108            Log.d(TAG, "Fallback to use default AP configuration");
109            mWifiApConfig = getDefaultApConfiguration();
110
111            /* Save the default configuration to persistent storage. */
112            writeApConfiguration(mApConfigFile, mWifiApConfig);
113        }
114    }
115
116    /**
117     * Return the current soft access point configuration.
118     */
119    public synchronized WifiConfiguration getApConfiguration() {
120        WifiConfiguration config = apBandCheckConvert(mWifiApConfig);
121        if (mWifiApConfig != config) {
122            Log.d(TAG, "persisted config was converted, need to resave it");
123            mWifiApConfig = config;
124            persistConfigAndTriggerBackupManagerProxy(mWifiApConfig);
125        }
126        return mWifiApConfig;
127    }
128
129    /**
130     * Update the current soft access point configuration.
131     * Restore to default AP configuration if null is provided.
132     * This can be invoked under context of binder threads (WifiManager.setWifiApConfiguration)
133     * and WifiStateMachine thread (CMD_START_AP).
134     */
135    public synchronized void setApConfiguration(WifiConfiguration config) {
136        if (config == null) {
137            mWifiApConfig = getDefaultApConfiguration();
138        } else {
139            mWifiApConfig = apBandCheckConvert(config);
140        }
141        persistConfigAndTriggerBackupManagerProxy(mWifiApConfig);
142    }
143
144    public ArrayList<Integer> getAllowed2GChannel() {
145        return mAllowed2GChannel;
146    }
147
148    private WifiConfiguration apBandCheckConvert(WifiConfiguration config) {
149        if (mRequiresApBandConversion) {
150            // some devices are unable to support 5GHz only operation, check for 5GHz and
151            // move to ANY if apBand conversion is required.
152            if (config.apBand == WifiConfiguration.AP_BAND_5GHZ) {
153                Log.w(TAG, "Supplied ap config band was 5GHz only, converting to ANY");
154                WifiConfiguration convertedConfig = new WifiConfiguration(config);
155                convertedConfig.apBand = WifiConfiguration.AP_BAND_ANY;
156                convertedConfig.apChannel = AP_CHANNEL_DEFAULT;
157                return convertedConfig;
158            }
159        } else {
160            // this is a single mode device, we do not support ANY.  Convert all ANY to 5GHz
161            if (config.apBand == WifiConfiguration.AP_BAND_ANY) {
162                Log.w(TAG, "Supplied ap config band was ANY, converting to 5GHz");
163                WifiConfiguration convertedConfig = new WifiConfiguration(config);
164                convertedConfig.apBand = WifiConfiguration.AP_BAND_5GHZ;
165                convertedConfig.apChannel = AP_CHANNEL_DEFAULT;
166                return convertedConfig;
167            }
168        }
169        return config;
170    }
171
172    private void persistConfigAndTriggerBackupManagerProxy(WifiConfiguration config) {
173        writeApConfiguration(mApConfigFile, mWifiApConfig);
174        // Stage the backup of the SettingsProvider package which backs this up
175        mBackupManagerProxy.notifyDataChanged();
176    }
177
178    /**
179     * Load AP configuration from persistent storage.
180     */
181    private static WifiConfiguration loadApConfiguration(final String filename) {
182        WifiConfiguration config = null;
183        DataInputStream in = null;
184        try {
185            config = new WifiConfiguration();
186            in = new DataInputStream(
187                    new BufferedInputStream(new FileInputStream(filename)));
188
189            int version = in.readInt();
190            if ((version != 1) && (version != 2)) {
191                Log.e(TAG, "Bad version on hotspot configuration file");
192                return null;
193            }
194            config.SSID = in.readUTF();
195
196            if (version >= 2) {
197                config.apBand = in.readInt();
198                config.apChannel = in.readInt();
199            }
200
201            int authType = in.readInt();
202            config.allowedKeyManagement.set(authType);
203            if (authType != KeyMgmt.NONE) {
204                config.preSharedKey = in.readUTF();
205            }
206        } catch (IOException e) {
207            Log.e(TAG, "Error reading hotspot configuration " + e);
208            config = null;
209        } finally {
210            if (in != null) {
211                try {
212                    in.close();
213                } catch (IOException e) {
214                    Log.e(TAG, "Error closing hotspot configuration during read" + e);
215                }
216            }
217        }
218        return config;
219    }
220
221    /**
222     * Write AP configuration to persistent storage.
223     */
224    private static void writeApConfiguration(final String filename,
225                                             final WifiConfiguration config) {
226        try (DataOutputStream out = new DataOutputStream(new BufferedOutputStream(
227                        new FileOutputStream(filename)))) {
228            out.writeInt(AP_CONFIG_FILE_VERSION);
229            out.writeUTF(config.SSID);
230            out.writeInt(config.apBand);
231            out.writeInt(config.apChannel);
232            int authType = config.getAuthType();
233            out.writeInt(authType);
234            if (authType != KeyMgmt.NONE) {
235                out.writeUTF(config.preSharedKey);
236            }
237        } catch (IOException e) {
238            Log.e(TAG, "Error writing hotspot configuration" + e);
239        }
240    }
241
242    /**
243     * Generate a default WPA2 based configuration with a random password.
244     * We are changing the Wifi Ap configuration storage from secure settings to a
245     * flat file accessible only by the system. A WPA2 based default configuration
246     * will keep the device secure after the update.
247     */
248    private WifiConfiguration getDefaultApConfiguration() {
249        WifiConfiguration config = new WifiConfiguration();
250        config.apBand = WifiConfiguration.AP_BAND_2GHZ;
251        config.SSID = mContext.getResources().getString(
252                R.string.wifi_tether_configure_ssid_default) + "_" + getRandomIntForDefaultSsid();
253        config.allowedKeyManagement.set(KeyMgmt.WPA2_PSK);
254        String randomUUID = UUID.randomUUID().toString();
255        //first 12 chars from xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
256        config.preSharedKey = randomUUID.substring(0, 8) + randomUUID.substring(9, 13);
257        return config;
258    }
259
260    private static int getRandomIntForDefaultSsid() {
261        Random random = new Random();
262        return random.nextInt((RAND_SSID_INT_MAX - RAND_SSID_INT_MIN) + 1) + RAND_SSID_INT_MIN;
263    }
264
265    /**
266     * Generate a temporary WPA2 based configuration for use by the local only hotspot.
267     * This config is not persisted and will not be stored by the WifiApConfigStore.
268     */
269    public static WifiConfiguration generateLocalOnlyHotspotConfig(Context context) {
270        WifiConfiguration config = new WifiConfiguration();
271        // For local only hotspot we only use 2.4Ghz band.
272        config.apBand = WifiConfiguration.AP_BAND_2GHZ;
273        config.SSID = context.getResources().getString(
274              R.string.wifi_localhotspot_configure_ssid_default) + "_"
275                      + getRandomIntForDefaultSsid();
276        config.allowedKeyManagement.set(KeyMgmt.WPA2_PSK);
277        config.networkId = WifiConfiguration.LOCAL_ONLY_NETWORK_ID;
278        String randomUUID = UUID.randomUUID().toString();
279        // first 12 chars from xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
280        config.preSharedKey = randomUUID.substring(0, 8) + randomUUID.substring(9, 13);
281        return config;
282    }
283
284    /**
285     * Verify provided SSID for existence, length and conversion to bytes
286     *
287     * @param ssid String ssid name
288     * @return boolean indicating ssid met requirements
289     */
290    private static boolean validateApConfigSsid(String ssid) {
291        if (TextUtils.isEmpty(ssid)) {
292            Log.d(TAG, "SSID for softap configuration must be set.");
293            return false;
294        }
295
296        if (ssid.length() < SSID_MIN_LEN || ssid.length() > SSID_MAX_LEN) {
297            Log.d(TAG, "SSID for softap configuration string size must be at least "
298                    + SSID_MIN_LEN + " and not more than " + SSID_MAX_LEN);
299            return false;
300        }
301
302        try {
303            ssid.getBytes(StandardCharsets.UTF_8);
304        } catch (IllegalArgumentException e) {
305            Log.e(TAG, "softap config SSID verification failed: malformed string " + ssid);
306            return false;
307        }
308        return true;
309    }
310
311    /**
312     * Verify provided preSharedKey in ap config for WPA2_PSK network meets requirements.
313     */
314    private static boolean validateApConfigPreSharedKey(String preSharedKey) {
315        if (preSharedKey.length() < PSK_MIN_LEN || preSharedKey.length() > PSK_MAX_LEN) {
316            Log.d(TAG, "softap network password string size must be at least " + PSK_MIN_LEN
317                    + " and no more than " + PSK_MAX_LEN);
318            return false;
319        }
320
321        try {
322            preSharedKey.getBytes(StandardCharsets.UTF_8);
323        } catch (IllegalArgumentException e) {
324            Log.e(TAG, "softap network password verification failed: malformed string");
325            return false;
326        }
327        return true;
328    }
329
330    /**
331     * Validate a WifiConfiguration is properly configured for use by SoftApManager.
332     *
333     * This method checks the length of the SSID and for sanity between security settings (if it
334     * requires a password, was one provided?).
335     *
336     * @param apConfig {@link WifiConfiguration} to use for softap mode
337     * @return boolean true if the provided config meets the minimum set of details, false
338     * otherwise.
339     */
340    static boolean validateApWifiConfiguration(@NonNull WifiConfiguration apConfig) {
341        // first check the SSID
342        if (!validateApConfigSsid(apConfig.SSID)) {
343            // failed SSID verificiation checks
344            return false;
345        }
346
347        // now check security settings: settings app allows open and WPA2 PSK
348        if (apConfig.allowedKeyManagement == null) {
349            Log.d(TAG, "softap config key management bitset was null");
350            return false;
351        }
352
353        String preSharedKey = apConfig.preSharedKey;
354        boolean hasPreSharedKey = !TextUtils.isEmpty(preSharedKey);
355        int authType;
356
357        try {
358            authType = apConfig.getAuthType();
359        } catch (IllegalStateException e) {
360            Log.d(TAG, "Unable to get AuthType for softap config: " + e.getMessage());
361            return false;
362        }
363
364        if (authType == KeyMgmt.NONE) {
365            // open networks should not have a password
366            if (hasPreSharedKey) {
367                Log.d(TAG, "open softap network should not have a password");
368                return false;
369            }
370        } else if (authType == KeyMgmt.WPA2_PSK) {
371            // this is a config that should have a password - check that first
372            if (!hasPreSharedKey) {
373                Log.d(TAG, "softap network password must be set");
374                return false;
375            }
376
377            if (!validateApConfigPreSharedKey(preSharedKey)) {
378                // failed preSharedKey checks
379                return false;
380            }
381        } else {
382            // this is not a supported security type
383            Log.d(TAG, "softap configs must either be open or WPA2 PSK networks");
384            return false;
385        }
386
387        return true;
388    }
389}
390