Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 7460c798 authored by Andrew Scull's avatar Andrew Scull Committed by Android (Google) Code Review
Browse files

Merge "Add safety source for cellular network security issues" into main

parents a7d13812 53911f4b
Loading
Loading
Loading
Loading
+16 −0
Original line number Diff line number Diff line
@@ -120,6 +120,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;
@@ -305,6 +306,7 @@ public class GsmCdmaPhone extends Phone {
    private final SubscriptionManager.OnSubscriptionsChangedListener mSubscriptionsChangedListener;
    private final CallWaitingController mCallWaitingController;

    private CellularNetworkSecuritySafetySource mSafetySource;
    private CellularIdentifierDisclosureNotifier mIdentifierDisclosureNotifier;
    private NullCipherNotifier mNullCipherNotifier;

@@ -534,6 +536,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 "
@@ -5435,4 +5443,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);
        }
    }
}
+6 −0
Original line number Diff line number Diff line
@@ -5213,6 +5213,12 @@ public abstract class Phone extends Handler implements PhoneInternalInterface {
    public void handleNullCipherNotificationPreferenceChanged() {
    }

    /**
     * Refresh the safety sources in response to the identified broadcast.
     */
    public void refreshSafetySources(String refreshBroadcastId) {
    }

    /**
     * Notifies the IMS call status to the modem.
     *
+7 −0
Original line number Diff line number Diff line
@@ -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();
+364 −0
Original line number Diff line number Diff line
/*
 * 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);
        }
    }
}
+5 −0
Original line number Diff line number Diff line
@@ -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();
Loading