diff options
5 files changed, 395 insertions, 13 deletions
diff --git a/service/java/com/android/server/wifi/WifiInjector.java b/service/java/com/android/server/wifi/WifiInjector.java index a5f132607..bfe6446af 100644 --- a/service/java/com/android/server/wifi/WifiInjector.java +++ b/service/java/com/android/server/wifi/WifiInjector.java @@ -91,6 +91,7 @@ public class WifiInjector { private final WifiPermissionsWrapper mWifiPermissionsWrapper; private final WifiPermissionsUtil mWifiPermissionsUtil; private final PasspointManager mPasspointManager; + private final SIMAccessor mSimAccessor; private final boolean mUseRealLogger; @@ -166,7 +167,8 @@ public class WifiInjector { mWifiPermissionsWrapper = new WifiPermissionsWrapper(mContext); mWifiPermissionsUtil = new WifiPermissionsUtil(mWifiPermissionsWrapper, mContext, mSettingsStore, UserManager.get(mContext)); - mPasspointManager = new PasspointManager(mContext, this); + mSimAccessor = new SIMAccessor(mContext); + mPasspointManager = new PasspointManager(mContext, this, mSimAccessor); } /** diff --git a/service/java/com/android/server/wifi/WifiServiceImpl.java b/service/java/com/android/server/wifi/WifiServiceImpl.java index 420f92366..c70247043 100644 --- a/service/java/com/android/server/wifi/WifiServiceImpl.java +++ b/service/java/com/android/server/wifi/WifiServiceImpl.java @@ -85,6 +85,7 @@ import com.android.internal.telephony.IccCardConstants; import com.android.internal.telephony.PhoneConstants; import com.android.internal.telephony.TelephonyIntents; import com.android.internal.util.AsyncChannel; +import com.android.server.wifi.hotspot2.PasspointManager; import com.android.server.wifi.util.WifiPermissionsUtil; import java.io.BufferedReader; @@ -160,6 +161,7 @@ public class WifiServiceImpl extends IWifiManager.Stub { private WifiPermissionsUtil mWifiPermissionsUtil; private final boolean mPermissionReviewRequired; + private final PasspointManager mPasspointManager; /** * Handles client connections @@ -341,6 +343,8 @@ public class WifiServiceImpl extends IWifiManager.Stub { mPermissionReviewRequired = Build.PERMISSIONS_REVIEW_REQUIRED || context.getResources().getBoolean( com.android.internal.R.bool.config_permissionReviewRequired); + mPasspointManager = mWifiInjector.getPasspointManager(); + enableVerboseLoggingInternal(getVerboseLoggingLevel()); } @@ -1029,8 +1033,8 @@ public class WifiServiceImpl extends IWifiManager.Stub { */ @Override public boolean addPasspointConfiguration(PasspointConfiguration config) { - // TO BE IMPLEMENTED. - return true; + enforceChangePermission(); + return mPasspointManager.addProvider(config); } /** @@ -1041,8 +1045,8 @@ public class WifiServiceImpl extends IWifiManager.Stub { */ @Override public boolean removePasspointConfiguration(String fqdn) { - // TO BE IMPLEMENTED. - return true; + enforceChangePermission(); + return mPasspointManager.removeProvider(fqdn); } /** @@ -1052,8 +1056,8 @@ public class WifiServiceImpl extends IWifiManager.Stub { */ @Override public List<PasspointConfiguration> getPasspointConfigurations() { - // TO BE IMPLEMENTED. - return null; + enforceAccessPermission(); + return mPasspointManager.getProviderConfigs(); } /** diff --git a/service/java/com/android/server/wifi/hotspot2/PasspointManager.java b/service/java/com/android/server/wifi/hotspot2/PasspointManager.java index 762917170..2344440f2 100644 --- a/service/java/com/android/server/wifi/hotspot2/PasspointManager.java +++ b/service/java/com/android/server/wifi/hotspot2/PasspointManager.java @@ -29,20 +29,33 @@ import static android.net.wifi.WifiManager.PASSPOINT_WNM_FRAME_RECEIVED_ACTION; import android.content.Context; import android.content.Intent; +import android.net.wifi.hotspot2.PasspointConfiguration; import android.os.UserHandle; +import android.text.TextUtils; +import android.util.Log; +import com.android.server.wifi.IMSIParameter; +import com.android.server.wifi.SIMAccessor; import com.android.server.wifi.WifiInjector; import com.android.server.wifi.anqp.ANQPElement; import com.android.server.wifi.anqp.Constants; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; import java.util.Map; /** * Responsible for managing passpoint networks. */ public class PasspointManager { - private final Context mContext; + private static final String TAG = "PasspointManager"; + private final PasspointEventHandler mHandler; + private final SIMAccessor mSimAccessor; + private final Map<String, PasspointProvider> mProviders; private class CallbackHandler implements PasspointEventHandler.Callbacks { private final Context mContext; @@ -91,9 +104,102 @@ public class PasspointManager { } } - public PasspointManager(Context context, WifiInjector wifiInjector) { - mContext = context; + public PasspointManager(Context context, WifiInjector wifiInjector, SIMAccessor simAccessor) { mHandler = wifiInjector.makePasspointEventHandler(new CallbackHandler(context)); + mSimAccessor = simAccessor; + mProviders = new HashMap<>(); + // TODO(zqiu): load providers from the persistent storage. + } + + /** + * Add or install a Passpoint provider with the given configuration. + * + * Each provider is uniquely identified by its FQDN (Fully Qualified Domain Name). + * In the case when there is an existing configuration with the same base + * domain, a provider with the new configuration will replace the existing provider. + * + * @param config Configuration of the Passpoint provider to be added + * @return true if provider is added, false otherwise + */ + public boolean addProvider(PasspointConfiguration config) { + if (config == null) { + Log.e(TAG, "Configuration not provided"); + return false; + } + if (!config.validate()) { + Log.e(TAG, "Invalid configuration"); + return false; + } + + // Verify IMSI against the IMSI of the installed SIM cards for SIM credential. + if (config.credential.simCredential != null) { + try { + if (mSimAccessor.getMatchingImsis( + new IMSIParameter(config.credential.simCredential.imsi)) == null) { + Log.e(TAG, "IMSI does not match any SIM card"); + return false; + } + } catch (IOException e) { + return false; + } + } + + // TODO(b/32619189): install new key and certificates to the keystore. + + // Detect existing configuration in the same base domain. + PasspointProvider existingProvider = findProviderInSameBaseDomain(config.homeSp.fqdn); + if (existingProvider != null) { + Log.d(TAG, "Replacing configuration for " + existingProvider.getConfig().homeSp.fqdn + + " with " + config.homeSp.fqdn); + // TODO(b/32619189): Remove existing key and certificates from the keystore. + + mProviders.remove(existingProvider.getConfig().homeSp.fqdn); + } + + // TODO(b/32714562): create/use a copy of configuration to avoid others from modifying it, + // since others might still have reference to it? + mProviders.put(config.homeSp.fqdn, new PasspointProvider(config)); + + // TODO(b/31065385): Persist updated providers configuration to the persistent storage. + + return true; + } + + /** + * Remove a Passpoint provider identified by the given FQDN. + * + * @param fqdn The FQDN of the provider to remove + * @return true if a provider is removed, false otherwise + */ + public boolean removeProvider(String fqdn) { + if (!mProviders.containsKey(fqdn)) { + Log.e(TAG, "Config doesn't exist"); + return false; + } + + // TODO(b/32619189): Remove key and certificates from the keystore. + + mProviders.remove(fqdn); + return true; + } + + /** + * Return the installed Passpoint provider configurations. + * + * @return A list of {@link PasspointConfiguration} or null if none is installed + */ + public List<PasspointConfiguration> getProviderConfigs() { + if (mProviders.size() == 0) { + return null; + } + + List<PasspointConfiguration> configs = new ArrayList<>(); + for (Map.Entry<String, PasspointProvider> entry : mProviders.entrySet()) { + // TODO(zqiu): return a copy of the configuration instead, to prevent others from + // modifying it? + configs.add(entry.getValue().getConfig()); + } + return configs; } /** @@ -130,4 +236,45 @@ public class PasspointManager { public boolean queryPasspointIcon(long bssid, String fileName) { return mHandler.requestIcon(bssid, fileName); } + + /** + * Find a provider that have FQDN in the same base domain as the given domain. + * + * @param domain The domain to be compared + * @return {@link PasspointProvider} if a match is found, null otherwise + */ + private PasspointProvider findProviderInSameBaseDomain(String domain) { + for (Map.Entry<String, PasspointProvider> entry : mProviders.entrySet()) { + if (isSameBaseDomain(entry.getKey(), domain)) { + return entry.getValue(); + } + } + return null; + } + + /** + * Check if one domain is the base domain for the other. For example, "test1.test.com" + * and "test.com" should return true. + * + * @param domain1 First domain to be compared + * @param domain2 Second domain to be compared + * @return true if one domain is a base domain for the other, false otherwise. + */ + private static boolean isSameBaseDomain(String domain1, String domain2) { + if (domain1 == null || domain2 == null) { + return false; + } + + List<String> labelList1 = Utils.splitDomain(domain1); + List<String> labelList2 = Utils.splitDomain(domain2); + Iterator<String> l1 = labelList1.iterator(); + Iterator<String> l2 = labelList2.iterator(); + + while (l1.hasNext() && l2.hasNext()) { + if (!TextUtils.equals(l1.next(), l2.next())) { + return false; + } + } + return true; + } } diff --git a/service/java/com/android/server/wifi/hotspot2/PasspointProvider.java b/service/java/com/android/server/wifi/hotspot2/PasspointProvider.java new file mode 100644 index 000000000..2cf8c9f68 --- /dev/null +++ b/service/java/com/android/server/wifi/hotspot2/PasspointProvider.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wifi.hotspot2; + +import android.net.wifi.hotspot2.PasspointConfiguration; + +/** + * Abstraction for Passpoint service provider. This class contains the both static + * Passpoint configuration data and the runtime data (e.g. blacklisted SSIDs, statistics). + */ +public class PasspointProvider { + private final PasspointConfiguration mConfig; + + public PasspointProvider(PasspointConfiguration config) { + mConfig = config; + } + + public PasspointConfiguration getConfig() { + return mConfig; + } +} diff --git a/tests/wifitests/src/com/android/server/wifi/hotspot2/PasspointManagerTest.java b/tests/wifitests/src/com/android/server/wifi/hotspot2/PasspointManagerTest.java index ffc212230..1d9f9372d 100644 --- a/tests/wifitests/src/com/android/server/wifi/hotspot2/PasspointManagerTest.java +++ b/tests/wifitests/src/com/android/server/wifi/hotspot2/PasspointManagerTest.java @@ -22,16 +22,25 @@ import static android.net.wifi.WifiManager.EXTRA_PASSPOINT_ICON_FILE; import static android.net.wifi.WifiManager.PASSPOINT_ICON_RECEIVED_ACTION; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; import android.content.Context; import android.content.Intent; +import android.net.wifi.EAPConstants; +import android.net.wifi.hotspot2.PasspointConfiguration; +import android.net.wifi.hotspot2.pps.Credential; +import android.net.wifi.hotspot2.pps.HomeSP; import android.os.UserHandle; import android.test.suitebuilder.annotation.SmallTest; +import com.android.server.wifi.FakeKeys; +import com.android.server.wifi.IMSIParameter; +import com.android.server.wifi.SIMAccessor; import com.android.server.wifi.WifiInjector; import org.junit.Before; @@ -39,6 +48,9 @@ import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mock; +import java.util.ArrayList; +import java.util.List; + /** * Unit tests for {@link com.android.server.wifi.hotspot2.PasspointManager}. */ @@ -46,17 +58,23 @@ import org.mockito.Mock; public class PasspointManagerTest { private static final long BSSID = 0x112233445566L; private static final String ICON_FILENAME = "test"; + private static final String TEST_FQDN = "test1.test.com"; + private static final String TEST_FQDN1 = "test.com"; + private static final String TEST_FRIENDLY_NAME = "friendly name"; + private static final String TEST_REALM = "realm.test.com"; + private static final String TEST_IMSI = "1234*"; @Mock Context mContext; @Mock WifiInjector mWifiInjector; @Mock PasspointEventHandler.Callbacks mCallbacks; + @Mock SIMAccessor mSimAccessor; PasspointManager mManager; /** Sets up test. */ @Before public void setUp() throws Exception { initMocks(this); - mManager = new PasspointManager(mContext, mWifiInjector); + mManager = new PasspointManager(mContext, mWifiInjector, mSimAccessor); ArgumentCaptor<PasspointEventHandler.Callbacks> callbacks = ArgumentCaptor.forClass(PasspointEventHandler.Callbacks.class); verify(mWifiInjector).makePasspointEventHandler(callbacks.capture()); @@ -85,10 +103,24 @@ public class PasspointManagerTest { } /** + * Verify that the given Passpoint configuration matches the one that's added to + * the PasspointManager. + * + * @param expectedConfig The expected installed Passpoint configuration + */ + private void verifyInstalledConfig(PasspointConfiguration expectedConfig) { + List<PasspointConfiguration> installedConfigs = mManager.getProviderConfigs(); + assertEquals(1, installedConfigs.size()); + assertEquals(expectedConfig, installedConfigs.get(0)); + } + + /** * Validate the broadcast intent when icon file retrieval succeeded. + * + * @throws Exception */ @Test - public void iconResponseSuccess() { + public void iconResponseSuccess() throws Exception { byte[] iconData = new byte[] {0x00, 0x11}; mCallbacks.onIconResponse(BSSID, ICON_FILENAME, iconData); verifyIconIntent(BSSID, ICON_FILENAME, iconData); @@ -96,10 +128,172 @@ public class PasspointManagerTest { /** * Validate the broadcast intent when icon file retrieval failed. + * + * @throws Exception */ @Test - public void iconResponseFailure() { + public void iconResponseFailure() throws Exception { mCallbacks.onIconResponse(BSSID, ICON_FILENAME, null); verifyIconIntent(BSSID, ICON_FILENAME, null); } + + /** + * Verify that adding a provider with a null configuration will fail. + * + * @throws Exception + */ + @Test + public void addProviderWithNullConfig() throws Exception { + assertFalse(mManager.addProvider(null)); + } + + /** + * Verify that adding a provider with a empty configuration will fail. + * + * @throws Exception + */ + @Test + public void addProviderWithEmptyConfig() throws Exception { + assertFalse(mManager.addProvider(new PasspointConfiguration())); + } + + /** + * Verify taht adding a provider with an invalid credential will fail (using EAP-TLS + * for user credential). + * + * @throws Exception + */ + @Test + public void addProviderWithInvalidCredential() throws Exception { + PasspointConfiguration config = new PasspointConfiguration(); + config.homeSp = new HomeSP(); + config.homeSp.fqdn = TEST_FQDN; + config.homeSp.friendlyName = TEST_FRIENDLY_NAME; + config.credential = new Credential(); + config.credential.realm = TEST_REALM; + config.credential.caCertificate = FakeKeys.CA_CERT0; + config.credential.userCredential = new Credential.UserCredential(); + config.credential.userCredential.username = "username"; + config.credential.userCredential.password = "password"; + // EAP-TLS not allowed for user credential. + config.credential.userCredential.eapType = EAPConstants.EAP_TLS; + config.credential.userCredential.nonEapInnerMethod = "MS-CHAP"; + assertFalse(mManager.addProvider(config)); + } + + /** + * Verify that adding a provider with a valid configuration and user credential will succeed. + * + * @throws Exception + */ + @Test + public void addRemoveProviderWithValidUserCredential() throws Exception { + PasspointConfiguration config = new PasspointConfiguration(); + config.homeSp = new HomeSP(); + config.homeSp.fqdn = TEST_FQDN; + config.homeSp.friendlyName = TEST_FRIENDLY_NAME; + config.credential = new Credential(); + config.credential.realm = TEST_REALM; + config.credential.caCertificate = FakeKeys.CA_CERT0; + config.credential.userCredential = new Credential.UserCredential(); + config.credential.userCredential.username = "username"; + config.credential.userCredential.password = "password"; + config.credential.userCredential.eapType = EAPConstants.EAP_TTLS; + config.credential.userCredential.nonEapInnerMethod = "MS-CHAP"; + assertTrue(mManager.addProvider(config)); + verifyInstalledConfig(config); + + // Remove the provider. + assertTrue(mManager.removeProvider(TEST_FQDN)); + assertEquals(null, mManager.getProviderConfigs()); + } + + /** + * Verify that adding a provider with a valid configuration and SIM credential will succeed. + * + * @throws Exception + */ + @Test + public void addRemoveProviderWithValidSimCredential() throws Exception { + PasspointConfiguration config = new PasspointConfiguration(); + config.homeSp = new HomeSP(); + config.homeSp.fqdn = TEST_FQDN; + config.homeSp.friendlyName = TEST_FRIENDLY_NAME; + config.credential = new Credential(); + config.credential.realm = TEST_REALM; + config.credential.simCredential = new Credential.SimCredential(); + config.credential.simCredential.imsi = TEST_IMSI; + config.credential.simCredential.eapType = EAPConstants.EAP_SIM; + when(mSimAccessor.getMatchingImsis(new IMSIParameter(TEST_IMSI))) + .thenReturn(new ArrayList<String>()); + assertTrue(mManager.addProvider(config)); + verifyInstalledConfig(config); + + // Remove the provider. + assertTrue(mManager.removeProvider(TEST_FQDN)); + assertEquals(null, mManager.getProviderConfigs()); + } + + /** + * Verify that adding a provider with an invalid SIM credential (configured IMSI doesn't + * match the IMSI of the installed SIM cards) will fail. + * + * @throws Exception + */ + @Test + public void addProviderWithValidSimCredentialWithInvalidIMSI() throws Exception { + PasspointConfiguration config = new PasspointConfiguration(); + config.homeSp = new HomeSP(); + config.homeSp.fqdn = TEST_FQDN; + config.homeSp.friendlyName = TEST_FRIENDLY_NAME; + config.credential = new Credential(); + config.credential.realm = TEST_REALM; + config.credential.simCredential = new Credential.SimCredential(); + config.credential.simCredential.imsi = TEST_IMSI; + config.credential.simCredential.eapType = EAPConstants.EAP_SIM; + when(mSimAccessor.getMatchingImsis(new IMSIParameter(TEST_IMSI))).thenReturn(null); + assertFalse(mManager.addProvider(config)); + } + + /** + * Verify that adding a provider with the same base domain as the existing provider will + * succeed, and verify that the existing provider is replaced by the new provider with + * the new configuration. + * + * @throws Exception + */ + @Test + public void addProviderWithExistingConfig() throws Exception { + // Add a provider with the original configuration. + PasspointConfiguration origConfig = new PasspointConfiguration(); + origConfig.homeSp = new HomeSP(); + origConfig.homeSp.fqdn = TEST_FQDN; + origConfig.homeSp.friendlyName = TEST_FRIENDLY_NAME; + origConfig.credential = new Credential(); + origConfig.credential.realm = TEST_REALM; + origConfig.credential.simCredential = new Credential.SimCredential(); + origConfig.credential.simCredential.imsi = TEST_IMSI; + origConfig.credential.simCredential.eapType = EAPConstants.EAP_SIM; + when(mSimAccessor.getMatchingImsis(new IMSIParameter(TEST_IMSI))) + .thenReturn(new ArrayList<String>()); + assertTrue(mManager.addProvider(origConfig)); + verifyInstalledConfig(origConfig); + + // Add another provider with the same base domain as the existing provider. + // This should replace the existing provider with the new configuration. + PasspointConfiguration newConfig = new PasspointConfiguration(); + newConfig.homeSp = new HomeSP(); + newConfig.homeSp.fqdn = TEST_FQDN1; + newConfig.homeSp.friendlyName = TEST_FRIENDLY_NAME; + newConfig.credential = new Credential(); + newConfig.credential.realm = TEST_REALM; + newConfig.credential.caCertificate = FakeKeys.CA_CERT0; + newConfig.credential.userCredential = new Credential.UserCredential(); + newConfig.credential.userCredential.username = "username"; + newConfig.credential.userCredential.password = "password"; + newConfig.credential.userCredential.eapType = EAPConstants.EAP_TTLS; + newConfig.credential.userCredential.nonEapInnerMethod = "MS-CHAP"; + assertTrue(mManager.addProvider(newConfig)); + verifyInstalledConfig(newConfig); + } } |