aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndrew Scull <ascull@google.com>2024-01-08 17:45:43 +0000
committerAndrew Scull <ascull@google.com>2024-02-01 16:51:33 +0000
commit53911f4b99da7bdbbdc03110c369f8eccaa2c3ba (patch)
tree53857192e994b6fb5b757f3bfb7ccfe843b47327
parent6628b3d7d9c3c28f0aa1104d5b677bbd8e517e06 (diff)
downloadtelephony-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
-rw-r--r--src/java/com/android/internal/telephony/GsmCdmaPhone.java16
-rw-r--r--src/java/com/android/internal/telephony/Phone.java6
-rw-r--r--src/java/com/android/internal/telephony/TelephonyComponentFactory.java7
-rw-r--r--src/java/com/android/internal/telephony/security/CellularNetworkSecuritySafetySource.java364
-rw-r--r--tests/telephonytests/src/com/android/internal/telephony/TelephonyTest.java5
-rw-r--r--tests/telephonytests/src/com/android/internal/telephony/security/CellularNetworkSecuritySafetySourceTest.java230
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);
+ }
+}