/* * 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; import static android.Manifest.permission.NETWORK_SETTINGS; import static android.app.BroadcastOptions.DEFERRAL_POLICY_UNTIL_ACTIVE; import static android.app.BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT; import android.annotation.IntDef; import android.annotation.NonNull; import android.app.BroadcastOptions; import android.app.Notification; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.database.ContentObserver; import android.graphics.drawable.Icon; import android.net.Uri; import android.net.wifi.WifiConfiguration; import android.net.wifi.WifiContext; import android.net.wifi.WifiEnterpriseConfig; import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; import android.net.wifi.WifiStringResourceWrapper; import android.net.wifi.hotspot2.PasspointConfiguration; import android.net.wifi.hotspot2.pps.Credential; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.ParcelUuid; import android.os.PersistableBundle; import android.os.UserHandle; import android.telephony.CarrierConfigManager; import android.telephony.ImsiEncryptionInfo; import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; import android.telephony.TelephonyCallback; import android.telephony.TelephonyManager; import android.telephony.ims.ImsManager; import android.telephony.ims.ImsMmTelManager; import android.telephony.ims.feature.MmTelFeature; import android.telephony.ims.stub.ImsRegistrationImplBase; import android.text.TextUtils; import android.util.ArraySet; import android.util.Base64; import android.util.Log; import android.util.Pair; import android.util.SparseArray; import android.util.SparseBooleanArray; import androidx.annotation.RequiresApi; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.messages.nano.SystemMessageProto.SystemMessage; import com.android.modules.utils.HandlerExecutor; import com.android.modules.utils.build.SdkLevel; import com.android.server.wifi.entitlement.PseudonymInfo; import com.android.wifi.resources.R; import java.io.FileDescriptor; import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.PublicKey; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import javax.annotation.Nullable; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.NoSuchPaddingException; /** * This class provide APIs to get carrier info from telephony service. * TODO(b/132188983): Refactor into TelephonyFacade which owns all instances of * TelephonyManager/SubscriptionManager in Wifi */ public class WifiCarrierInfoManager { public static final String TAG = "WifiCarrierInfoManager"; public static final String DEFAULT_EAP_PREFIX = "\0"; public static final int CARRIER_INVALID_TYPE = -1; public static final int CARRIER_MNO_TYPE = 0; // Mobile Network Operator public static final int CARRIER_MVNO_TYPE = 1; // Mobile Virtual Network Operator public static final String ANONYMOUS_IDENTITY = "anonymous"; public static final String THREE_GPP_NAI_REALM_FORMAT = "wlan.mnc%s.mcc%s.3gppnetwork.org"; /** Intent when user tapped action button to allow the app. */ @VisibleForTesting public static final String NOTIFICATION_USER_ALLOWED_CARRIER_INTENT_ACTION = "com.android.server.wifi.action.CarrierNetwork.USER_ALLOWED_CARRIER"; /** Intent when user tapped action button to disallow the app. */ @VisibleForTesting public static final String NOTIFICATION_USER_DISALLOWED_CARRIER_INTENT_ACTION = "com.android.server.wifi.action.CarrierNetwork.USER_DISALLOWED_CARRIER"; /** Intent when user dismissed the notification. */ @VisibleForTesting public static final String NOTIFICATION_USER_DISMISSED_INTENT_ACTION = "com.android.server.wifi.action.CarrierNetwork.USER_DISMISSED"; /** Intent when user clicked on the notification. */ @VisibleForTesting public static final String NOTIFICATION_USER_CLICKED_INTENT_ACTION = "com.android.server.wifi.action.CarrierNetwork.USER_CLICKED"; @VisibleForTesting public static final String EXTRA_CARRIER_NAME = "com.android.server.wifi.extra.CarrierNetwork.CARRIER_NAME"; @VisibleForTesting public static final String EXTRA_CARRIER_ID = "com.android.server.wifi.extra.CarrierNetwork.CARRIER_ID"; @VisibleForTesting public static final String CONFIG_WIFI_OOB_PSEUDONYM_ENABLED = "config_wifiOobPseudonymEnabled"; // IMSI encryption method: RSA-OAEP with SHA-256 hash function private static final String IMSI_CIPHER_TRANSFORMATION = "RSA/ECB/OAEPwithSHA-256andMGF1Padding"; private static final HashMap EAP_METHOD_PREFIX = new HashMap<>(); static { EAP_METHOD_PREFIX.put(WifiEnterpriseConfig.Eap.AKA, "0"); EAP_METHOD_PREFIX.put(WifiEnterpriseConfig.Eap.SIM, "1"); EAP_METHOD_PREFIX.put(WifiEnterpriseConfig.Eap.AKA_PRIME, "6"); } public static final int ACTION_USER_ALLOWED_CARRIER = 1; public static final int ACTION_USER_DISALLOWED_CARRIER = 2; public static final int ACTION_USER_DISMISS = 3; @IntDef(prefix = { "ACTION_USER_" }, value = { ACTION_USER_ALLOWED_CARRIER, ACTION_USER_DISALLOWED_CARRIER, ACTION_USER_DISMISS }) @Retention(RetentionPolicy.SOURCE) public @interface UserActionCode { } /** * 3GPP TS 11.11 2G_authentication command/response * Input: [RAND] * Output: [SRES][Cipher Key Kc] */ private static final int START_SRES_POS = 0; // MUST be 0 private static final int SRES_LEN = 4; private static final int START_KC_POS = START_SRES_POS + SRES_LEN; private static final int KC_LEN = 8; private static final Uri CONTENT_URI = Uri.parse("content://carrier_information/carrier"); /** * Expiration timeout for user notification in milliseconds. (15 min) */ private static final long NOTIFICATION_EXPIRY_MILLS = 15 * 60 * 1000; /** * Notification update delay in milliseconds. (10 min) */ private static final long NOTIFICATION_UPDATE_DELAY_MILLS = 10 * 60 * 1000; private final WifiContext mContext; private final Handler mHandler; private final WifiInjector mWifiInjector; private final TelephonyManager mTelephonyManager; private final SubscriptionManager mSubscriptionManager; private final WifiNotificationManager mNotificationManager; private final WifiMetrics mWifiMetrics; private final Clock mClock; private final WifiPseudonymManager mWifiPseudonymManager; private ImsManager mImsManager; private Map mImsMmTelManagerMap = new HashMap<>(); /** * Cached Map of since retrieving the * PersistableBundle from CarrierConfigManager is somewhat expensive as it has hundreds of * fields. This cache is cleared when the CarrierConfig changes to ensure data freshness. */ private final SparseArray mCachedCarrierConfigPerSubId = new SparseArray<>(); /** * Intent filter for processing notification actions. */ private final IntentFilter mIntentFilter; private final FrameworkFacade mFrameworkFacade; private boolean mVerboseLogEnabled = false; private SparseBooleanArray mImsiEncryptionInfoAvailable = new SparseBooleanArray(); private final Map mImsiPrivacyProtectionExemptionMap = new HashMap<>(); private final Object mCarrierNetworkOffloadMapLock = new Object(); @GuardedBy("mCarrierNetworkOffloadMapLock") private SparseBooleanArray mMergedCarrierNetworkOffloadMap = new SparseBooleanArray(); @GuardedBy("mCarrierNetworkOffloadMapLock") private SparseBooleanArray mUnmergedCarrierNetworkOffloadMap = new SparseBooleanArray(); private final List mOnImsiProtectedOrUserApprovedListeners = new ArrayList<>(); private final SparseBooleanArray mUserDataEnabled = new SparseBooleanArray(); private final List mUserDataEnabledListenerList = new ArrayList<>(); private final List mOnCarrierOffloadDisabledListeners = new ArrayList<>(); private final SparseArray mSubIdToSimInfoSparseArray = new SparseArray<>(); private final Map> mSubscriptionGroupMap = new HashMap<>(); private List mCarrierPrivilegeCallbacks; private final SparseArray> mCarrierPrivilegedPackagesBySimSlot = new SparseArray<>(); private List mActiveSubInfos = null; private boolean mHasNewUserDataToSerialize = false; private boolean mHasNewSharedDataToSerialize = false; private boolean mUserDataLoaded = false; private boolean mIsLastUserApprovalUiDialog = false; private CarrierConfigManager mCarrierConfigManager; private DeviceConfigFacade mDeviceConfigFacade; /** * The {@link Clock#getElapsedSinceBootMillis()} must be at least this value for us * to update/show the notification. */ private long mNotificationUpdateTime = 0L; /** * When the OOB feature is enabled, the auto-join bit in {@link WifiConfiguration} should be * flipped on, and it should only be done once, this field indicated the flip had been done. */ private volatile boolean mAutoJoinFlippedOnOobPseudonymEnabled = false; /** * The SIM information of IMSI, MCCMNC, carrier ID etc. */ public static class SimInfo { public final String imsi; public final String mccMnc; public final int carrierIdFromSimMccMnc; public final int simCarrierId; SimInfo(String imsi, String mccMnc, int carrierIdFromSimMccMnc, int simCarrierId) { this.imsi = imsi; this.mccMnc = mccMnc; this.carrierIdFromSimMccMnc = carrierIdFromSimMccMnc; this.simCarrierId = simCarrierId; } /** * Get the carrier type of current SIM. */ public int getCarrierType() { if (carrierIdFromSimMccMnc == simCarrierId) { return CARRIER_MNO_TYPE; } return CARRIER_MVNO_TYPE; } @Override public String toString() { StringBuilder sb = new StringBuilder("SimInfo[ ") .append("IMSI=").append(imsi) .append(", MCCMNC=").append(mccMnc) .append(", carrierIdFromSimMccMnc=").append(carrierIdFromSimMccMnc) .append(", simCarrierId=").append(simCarrierId) .append(" ]"); return sb.toString(); } } /** * Implement of {@link TelephonyCallback.DataEnabledListener} */ @VisibleForTesting @RequiresApi(Build.VERSION_CODES.S) public final class UserDataEnabledChangedListener extends TelephonyCallback implements TelephonyCallback.DataEnabledListener { private final int mSubscriptionId; public UserDataEnabledChangedListener(int subscriptionId) { mSubscriptionId = subscriptionId; } @Override public void onDataEnabledChanged(boolean enabled, int reason) { Log.d(TAG, "Mobile data change by reason " + reason + " to " + (enabled ? "enabled" : "disabled") + " for subId: " + mSubscriptionId); mUserDataEnabled.put(mSubscriptionId, enabled); if (!enabled) { for (OnCarrierOffloadDisabledListener listener : mOnCarrierOffloadDisabledListeners) { listener.onCarrierOffloadDisabled(mSubscriptionId, true); } } } /** * Unregister the listener from TelephonyManager, */ public void unregisterListener() { mTelephonyManager.createForSubscriptionId(mSubscriptionId) .unregisterTelephonyCallback(this); } } /** * Interface for other modules to listen to the status of IMSI protection, approved by user * or protected by a new IMSI protection feature. */ public interface OnImsiProtectedOrUserApprovedListener { /** * Invoke when user approve the IMSI protection exemption * or the IMSI protection feature is enabled. */ void onImsiProtectedOrUserApprovalChanged(int carrierId, boolean allowAutoJoin); } /** * Interface for other modules to listen to the carrier network offload disabled. */ public interface OnCarrierOffloadDisabledListener { /** * Invoke when carrier offload on target subscriptionId is disabled. */ void onCarrierOffloadDisabled(int subscriptionId, boolean merged); } /** * Module to interact with the wifi config store. */ private class WifiCarrierInfoStoreManagerDataSource implements WifiCarrierInfoStoreManagerData.DataSource { @Override public SparseBooleanArray getCarrierNetworkOffloadMap(boolean isMerged) { synchronized (mCarrierNetworkOffloadMapLock) { return isMerged ? mMergedCarrierNetworkOffloadMap : mUnmergedCarrierNetworkOffloadMap; } } @Override public void serializeComplete() { mHasNewSharedDataToSerialize = false; } @Override public void setCarrierNetworkOffloadMap(SparseBooleanArray carrierOffloadMap, boolean isMerged) { synchronized (mCarrierNetworkOffloadMapLock) { if (isMerged) { mMergedCarrierNetworkOffloadMap = carrierOffloadMap; } else { mUnmergedCarrierNetworkOffloadMap = carrierOffloadMap; } } } @Override public void setAutoJoinFlippedOnOobPseudonymEnabled(boolean autoJoinFlipped) { mAutoJoinFlippedOnOobPseudonymEnabled = autoJoinFlipped; // user data loaded if (!mAutoJoinFlippedOnOobPseudonymEnabled) { mHandler.post(() -> { if (mDeviceConfigFacade.isOobPseudonymEnabled()) { tryResetAutoJoinOnOobPseudonymFlagChanged(true); } }); } } @Override public boolean getAutoJoinFlippedOnOobPseudonymEnabled() { return mAutoJoinFlippedOnOobPseudonymEnabled; } @Override public void reset() { synchronized (mCarrierNetworkOffloadMapLock) { mMergedCarrierNetworkOffloadMap.clear(); mUnmergedCarrierNetworkOffloadMap.clear(); } } @Override public boolean hasNewDataToSerialize() { return mHasNewSharedDataToSerialize; } } /** * Module to interact with the wifi config store. */ private class ImsiProtectionExemptionDataSource implements ImsiPrivacyProtectionExemptionStoreData.DataSource { @Override public Map toSerialize() { // Clear the flag after writing to disk. mHasNewUserDataToSerialize = false; return mImsiPrivacyProtectionExemptionMap; } @Override public void fromDeserialized(Map imsiProtectionExemptionMap) { mImsiPrivacyProtectionExemptionMap.clear(); mImsiPrivacyProtectionExemptionMap.putAll(imsiProtectionExemptionMap); mUserDataLoaded = true; } @Override public void reset() { mUserDataLoaded = false; mImsiPrivacyProtectionExemptionMap.clear(); } @Override public boolean hasNewDataToSerialize() { return mHasNewUserDataToSerialize; } } private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String carrierName = intent.getStringExtra(EXTRA_CARRIER_NAME); int carrierId = intent.getIntExtra(EXTRA_CARRIER_ID, -1); if (carrierName == null || carrierId == -1) { Log.e(TAG, "No carrier name or carrier id found in intent"); return; } switch (intent.getAction()) { case NOTIFICATION_USER_ALLOWED_CARRIER_INTENT_ACTION: handleUserAllowCarrierExemptionAction(carrierName, carrierId); break; case NOTIFICATION_USER_DISALLOWED_CARRIER_INTENT_ACTION: handleUserDisallowCarrierExemptionAction(carrierName, carrierId); break; case NOTIFICATION_USER_CLICKED_INTENT_ACTION: sendImsiPrivacyConfirmationDialog(carrierName, carrierId); // Collapse the notification bar Bundle options = null; if (SdkLevel.isAtLeastU()) { options = BroadcastOptions.makeBasic() .setDeliveryGroupPolicy(DELIVERY_GROUP_POLICY_MOST_RECENT) .setDeferralPolicy(DEFERRAL_POLICY_UNTIL_ACTIVE) .toBundle(); } final Intent broadcast = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS) .addFlags(Intent.FLAG_RECEIVER_FOREGROUND); mContext.sendBroadcast(broadcast, null, options); break; case NOTIFICATION_USER_DISMISSED_INTENT_ACTION: handleUserDismissAction(); return; // no need to cancel a dismissed notification, return. default: Log.e(TAG, "Unknown action " + intent.getAction()); return; } // Clear notification once the user interacts with it. mNotificationManager.cancel(SystemMessage.NOTE_CARRIER_SUGGESTION_AVAILABLE); } }; private void handleUserDismissAction() { Log.i(TAG, "User dismissed the notification"); mNotificationUpdateTime = mClock.getElapsedSinceBootMillis() + mContext.getResources() .getInteger(R.integer.config_wifiImsiProtectionNotificationDelaySeconds) * 1000L; mWifiMetrics.addUserApprovalCarrierUiReaction(ACTION_USER_DISMISS, mIsLastUserApprovalUiDialog); } private void handleUserAllowCarrierExemptionAction(String carrierName, int carrierId) { Log.i(TAG, "User clicked to allow carrier:" + carrierName); setHasUserApprovedImsiPrivacyExemptionForCarrier(true, carrierId); mNotificationUpdateTime = 0; mWifiMetrics.addUserApprovalCarrierUiReaction(ACTION_USER_ALLOWED_CARRIER, mIsLastUserApprovalUiDialog); } private void handleUserDisallowCarrierExemptionAction(String carrierName, int carrierId) { Log.i(TAG, "User clicked to disallow carrier:" + carrierName); setHasUserApprovedImsiPrivacyExemptionForCarrier(false, carrierId); mNotificationUpdateTime = 0; mWifiMetrics.addUserApprovalCarrierUiReaction( ACTION_USER_DISALLOWED_CARRIER, mIsLastUserApprovalUiDialog); } private void updateSubIdsInNetworkFactoryFilters(List activeSubInfos) { if (activeSubInfos == null || activeSubInfos.isEmpty()) { return; } Set subIds = new ArraySet<>(); for (SubscriptionInfo subInfo : activeSubInfos) { if (subInfo.getSubscriptionId() != SubscriptionManager.INVALID_SUBSCRIPTION_ID) { subIds.add(subInfo.getSubscriptionId()); } } if (mWifiInjector.getWifiNetworkFactory() != null) { mWifiInjector.getWifiNetworkFactory().updateSubIdsInCapabilitiesFilter(subIds); } if (mWifiInjector.getUntrustedWifiNetworkFactory() != null) { mWifiInjector.getUntrustedWifiNetworkFactory().updateSubIdsInCapabilitiesFilter(subIds); } if (mWifiInjector.getRestrictedWifiNetworkFactory() != null) { mWifiInjector.getRestrictedWifiNetworkFactory() .updateSubIdsInCapabilitiesFilter(subIds); } } private class SubscriptionChangeListener extends SubscriptionManager.OnSubscriptionsChangedListener { @Override public void onSubscriptionsChanged() { mActiveSubInfos = mSubscriptionManager.getCompleteActiveSubscriptionInfoList(); mImsMmTelManagerMap.clear(); updateSubIdsInNetworkFactoryFilters(mActiveSubInfos); mSubIdToSimInfoSparseArray.clear(); mSubscriptionGroupMap.clear(); if (mVerboseLogEnabled) { Log.v(TAG, "active subscription changes: " + mActiveSubInfos); } if (SdkLevel.isAtLeastT()) { for (int simSlot = 0; simSlot < mTelephonyManager.getActiveModemCount(); simSlot++) { if (!mCarrierPrivilegedPackagesBySimSlot.contains(simSlot)) { WifiCarrierPrivilegeCallback callback = new WifiCarrierPrivilegeCallback(simSlot); mTelephonyManager.registerCarrierPrivilegesCallback(simSlot, new HandlerExecutor(mHandler), callback); mCarrierPrivilegedPackagesBySimSlot.append(simSlot, Collections.emptySet()); mCarrierPrivilegeCallbacks.add(callback); } } } } } /** * Listener for carrier privilege changes. */ @VisibleForTesting @RequiresApi(Build.VERSION_CODES.TIRAMISU) public final class WifiCarrierPrivilegeCallback implements TelephonyManager.CarrierPrivilegesCallback { private int mSimSlot = -1; public WifiCarrierPrivilegeCallback(int simSlot) { mSimSlot = simSlot; } @Override public void onCarrierPrivilegesChanged( @androidx.annotation.NonNull Set privilegedPackageNames, @androidx.annotation.NonNull Set privilegedUids) { mCarrierPrivilegedPackagesBySimSlot.put(mSimSlot, privilegedPackageNames); resetCarrierPrivilegedApps(); } } /** * Gets the instance of WifiCarrierInfoManager. * @param telephonyManager Instance of {@link TelephonyManager} * @param subscriptionManager Instance of {@link SubscriptionManager} * @param wifiInjector Instance of {@link WifiInjector} * @return The instance of WifiCarrierInfoManager */ public WifiCarrierInfoManager(@NonNull TelephonyManager telephonyManager, @NonNull SubscriptionManager subscriptionManager, @NonNull WifiInjector wifiInjector, @NonNull FrameworkFacade frameworkFacade, @NonNull WifiContext context, @NonNull WifiConfigStore configStore, @NonNull Handler handler, @NonNull WifiMetrics wifiMetrics, @NonNull Clock clock, @NonNull WifiPseudonymManager wifiPseudonymManager) { mTelephonyManager = telephonyManager; mContext = context; mWifiInjector = wifiInjector; mHandler = handler; mSubscriptionManager = subscriptionManager; mFrameworkFacade = frameworkFacade; mWifiMetrics = wifiMetrics; mNotificationManager = mWifiInjector.getWifiNotificationManager(); mDeviceConfigFacade = mWifiInjector.getDeviceConfigFacade(); mClock = clock; mWifiPseudonymManager = wifiPseudonymManager; // Register broadcast receiver for UI interactions. mIntentFilter = new IntentFilter(); mIntentFilter.addAction(NOTIFICATION_USER_DISMISSED_INTENT_ACTION); mIntentFilter.addAction(NOTIFICATION_USER_ALLOWED_CARRIER_INTENT_ACTION); mIntentFilter.addAction(NOTIFICATION_USER_DISALLOWED_CARRIER_INTENT_ACTION); mIntentFilter.addAction(NOTIFICATION_USER_CLICKED_INTENT_ACTION); mContext.registerReceiver(mBroadcastReceiver, mIntentFilter, NETWORK_SETTINGS, handler); configStore.registerStoreData(wifiInjector.makeWifiCarrierInfoStoreManagerData( new WifiCarrierInfoStoreManagerDataSource())); configStore.registerStoreData(wifiInjector.makeImsiPrivacyProtectionExemptionStoreData( new ImsiProtectionExemptionDataSource())); mSubscriptionManager.addOnSubscriptionsChangedListener(new HandlerExecutor(mHandler), new SubscriptionChangeListener()); onCarrierConfigChanged(context); // Monitor for carrier config changes. IntentFilter filter = new IntentFilter(); filter.addAction(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED); filter.addAction(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED); context.registerReceiver(new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED .equals(intent.getAction())) { mHandler.post(() -> { onCarrierConfigChanged(context); if (mDeviceConfigFacade.isOobPseudonymEnabled()) { tryResetAutoJoinOnOobPseudonymFlagChanged(/*isEnabled=*/ true); } }); } else if (TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED .equals(intent.getAction())) { int extraSubId = intent.getIntExtra( "subscription", SubscriptionManager.INVALID_SUBSCRIPTION_ID); if (extraSubId == SubscriptionManager.INVALID_SUBSCRIPTION_ID || mTelephonyManager.getActiveModemCount() < 2) { return; } mHandler.post(() -> { if (mActiveSubInfos == null || mActiveSubInfos.isEmpty()) { return; } for (SubscriptionInfo subInfo : mActiveSubInfos) { if (subInfo.getSubscriptionId() == extraSubId && isOobPseudonymFeatureEnabled(subInfo.getCarrierId()) && isSimReady(subInfo.getSubscriptionId())) { mWifiPseudonymManager.retrieveOobPseudonymIfNeeded( subInfo.getCarrierId()); return; } } }); } } }, filter); frameworkFacade.registerContentObserver(context, CONTENT_URI, false, new ContentObserver(handler) { @Override public void onChange(boolean selfChange) { mHandler.post(() -> { onCarrierConfigChanged(context); if (mDeviceConfigFacade.isOobPseudonymEnabled()) { tryResetAutoJoinOnOobPseudonymFlagChanged(/*isEnabled=*/ true); } }); } }); if (SdkLevel.isAtLeastT()) { mCarrierPrivilegeCallbacks = new ArrayList<>(); } mDeviceConfigFacade.setOobPseudonymFeatureFlagChangedListener(isOobPseudonymEnabled -> { // when feature is disabled, it is handled on the fly only once. // run on WifiThread tryResetAutoJoinOnOobPseudonymFlagChanged(isOobPseudonymEnabled); onCarrierConfigChanged(context); }); } /** * Enable/disable verbose logging. */ public void enableVerboseLogging(boolean verboseEnabled) { mVerboseLogEnabled = verboseEnabled; } void onUnlockedUserSwitching(int currentUserId) { // Retrieve list of broadcast receivers for this broadcast & send them directed // broadcasts to wake them up (if they're in background). final List provisioningPackageInfos = mContext.getPackageManager().getPackagesHoldingPermissions( new String[] {android.Manifest.permission.NETWORK_CARRIER_PROVISIONING}, PackageManager.MATCH_UNINSTALLED_PACKAGES); vlogd("switched to current unlocked user. notify apps with" + " NETWORK_CARRIER_PROVISIONING permission for user - " + currentUserId); for (PackageInfo packageInfo : provisioningPackageInfos) { Intent intentToSend = new Intent(WifiManager.ACTION_REFRESH_USER_PROVISIONING); intentToSend.setPackage(packageInfo.packageName); mContext.sendBroadcastAsUser(intentToSend, UserHandle.CURRENT, android.Manifest.permission.NETWORK_CARRIER_PROVISIONING); } } private PersistableBundle getCarrierConfigForSubId(int subId) { if (mCachedCarrierConfigPerSubId.contains(subId)) { return mCachedCarrierConfigPerSubId.get(subId); } TelephonyManager specifiedTm = mTelephonyManager.createForSubscriptionId(subId); if (specifiedTm.getSimApplicationState() != TelephonyManager.SIM_STATE_LOADED) { return null; } if (mCarrierConfigManager == null) { mCarrierConfigManager = mContext.getSystemService(CarrierConfigManager.class); } if (mCarrierConfigManager == null) { return null; } PersistableBundle carrierConfig = mCarrierConfigManager.getConfigForSubId(subId); if (!CarrierConfigManager.isConfigForIdentifiedCarrier(carrierConfig)) { return null; } mCachedCarrierConfigPerSubId.put(subId, carrierConfig); return carrierConfig; } /** * Checks whether MAC randomization should be disabled for the provided WifiConfiguration, * based on an exception list in the CarrierConfigManager per subId. * @param ssid the SSID of a WifiConfiguration, surrounded by double quotes. * @param carrierId the ID associated with the network operator for this network suggestion. * @param subId the best match subscriptionId for this network suggestion. */ public boolean shouldDisableMacRandomization(String ssid, int carrierId, int subId) { if (!SdkLevel.isAtLeastS()) { return false; } if (carrierId == TelephonyManager.UNKNOWN_CARRIER_ID) { // only carrier networks are allowed to disable MAC randomization through this path. return false; } PersistableBundle carrierConfig = getCarrierConfigForSubId(subId); if (carrierConfig == null) { return false; } String sanitizedSsid = WifiInfo.sanitizeSsid(ssid); String[] macRandDisabledSsids = carrierConfig.getStringArray(CarrierConfigManager.Wifi .KEY_SUGGESTION_SSID_LIST_WITH_MAC_RANDOMIZATION_DISABLED); if (macRandDisabledSsids == null) { return false; } for (String curSsid : macRandDisabledSsids) { if (TextUtils.equals(sanitizedSsid, curSsid)) { return true; } } return false; } /** * Checks whether merged carrier WiFi networks are permitted for the carrier based on a flag * in the CarrierConfigManager. * * @param subId the best match subscriptionId for this network suggestion. */ public boolean areMergedCarrierWifiNetworksAllowed(int subId) { if (!SdkLevel.isAtLeastS()) { return false; } PersistableBundle carrierConfig = getCarrierConfigForSubId(subId); if (carrierConfig == null) { return false; } return carrierConfig.getBoolean( CarrierConfigManager.KEY_CARRIER_PROVISIONS_WIFI_MERGED_NETWORKS_BOOL, false); } /** * If the OOB Pseudonym is disabled, it should only be called once when the feature is turned to * 'disable' from 'enable' on the fly. */ private void tryResetAutoJoinOnOobPseudonymFlagChanged(boolean isEnabled) { if (!mUserDataLoaded) { return; } if (mActiveSubInfos == null || mActiveSubInfos.isEmpty()) { return; } boolean allowAutoJoin = false; if (isEnabled) { if (!shouldFlipOnAutoJoinForOobPseudonym()) { return; } // one-off operation, disable it anyway. disableFlipOnAutoJoinForOobPseudonym(); mNotificationManager.cancel(SystemMessage.NOTE_CARRIER_SUGGESTION_AVAILABLE); allowAutoJoin = true; } else { enableFlipOnAutoJoinForOobPseudonym(); } for (SubscriptionInfo subInfo : mActiveSubInfos) { Log.d(TAG, "may reset auto-join for current SIM: " + subInfo.getCarrierId()); if (!isOobPseudonymFeatureEnabledInResource(subInfo.getCarrierId())) { continue; } for (OnImsiProtectedOrUserApprovedListener listener : mOnImsiProtectedOrUserApprovedListeners) { listener.onImsiProtectedOrUserApprovalChanged( subInfo.getCarrierId(), allowAutoJoin); } } } /** * Updates the IMSI encryption information and clears cached CarrierConfig data. */ private void onCarrierConfigChanged(Context context) { SparseArray cachedCarrierConfigPerSubIdOld = mCachedCarrierConfigPerSubId.clone(); mCachedCarrierConfigPerSubId.clear(); mImsiEncryptionInfoAvailable.clear(); if (mActiveSubInfos == null || mActiveSubInfos.isEmpty()) { return; } for (SubscriptionInfo subInfo : mActiveSubInfos) { int subId = subInfo.getSubscriptionId(); PersistableBundle bundle = getCarrierConfigForSubId(subId); if (bundle == null) { Log.e(TAG, "Carrier config is missing for: " + subId); } else { try { if (requiresImsiEncryption(subId) && mTelephonyManager.createForSubscriptionId(subId) .getCarrierInfoForImsiEncryption(TelephonyManager.KEY_TYPE_WLAN) != null) { vlogd("IMSI encryption info is available for " + subId); mImsiEncryptionInfoAvailable.put(subId, true); } } catch (IllegalArgumentException e) { vlogd("IMSI encryption info is not available."); } if (isOobPseudonymFeatureEnabled(subInfo.getCarrierId()) && isSimReady(subId)) { mWifiPseudonymManager.retrieveOobPseudonymIfNeeded(subInfo.getCarrierId()); } } PersistableBundle bundleOld = cachedCarrierConfigPerSubIdOld.get(subId); if (bundleOld != null && bundleOld.getBoolean(CarrierConfigManager .KEY_CARRIER_PROVISIONS_WIFI_MERGED_NETWORKS_BOOL) && !areMergedCarrierWifiNetworksAllowed(subId)) { vlogd("Allow carrier merged change from true to false"); for (OnCarrierOffloadDisabledListener listener : mOnCarrierOffloadDisabledListeners) { listener.onCarrierOffloadDisabled(subId, true); } } } } /** * Check if the IMSI encryption is required for the SIM card. * Note: should only be called when {@link #isSimReady(int)} is true, or the result may not be * correct. * * @param subId The subscription ID of SIM card. * @return true if the IMSI encryption is required, otherwise false. */ public boolean requiresImsiEncryption(int subId) { PersistableBundle bundle = getCarrierConfigForSubId(subId); if (bundle == null) { Log.wtf(TAG, "requiresImsiEncryption is called when SIM is not ready!"); return false; } return (bundle.getInt(CarrierConfigManager.IMSI_KEY_AVAILABILITY_INT) & TelephonyManager.KEY_TYPE_WLAN) != 0; } /** * Check if the IMSI encryption is downloaded(available) for the SIM card. * * @param subId The subscription ID of SIM card. * @return true if the IMSI encryption is available, otherwise false. */ public boolean isImsiEncryptionInfoAvailable(int subId) { return mImsiEncryptionInfoAvailable.get(subId); } /** * Gets the SubscriptionId of SIM card which is from the carrier specified in config. * * @param config the instance of {@link WifiConfiguration} * @return the best match SubscriptionId */ public int getBestMatchSubscriptionId(@NonNull WifiConfiguration config) { if (config == null) { Log.wtf(TAG, "getBestMatchSubscriptionId: Config must be NonNull!"); return SubscriptionManager.INVALID_SUBSCRIPTION_ID; } if (config.subscriptionId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) { return config.subscriptionId; } if (config.getSubscriptionGroup() != null) { return getActiveSubscriptionIdInGroup(config.getSubscriptionGroup()); } if (config.isPasspoint()) { return getMatchingSubId(config.carrierId); } else { return getBestMatchSubscriptionIdForEnterprise(config); } } /** * Gets the SubscriptionId of SIM card for given carrier Id * * @param carrierId carrier id for target carrier * @return the matched SubscriptionId */ public int getMatchingSubId(int carrierId) { if (mActiveSubInfos == null || mActiveSubInfos.isEmpty()) { return SubscriptionManager.INVALID_SUBSCRIPTION_ID; } int dataSubId = SubscriptionManager.getDefaultDataSubscriptionId(); int matchSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID; for (SubscriptionInfo subInfo : mActiveSubInfos) { if (subInfo.getCarrierId() == carrierId && getCarrierConfigForSubId(subInfo.getSubscriptionId()) != null) { matchSubId = subInfo.getSubscriptionId(); if (matchSubId == dataSubId) { // Priority of Data sub is higher than non data sub. break; } } } vlogd("matching subId is " + matchSubId); return matchSubId; } private int getBestMatchSubscriptionIdForEnterprise(WifiConfiguration config) { if (config.carrierId != TelephonyManager.UNKNOWN_CARRIER_ID) { return getMatchingSubId(config.carrierId); } // Legacy WifiConfiguration without carrier ID if (config.enterpriseConfig == null || !config.enterpriseConfig.isAuthenticationSimBased()) { Log.w(TAG, "The legacy config is not using EAP-SIM."); return SubscriptionManager.INVALID_SUBSCRIPTION_ID; } int dataSubId = SubscriptionManager.getDefaultDataSubscriptionId(); if (isSimReady(dataSubId)) { vlogd("carrierId is not assigned, using the default data sub."); return dataSubId; } vlogd("data sim is not present."); return SubscriptionManager.INVALID_SUBSCRIPTION_ID; } /** * Check if the specified SIM card is ready for Wi-Fi connection on the device. * * @param subId subscription ID of SIM card in the device. * @return true if the SIM is active and all info are available, otherwise false. */ public boolean isSimReady(int subId) { if (!SubscriptionManager.isValidSubscriptionId(subId)) { return false; } if (mActiveSubInfos == null || mActiveSubInfos.isEmpty()) { return false; } if (getSimInfo(subId) == null || getCarrierConfigForSubId(subId) == null) { return false; } return mActiveSubInfos.stream().anyMatch(info -> info.getSubscriptionId() == subId); } /** * Get the identity for the current SIM or null if the SIM is not available * * @param config WifiConfiguration that indicates what sort of authentication is necessary * @return Pair or null if the SIM is not available * or config is invalid */ public Pair getSimIdentity(WifiConfiguration config) { int subId = getBestMatchSubscriptionId(config); if (!SubscriptionManager.isValidSubscriptionId(subId)) { return null; } SimInfo simInfo = getSimInfo(subId); if (simInfo == null) { return null; } if (isOobPseudonymFeatureEnabled(config.carrierId)) { Optional pseudonymInfo = mWifiPseudonymManager.getValidPseudonymInfo(config.carrierId); if (pseudonymInfo.isEmpty()) { return null; } return Pair.create(pseudonymInfo.get().getPseudonym(), ""); } String identity = buildIdentity(getSimMethodForConfig(config), simInfo.imsi, simInfo.mccMnc, false); if (identity == null) { Log.e(TAG, "Failed to build the identity"); return null; } if (!requiresImsiEncryption(subId)) { return Pair.create(identity, ""); } TelephonyManager specifiedTm = mTelephonyManager.createForSubscriptionId(subId); ImsiEncryptionInfo imsiEncryptionInfo; try { imsiEncryptionInfo = specifiedTm.getCarrierInfoForImsiEncryption( TelephonyManager.KEY_TYPE_WLAN); } catch (RuntimeException e) { Log.e(TAG, "Failed to get imsi encryption info: " + e.getMessage()); return null; } if (imsiEncryptionInfo == null) { // Does not support encrypted identity. return Pair.create(identity, ""); } String encryptedIdentity = buildEncryptedIdentity(identity, imsiEncryptionInfo); // In case of failure for encryption, abort current EAP authentication. if (encryptedIdentity == null) { Log.e(TAG, "failed to encrypt the identity"); return null; } return Pair.create(identity, encryptedIdentity); } /** * Gets Anonymous identity for current active SIM. * * @param config the instance of WifiConfiguration. * @return anonymous identity@realm which is based on current MCC/MNC, {@code null} if SIM is * not ready or absent. */ public String getAnonymousIdentityWith3GppRealm(@NonNull WifiConfiguration config) { int subId = getBestMatchSubscriptionId(config); if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) { return null; } SimInfo simInfo = getSimInfo(subId); if (simInfo == null) { return null; } Pair mccMncPair = extractMccMnc(simInfo.imsi, simInfo.mccMnc); if (mccMncPair == null) { return null; } String realm = String.format(THREE_GPP_NAI_REALM_FORMAT, mccMncPair.second, mccMncPair.first); StringBuilder sb = new StringBuilder(); if (isEapMethodPrefixEnabled(subId)) { // Set the EAP method as a prefix String eapMethod = EAP_METHOD_PREFIX.get(config.enterpriseConfig.getEapMethod()); if (!TextUtils.isEmpty(eapMethod)) { sb.append(eapMethod); } } return sb.append(ANONYMOUS_IDENTITY).append("@").append(realm).toString(); } /** * Encrypt the given data with the given public key. The encrypted data will be returned as * a Base64 encoded string. * * @param key The public key to use for encryption * @param data The data need to be encrypted * @param encodingFlag base64 encoding flag * @return Base64 encoded string, or null if encryption failed */ @VisibleForTesting public static String encryptDataUsingPublicKey(PublicKey key, byte[] data, int encodingFlag) { try { Cipher cipher = Cipher.getInstance(IMSI_CIPHER_TRANSFORMATION); cipher.init(Cipher.ENCRYPT_MODE, key); byte[] encryptedBytes = cipher.doFinal(data); return Base64.encodeToString(encryptedBytes, 0, encryptedBytes.length, encodingFlag); } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException e) { Log.e(TAG, "Encryption failed: " + e.getMessage()); return null; } } /** * Create the encrypted identity. * * Prefix value: * "0" - EAP-AKA Identity * "1" - EAP-SIM Identity * "6" - EAP-AKA' Identity * Encrypted identity format: prefix|IMSI@ * @param identity permanent identity with format based on section 4.1.1.6 of RFC 4187 * and 4.2.1.6 of RFC 4186. * @param imsiEncryptionInfo The IMSI encryption info retrieved from the SIM * @return "\0" + encryptedIdentity + "{, Key Identifier AVP}" */ private static String buildEncryptedIdentity(String identity, ImsiEncryptionInfo imsiEncryptionInfo) { if (imsiEncryptionInfo == null) { Log.e(TAG, "imsiEncryptionInfo is not valid"); return null; } if (identity == null) { Log.e(TAG, "identity is not valid"); return null; } // Build and return the encrypted identity. String encryptedIdentity = encryptDataUsingPublicKey( imsiEncryptionInfo.getPublicKey(), identity.getBytes(), Base64.NO_WRAP); if (encryptedIdentity == null) { Log.e(TAG, "Failed to encrypt IMSI"); return null; } encryptedIdentity = DEFAULT_EAP_PREFIX + encryptedIdentity; if (imsiEncryptionInfo.getKeyIdentifier() != null) { // Include key identifier AVP (Attribute Value Pair). encryptedIdentity = encryptedIdentity + "," + imsiEncryptionInfo.getKeyIdentifier(); } return encryptedIdentity; } /** * Create an identity used for SIM-based EAP authentication. The identity will be based on * the info retrieved from the SIM card, such as IMSI and IMSI encryption info. The IMSI * contained in the identity will be encrypted if IMSI encryption info is provided. * * See rfc4186 & rfc4187 & rfc5448: * * Identity format: * Prefix | [IMSI || Encrypted IMSI] | @realm | {, Key Identifier AVP} * where "|" denotes concatenation, "||" denotes exclusive value, "{}" * denotes optional value, and realm is the 3GPP network domain name derived from the given * MCC/MNC according to the 3GGP spec(TS23.003). * * Prefix value: * "\0" - Encrypted Identity * "0" - EAP-AKA Identity * "1" - EAP-SIM Identity * "6" - EAP-AKA' Identity * * Encrypted IMSI: * Base64{RSA_Public_Key_Encryption{eapPrefix | IMSI}} * where "|" denotes concatenation, * * @param eapMethod EAP authentication method: EAP-SIM, EAP-AKA, EAP-AKA' * @param imsi The IMSI retrieved from the SIM * @param mccMnc The MCC MNC identifier retrieved from the SIM * @param isEncrypted Whether the imsi is encrypted or not. * @return the eap identity, built using either the encrypted or un-encrypted IMSI. */ private String buildIdentity(int eapMethod, String imsi, String mccMnc, boolean isEncrypted) { if (imsi == null || imsi.isEmpty()) { Log.e(TAG, "No IMSI or IMSI is null"); return null; } String prefix = isEncrypted ? DEFAULT_EAP_PREFIX : EAP_METHOD_PREFIX.get(eapMethod); if (prefix == null) { return null; } Pair mccMncPair = extractMccMnc(imsi, mccMnc); String naiRealm = String.format(THREE_GPP_NAI_REALM_FORMAT, mccMncPair.second, mccMncPair.first); return prefix + imsi + "@" + naiRealm; } /** * Return the associated SIM method for the configuration. * * @param config WifiConfiguration corresponding to the network. * @return the outer EAP method associated with this SIM configuration. */ private static int getSimMethodForConfig(WifiConfiguration config) { if (config == null || config.enterpriseConfig == null || !config.enterpriseConfig.isAuthenticationSimBased()) { return WifiEnterpriseConfig.Eap.NONE; } int eapMethod = config.enterpriseConfig.getEapMethod(); if (eapMethod == WifiEnterpriseConfig.Eap.PEAP) { // Translate known inner eap methods into an equivalent outer eap method. switch (config.enterpriseConfig.getPhase2Method()) { case WifiEnterpriseConfig.Phase2.SIM: eapMethod = WifiEnterpriseConfig.Eap.SIM; break; case WifiEnterpriseConfig.Phase2.AKA: eapMethod = WifiEnterpriseConfig.Eap.AKA; break; case WifiEnterpriseConfig.Phase2.AKA_PRIME: eapMethod = WifiEnterpriseConfig.Eap.AKA_PRIME; break; } } return eapMethod; } /** * Returns true if {@code identity} contains an anonymous@realm identity, false otherwise. */ public static boolean isAnonymousAtRealmIdentity(String identity) { if (TextUtils.isEmpty(identity)) return false; final String anonymousId = WifiCarrierInfoManager.ANONYMOUS_IDENTITY + "@"; return identity.startsWith(anonymousId) || identity.substring(1).startsWith(anonymousId); } // TODO replace some of this code with Byte.parseByte private static int parseHex(char ch) { if ('0' <= ch && ch <= '9') { return ch - '0'; } else if ('a' <= ch && ch <= 'f') { return ch - 'a' + 10; } else if ('A' <= ch && ch <= 'F') { return ch - 'A' + 10; } else { throw new NumberFormatException("" + ch + " is not a valid hex digit"); } } private static byte[] parseHex(String hex) { /* This only works for good input; don't throw bad data at it */ if (hex == null) { return new byte[0]; } if (hex.length() % 2 != 0) { throw new NumberFormatException(hex + " is not a valid hex string"); } byte[] result = new byte[(hex.length()) / 2 + 1]; result[0] = (byte) ((hex.length()) / 2); for (int i = 0, j = 1; i < hex.length(); i += 2, j++) { int val = parseHex(hex.charAt(i)) * 16 + parseHex(hex.charAt(i + 1)); byte b = (byte) (val & 0xFF); result[j] = b; } return result; } private static byte[] parseHexWithoutLength(String hex) { byte[] tmpRes = parseHex(hex); if (tmpRes.length == 0) { return tmpRes; } byte[] result = new byte[tmpRes.length - 1]; System.arraycopy(tmpRes, 1, result, 0, tmpRes.length - 1); return result; } private static String makeHex(byte[] bytes) { StringBuilder sb = new StringBuilder(); for (byte b : bytes) { sb.append(String.format("%02x", b)); } return sb.toString(); } private static String makeHex(byte[] bytes, int from, int len) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < len; i++) { sb.append(String.format("%02x", bytes[from + i])); } return sb.toString(); } private static byte[] concatHex(byte[] array1, byte[] array2) { int len = array1.length + array2.length; byte[] result = new byte[len]; int index = 0; if (array1.length != 0) { for (byte b : array1) { result[index] = b; index++; } } if (array2.length != 0) { for (byte b : array2) { result[index] = b; index++; } } return result; } /** * Calculate SRES and KC as 3G authentication. * * Standard Cellular_auth Type Command * * 3GPP TS 31.102 3G_authentication [Length][RAND][Length][AUTN] * [Length][RES][Length][CK][Length][IK] and more * * @param requestData RAND data from server. * @param config The instance of WifiConfiguration. * @return the response data processed by SIM. If all request data is malformed, then returns * empty string. If request data is invalid, then returns null. */ public String getGsmSimAuthResponse(String[] requestData, WifiConfiguration config) { return getGsmAuthResponseWithLength(requestData, config, TelephonyManager.APPTYPE_USIM); } /** * Calculate SRES and KC as 2G authentication. * * Standard Cellular_auth Type Command * * 3GPP TS 31.102 2G_authentication [Length][RAND] * [Length][SRES][Length][Cipher Key Kc] * * @param requestData RAND data from server. * @param config The instance of WifiConfiguration. * @return the response data processed by SIM. If all request data is malformed, then returns * empty string. If request data is invalid, then returns null. */ public String getGsmSimpleSimAuthResponse(String[] requestData, WifiConfiguration config) { return getGsmAuthResponseWithLength(requestData, config, TelephonyManager.APPTYPE_SIM); } private String getGsmAuthResponseWithLength(String[] requestData, WifiConfiguration config, int appType) { int subId = getBestMatchSubscriptionId(config); if (!SubscriptionManager.isValidSubscriptionId(subId)) { return null; } StringBuilder sb = new StringBuilder(); for (String challenge : requestData) { if (challenge == null || challenge.isEmpty()) { continue; } Log.d(TAG, "RAND = " + challenge); byte[] rand = null; try { rand = parseHex(challenge); } catch (NumberFormatException e) { Log.e(TAG, "malformed challenge"); continue; } String base64Challenge = Base64.encodeToString(rand, Base64.NO_WRAP); TelephonyManager specifiedTm = mTelephonyManager.createForSubscriptionId(subId); String tmResponse = specifiedTm.getIccAuthentication( appType, TelephonyManager.AUTHTYPE_EAP_SIM, base64Challenge); Log.v(TAG, "Raw Response - " + tmResponse); if (tmResponse == null || tmResponse.length() <= 4) { Log.e(TAG, "bad response - " + tmResponse); return null; } byte[] result = Base64.decode(tmResponse, Base64.DEFAULT); Log.v(TAG, "Hex Response -" + makeHex(result)); int sresLen = result[0]; if (sresLen < 0 || sresLen >= result.length) { Log.e(TAG, "malformed response - " + tmResponse); return null; } String sres = makeHex(result, 1, sresLen); int kcOffset = 1 + sresLen; if (kcOffset >= result.length) { Log.e(TAG, "malformed response - " + tmResponse); return null; } int kcLen = result[kcOffset]; if (kcLen < 0 || kcOffset + kcLen > result.length) { Log.e(TAG, "malformed response - " + tmResponse); return null; } String kc = makeHex(result, 1 + kcOffset, kcLen); sb.append(":" + kc + ":" + sres); Log.v(TAG, "kc:" + kc + " sres:" + sres); } return sb.toString(); } /** * Calculate SRES and KC as 2G authentication. * * Standard Cellular_auth Type Command * * 3GPP TS 11.11 2G_authentication [RAND] * [SRES][Cipher Key Kc] * * @param requestData RAND data from server. * @param config the instance of WifiConfiguration. * @return the response data processed by SIM. If all request data is malformed, then returns * empty string. If request data is invalid, then returns null. */ public String getGsmSimpleSimNoLengthAuthResponse(String[] requestData, @NonNull WifiConfiguration config) { int subId = getBestMatchSubscriptionId(config); if (!SubscriptionManager.isValidSubscriptionId(subId)) { return null; } StringBuilder sb = new StringBuilder(); for (String challenge : requestData) { if (challenge == null || challenge.isEmpty()) { continue; } Log.d(TAG, "RAND = " + challenge); byte[] rand = null; try { rand = parseHexWithoutLength(challenge); } catch (NumberFormatException e) { Log.e(TAG, "malformed challenge"); continue; } String base64Challenge = Base64.encodeToString(rand, Base64.NO_WRAP); TelephonyManager specifiedTm = mTelephonyManager.createForSubscriptionId(subId); String tmResponse = specifiedTm.getIccAuthentication(TelephonyManager.APPTYPE_SIM, TelephonyManager.AUTHTYPE_EAP_SIM, base64Challenge); Log.v(TAG, "Raw Response - " + tmResponse); if (tmResponse == null || tmResponse.length() <= 4) { Log.e(TAG, "bad response - " + tmResponse); return null; } byte[] result = Base64.decode(tmResponse, Base64.DEFAULT); if (SRES_LEN + KC_LEN != result.length) { Log.e(TAG, "malformed response - " + tmResponse); return null; } Log.v(TAG, "Hex Response -" + makeHex(result)); String sres = makeHex(result, START_SRES_POS, SRES_LEN); String kc = makeHex(result, START_KC_POS, KC_LEN); sb.append(":" + kc + ":" + sres); Log.v(TAG, "kc:" + kc + " sres:" + sres); } return sb.toString(); } /** * Data supplied when making a SIM Auth Request */ public static class SimAuthRequestData { public SimAuthRequestData() {} public SimAuthRequestData(int networkId, int protocol, String ssid, String[] data) { this.networkId = networkId; this.protocol = protocol; this.ssid = ssid; this.data = data; } public int networkId; public int protocol; public String ssid; // EAP-SIM: data[] contains the 3 rand, one for each of the 3 challenges // EAP-AKA/AKA': data[] contains rand & authn couple for the single challenge public String[] data; } /** * The response to a SIM Auth request if successful */ public static class SimAuthResponseData { public SimAuthResponseData(String type, String response) { this.type = type; this.response = response; } public String type; public String response; } /** * Get the response data for 3G authentication. * * @param requestData authentication request data from server. * @param config the instance of WifiConfiguration. * @return the response data processed by SIM. If request data is invalid, then returns null. */ public SimAuthResponseData get3GAuthResponse(SimAuthRequestData requestData, WifiConfiguration config) { StringBuilder sb = new StringBuilder(); byte[] rand = null; byte[] authn = null; String resType = WifiNative.SIM_AUTH_RESP_TYPE_UMTS_AUTH; if (requestData.data.length == 2) { try { rand = parseHex(requestData.data[0]); authn = parseHex(requestData.data[1]); } catch (NumberFormatException e) { Log.e(TAG, "malformed challenge"); } } else { Log.e(TAG, "malformed challenge"); } String tmResponse = ""; if (rand != null && authn != null) { String base64Challenge = Base64.encodeToString(concatHex(rand, authn), Base64.NO_WRAP); int subId = getBestMatchSubscriptionId(config); if (!SubscriptionManager.isValidSubscriptionId(subId)) { return null; } tmResponse = mTelephonyManager .createForSubscriptionId(subId) .getIccAuthentication(TelephonyManager.APPTYPE_USIM, TelephonyManager.AUTHTYPE_EAP_AKA, base64Challenge); Log.v(TAG, "Raw Response - " + tmResponse); } boolean goodResponse = false; if (tmResponse != null && tmResponse.length() > 4) { byte[] result = Base64.decode(tmResponse, Base64.DEFAULT); Log.e(TAG, "Hex Response - " + makeHex(result)); byte tag = result[0]; if (tag == (byte) 0xdb) { Log.v(TAG, "successful 3G authentication "); try { int resLen = result[1]; String res = makeHex(result, 2, resLen); int ckLen = result[resLen + 2]; String ck = makeHex(result, resLen + 3, ckLen); int ikLen = result[resLen + ckLen + 3]; String ik = makeHex(result, resLen + ckLen + 4, ikLen); sb.append(":" + ik + ":" + ck + ":" + res); Log.v(TAG, "ik:" + ik + "ck:" + ck + " res:" + res); goodResponse = true; } catch (ArrayIndexOutOfBoundsException e) { Log.e(TAG, "ArrayIndexOutOfBoundsException in get3GAuthResponse: " + e); } } else if (tag == (byte) 0xdc) { Log.e(TAG, "synchronisation failure"); int autsLen = result[1]; String auts = makeHex(result, 2, autsLen); resType = WifiNative.SIM_AUTH_RESP_TYPE_UMTS_AUTS; sb.append(":" + auts); Log.v(TAG, "auts:" + auts); goodResponse = true; } else { Log.e(TAG, "bad response - unknown tag = " + tag); } } else { Log.e(TAG, "bad response - " + tmResponse); } if (goodResponse) { String response = sb.toString(); Log.v(TAG, "Supplicant Response -" + response); return new SimAuthResponseData(resType, response); } else { return null; } } /** * Decorates a pseudonym with the NAI realm, in case it wasn't provided by the server * * @param config The instance of WifiConfiguration * @param pseudonym The pseudonym (temporary identity) provided by the server * @return pseudonym@realm which is based on current MCC/MNC, {@code null} if SIM is * not ready or absent. */ public String decoratePseudonymWith3GppRealm(@NonNull WifiConfiguration config, String pseudonym) { if (TextUtils.isEmpty(pseudonym)) { return null; } if (pseudonym.contains("@")) { // Pseudonym is already decorated return pseudonym; } int subId = getBestMatchSubscriptionId(config); SimInfo simInfo = getSimInfo(subId); if (simInfo == null) { return null; } Pair mccMncPair = extractMccMnc(simInfo.imsi, simInfo.mccMnc); if (mccMncPair == null) { return null; } String realm = String.format(THREE_GPP_NAI_REALM_FORMAT, mccMncPair.second, mccMncPair.first); return String.format("%s@%s", pseudonym, realm); } /** * Reset the downloaded IMSI encryption key. * @param config Instance of WifiConfiguration */ public void resetCarrierKeysForImsiEncryption(@NonNull WifiConfiguration config) { int subId = getBestMatchSubscriptionId(config); if (!SubscriptionManager.isValidSubscriptionId(subId)) { return; } TelephonyManager specifiedTm = mTelephonyManager.createForSubscriptionId(subId); specifiedTm.resetCarrierKeysForImsiEncryption(); } /** * Updates the carrier ID for passpoint configuration with SIM credential. * * @param config The instance of PasspointConfiguration. * @return true if the carrier ID is updated, false otherwise */ public boolean tryUpdateCarrierIdForPasspoint(PasspointConfiguration config) { if (config.getCarrierId() != TelephonyManager.UNKNOWN_CARRIER_ID) { return false; } Credential.SimCredential simCredential = config.getCredential().getSimCredential(); if (simCredential == null) { // carrier ID is not required. return false; } IMSIParameter imsiParameter = IMSIParameter.build(simCredential.getImsi()); // If the IMSI is not full, the carrier ID can not be matched for sure, so it should // be ignored. if (imsiParameter == null || !imsiParameter.isFullImsi()) { vlogd("IMSI is not available or not full"); return false; } if (mActiveSubInfos == null || mActiveSubInfos.isEmpty()) { return false; } // Find the active matching SIM card with the full IMSI from passpoint profile. for (SubscriptionInfo subInfo : mActiveSubInfos) { SimInfo simInfo = getSimInfo(subInfo.getSubscriptionId()); if (simInfo == null) { continue; } if (imsiParameter.matchesImsi(simInfo.imsi)) { config.setCarrierId(subInfo.getCarrierId()); return true; } } return false; } /** * Get the IMSI and carrier ID of the SIM card which is matched with the given subscription ID. * * @param subId The subscription ID see {@link SubscriptionInfo#getSubscriptionId()} * @return null if there is no matching SIM card, otherwise the IMSI and carrier ID of the * matching SIM card */ public @Nullable String getMatchingImsiBySubId(int subId) { if (!isSimReady(subId)) { return null; } if (subId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) { if (requiresImsiEncryption(subId) && !isImsiEncryptionInfoAvailable(subId)) { vlogd("required IMSI encryption information is not available."); return null; } SimInfo simInfo = getSimInfo(subId); if (simInfo == null) { vlogd("no active SIM card to match the carrier ID."); return null; } if (isOobPseudonymFeatureEnabled(simInfo.simCarrierId)) { if (mWifiPseudonymManager.getValidPseudonymInfo(simInfo.simCarrierId).isEmpty()) { vlogd("valid pseudonym is not available."); // matching only when the network is seen. mWifiPseudonymManager.retrievePseudonymOnFailureTimeoutExpired( simInfo.simCarrierId); return null; } } return simInfo.imsi; } return null; } /** * Get the IMSI and carrier ID of the SIM card which is matched with the given IMSI * (only prefix of IMSI - mccmnc*) from passpoint profile. * * @param imsiPrefix The IMSI parameter from passpoint profile. * @return null if there is no matching SIM card, otherwise the IMSI and carrier ID of the * matching SIM card */ public @Nullable Pair getMatchingImsiCarrierId( String imsiPrefix) { IMSIParameter imsiParameter = IMSIParameter.build(imsiPrefix); if (imsiParameter == null) { return null; } if (mActiveSubInfos == null) { return null; } int dataSubId = SubscriptionManager.getDefaultDataSubscriptionId(); //Pair the IMSI and carrier ID of matched SIM card Pair matchedPair = null; // matchedDataPair check if the data SIM is matched. Pair matchedDataPair = null; // matchedMnoPair check if any matched SIM card is MNO. Pair matchedMnoPair = null; // Find the active matched SIM card with the priority order of Data MNO SIM, // Nondata MNO SIM, Data MVNO SIM, Nondata MVNO SIM. for (SubscriptionInfo subInfo : mActiveSubInfos) { int subId = subInfo.getSubscriptionId(); if (requiresImsiEncryption(subId) && !isImsiEncryptionInfoAvailable(subId)) { vlogd("required IMSI encryption information is not available."); continue; } int carrierId = subInfo.getCarrierId(); if (isOobPseudonymFeatureEnabled(carrierId)) { if (mWifiPseudonymManager.getValidPseudonymInfo(carrierId).isEmpty()) { vlogd("valid pseudonym is not available."); // matching only when the network is seen. mWifiPseudonymManager.retrievePseudonymOnFailureTimeoutExpired(carrierId); continue; } } SimInfo simInfo = getSimInfo(subId); if (simInfo == null) { continue; } if (simInfo.mccMnc != null && imsiParameter.matchesMccMnc(simInfo.mccMnc)) { if (TextUtils.isEmpty(simInfo.imsi)) { continue; } matchedPair = new Pair<>(simInfo.imsi, subInfo.getCarrierId()); if (subId == dataSubId) { matchedDataPair = matchedPair; if (simInfo.getCarrierType() == CARRIER_MNO_TYPE) { vlogd("MNO data is matched via IMSI."); return matchedDataPair; } } if (simInfo.getCarrierType() == CARRIER_MNO_TYPE) { matchedMnoPair = matchedPair; } } } if (matchedMnoPair != null) { vlogd("MNO sub is matched via IMSI."); return matchedMnoPair; } if (matchedDataPair != null) { vlogd("MVNO data sub is matched via IMSI."); return matchedDataPair; } return matchedPair; } private void vlogd(String msg) { if (!mVerboseLogEnabled) { return; } Log.d(TAG, msg, null); } /** Dump state. */ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { pw.println(TAG + ": "); pw.println("mImsiEncryptionInfoAvailable=" + mImsiEncryptionInfoAvailable); pw.println("mImsiPrivacyProtectionExemptionMap=" + mImsiPrivacyProtectionExemptionMap); pw.println("mMergedCarrierNetworkOffloadMap=" + mMergedCarrierNetworkOffloadMap); pw.println("mSubIdToSimInfoSparseArray=" + mSubIdToSimInfoSparseArray); pw.println("mActiveSubInfos=" + mActiveSubInfos); pw.println("mCachedCarrierConfigPerSubId=" + mCachedCarrierConfigPerSubId); pw.println("mCarrierAutoJoinResetCheckedForOobPseudonym=" + mAutoJoinFlippedOnOobPseudonymEnabled); pw.println("mCarrierPrivilegedPackagesBySimSlot=[ "); for (int i = 0; i < mCarrierPrivilegedPackagesBySimSlot.size(); i++) { pw.println(mCarrierPrivilegedPackagesBySimSlot.valueAt(i)); } pw.println("]"); } private void resetCarrierPrivilegedApps() { Set packageNames = new HashSet<>(); for (int i = 0; i < mCarrierPrivilegedPackagesBySimSlot.size(); i++) { packageNames.addAll(mCarrierPrivilegedPackagesBySimSlot.valueAt(i)); } mWifiInjector.getWifiNetworkSuggestionsManager().updateCarrierPrivilegedApps(packageNames); } /** * Get the carrier ID {@link TelephonyManager#getSimCarrierId()} of the carrier which give * target package carrier privileges. * * @param packageName target package to check if grant privileges by any carrier. * @return Carrier ID who give privilege to this package. If package isn't granted privilege * by any available carrier, will return UNKNOWN_CARRIER_ID. */ public int getCarrierIdForPackageWithCarrierPrivileges(String packageName) { if (mActiveSubInfos == null || mActiveSubInfos.isEmpty()) { if (mVerboseLogEnabled) Log.v(TAG, "No subs for carrier privilege check"); return TelephonyManager.UNKNOWN_CARRIER_ID; } for (SubscriptionInfo info : mActiveSubInfos) { TelephonyManager specifiedTm = mTelephonyManager.createForSubscriptionId(info.getSubscriptionId()); if (specifiedTm.checkCarrierPrivilegesForPackage(packageName) == TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS) { return info.getCarrierId(); } } return TelephonyManager.UNKNOWN_CARRIER_ID; } /** * Get the carrier name for target subscription id. * @param subId Subscription id * @return String of carrier name. */ public String getCarrierNameForSubId(int subId) { TelephonyManager specifiedTm = mTelephonyManager.createForSubscriptionId(subId); CharSequence name = specifiedTm.getSimCarrierIdName(); if (name == null) { return null; } return name.toString(); } /** * Check if a config is carrier network and from the non default data SIM. * @return True if it is carrier network and from non default data SIM,otherwise return false. */ public boolean isCarrierNetworkFromNonDefaultDataSim(WifiConfiguration config) { if (config.carrierId == TelephonyManager.UNKNOWN_CARRIER_ID) { return false; } int subId = getBestMatchSubscriptionId(config); return subId != SubscriptionManager.getDefaultDataSubscriptionId(); } /** * Get the carrier Id of the default data sim. */ public int getDefaultDataSimCarrierId() { int subId = SubscriptionManager.getDefaultDataSubscriptionId(); SimInfo simInfo = getSimInfo(subId); if (simInfo == null) { return TelephonyManager.UNKNOWN_CARRIER_ID; } return simInfo.simCarrierId; } /** * Add a listener to monitor user approval IMSI protection exemption. */ public void addImsiProtectedOrUserApprovedListener( OnImsiProtectedOrUserApprovedListener listener) { mOnImsiProtectedOrUserApprovedListeners.add(listener); } /** * Add a listener to monitor carrier offload disabled. */ public void addOnCarrierOffloadDisabledListener( OnCarrierOffloadDisabledListener listener) { mOnCarrierOffloadDisabledListeners.add(listener); } /** * remove a {@link OnCarrierOffloadDisabledListener}. */ public void removeOnCarrierOffloadDisabledListener( OnCarrierOffloadDisabledListener listener) { mOnCarrierOffloadDisabledListeners.remove(listener); } /** * Clear the Imsi Privacy Exemption user approval info the target carrier. */ public void clearImsiPrivacyExemptionForCarrier(int carrierId) { mImsiPrivacyProtectionExemptionMap.remove(carrierId); saveToStore(); } /** * Check if carrier have user approved exemption for IMSI protection */ public boolean hasUserApprovedImsiPrivacyExemptionForCarrier(int carrierId) { return mImsiPrivacyProtectionExemptionMap.getOrDefault(carrierId, false); } /** * Enable or disable exemption on IMSI protection. */ public void setHasUserApprovedImsiPrivacyExemptionForCarrier(boolean approved, int carrierId) { if (mVerboseLogEnabled) { Log.v(TAG, "Setting Imsi privacy exemption for carrier " + carrierId + (approved ? " approved" : " not approved")); } mImsiPrivacyProtectionExemptionMap.put(carrierId, approved); // If user approved the exemption restore to initial auto join configure. if (approved) { for (OnImsiProtectedOrUserApprovedListener listener : mOnImsiProtectedOrUserApprovedListeners) { listener.onImsiProtectedOrUserApprovalChanged(carrierId, /*allowAutoJoin=*/ true); } } saveToStore(); } private void sendImsiPrivacyNotification(int carrierId) { String carrierName = getCarrierNameForSubId(getMatchingSubId(carrierId)); if (carrierName == null) { // If carrier name could not be retrieved, do not send notification. return; } Notification.Action userAllowAppNotificationAction = new Notification.Action.Builder(null, mContext.getResources().getText(R.string .wifi_suggestion_action_allow_imsi_privacy_exemption_carrier), getPrivateBroadcast(NOTIFICATION_USER_ALLOWED_CARRIER_INTENT_ACTION, Pair.create(EXTRA_CARRIER_NAME, carrierName), Pair.create(EXTRA_CARRIER_ID, carrierId))) .build(); Notification.Action userDisallowAppNotificationAction = new Notification.Action.Builder(null, mContext.getResources().getText(R.string .wifi_suggestion_action_disallow_imsi_privacy_exemption_carrier), getPrivateBroadcast(NOTIFICATION_USER_DISALLOWED_CARRIER_INTENT_ACTION, Pair.create(EXTRA_CARRIER_NAME, carrierName), Pair.create(EXTRA_CARRIER_ID, carrierId))) .build(); Notification notification = mFrameworkFacade.makeNotificationBuilder( mContext, WifiService.NOTIFICATION_NETWORK_STATUS) .setSmallIcon(Icon.createWithResource(mContext.getWifiOverlayApkPkgName(), com.android.wifi.resources.R.drawable.stat_notify_wifi_in_range)) .setTicker(mContext.getResources().getString( R.string.wifi_suggestion_imsi_privacy_title, carrierName)) .setContentTitle(mContext.getResources().getString( R.string.wifi_suggestion_imsi_privacy_title, carrierName)) .setStyle(new Notification.BigTextStyle() .bigText(mContext.getResources().getString( R.string.wifi_suggestion_imsi_privacy_content))) .setContentIntent(getPrivateBroadcast(NOTIFICATION_USER_CLICKED_INTENT_ACTION, Pair.create(EXTRA_CARRIER_NAME, carrierName), Pair.create(EXTRA_CARRIER_ID, carrierId))) .setDeleteIntent(getPrivateBroadcast(NOTIFICATION_USER_DISMISSED_INTENT_ACTION, Pair.create(EXTRA_CARRIER_NAME, carrierName), Pair.create(EXTRA_CARRIER_ID, carrierId))) .setShowWhen(false) .setLocalOnly(true) .setColor(mContext.getResources() .getColor(android.R.color.system_notification_accent_color, mContext.getTheme())) .addAction(userDisallowAppNotificationAction) .addAction(userAllowAppNotificationAction) .setTimeoutAfter(NOTIFICATION_EXPIRY_MILLS) .build(); // Post the notification. mNotificationManager.notify(SystemMessage.NOTE_CARRIER_SUGGESTION_AVAILABLE, notification); mNotificationUpdateTime = mClock.getElapsedSinceBootMillis() + NOTIFICATION_UPDATE_DELAY_MILLS; mIsLastUserApprovalUiDialog = false; } private void sendImsiPrivacyConfirmationDialog(@NonNull String carrierName, int carrierId) { mWifiMetrics.addUserApprovalCarrierUiReaction(ACTION_USER_ALLOWED_CARRIER, mIsLastUserApprovalUiDialog); mWifiInjector.getWifiDialogManager().createSimpleDialog( mContext.getResources().getString( R.string.wifi_suggestion_imsi_privacy_exemption_confirmation_title), mContext.getResources().getString( R.string.wifi_suggestion_imsi_privacy_exemption_confirmation_content, carrierName), mContext.getResources().getString( R.string.wifi_suggestion_action_allow_imsi_privacy_exemption_confirmation), mContext.getResources().getString(R.string .wifi_suggestion_action_disallow_imsi_privacy_exemption_confirmation), null /* neutralButtonText */, new WifiDialogManager.SimpleDialogCallback() { @Override public void onPositiveButtonClicked() { handleUserAllowCarrierExemptionAction(carrierName, carrierId); } @Override public void onNegativeButtonClicked() { handleUserDisallowCarrierExemptionAction(carrierName, carrierId); } @Override public void onNeutralButtonClicked() { // Not used. handleUserDismissAction(); } @Override public void onCancelled() { handleUserDismissAction(); } }, new WifiThreadRunner(mHandler)).launchDialog(); mIsLastUserApprovalUiDialog = true; } /** * Send notification for exemption of IMSI protection if user never made choice before. */ public void sendImsiProtectionExemptionNotificationIfRequired(int carrierId) { int subId = getMatchingSubId(carrierId); // If user data isn't loaded, don't send notification. if (!mUserDataLoaded) { return; } PersistableBundle bundle = getCarrierConfigForSubId(subId); if (bundle == null) { return; } if (requiresImsiEncryption(subId)) { return; } if (isOobPseudonymFeatureEnabled(carrierId)) { return; } if (mImsiPrivacyProtectionExemptionMap.containsKey(carrierId)) { return; } if (mNotificationUpdateTime > mClock.getElapsedSinceBootMillis()) { return; // Active notification is still available, do not update. } Log.i(TAG, "Sending IMSI protection notification for " + carrierId); sendImsiPrivacyNotification(carrierId); } /** * Check if the target subscription has a matched carrier Id. * @param subId Subscription Id for which this carrier network is valid. * @param carrierId Carrier Id for this carrier network. * @return true if matches, false otherwise. */ public boolean isSubIdMatchingCarrierId(int subId, int carrierId) { if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) { // If Subscription Id is not set, consider it matches. Best matching one will be used to // connect. return true; } if (mActiveSubInfos == null || mActiveSubInfos.isEmpty()) { return false; } for (SubscriptionInfo info : mActiveSubInfos) { if (info.getSubscriptionId() == subId) { return info.getCarrierId() == carrierId; } } return false; } private PendingIntent getPrivateBroadcast(@NonNull String action, @NonNull Pair extra1, @NonNull Pair extra2) { Intent intent = new Intent(action) .setPackage(mContext.getServiceWifiPackageName()) .putExtra(extra1.first, extra1.second) .putExtra(extra2.first, extra2.second); return mFrameworkFacade.getBroadcast(mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); } /** * Set carrier network offload enabled/disabled. * @param subscriptionId Subscription Id to change carrier offload * @param merged True for carrier merged network, false otherwise. * @param enabled True for enabled carrier offload, false otherwise. */ public void setCarrierNetworkOffloadEnabled(int subscriptionId, boolean merged, boolean enabled) { synchronized (mCarrierNetworkOffloadMapLock) { if (merged) { mMergedCarrierNetworkOffloadMap.put(subscriptionId, enabled); } else { mUnmergedCarrierNetworkOffloadMap.put(subscriptionId, enabled); } } mHandler.post(() -> { if (!enabled) { for (OnCarrierOffloadDisabledListener listener : mOnCarrierOffloadDisabledListeners) { listener.onCarrierOffloadDisabled(subscriptionId, merged); } } saveToStore(); }); } /** * Check if carrier network offload is enabled/disabled. * @param subId Subscription Id to check offload state. * @param merged True for carrier merged network, false otherwise. * @return True to indicate carrier offload is enabled, false otherwise. */ public boolean isCarrierNetworkOffloadEnabled(int subId, boolean merged) { synchronized (mCarrierNetworkOffloadMapLock) { if (merged) { return mMergedCarrierNetworkOffloadMap.get(subId, true) && isMobileDataEnabled(subId); } else { return mUnmergedCarrierNetworkOffloadMap.get(subId, true); } } } private boolean isMobileDataEnabled(int subId) { if (!SdkLevel.isAtLeastS()) { // As the carrier offload enabled API and the merged carrier API (which is controlled by // this toggle) were added in S. Don't check the mobile data toggle when Sdk is less // than S. return true; } if (mUserDataEnabled.indexOfKey(subId) >= 0) { return mUserDataEnabled.get(subId); } TelephonyManager specifiedTm = mTelephonyManager.createForSubscriptionId(subId); boolean enabled = specifiedTm.isDataEnabled(); mUserDataEnabled.put(subId, enabled); UserDataEnabledChangedListener listener = new UserDataEnabledChangedListener(subId); specifiedTm.registerTelephonyCallback(new HandlerExecutor(mHandler), listener); mUserDataEnabledListenerList.add(listener); return enabled; } private void saveToStore() { // Set the flag to let WifiConfigStore that we have new data to write. mHasNewUserDataToSerialize = true; mHasNewSharedDataToSerialize = true; if (!mWifiInjector.getWifiConfigManager().saveToStore()) { Log.w(TAG, "Failed to save to store"); } } /** * Helper method for user factory reset network setting. */ public void clear() { synchronized (mCarrierNetworkOffloadMapLock) { mMergedCarrierNetworkOffloadMap.clear(); mUnmergedCarrierNetworkOffloadMap.clear(); } mImsiPrivacyProtectionExemptionMap.clear(); mUserDataEnabled.clear(); mCarrierPrivilegedPackagesBySimSlot.clear(); if (SdkLevel.isAtLeastS()) { for (UserDataEnabledChangedListener listener : mUserDataEnabledListenerList) { listener.unregisterListener(); } mUserDataEnabledListenerList.clear(); } if (SdkLevel.isAtLeastT()) { for (WifiCarrierPrivilegeCallback callback : mCarrierPrivilegeCallbacks) { mTelephonyManager.unregisterCarrierPrivilegesCallback(callback); } mCarrierPrivilegeCallbacks.clear(); } resetNotification(); saveToStore(); } public void resetNotification() { mNotificationManager.cancel(SystemMessage.NOTE_CARRIER_SUGGESTION_AVAILABLE); mNotificationUpdateTime = 0; } /** * Get the SimInfo for the target subId. * * @param subId The subscriber ID for which to get the SIM info. * @return SimInfo The SimInfo for the target subId. */ public SimInfo getSimInfo(int subId) { SimInfo simInfo = mSubIdToSimInfoSparseArray.get(subId); // If mccmnc is not available, try to get it again. if (simInfo != null && simInfo.mccMnc != null && !simInfo.mccMnc.isEmpty()) { return simInfo; } TelephonyManager specifiedTm = mTelephonyManager.createForSubscriptionId(subId); if (specifiedTm.getSimApplicationState() != TelephonyManager.SIM_STATE_LOADED) { return null; } String imsi = specifiedTm.getSubscriberId(); String mccMnc = specifiedTm.getSimOperator(); if (imsi == null || imsi.isEmpty()) { Log.wtf(TAG, "Get invalid imsi when SIM is ready!"); return null; } int CarrierIdFromSimMccMnc = specifiedTm.getCarrierIdFromSimMccMnc(); int SimCarrierId = specifiedTm.getSimCarrierId(); simInfo = new SimInfo(imsi, mccMnc, CarrierIdFromSimMccMnc, SimCarrierId); mSubIdToSimInfoSparseArray.put(subId, simInfo); return simInfo; } /** * Try to extract mcc and mnc from IMSI and MCCMNC * @return a pair of string represent . Pair.first is mcc, pair.second is mnc. */ private Pair extractMccMnc(String imsi, String mccMnc) { String mcc; String mnc; if (mccMnc != null && !mccMnc.isEmpty() && mccMnc.length() >= 5) { mcc = mccMnc.substring(0, 3); mnc = mccMnc.substring(3); if (mnc.length() == 2) { mnc = "0" + mnc; } } else if (imsi != null && !imsi.isEmpty() && imsi.length() >= 6) { // extract mcc & mnc from IMSI, assume mnc size is 3 mcc = imsi.substring(0, 3); mnc = imsi.substring(3, 6); vlogd("Guessing from IMSI, MCC=" + mcc + "; MNC=" + mnc); } else { return null; } return Pair.create(mcc, mnc); } private boolean isEapMethodPrefixEnabled(int subId) { PersistableBundle bundle = getCarrierConfigForSubId(subId); if (bundle == null) { return false; } return bundle.getBoolean(CarrierConfigManager.ENABLE_EAP_METHOD_PREFIX_BOOL); } private @NonNull List getSubscriptionsInGroup(@NonNull ParcelUuid groupUuid) { if (groupUuid == null) { return Collections.emptyList(); } if (mSubscriptionGroupMap.containsKey(groupUuid)) { return mSubscriptionGroupMap.get(groupUuid); } List subIdList = mSubscriptionManager.getSubscriptionsInGroup(groupUuid) .stream() .map(SubscriptionInfo::getSubscriptionId) .collect(Collectors.toList()); mSubscriptionGroupMap.put(groupUuid, subIdList); return subIdList; } /** * Get an active subscription id in this Subscription Group. If multiple subscriptions are * active, will return default data subscription id if possible, otherwise an arbitrary one. * @param groupUuid UUID of the Subscription group * @return SubscriptionId which is active. */ public int getActiveSubscriptionIdInGroup(@NonNull ParcelUuid groupUuid) { if (groupUuid == null) { return SubscriptionManager.INVALID_SUBSCRIPTION_ID; } int activeSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID; int dataSubId = SubscriptionManager.getDefaultDataSubscriptionId(); for (int subId : getSubscriptionsInGroup(groupUuid)) { if (isSimReady(subId)) { if (subId == dataSubId) { return subId; } activeSubId = subId; } } return activeSubId; } /** * Get the packages name of the apps current have carrier privilege. */ public Set getCurrentCarrierPrivilegedPackages() { Set packages = new HashSet<>(); for (int i = 0; i < mCarrierPrivilegedPackagesBySimSlot.size(); i++) { packages.addAll(mCarrierPrivilegedPackagesBySimSlot.valueAt(i)); } return packages; } /** * Checks if the OOB pseudonym feature is enabled for the specified carrier. */ public boolean isOobPseudonymFeatureEnabled(int carrierId) { if (!mDeviceConfigFacade.isOobPseudonymEnabled()) { return false; } boolean ret = isOobPseudonymFeatureEnabledInResource(carrierId); vlogd("isOobPseudonymFeatureEnabled(" + carrierId + ") = " + ret); return ret; } /** * Check if wifi calling is being available. */ public boolean isWifiCallingAvailable() { if (mActiveSubInfos == null || mActiveSubInfos.isEmpty()) { return false; } if (mImsManager == null) { mImsManager = mContext.getSystemService(ImsManager.class); } for (SubscriptionInfo subInfo : mActiveSubInfos) { int subscriptionId = subInfo.getSubscriptionId(); try { if (mImsManager != null) { ImsMmTelManager imsMmTelManager = mImsMmTelManagerMap.get(subscriptionId); if (imsMmTelManager == null) { imsMmTelManager = mImsManager.getImsMmTelManager(subscriptionId); mImsMmTelManagerMap.put(subscriptionId, imsMmTelManager); } if (imsMmTelManager != null && imsMmTelManager.isAvailable( MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE, ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN)) { Log.d(TAG, "WifiCalling is available on subId " + subscriptionId); return true; } } } catch (RuntimeException e) { Log.d(TAG, "RuntimeException while checking if wifi calling is available: " + e); } } return false; } private boolean isOobPseudonymFeatureEnabledInResource(int carrierId) { WifiStringResourceWrapper wifiStringResourceWrapper = mContext.getStringResourceWrapper(getMatchingSubId(carrierId), carrierId); return wifiStringResourceWrapper.getBoolean(CONFIG_WIFI_OOB_PSEUDONYM_ENABLED, false); } /** * Checks if the auto-join of carrier wifi had been reset when the OOB * pseudonym feature is enabled. */ @VisibleForTesting boolean shouldFlipOnAutoJoinForOobPseudonym() { return mDeviceConfigFacade.isOobPseudonymEnabled() && !mAutoJoinFlippedOnOobPseudonymEnabled; } /** * Persists the bit to disable the auto-join reset for OOB pseudonym, the reset should be * done 1 time. */ private void disableFlipOnAutoJoinForOobPseudonym() { mAutoJoinFlippedOnOobPseudonymEnabled = true; saveToStore(); } private void enableFlipOnAutoJoinForOobPseudonym() { mAutoJoinFlippedOnOobPseudonymEnabled = false; saveToStore(); } }