/* * Copyright 2014, 2015 Bram Bonné * * This file is part of Wi-Fi PrivacyPolice. * * Wi-Fi PrivacyPolice is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * Wi-Fi PrivacyPolice is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Wi-Fi PrivacyPolice. If not, see <http://www.gnu.org/licenses/>. */ package be.uhasselt.privacypolice; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.wifi.ScanResult; import android.net.wifi.WifiConfiguration; import android.net.wifi.WifiManager; import android.os.Handler; import android.util.Log; import java.util.List; import java.util.Set; /** * This class contains the actual logic for deciding whether a network connection is allowed. * It receives the broadcast intents for new scan results, in order to decide whether a network is * available. It then checks whether we trust the AP's MAC address, based on the user's configuration. */ public class ScanResultsChecker extends BroadcastReceiver { public enum AccessPointSafety { TRUSTED, UNTRUSTED, UNKNOWN } // The last time we checked all networks. private static long lastCheck = 0; private static PreferencesStorage prefs = null; private static WifiManager wifiManager = null; private static ConnectivityManager connectivityManager = null; private static NotificationHandler notificationHandler = null; private static Context context = null; /** * Default constructor allowing to use this class as a receiver. * DO NOT USE THIS CONSTRUCTOR WHEN INSTANTIATING THIS CLASS MANUALLY. Pass the context as the * single parameter to this constructor instead. */ public ScanResultsChecker() { super(); } /** * Non-default constructor which allows other classes to instantiate this class with a given context * @param ctx Context of the caller */ public ScanResultsChecker(Context ctx) { init(ctx); } /** * Initialize static variables, depending on the current context * @param ctx The current context */ public void init(Context ctx) { // Use getApplicationContext() to prevent memory leaks on devices < Android N wifiManager = (WifiManager) ctx.getApplicationContext().getSystemService(Context.WIFI_SERVICE); connectivityManager = (ConnectivityManager) ctx.getSystemService(Context.CONNECTIVITY_SERVICE); prefs = new PreferencesStorage(ctx); notificationHandler = new NotificationHandler(ctx); context = ctx; } /** * Called for the following intents: * - SCAN_RESULTS available * - BOOT_COMPLETED */ public void onReceive(Context ctx, Intent intent) { if (wifiManager == null) // TODO: make this class a singleton init(ctx); // Make sure the wakelockHandler keeps running (to prevent Android 6.0 and up from completely suspending our operations) WakelockHandler.getInstance(ctx).ensureAwake(); // WiFi scan performed // Older devices might try to scan constantly. Allow them some rest by checking max. once every 0.5 seconds if (System.currentTimeMillis() - lastCheck < 500) return; lastCheck = System.currentTimeMillis(); // Check our location permission, and request if needed LocationAccess.checkAccessDisplayNotification(context); try { List<ScanResult> scanResults = wifiManager.getScanResults(); Log.d("PrivacyPolice", "Wi-Fi scan performed, results are: " + scanResults.toString()); checkResults(scanResults); } catch (NullPointerException npe) { Log.e("PrivacyPolice", "Null pointer exception when handling networks. Wi-Fi was probably suddenly disabled after a scan", npe); } } /** * Check which networks should be enabled, and enable them accordingly. Ask for user input when * a network's safety level can not be determined * @param scanResults The results of the last network scan */ private void checkResults(List<ScanResult> scanResults) { // Keep whether the getNetworkSafety function asked the user for input (to know whether we // have to disable any notifications afterwards, and to keep the UX as smooth as possible). // Alternatively, we would disable previous notifications here, but that would lead to the // notification being jittery (disappearing and re-appearing instantly, instead of just // updating). boolean notificationShown = false; // Collect number of found networks, if allowed by user /*Analytics analytics = new Analytics(ctx); analytics.scanCompleted(scanResults.size());*/ List<WifiConfiguration> networkList = wifiManager.getConfiguredNetworks(); if (networkList == null) { Log.i("PrivacyPolice", "WifiManager did not return any configured networks. This is "+ "most likely caused by background location services being allowed to scan for " + "Wi-Fi networks, while Wi-Fi is disabled. Keep all networks as before."); return; } // Check for every network in our network list whether it should be enabled for (WifiConfiguration network : networkList) { AccessPointSafety networkSafety = getNetworkSafety(network, scanResults); if (networkSafety == AccessPointSafety.TRUSTED) { Log.i("PrivacyPolice", "Enabling " + network.SSID); connectTo(network.networkId); } else if (networkSafety == AccessPointSafety.UNTRUSTED) { // Make sure all other networks are disabled, by disabling them separately // (See comment in connectTo() method to see why we don't disable all of them at the // same time) wifiManager.disableNetwork(network.networkId); } else if (networkSafety == AccessPointSafety.UNKNOWN) { wifiManager.disableNetwork(network.networkId); notificationShown = true; } } if (!notificationShown) { // Disable previous notifications, to make sure that we only request permission for the // currently available networks (and not at the wrong location) notificationHandler.disableNetworkNotifications(); } } /** * Enable a given Wi-Fi network, and force Android to connect to it. This function makes sure * that connecting also works in Android 5.0 and up. * @param networkId The id of the network (found in its configuration) to enable */ private void connectTo(int networkId) { // Do not disable other networks, as multiple networks may be available wifiManager.enableNetwork(networkId, false); // If we aren't already connected to a network, make sure that Android connects. // This is required for devices running Android Lollipop (5.0) and up, because // they would otherwise never connect. wifiManager.reconnect(); // In some instances (since wpa_supplicant 2.3), even the previous is not sufficient // Check if we are in a CONNECTING state, or reassociate to force connection Handler handler = new Handler(); // Wait for 1 second before checking handler.postDelayed(new Runnable() { public void run() { NetworkInfo wifiState = connectivityManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI); if (!wifiState.isConnectedOrConnecting()) { Log.i("PrivacyPolice", "Reassociating, because WifiManager doesn't seem to be eager to reconnect."); wifiManager.reassociate(); } } }, 1000); } /** * Checks whether we should allow connection to a given network, based on the user's preferences * It will also ask the user if it is unknown whether the network should be trusted. * @param network The network that should be checked * @param scanResults The networks that are currently available * @return TRUSTED or UNTRUSTED, based on the user's preferences, or UNKNOWN if the user didn't * specify anything yet */ public AccessPointSafety getNetworkSafety(WifiConfiguration network, List<ScanResult> scanResults) { // If all settings are disabled by the user, then allow every network // This effectively disables all of the app's functionalities if (!(prefs.getEnableOnlyAvailableNetworks() || prefs.getOnlyConnectToKnownAccessPoints())) { return AccessPointSafety.TRUSTED; // Allow every network } // If location access is disabled by the user (or if it is not granted to our app), allow // every network (as otherwise PrivacyPolice would block normal operation of the phone). // Rationale: a huge warning is displayed both as a notification, and in the main activity // when the user does not enable location access. It is unfortunately the only way for us // to view scan results // Some devices still allow scan results to be passed on even if the location is disabled. // In this case, we operate as normally by checking if any network is in range if (!LocationAccess.isNetworkLocationEnabled(context) && scanResults.size() == 0) { return AccessPointSafety.TRUSTED; // Allow every network } // Always enable hidden networks, since they *need* to use directed probe requests // in order to be discovered. Note that using hidden SSID's does not add any // real security , so it's best to avoid them whenever possible. if (network.hiddenSSID) return AccessPointSafety.TRUSTED; // Strip double quotes (") from the SSID string String plainSSID = network.SSID.substring(1, network.SSID.length() - 1); return getNetworkSafety(plainSSID, scanResults); } /** * Checks whether we should allow connection to a given SSID, based on the user's preferences * It will also ask the user if it is unknown whether the network should be trusted. * @param SSID The SSID of the network that should be checked * @param scanResults The networks that are currently available * @return TRUSTED or UNTRUSTED, based on the user's preferences, or UNKNOWN if the user didn't * specify anything yet */ public AccessPointSafety getNetworkSafety(String SSID, List<ScanResult> scanResults) { for (ScanResult scanResult : scanResults) { if (scanResult.SSID.equals(SSID)) { // Check whether the user wants to filter by MAC address if (!prefs.getOnlyConnectToKnownAccessPoints()) { // Any MAC address is fair game // Enabling now makes sure that we only want to connect when it is in range return AccessPointSafety.TRUSTED; } else { // Check access point's MAC address // Check if the MAC address is in the list of allowed MAC's for this SSID Set<String> allowedBSSIDs = prefs.getAllowedBSSIDs(scanResult.SSID); if (allowedBSSIDs.contains(scanResult.BSSID)) { return AccessPointSafety.TRUSTED; } else { // Not an allowed BSSID if (prefs.getBlockedBSSIDs(scanResult.SSID).contains(scanResult.BSSID)) { // This SSID was explicitly blocked by the user! Log.w("PrivacyPolice", "Spoofed network for " + scanResult.SSID + " detected! (BSSID is " + scanResult.BSSID + ")"); return AccessPointSafety.UNTRUSTED; } else { // We don't know yet whether the user wants to allow this network // Ask the user what needs to be done notificationHandler.askNetworkPermission(scanResult.SSID, scanResult.BSSID); return AccessPointSafety.UNKNOWN; } } } } } return AccessPointSafety.UNTRUSTED; // Network not in range } }