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