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 
17 package com.android.server.wifi;
18 
19 import android.annotation.NonNull;
20 import android.app.Notification;
21 import android.app.NotificationManager;
22 import android.app.PendingIntent;
23 import android.content.BroadcastReceiver;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.IntentFilter;
27 import android.net.wifi.WifiConfiguration;
28 import android.net.wifi.WifiConfiguration.KeyMgmt;
29 import android.os.Environment;
30 import android.os.Handler;
31 import android.os.Looper;
32 import android.text.TextUtils;
33 import android.util.Log;
34 
35 import com.android.internal.R;
36 import com.android.internal.annotations.VisibleForTesting;
37 import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
38 import com.android.internal.notification.SystemNotificationChannels;
39 
40 import java.io.BufferedInputStream;
41 import java.io.BufferedOutputStream;
42 import java.io.DataInputStream;
43 import java.io.DataOutputStream;
44 import java.io.FileInputStream;
45 import java.io.FileOutputStream;
46 import java.io.IOException;
47 import java.nio.charset.StandardCharsets;
48 import java.util.ArrayList;
49 import java.util.Random;
50 import java.util.UUID;
51 
52 /**
53  * Provides API for reading/writing soft access point configuration.
54  */
55 public class WifiApConfigStore {
56 
57     // Intent when user has interacted with the softap settings change notification
58     public static final String ACTION_HOTSPOT_CONFIG_USER_TAPPED_CONTENT =
59             "com.android.server.wifi.WifiApConfigStoreUtil.HOTSPOT_CONFIG_USER_TAPPED_CONTENT";
60 
61     private static final String TAG = "WifiApConfigStore";
62 
63     private static final String DEFAULT_AP_CONFIG_FILE =
64             Environment.getDataDirectory() + "/misc/wifi/softap.conf";
65 
66     private static final int AP_CONFIG_FILE_VERSION = 3;
67 
68     private static final int RAND_SSID_INT_MIN = 1000;
69     private static final int RAND_SSID_INT_MAX = 9999;
70 
71     @VisibleForTesting
72     static final int SSID_MIN_LEN = 1;
73     @VisibleForTesting
74     static final int SSID_MAX_LEN = 32;
75     @VisibleForTesting
76     static final int PSK_MIN_LEN = 8;
77     @VisibleForTesting
78     static final int PSK_MAX_LEN = 63;
79 
80     @VisibleForTesting
81     static final int AP_CHANNEL_DEFAULT = 0;
82 
83     private WifiConfiguration mWifiApConfig = null;
84 
85     private ArrayList<Integer> mAllowed2GChannel = null;
86 
87     private final Context mContext;
88     private final Handler mHandler;
89     private final String mApConfigFile;
90     private final BackupManagerProxy mBackupManagerProxy;
91     private final FrameworkFacade mFrameworkFacade;
92     private boolean mRequiresApBandConversion = false;
93 
WifiApConfigStore(Context context, Looper looper, BackupManagerProxy backupManagerProxy, FrameworkFacade frameworkFacade)94     WifiApConfigStore(Context context, Looper looper,
95             BackupManagerProxy backupManagerProxy, FrameworkFacade frameworkFacade) {
96         this(context, looper, backupManagerProxy, frameworkFacade, DEFAULT_AP_CONFIG_FILE);
97     }
98 
WifiApConfigStore(Context context, Looper looper, BackupManagerProxy backupManagerProxy, FrameworkFacade frameworkFacade, String apConfigFile)99     WifiApConfigStore(Context context,
100                       Looper looper,
101                       BackupManagerProxy backupManagerProxy,
102                       FrameworkFacade frameworkFacade,
103                       String apConfigFile) {
104         mContext = context;
105         mHandler = new Handler(looper);
106         mBackupManagerProxy = backupManagerProxy;
107         mFrameworkFacade = frameworkFacade;
108         mApConfigFile = apConfigFile;
109 
110         String ap2GChannelListStr = mContext.getResources().getString(
111                 R.string.config_wifi_framework_sap_2G_channel_list);
112         Log.d(TAG, "2G band allowed channels are:" + ap2GChannelListStr);
113 
114         if (ap2GChannelListStr != null) {
115             mAllowed2GChannel = new ArrayList<Integer>();
116             String channelList[] = ap2GChannelListStr.split(",");
117             for (String tmp : channelList) {
118                 mAllowed2GChannel.add(Integer.parseInt(tmp));
119             }
120         }
121 
122         mRequiresApBandConversion = mContext.getResources().getBoolean(
123                 R.bool.config_wifi_convert_apband_5ghz_to_any);
124 
125         /* Load AP configuration from persistent storage. */
126         mWifiApConfig = loadApConfiguration(mApConfigFile);
127         if (mWifiApConfig == null) {
128             /* Use default configuration. */
129             Log.d(TAG, "Fallback to use default AP configuration");
130             mWifiApConfig = getDefaultApConfiguration();
131 
132             /* Save the default configuration to persistent storage. */
133             writeApConfiguration(mApConfigFile, mWifiApConfig);
134         }
135 
136         IntentFilter filter = new IntentFilter();
137         filter.addAction(ACTION_HOTSPOT_CONFIG_USER_TAPPED_CONTENT);
138         mContext.registerReceiver(
139                 mBroadcastReceiver, filter, null /* broadcastPermission */, mHandler);
140     }
141 
142     private final BroadcastReceiver mBroadcastReceiver =
143             new BroadcastReceiver() {
144                 @Override
145                 public void onReceive(Context context, Intent intent) {
146                     // For now we only have one registered listener, but we easily could expand this
147                     // to support multiple signals.  Starting off with a switch to support trivial
148                     // expansion.
149                     switch(intent.getAction()) {
150                         case ACTION_HOTSPOT_CONFIG_USER_TAPPED_CONTENT:
151                             handleUserHotspotConfigTappedContent();
152                             break;
153                         default:
154                             Log.e(TAG, "Unknown action " + intent.getAction());
155                     }
156                 }
157             };
158 
159     /**
160      * Return the current soft access point configuration.
161      */
getApConfiguration()162     public synchronized WifiConfiguration getApConfiguration() {
163         WifiConfiguration config = apBandCheckConvert(mWifiApConfig);
164         if (mWifiApConfig != config) {
165             Log.d(TAG, "persisted config was converted, need to resave it");
166             mWifiApConfig = config;
167             persistConfigAndTriggerBackupManagerProxy(mWifiApConfig);
168         }
169         return mWifiApConfig;
170     }
171 
172     /**
173      * Update the current soft access point configuration.
174      * Restore to default AP configuration if null is provided.
175      * This can be invoked under context of binder threads (WifiManager.setWifiApConfiguration)
176      * and ClientModeImpl thread (CMD_START_AP).
177      */
setApConfiguration(WifiConfiguration config)178     public synchronized void setApConfiguration(WifiConfiguration config) {
179         if (config == null) {
180             mWifiApConfig = getDefaultApConfiguration();
181         } else {
182             mWifiApConfig = apBandCheckConvert(config);
183         }
184         persistConfigAndTriggerBackupManagerProxy(mWifiApConfig);
185     }
186 
getAllowed2GChannel()187     public ArrayList<Integer> getAllowed2GChannel() {
188         return mAllowed2GChannel;
189     }
190 
191     /**
192      * Helper method to create and send notification to user of apBand conversion.
193      *
194      * @param packageName name of the calling app
195      */
notifyUserOfApBandConversion(String packageName)196     public void notifyUserOfApBandConversion(String packageName) {
197         Log.w(TAG, "ready to post notification - triggered by " + packageName);
198         Notification notification = createConversionNotification();
199         NotificationManager notificationManager = (NotificationManager)
200                     mContext.getSystemService(Context.NOTIFICATION_SERVICE);
201         notificationManager.notify(SystemMessage.NOTE_SOFTAP_CONFIG_CHANGED, notification);
202     }
203 
createConversionNotification()204     private Notification createConversionNotification() {
205         CharSequence title =
206                 mContext.getResources().getText(R.string.wifi_softap_config_change);
207         CharSequence contentSummary =
208                 mContext.getResources().getText(R.string.wifi_softap_config_change_summary);
209         CharSequence content =
210                 mContext.getResources().getText(R.string.wifi_softap_config_change_detailed);
211         int color =
212                 mContext.getResources().getColor(
213                         R.color.system_notification_accent_color, mContext.getTheme());
214 
215         return new Notification.Builder(mContext, SystemNotificationChannels.NETWORK_STATUS)
216                 .setSmallIcon(R.drawable.ic_wifi_settings)
217                 .setPriority(Notification.PRIORITY_HIGH)
218                 .setCategory(Notification.CATEGORY_SYSTEM)
219                 .setContentTitle(title)
220                 .setContentText(contentSummary)
221                 .setContentIntent(getPrivateBroadcast(ACTION_HOTSPOT_CONFIG_USER_TAPPED_CONTENT))
222                 .setTicker(title)
223                 .setShowWhen(false)
224                 .setLocalOnly(true)
225                 .setColor(color)
226                 .setStyle(new Notification.BigTextStyle().bigText(content)
227                                                          .setBigContentTitle(title)
228                                                          .setSummaryText(contentSummary))
229                 .build();
230     }
231 
apBandCheckConvert(WifiConfiguration config)232     private WifiConfiguration apBandCheckConvert(WifiConfiguration config) {
233         if (mRequiresApBandConversion) {
234             // some devices are unable to support 5GHz only operation, check for 5GHz and
235             // move to ANY if apBand conversion is required.
236             if (config.apBand == WifiConfiguration.AP_BAND_5GHZ) {
237                 Log.w(TAG, "Supplied ap config band was 5GHz only, converting to ANY");
238                 WifiConfiguration convertedConfig = new WifiConfiguration(config);
239                 convertedConfig.apBand = WifiConfiguration.AP_BAND_ANY;
240                 convertedConfig.apChannel = AP_CHANNEL_DEFAULT;
241                 return convertedConfig;
242             }
243         } else {
244             // this is a single mode device, we do not support ANY.  Convert all ANY to 5GHz
245             if (config.apBand == WifiConfiguration.AP_BAND_ANY) {
246                 Log.w(TAG, "Supplied ap config band was ANY, converting to 5GHz");
247                 WifiConfiguration convertedConfig = new WifiConfiguration(config);
248                 convertedConfig.apBand = WifiConfiguration.AP_BAND_5GHZ;
249                 convertedConfig.apChannel = AP_CHANNEL_DEFAULT;
250                 return convertedConfig;
251             }
252         }
253         return config;
254     }
255 
persistConfigAndTriggerBackupManagerProxy(WifiConfiguration config)256     private void persistConfigAndTriggerBackupManagerProxy(WifiConfiguration config) {
257         writeApConfiguration(mApConfigFile, mWifiApConfig);
258         // Stage the backup of the SettingsProvider package which backs this up
259         mBackupManagerProxy.notifyDataChanged();
260     }
261 
262     /**
263      * Load AP configuration from persistent storage.
264      */
loadApConfiguration(final String filename)265     private static WifiConfiguration loadApConfiguration(final String filename) {
266         WifiConfiguration config = null;
267         DataInputStream in = null;
268         try {
269             config = new WifiConfiguration();
270             in = new DataInputStream(
271                     new BufferedInputStream(new FileInputStream(filename)));
272 
273             int version = in.readInt();
274             if (version < 1 || version > AP_CONFIG_FILE_VERSION) {
275                 Log.e(TAG, "Bad version on hotspot configuration file");
276                 return null;
277             }
278             config.SSID = in.readUTF();
279 
280             if (version >= 2) {
281                 config.apBand = in.readInt();
282                 config.apChannel = in.readInt();
283             }
284 
285             if (version >= 3) {
286                 config.hiddenSSID = in.readBoolean();
287             }
288 
289             int authType = in.readInt();
290             config.allowedKeyManagement.set(authType);
291             if (authType != KeyMgmt.NONE) {
292                 config.preSharedKey = in.readUTF();
293             }
294         } catch (IOException e) {
295             Log.e(TAG, "Error reading hotspot configuration " + e);
296             config = null;
297         } finally {
298             if (in != null) {
299                 try {
300                     in.close();
301                 } catch (IOException e) {
302                     Log.e(TAG, "Error closing hotspot configuration during read" + e);
303                 }
304             }
305         }
306         return config;
307     }
308 
309     /**
310      * Write AP configuration to persistent storage.
311      */
writeApConfiguration(final String filename, final WifiConfiguration config)312     private static void writeApConfiguration(final String filename,
313                                              final WifiConfiguration config) {
314         try (DataOutputStream out = new DataOutputStream(new BufferedOutputStream(
315                         new FileOutputStream(filename)))) {
316             out.writeInt(AP_CONFIG_FILE_VERSION);
317             out.writeUTF(config.SSID);
318             out.writeInt(config.apBand);
319             out.writeInt(config.apChannel);
320             out.writeBoolean(config.hiddenSSID);
321             int authType = config.getAuthType();
322             out.writeInt(authType);
323             if (authType != KeyMgmt.NONE) {
324                 out.writeUTF(config.preSharedKey);
325             }
326         } catch (IOException e) {
327             Log.e(TAG, "Error writing hotspot configuration" + e);
328         }
329     }
330 
331     /**
332      * Generate a default WPA2 based configuration with a random password.
333      * We are changing the Wifi Ap configuration storage from secure settings to a
334      * flat file accessible only by the system. A WPA2 based default configuration
335      * will keep the device secure after the update.
336      */
getDefaultApConfiguration()337     private WifiConfiguration getDefaultApConfiguration() {
338         WifiConfiguration config = new WifiConfiguration();
339         config.apBand = WifiConfiguration.AP_BAND_2GHZ;
340         config.SSID = mContext.getResources().getString(
341                 R.string.wifi_tether_configure_ssid_default) + "_" + getRandomIntForDefaultSsid();
342         config.allowedKeyManagement.set(KeyMgmt.WPA2_PSK);
343         String randomUUID = UUID.randomUUID().toString();
344         //first 12 chars from xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
345         config.preSharedKey = randomUUID.substring(0, 8) + randomUUID.substring(9, 13);
346         return config;
347     }
348 
getRandomIntForDefaultSsid()349     private static int getRandomIntForDefaultSsid() {
350         Random random = new Random();
351         return random.nextInt((RAND_SSID_INT_MAX - RAND_SSID_INT_MIN) + 1) + RAND_SSID_INT_MIN;
352     }
353 
354     /**
355      * Generate a temporary WPA2 based configuration for use by the local only hotspot.
356      * This config is not persisted and will not be stored by the WifiApConfigStore.
357      */
generateLocalOnlyHotspotConfig(Context context, int apBand)358     public static WifiConfiguration generateLocalOnlyHotspotConfig(Context context, int apBand) {
359         WifiConfiguration config = new WifiConfiguration();
360 
361         config.SSID = context.getResources().getString(
362               R.string.wifi_localhotspot_configure_ssid_default) + "_"
363                       + getRandomIntForDefaultSsid();
364         config.apBand = apBand;
365         config.allowedKeyManagement.set(KeyMgmt.WPA2_PSK);
366         config.networkId = WifiConfiguration.LOCAL_ONLY_NETWORK_ID;
367         String randomUUID = UUID.randomUUID().toString();
368         // first 12 chars from xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
369         config.preSharedKey = randomUUID.substring(0, 8) + randomUUID.substring(9, 13);
370         return config;
371     }
372 
373     /**
374      * Verify provided SSID for existence, length and conversion to bytes
375      *
376      * @param ssid String ssid name
377      * @return boolean indicating ssid met requirements
378      */
validateApConfigSsid(String ssid)379     private static boolean validateApConfigSsid(String ssid) {
380         if (TextUtils.isEmpty(ssid)) {
381             Log.d(TAG, "SSID for softap configuration must be set.");
382             return false;
383         }
384 
385         try {
386             byte[] ssid_bytes = ssid.getBytes(StandardCharsets.UTF_8);
387 
388             if (ssid_bytes.length < SSID_MIN_LEN || ssid_bytes.length > SSID_MAX_LEN) {
389                 Log.d(TAG, "softap SSID is defined as UTF-8 and it must be at least "
390                         + SSID_MIN_LEN + " byte and not more than " + SSID_MAX_LEN + " bytes");
391                 return false;
392             }
393         } catch (IllegalArgumentException e) {
394             Log.e(TAG, "softap config SSID verification failed: malformed string " + ssid);
395             return false;
396         }
397         return true;
398     }
399 
400     /**
401      * Verify provided preSharedKey in ap config for WPA2_PSK network meets requirements.
402      */
validateApConfigPreSharedKey(String preSharedKey)403     private static boolean validateApConfigPreSharedKey(String preSharedKey) {
404         if (preSharedKey.length() < PSK_MIN_LEN || preSharedKey.length() > PSK_MAX_LEN) {
405             Log.d(TAG, "softap network password string size must be at least " + PSK_MIN_LEN
406                     + " and no more than " + PSK_MAX_LEN);
407             return false;
408         }
409 
410         try {
411             preSharedKey.getBytes(StandardCharsets.UTF_8);
412         } catch (IllegalArgumentException e) {
413             Log.e(TAG, "softap network password verification failed: malformed string");
414             return false;
415         }
416         return true;
417     }
418 
419     /**
420      * Validate a WifiConfiguration is properly configured for use by SoftApManager.
421      *
422      * This method checks the length of the SSID and validates security settings (if it
423      * requires a password, was one provided?).
424      *
425      * @param apConfig {@link WifiConfiguration} to use for softap mode
426      * @return boolean true if the provided config meets the minimum set of details, false
427      * otherwise.
428      */
validateApWifiConfiguration(@onNull WifiConfiguration apConfig)429     static boolean validateApWifiConfiguration(@NonNull WifiConfiguration apConfig) {
430         // first check the SSID
431         if (!validateApConfigSsid(apConfig.SSID)) {
432             // failed SSID verificiation checks
433             return false;
434         }
435 
436         // now check security settings: settings app allows open and WPA2 PSK
437         if (apConfig.allowedKeyManagement == null) {
438             Log.d(TAG, "softap config key management bitset was null");
439             return false;
440         }
441 
442         String preSharedKey = apConfig.preSharedKey;
443         boolean hasPreSharedKey = !TextUtils.isEmpty(preSharedKey);
444         int authType;
445 
446         try {
447             authType = apConfig.getAuthType();
448         } catch (IllegalStateException e) {
449             Log.d(TAG, "Unable to get AuthType for softap config: " + e.getMessage());
450             return false;
451         }
452 
453         if (authType == KeyMgmt.NONE) {
454             // open networks should not have a password
455             if (hasPreSharedKey) {
456                 Log.d(TAG, "open softap network should not have a password");
457                 return false;
458             }
459         } else if (authType == KeyMgmt.WPA2_PSK) {
460             // this is a config that should have a password - check that first
461             if (!hasPreSharedKey) {
462                 Log.d(TAG, "softap network password must be set");
463                 return false;
464             }
465 
466             if (!validateApConfigPreSharedKey(preSharedKey)) {
467                 // failed preSharedKey checks
468                 return false;
469             }
470         } else {
471             // this is not a supported security type
472             Log.d(TAG, "softap configs must either be open or WPA2 PSK networks");
473             return false;
474         }
475 
476         return true;
477     }
478 
479     /**
480      * Helper method to start up settings on the softap config page.
481      */
startSoftApSettings()482     private void startSoftApSettings() {
483         mContext.startActivity(
484                 new Intent("com.android.settings.WIFI_TETHER_SETTINGS")
485                         .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
486     }
487 
488     /**
489      * Helper method to trigger settings to open the softap config page
490      */
handleUserHotspotConfigTappedContent()491     private void handleUserHotspotConfigTappedContent() {
492         startSoftApSettings();
493         NotificationManager notificationManager = (NotificationManager)
494                 mContext.getSystemService(Context.NOTIFICATION_SERVICE);
495         notificationManager.cancel(SystemMessage.NOTE_SOFTAP_CONFIG_CHANGED);
496     }
497 
getPrivateBroadcast(String action)498     private PendingIntent getPrivateBroadcast(String action) {
499         Intent intent = new Intent(action).setPackage("android");
500         return mFrameworkFacade.getBroadcast(
501                 mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
502     }
503 }
504