aboutsummaryrefslogtreecommitdiff
path: root/src/java/com/android/internal/telephony/security/CellularNetworkSecuritySafetySource.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/java/com/android/internal/telephony/security/CellularNetworkSecuritySafetySource.java')
-rw-r--r--src/java/com/android/internal/telephony/security/CellularNetworkSecuritySafetySource.java364
1 files changed, 364 insertions, 0 deletions
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);
+ }
+ }
+}