diff options
author | Andrew Scull <ascull@google.com> | 2024-01-08 17:45:43 +0000 |
---|---|---|
committer | Andrew Scull <ascull@google.com> | 2024-02-01 16:51:33 +0000 |
commit | 53911f4b99da7bdbbdc03110c369f8eccaa2c3ba (patch) | |
tree | 53857192e994b6fb5b757f3bfb7ccfe843b47327 | |
parent | 6628b3d7d9c3c28f0aa1104d5b677bbd8e517e06 (diff) | |
download | telephony-53911f4b99da7bdbbdc03110c369f8eccaa2c3ba.tar.gz |
Add safety source for cellular network security issues
Instantiate a singleton safety source that holds the state of the
cellular network security issues to report to safety center.
Test: atets CellularNetworkSecuritySafetySourceTest
Bug: 308985417
Change-Id: I6ea58f769f1fb2d5dbc6000a9b61a37b37259a84
6 files changed, 628 insertions, 0 deletions
diff --git a/src/java/com/android/internal/telephony/GsmCdmaPhone.java b/src/java/com/android/internal/telephony/GsmCdmaPhone.java index 18a52625ec..81054cdc06 100644 --- a/src/java/com/android/internal/telephony/GsmCdmaPhone.java +++ b/src/java/com/android/internal/telephony/GsmCdmaPhone.java @@ -119,6 +119,7 @@ import com.android.internal.telephony.imsphone.ImsPhoneMmiCode; import com.android.internal.telephony.metrics.TelephonyMetrics; import com.android.internal.telephony.metrics.VoiceCallSessionStats; import com.android.internal.telephony.security.CellularIdentifierDisclosureNotifier; +import com.android.internal.telephony.security.CellularNetworkSecuritySafetySource; import com.android.internal.telephony.security.NullCipherNotifier; import com.android.internal.telephony.subscription.SubscriptionInfoInternal; import com.android.internal.telephony.subscription.SubscriptionManagerService.SubscriptionManagerServiceCallback; @@ -304,6 +305,7 @@ public class GsmCdmaPhone extends Phone { private final SubscriptionManager.OnSubscriptionsChangedListener mSubscriptionsChangedListener; private final CallWaitingController mCallWaitingController; + private CellularNetworkSecuritySafetySource mSafetySource; private CellularIdentifierDisclosureNotifier mIdentifierDisclosureNotifier; private NullCipherNotifier mNullCipherNotifier; @@ -525,6 +527,12 @@ public class GsmCdmaPhone extends Phone { mCi.registerForImeiMappingChanged(this, EVENT_IMEI_MAPPING_CHANGED, null); + if (mFeatureFlags.enableIdentifierDisclosureTransparencyUnsolEvents() + || mFeatureFlags.enableModemCipherTransparencyUnsolEvents()) { + mSafetySource = + mTelephonyComponentFactory.makeCellularNetworkSecuritySafetySource(mContext); + } + if (mFeatureFlags.enableIdentifierDisclosureTransparencyUnsolEvents()) { logi( "enable_identifier_disclosure_transparency_unsol_events is on. Registering for " @@ -5415,4 +5423,12 @@ public class GsmCdmaPhone extends Phone { public boolean isNullCipherNotificationSupported() { return mIsNullCipherNotificationSupported; } + + @Override + public void refreshSafetySources(String refreshBroadcastId) { + if (mFeatureFlags.enableIdentifierDisclosureTransparencyUnsolEvents() + || mFeatureFlags.enableModemCipherTransparencyUnsolEvents()) { + mSafetySource.refresh(mContext, refreshBroadcastId); + } + } } diff --git a/src/java/com/android/internal/telephony/Phone.java b/src/java/com/android/internal/telephony/Phone.java index 3b47670ad1..97eb4475e1 100644 --- a/src/java/com/android/internal/telephony/Phone.java +++ b/src/java/com/android/internal/telephony/Phone.java @@ -5214,6 +5214,12 @@ public abstract class Phone extends Handler implements PhoneInternalInterface { } /** + * Refresh the safety sources in response to the identified broadcast. + */ + public void refreshSafetySources(String refreshBroadcastId) { + } + + /** * Notifies the IMS call status to the modem. * * @param imsCallInfo The list of {@link ImsCallInfo}. diff --git a/src/java/com/android/internal/telephony/TelephonyComponentFactory.java b/src/java/com/android/internal/telephony/TelephonyComponentFactory.java index 2c68457509..5533c18684 100644 --- a/src/java/com/android/internal/telephony/TelephonyComponentFactory.java +++ b/src/java/com/android/internal/telephony/TelephonyComponentFactory.java @@ -48,6 +48,7 @@ import com.android.internal.telephony.imsphone.ImsPhone; import com.android.internal.telephony.imsphone.ImsPhoneCallTracker; import com.android.internal.telephony.nitz.NitzStateMachineImpl; import com.android.internal.telephony.security.CellularIdentifierDisclosureNotifier; +import com.android.internal.telephony.security.CellularNetworkSecuritySafetySource; import com.android.internal.telephony.security.NullCipherNotifier; import com.android.internal.telephony.uicc.IccCardStatus; import com.android.internal.telephony.uicc.UiccCard; @@ -575,6 +576,12 @@ public class TelephonyComponentFactory { return new DataSettingsManager(phone, dataNetworkController, looper, callback); } + /** Create CellularNetworkSecuritySafetySource. */ + public CellularNetworkSecuritySafetySource makeCellularNetworkSecuritySafetySource( + Context context) { + return CellularNetworkSecuritySafetySource.getInstance(context); + } + /** Create CellularIdentifierDisclosureNotifier. */ public CellularIdentifierDisclosureNotifier makeIdentifierDisclosureNotifier() { return CellularIdentifierDisclosureNotifier.getInstance(); diff --git a/src/java/com/android/internal/telephony/security/CellularNetworkSecuritySafetySource.java b/src/java/com/android/internal/telephony/security/CellularNetworkSecuritySafetySource.java new file mode 100644 index 0000000000..ff09bcb801 --- /dev/null +++ b/src/java/com/android/internal/telephony/security/CellularNetworkSecuritySafetySource.java @@ -0,0 +1,364 @@ +/* + * Copyright (C) 2024 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.internal.telephony.security; + +import static android.safetycenter.SafetyEvent.SAFETY_EVENT_TYPE_REFRESH_REQUESTED; +import static android.safetycenter.SafetyEvent.SAFETY_EVENT_TYPE_SOURCE_STATE_CHANGED; +import static android.safetycenter.SafetySourceData.SEVERITY_LEVEL_INFORMATION; +import static android.safetycenter.SafetySourceData.SEVERITY_LEVEL_RECOMMENDATION; + +import android.annotation.IntDef; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.safetycenter.SafetyCenterManager; +import android.safetycenter.SafetyEvent; +import android.safetycenter.SafetySourceData; +import android.safetycenter.SafetySourceIssue; +import android.safetycenter.SafetySourceStatus; + +import com.android.internal.R; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.telephony.subscription.SubscriptionInfoInternal; +import com.android.internal.telephony.subscription.SubscriptionManagerService; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.time.Instant; +import java.util.Date; +import java.util.HashMap; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Stream; + +/** + * Holds the state needed to report the Safety Center status and issues related to cellular + * network security. + */ +public class CellularNetworkSecuritySafetySource { + private static final String SAFETY_SOURCE_ID = "AndroidCellularNetworkSecurity"; + + private static final String NULL_CIPHER_ISSUE_NON_ENCRYPTED_ID = "null_cipher_non_encrypted"; + private static final String NULL_CIPHER_ISSUE_ENCRYPTED_ID = "null_cipher_encrypted"; + + private static final String NULL_CIPHER_ACTION_SETTINGS_ID = "cellular_security_settings"; + private static final String NULL_CIPHER_ACTION_LEARN_MORE_ID = "learn_more"; + + private static final String IDENTIFIER_DISCLOSURE_ISSUE_ID = "identifier_disclosure"; + + private static final Intent CELLULAR_NETWORK_SECURITY_SETTINGS_INTENT = + new Intent("android.settings.CELLULAR_NETWORK_SECURITY"); + // TODO(b/321999913): direct to a help page URL e.g. + // new Intent(Intent.ACTION_VIEW, Uri.parse("https://...")); + private static final Intent LEARN_MORE_INTENT = new Intent(); + + static final int NULL_CIPHER_STATE_ENCRYPTED = 0; + static final int NULL_CIPHER_STATE_NOTIFY_ENCRYPTED = 1; + static final int NULL_CIPHER_STATE_NOTIFY_NON_ENCRYPTED = 2; + + @IntDef( + prefix = {"NULL_CIPHER_STATE_"}, + value = { + NULL_CIPHER_STATE_ENCRYPTED, + NULL_CIPHER_STATE_NOTIFY_ENCRYPTED, + NULL_CIPHER_STATE_NOTIFY_NON_ENCRYPTED}) + @Retention(RetentionPolicy.SOURCE) + @interface NullCipherState {} + + private static CellularNetworkSecuritySafetySource sInstance; + + private final SafetyCenterManagerWrapper mSafetyCenterManagerWrapper; + private final SubscriptionManagerService mSubscriptionManagerService; + + private boolean mNullCipherStateIssuesEnabled; + private HashMap<Integer, Integer> mNullCipherStates = new HashMap<>(); + + private boolean mIdentifierDisclosureIssuesEnabled; + private HashMap<Integer, IdentifierDisclosure> mIdentifierDisclosures = new HashMap<>(); + + /** + * Gets a singleton CellularNetworkSecuritySafetySource. + */ + public static synchronized CellularNetworkSecuritySafetySource getInstance(Context context) { + if (sInstance == null) { + sInstance = new CellularNetworkSecuritySafetySource( + new SafetyCenterManagerWrapper(context)); + } + return sInstance; + } + + @VisibleForTesting + public CellularNetworkSecuritySafetySource( + SafetyCenterManagerWrapper safetyCenterManagerWrapper) { + mSafetyCenterManagerWrapper = safetyCenterManagerWrapper; + mSubscriptionManagerService = SubscriptionManagerService.getInstance(); + } + + /** Enables or disables the null cipher issue and clears any current issues. */ + public synchronized void setNullCipherIssueEnabled(Context context, boolean enabled) { + mNullCipherStateIssuesEnabled = enabled; + mNullCipherStates.clear(); + updateSafetyCenter(context); + } + + /** Sets the null cipher issue state for the identified subscription. */ + public synchronized void setNullCipherState( + Context context, int subId, @NullCipherState int nullCipherState) { + mNullCipherStates.put(subId, nullCipherState); + updateSafetyCenter(context); + } + + /** Enables or disables the identifier disclosure issue and clears any current issues. */ + public synchronized void setIdentifierDisclosureIssueEnabled(Context context, boolean enabled) { + mIdentifierDisclosureIssuesEnabled = enabled; + mIdentifierDisclosures.clear(); + updateSafetyCenter(context); + } + + /** Sets the identifier disclosure issue state for the identifier subscription. */ + public synchronized void setIdentifierDisclosure( + Context context, int subId, int count, Instant start, Instant end) { + IdentifierDisclosure disclosure = new IdentifierDisclosure(count, start, end); + mIdentifierDisclosures.put(subId, disclosure); + updateSafetyCenter(context); + } + + /** Clears the identifier disclosure issue state for the identified subscription. */ + public synchronized void clearIdentifierDisclosure(Context context, int subId) { + mIdentifierDisclosures.remove(subId); + updateSafetyCenter(context); + } + + /** Refreshed the safety source in response to the identified broadcast. */ + public synchronized void refresh(Context context, String refreshBroadcastId) { + mSafetyCenterManagerWrapper.setRefreshedSafetySourceData( + refreshBroadcastId, getSafetySourceData(context)); + } + + private void updateSafetyCenter(Context context) { + mSafetyCenterManagerWrapper.setSafetySourceData(getSafetySourceData(context)); + } + + private boolean isSafetySourceHidden() { + return !mNullCipherStateIssuesEnabled && !mIdentifierDisclosureIssuesEnabled; + } + + private SafetySourceData getSafetySourceData(Context context) { + if (isSafetySourceHidden()) { + // The cellular network security safety source is configured with + // initialDisplayState="hidden" + return null; + } + + Stream<Optional<SafetySourceIssue>> nullCipherIssues = + mNullCipherStates.entrySet().stream() + .map(e -> getNullCipherIssue(context, e.getKey(), e.getValue())); + Stream<Optional<SafetySourceIssue>> identifierDisclosureIssues = + mIdentifierDisclosures.entrySet().stream() + .map(e -> getIdentifierDisclosureIssue(context, e.getKey(), e.getValue())); + SafetySourceIssue[] issues = Stream.concat(nullCipherIssues, identifierDisclosureIssues) + .flatMap(Optional::stream) + .toArray(SafetySourceIssue[]::new); + + SafetySourceData.Builder builder = new SafetySourceData.Builder(); + int maxSeverity = SEVERITY_LEVEL_INFORMATION; + for (SafetySourceIssue issue : issues) { + builder.addIssue(issue); + maxSeverity = Math.max(maxSeverity, issue.getSeverityLevel()); + } + + builder.setStatus( + new SafetySourceStatus.Builder( + context.getString(R.string.scCellularNetworkSecurityTitle), + context.getString(R.string.scCellularNetworkSecuritySummary), + maxSeverity) + .setPendingIntent(mSafetyCenterManagerWrapper.getActivityPendingIntent( + context, CELLULAR_NETWORK_SECURITY_SETTINGS_INTENT)) + .build()); + return builder.build(); + } + + /** Builds the null cipher issue if it's enabled and there are null ciphers to report. */ + private Optional<SafetySourceIssue> getNullCipherIssue( + Context context, int subId, @NullCipherState int state) { + if (!mNullCipherStateIssuesEnabled) { + return Optional.empty(); + } + + SubscriptionInfoInternal subInfo = + mSubscriptionManagerService.getSubscriptionInfoInternal(subId); + final SafetySourceIssue.Builder builder; + switch (state) { + case NULL_CIPHER_STATE_ENCRYPTED: + return Optional.empty(); + case NULL_CIPHER_STATE_NOTIFY_NON_ENCRYPTED: + builder = new SafetySourceIssue.Builder( + NULL_CIPHER_ISSUE_NON_ENCRYPTED_ID + "_" + subId, + context.getString( + R.string.scNullCipherIssueNonEncryptedTitle, subInfo.getDisplayName()), + context.getString(R.string.scNullCipherIssueNonEncryptedSummary), + SEVERITY_LEVEL_RECOMMENDATION, + NULL_CIPHER_ISSUE_NON_ENCRYPTED_ID); + break; + case NULL_CIPHER_STATE_NOTIFY_ENCRYPTED: + builder = new SafetySourceIssue.Builder( + NULL_CIPHER_ISSUE_NON_ENCRYPTED_ID + "_" + subId, + context.getString( + R.string.scNullCipherIssueEncryptedTitle, subInfo.getDisplayName()), + context.getString(R.string.scNullCipherIssueEncryptedSummary), + SEVERITY_LEVEL_INFORMATION, + NULL_CIPHER_ISSUE_ENCRYPTED_ID); + break; + default: + throw new AssertionError(); + } + + return Optional.of( + builder + .setNotificationBehavior(SafetySourceIssue.NOTIFICATION_BEHAVIOR_IMMEDIATELY) + .setIssueCategory(SafetySourceIssue.ISSUE_CATEGORY_DEVICE) + .addAction( + new SafetySourceIssue.Action.Builder( + NULL_CIPHER_ACTION_SETTINGS_ID, + context.getString(R.string.scNullCipherIssueActionSettings), + mSafetyCenterManagerWrapper.getActivityPendingIntent( + context, CELLULAR_NETWORK_SECURITY_SETTINGS_INTENT)) + .build()) + .addAction( + new SafetySourceIssue.Action.Builder( + NULL_CIPHER_ACTION_LEARN_MORE_ID, + context.getString(R.string.scNullCipherIssueActionLearnMore), + mSafetyCenterManagerWrapper.getActivityPendingIntent( + context, LEARN_MORE_INTENT)) + .build()) + .build()); + } + + /** Builds the identity disclosure issue if it's enabled and there are disclosures to report. */ + private Optional<SafetySourceIssue> getIdentifierDisclosureIssue( + Context context, int subId, IdentifierDisclosure disclosure) { + if (!mIdentifierDisclosureIssuesEnabled || disclosure.getDisclosureCount() == 0) { + return Optional.empty(); + } + + SubscriptionInfoInternal subInfo = + mSubscriptionManagerService.getSubscriptionInfoInternal(subId); + return Optional.of( + new SafetySourceIssue.Builder( + IDENTIFIER_DISCLOSURE_ISSUE_ID + "_" + subId, + context.getString(R.string.scIdentifierDisclosureIssueTitle), + context.getString( + R.string.scIdentifierDisclosureIssueSummary, + disclosure.getDisclosureCount(), + Date.from(disclosure.getWindowStart()), + Date.from(disclosure.getWindowEnd()), + subInfo.getDisplayName()), + SEVERITY_LEVEL_RECOMMENDATION, + IDENTIFIER_DISCLOSURE_ISSUE_ID) + .setNotificationBehavior(SafetySourceIssue.NOTIFICATION_BEHAVIOR_IMMEDIATELY) + .setIssueCategory(SafetySourceIssue.ISSUE_CATEGORY_DEVICE) + .addAction( + new SafetySourceIssue.Action.Builder( + NULL_CIPHER_ACTION_SETTINGS_ID, + context.getString(R.string.scNullCipherIssueActionSettings), + mSafetyCenterManagerWrapper.getActivityPendingIntent( + context, CELLULAR_NETWORK_SECURITY_SETTINGS_INTENT)) + .build()) + .addAction( + new SafetySourceIssue.Action.Builder( + NULL_CIPHER_ACTION_LEARN_MORE_ID, + context.getString(R.string.scNullCipherIssueActionLearnMore), + mSafetyCenterManagerWrapper.getActivityPendingIntent( + context, LEARN_MORE_INTENT)) + .build()) + .build()); + } + + /** A wrapper around {@link SafetyCenterManager} that can be instrumented in tests. */ + @VisibleForTesting + public static class SafetyCenterManagerWrapper { + private final SafetyCenterManager mSafetyCenterManager; + + public SafetyCenterManagerWrapper(Context context) { + mSafetyCenterManager = context.getSystemService(SafetyCenterManager.class); + } + + /** Retrieve a {@link PendingIntent} that will start a new activity. */ + public PendingIntent getActivityPendingIntent(Context context, Intent intent) { + return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE); + } + + /** Set the {@link SafetySourceData} for this safety source. */ + public void setSafetySourceData(SafetySourceData safetySourceData) { + mSafetyCenterManager.setSafetySourceData( + SAFETY_SOURCE_ID, + safetySourceData, + new SafetyEvent.Builder(SAFETY_EVENT_TYPE_SOURCE_STATE_CHANGED).build()); + } + + /** Sets the {@link SafetySourceData} in response to a refresh request. */ + public void setRefreshedSafetySourceData( + String refreshBroadcastId, SafetySourceData safetySourceData) { + mSafetyCenterManager.setSafetySourceData( + SAFETY_SOURCE_ID, + safetySourceData, + new SafetyEvent.Builder(SAFETY_EVENT_TYPE_REFRESH_REQUESTED) + .setRefreshBroadcastId(refreshBroadcastId) + .build()); + } + } + + private static class IdentifierDisclosure { + private final int mDisclosureCount; + private final Instant mWindowStart; + private final Instant mWindowEnd; + + private IdentifierDisclosure(int count, Instant start, Instant end) { + mDisclosureCount = count; + mWindowStart = start; + mWindowEnd = end; + } + + private int getDisclosureCount() { + return mDisclosureCount; + } + + private Instant getWindowStart() { + return mWindowStart; + } + + private Instant getWindowEnd() { + return mWindowEnd; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof IdentifierDisclosure)) { + return false; + } + IdentifierDisclosure other = (IdentifierDisclosure) o; + return mDisclosureCount == other.mDisclosureCount + && Objects.equals(mWindowStart, other.mWindowStart) + && Objects.equals(mWindowEnd, other.mWindowEnd); + } + + @Override + public int hashCode() { + return Objects.hash(mDisclosureCount, mWindowStart, mWindowEnd); + } + } +} diff --git a/tests/telephonytests/src/com/android/internal/telephony/TelephonyTest.java b/tests/telephonytests/src/com/android/internal/telephony/TelephonyTest.java index 78b5e8c5eb..37b6416e55 100644 --- a/tests/telephonytests/src/com/android/internal/telephony/TelephonyTest.java +++ b/tests/telephonytests/src/com/android/internal/telephony/TelephonyTest.java @@ -125,6 +125,7 @@ import com.android.internal.telephony.metrics.SmsStats; import com.android.internal.telephony.metrics.VoiceCallSessionStats; import com.android.internal.telephony.satellite.SatelliteController; import com.android.internal.telephony.security.CellularIdentifierDisclosureNotifier; +import com.android.internal.telephony.security.CellularNetworkSecuritySafetySource; import com.android.internal.telephony.security.NullCipherNotifier; import com.android.internal.telephony.subscription.SubscriptionManagerService; import com.android.internal.telephony.test.SimulatedCommands; @@ -285,6 +286,7 @@ public abstract class TelephonyTest { protected ServiceStateStats mServiceStateStats; protected SatelliteController mSatelliteController; protected DeviceStateHelper mDeviceStateHelper; + protected CellularNetworkSecuritySafetySource mSafetySource; protected CellularIdentifierDisclosureNotifier mIdentifierDisclosureNotifier; protected DomainSelectionResolver mDomainSelectionResolver; protected NullCipherNotifier mNullCipherNotifier; @@ -560,6 +562,7 @@ public abstract class TelephonyTest { mServiceStateStats = Mockito.mock(ServiceStateStats.class); mSatelliteController = Mockito.mock(SatelliteController.class); mDeviceStateHelper = Mockito.mock(DeviceStateHelper.class); + mSafetySource = Mockito.mock(CellularNetworkSecuritySafetySource.class); mIdentifierDisclosureNotifier = Mockito.mock(CellularIdentifierDisclosureNotifier.class); mDomainSelectionResolver = Mockito.mock(DomainSelectionResolver.class); mNullCipherNotifier = Mockito.mock(NullCipherNotifier.class); @@ -676,6 +679,8 @@ public abstract class TelephonyTest { any(DataServiceManager.class), any(Looper.class), any(FeatureFlags.class), any(DataProfileManager.DataProfileManagerCallback.class)); + doReturn(mSafetySource).when(mTelephonyComponentFactory) + .makeCellularNetworkSecuritySafetySource(any(Context.class)); doReturn(mIdentifierDisclosureNotifier) .when(mTelephonyComponentFactory) .makeIdentifierDisclosureNotifier(); diff --git a/tests/telephonytests/src/com/android/internal/telephony/security/CellularNetworkSecuritySafetySourceTest.java b/tests/telephonytests/src/com/android/internal/telephony/security/CellularNetworkSecuritySafetySourceTest.java new file mode 100644 index 0000000000..169a57ca8e --- /dev/null +++ b/tests/telephonytests/src/com/android/internal/telephony/security/CellularNetworkSecuritySafetySourceTest.java @@ -0,0 +1,230 @@ +/* + * Copyright (C) 2024 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.internal.telephony.security; + +import static com.android.internal.telephony.security.CellularNetworkSecuritySafetySource.NULL_CIPHER_STATE_ENCRYPTED; +import static com.android.internal.telephony.security.CellularNetworkSecuritySafetySource.NULL_CIPHER_STATE_NOTIFY_ENCRYPTED; +import static com.android.internal.telephony.security.CellularNetworkSecuritySafetySource.NULL_CIPHER_STATE_NOTIFY_NON_ENCRYPTED; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.isNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.app.ActivityManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.safetycenter.SafetySourceData; +import android.util.Singleton; + +import com.android.internal.R; +import com.android.internal.telephony.TelephonyTest; +import com.android.internal.telephony.TestApplication; +import com.android.internal.telephony.security.CellularNetworkSecuritySafetySource.SafetyCenterManagerWrapper; +import com.android.internal.telephony.subscription.SubscriptionInfoInternal; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +import java.time.Instant; + +public final class CellularNetworkSecuritySafetySourceTest extends TelephonyTest { + + private SafetyCenterManagerWrapper mSafetyCenterManagerWrapper; + + @Before + public void setUp() throws Exception { + super.setUp(getClass().getSimpleName()); + + // unmock ActivityManager to be able to register receiver, create real PendingIntents. + restoreInstance(Singleton.class, "mInstance", mIActivityManagerSingleton); + restoreInstance(ActivityManager.class, "IActivityManagerSingleton", null); + + SubscriptionInfoInternal info0 = new SubscriptionInfoInternal.Builder() + .setId(0) + .setDisplayName("fake_name0") + .build(); + doReturn(info0).when(mSubscriptionManagerService).getSubscriptionInfoInternal(eq(0)); + SubscriptionInfoInternal info1 = new SubscriptionInfoInternal.Builder() + .setId(1) + .setDisplayName("fake_name1") + .build(); + doReturn(info1).when(mSubscriptionManagerService).getSubscriptionInfoInternal(eq(1)); + + mContextFixture.putResource(R.string.scCellularNetworkSecurityTitle, "fake"); + mContextFixture.putResource(R.string.scCellularNetworkSecuritySummary, "fake"); + mContextFixture.putResource(R.string.scNullCipherIssueNonEncryptedTitle, "fake %1$s"); + mContextFixture.putResource(R.string.scNullCipherIssueNonEncryptedSummary, "fake"); + mContextFixture.putResource(R.string.scNullCipherIssueEncryptedTitle, "fake %1$s"); + mContextFixture.putResource(R.string.scNullCipherIssueEncryptedSummary, "fake"); + mContextFixture.putResource(R.string.scIdentifierDisclosureIssueTitle, "fake"); + mContextFixture.putResource( + R.string.scIdentifierDisclosureIssueSummary, "fake %1$d %2$tr %3$tr %4$s"); + mContextFixture.putResource(R.string.scNullCipherIssueActionSettings, "fake"); + mContextFixture.putResource(R.string.scNullCipherIssueActionLearnMore, "fake"); + + mSafetyCenterManagerWrapper = mock(SafetyCenterManagerWrapper.class); + doAnswer(inv -> getActivityPendingIntent(inv.getArgument(1))) + .when(mSafetyCenterManagerWrapper) + .getActivityPendingIntent(any(Context.class), any(Intent.class)); + + mSafetySource = new CellularNetworkSecuritySafetySource(mSafetyCenterManagerWrapper); + } + + @After + public void tearDown() throws Exception { + super.tearDown(); + } + + private PendingIntent getActivityPendingIntent(Intent intent) { + Context context = TestApplication.getAppContext(); + return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE); + } + + @Test + public void disableNullCipherIssue_nullData() { + mSafetySource.setIdentifierDisclosureIssueEnabled(mContext, false); + + verify(mSafetyCenterManagerWrapper, times(1)).setSafetySourceData(isNull()); + } + + @Test + public void enableNullCipherIssue_statusWithoutIssues() { + ArgumentCaptor<SafetySourceData> data = ArgumentCaptor.forClass(SafetySourceData.class); + + mSafetySource.setNullCipherIssueEnabled(mContext, true); + + verify(mSafetyCenterManagerWrapper, times(1)).setSafetySourceData(data.capture()); + assertThat(data.getValue().getStatus()).isNotNull(); + assertThat(data.getValue().getIssues()).isEmpty(); + } + + @Test + public void setNullCipherState_encrypted_statusWithoutIssue() { + ArgumentCaptor<SafetySourceData> data = ArgumentCaptor.forClass(SafetySourceData.class); + + mSafetySource.setNullCipherIssueEnabled(mContext, true); + mSafetySource.setNullCipherState(mContext, 0, NULL_CIPHER_STATE_ENCRYPTED); + + verify(mSafetyCenterManagerWrapper, times(2)).setSafetySourceData(data.capture()); + assertThat(data.getAllValues().get(1).getStatus()).isNotNull(); + assertThat(data.getAllValues().get(1).getIssues()).isEmpty(); + } + + @Test + public void setNullCipherState_notifyEncrypted_statusWithIssue() { + ArgumentCaptor<SafetySourceData> data = ArgumentCaptor.forClass(SafetySourceData.class); + + mSafetySource.setNullCipherIssueEnabled(mContext, true); + mSafetySource.setNullCipherState(mContext, 0, NULL_CIPHER_STATE_NOTIFY_ENCRYPTED); + + verify(mSafetyCenterManagerWrapper, times(2)).setSafetySourceData(data.capture()); + assertThat(data.getAllValues().get(1).getStatus()).isNotNull(); + assertThat(data.getAllValues().get(1).getIssues()).hasSize(1); + } + + @Test + public void setNullCipherState_notifyNonEncrypted_statusWithIssue() { + ArgumentCaptor<SafetySourceData> data = ArgumentCaptor.forClass(SafetySourceData.class); + + mSafetySource.setNullCipherIssueEnabled(mContext, true); + mSafetySource.setNullCipherState(mContext, 0, NULL_CIPHER_STATE_NOTIFY_NON_ENCRYPTED); + + verify(mSafetyCenterManagerWrapper, times(2)).setSafetySourceData(data.capture()); + assertThat(data.getAllValues().get(1).getStatus()).isNotNull(); + assertThat(data.getAllValues().get(1).getIssues()).hasSize(1); + } + + @Test + public void setNullCipherState_multipleNonEncrypted_statusWithTwoIssues() { + ArgumentCaptor<SafetySourceData> data = ArgumentCaptor.forClass(SafetySourceData.class); + + mSafetySource.setNullCipherIssueEnabled(mContext, true); + mSafetySource.setNullCipherState(mContext, 0, NULL_CIPHER_STATE_NOTIFY_NON_ENCRYPTED); + mSafetySource.setNullCipherState(mContext, 1, NULL_CIPHER_STATE_NOTIFY_NON_ENCRYPTED); + + verify(mSafetyCenterManagerWrapper, times(3)).setSafetySourceData(data.capture()); + assertThat(data.getAllValues().get(2).getStatus()).isNotNull(); + assertThat(data.getAllValues().get(2).getIssues()).hasSize(2); + } + + @Test + public void disableIdentifierDisclosueIssue_nullData() { + mSafetySource.setIdentifierDisclosureIssueEnabled(mContext, false); + + verify(mSafetyCenterManagerWrapper, times(1)).setSafetySourceData(isNull()); + } + + @Test + public void enableIdentifierDisclosueIssue_statusWithoutIssues() { + ArgumentCaptor<SafetySourceData> data = ArgumentCaptor.forClass(SafetySourceData.class); + + mSafetySource.setIdentifierDisclosureIssueEnabled(mContext, true); + + verify(mSafetyCenterManagerWrapper, times(1)).setSafetySourceData(data.capture()); + assertThat(data.getValue().getStatus()).isNotNull(); + assertThat(data.getValue().getIssues()).isEmpty(); + } + + @Test + public void setIdentifierDisclosure_singleDisclosure_statusWithIssue() { + ArgumentCaptor<SafetySourceData> data = ArgumentCaptor.forClass(SafetySourceData.class); + + mSafetySource.setIdentifierDisclosureIssueEnabled(mContext, true); + mSafetySource.setIdentifierDisclosure(mContext, 0, 12, Instant.now(), Instant.now()); + + verify(mSafetyCenterManagerWrapper, times(2)).setSafetySourceData(data.capture()); + assertThat(data.getAllValues().get(1).getStatus()).isNotNull(); + assertThat(data.getAllValues().get(1).getIssues()).hasSize(1); + } + + @Test + public void setIdentifierDisclosure_multipleDisclosures_statusWithTwoIssues() { + ArgumentCaptor<SafetySourceData> data = ArgumentCaptor.forClass(SafetySourceData.class); + + mSafetySource.setIdentifierDisclosureIssueEnabled(mContext, true); + mSafetySource.setIdentifierDisclosure(mContext, 0, 12, Instant.now(), Instant.now()); + mSafetySource.setIdentifierDisclosure(mContext, 1, 3, Instant.now(), Instant.now()); + + verify(mSafetyCenterManagerWrapper, times(3)).setSafetySourceData(data.capture()); + assertThat(data.getAllValues().get(2).getStatus()).isNotNull(); + assertThat(data.getAllValues().get(2).getIssues()).hasSize(2); + } + + @Test + public void multipleIssuesKinds_statusWithTwoIssues() { + ArgumentCaptor<SafetySourceData> data = ArgumentCaptor.forClass(SafetySourceData.class); + + mSafetySource.setNullCipherIssueEnabled(mContext, true); + mSafetySource.setNullCipherState(mContext, 0, NULL_CIPHER_STATE_NOTIFY_NON_ENCRYPTED); + mSafetySource.setIdentifierDisclosureIssueEnabled(mContext, true); + mSafetySource.setIdentifierDisclosure(mContext, 0, 12, Instant.now(), Instant.now()); + + verify(mSafetyCenterManagerWrapper, times(4)).setSafetySourceData(data.capture()); + assertThat(data.getAllValues().get(3).getStatus()).isNotNull(); + assertThat(data.getAllValues().get(3).getIssues()).hasSize(2); + } +} |