aboutsummaryrefslogtreecommitdiff
path: root/src/java/com/android/internal/telephony/security/CellularNetworkSecuritySafetySource.java
blob: ff09bcb8014bde7dc2296c04d504ab135efec9a4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
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);
        }
    }
}