1 /*
2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.internal.telephony.security;
18 
19 import static android.safetycenter.SafetyEvent.SAFETY_EVENT_TYPE_REFRESH_REQUESTED;
20 import static android.safetycenter.SafetyEvent.SAFETY_EVENT_TYPE_SOURCE_STATE_CHANGED;
21 import static android.safetycenter.SafetySourceData.SEVERITY_LEVEL_INFORMATION;
22 import static android.safetycenter.SafetySourceData.SEVERITY_LEVEL_RECOMMENDATION;
23 
24 import android.annotation.IntDef;
25 import android.app.PendingIntent;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.content.res.Resources;
29 import android.net.Uri;
30 import android.safetycenter.SafetyCenterManager;
31 import android.safetycenter.SafetyEvent;
32 import android.safetycenter.SafetySourceData;
33 import android.safetycenter.SafetySourceIssue;
34 import android.safetycenter.SafetySourceStatus;
35 import android.text.format.DateFormat;
36 
37 import com.android.internal.R;
38 import com.android.internal.annotations.VisibleForTesting;
39 import com.android.internal.telephony.subscription.SubscriptionInfoInternal;
40 import com.android.internal.telephony.subscription.SubscriptionManagerService;
41 
42 import java.lang.annotation.Retention;
43 import java.lang.annotation.RetentionPolicy;
44 import java.time.Instant;
45 import java.time.ZoneId;
46 import java.time.format.DateTimeFormatter;
47 import java.util.HashMap;
48 import java.util.Locale;
49 import java.util.Objects;
50 import java.util.Optional;
51 import java.util.stream.Stream;
52 
53 /**
54  * Holds the state needed to report the Safety Center status and issues related to cellular
55  * network security.
56  */
57 public class CellularNetworkSecuritySafetySource {
58     private static final String SAFETY_SOURCE_ID = "AndroidCellularNetworkSecurity";
59 
60     private static final String NULL_CIPHER_ISSUE_NON_ENCRYPTED_ID = "null_cipher_non_encrypted";
61     private static final String NULL_CIPHER_ISSUE_ENCRYPTED_ID = "null_cipher_encrypted";
62 
63     private static final String NULL_CIPHER_ACTION_SETTINGS_ID = "cellular_security_settings";
64     private static final String NULL_CIPHER_ACTION_LEARN_MORE_ID = "learn_more";
65 
66     private static final String IDENTIFIER_DISCLOSURE_ISSUE_ID = "identifier_disclosure";
67 
68     private static final Intent CELLULAR_NETWORK_SECURITY_SETTINGS_INTENT =
69             new Intent("android.settings.CELLULAR_NETWORK_SECURITY");
70     static final int NULL_CIPHER_STATE_ENCRYPTED = 0;
71     static final int NULL_CIPHER_STATE_NOTIFY_ENCRYPTED = 1;
72     static final int NULL_CIPHER_STATE_NOTIFY_NON_ENCRYPTED = 2;
73 
74     @IntDef(
75         prefix = {"NULL_CIPHER_STATE_"},
76         value = {
77             NULL_CIPHER_STATE_ENCRYPTED,
78             NULL_CIPHER_STATE_NOTIFY_ENCRYPTED,
79             NULL_CIPHER_STATE_NOTIFY_NON_ENCRYPTED})
80     @Retention(RetentionPolicy.SOURCE)
81     @interface NullCipherState {}
82 
83     private static CellularNetworkSecuritySafetySource sInstance;
84 
85     private final SafetyCenterManagerWrapper mSafetyCenterManagerWrapper;
86     private final SubscriptionManagerService mSubscriptionManagerService;
87 
88     private boolean mNullCipherStateIssuesEnabled;
89     private HashMap<Integer, Integer> mNullCipherStates = new HashMap<>();
90 
91     private boolean mIdentifierDisclosureIssuesEnabled;
92     private HashMap<Integer, IdentifierDisclosure> mIdentifierDisclosures = new HashMap<>();
93 
94     /**
95      * Gets a singleton CellularNetworkSecuritySafetySource.
96      */
getInstance(Context context)97     public static synchronized CellularNetworkSecuritySafetySource getInstance(Context context) {
98         if (sInstance == null) {
99             sInstance = new CellularNetworkSecuritySafetySource(
100                     new SafetyCenterManagerWrapper(context));
101         }
102         return sInstance;
103     }
104 
105     @VisibleForTesting
CellularNetworkSecuritySafetySource( SafetyCenterManagerWrapper safetyCenterManagerWrapper)106     public CellularNetworkSecuritySafetySource(
107             SafetyCenterManagerWrapper safetyCenterManagerWrapper) {
108         mSafetyCenterManagerWrapper = safetyCenterManagerWrapper;
109         mSubscriptionManagerService = SubscriptionManagerService.getInstance();
110     }
111 
112     /** Enables or disables the null cipher issue and clears any current issues. */
setNullCipherIssueEnabled(Context context, boolean enabled)113     public synchronized void setNullCipherIssueEnabled(Context context, boolean enabled) {
114         mNullCipherStateIssuesEnabled = enabled;
115         mNullCipherStates.clear();
116         updateSafetyCenter(context);
117     }
118 
119     /** Sets the null cipher issue state for the identified subscription. */
setNullCipherState( Context context, int subId, @NullCipherState int nullCipherState)120     public synchronized void setNullCipherState(
121             Context context, int subId, @NullCipherState int nullCipherState) {
122         mNullCipherStates.put(subId, nullCipherState);
123         updateSafetyCenter(context);
124     }
125 
126     /**
127      * Clears issue state for the identified subscription
128      */
clearNullCipherState(Context context, int subId)129     public synchronized  void clearNullCipherState(Context context, int subId) {
130         mNullCipherStates.remove(subId);
131         updateSafetyCenter(context);
132     }
133     /**
134      * Enables or disables the identifier disclosure issue and clears any current issues if the
135      * enable state is changed.
136      */
setIdentifierDisclosureIssueEnabled(Context context, boolean enabled)137     public synchronized void setIdentifierDisclosureIssueEnabled(Context context, boolean enabled) {
138         // This check ensures that if we're enabled and we are asked to enable ourselves again (can
139         // happen if the modem restarts), we don't clear our state.
140         if (enabled != mIdentifierDisclosureIssuesEnabled) {
141             mIdentifierDisclosureIssuesEnabled = enabled;
142             mIdentifierDisclosures.clear();
143             updateSafetyCenter(context);
144         }
145     }
146 
147     /** Sets the identifier disclosure issue state for the identifier subscription. */
setIdentifierDisclosure( Context context, int subId, int count, Instant start, Instant end)148     public synchronized void setIdentifierDisclosure(
149             Context context, int subId, int count, Instant start, Instant end) {
150         IdentifierDisclosure disclosure = new IdentifierDisclosure(count, start, end);
151         mIdentifierDisclosures.put(subId, disclosure);
152         updateSafetyCenter(context);
153     }
154 
155     /** Clears the identifier disclosure issue state for the identified subscription. */
clearIdentifierDisclosure(Context context, int subId)156     public synchronized void clearIdentifierDisclosure(Context context, int subId) {
157         mIdentifierDisclosures.remove(subId);
158         updateSafetyCenter(context);
159     }
160 
161     /** Refreshed the safety source in response to the identified broadcast. */
refresh(Context context, String refreshBroadcastId)162     public synchronized void refresh(Context context, String refreshBroadcastId) {
163         mSafetyCenterManagerWrapper.setRefreshedSafetySourceData(
164                 refreshBroadcastId, getSafetySourceData(context));
165     }
166 
updateSafetyCenter(Context context)167     private void updateSafetyCenter(Context context) {
168         mSafetyCenterManagerWrapper.setSafetySourceData(getSafetySourceData(context));
169     }
170 
isSafetySourceHidden()171     private boolean isSafetySourceHidden() {
172         return !mNullCipherStateIssuesEnabled && !mIdentifierDisclosureIssuesEnabled;
173     }
174 
getSafetySourceData(Context context)175     private SafetySourceData getSafetySourceData(Context context) {
176         if (isSafetySourceHidden()) {
177             // The cellular network security safety source is configured with
178             // initialDisplayState="hidden"
179             return null;
180         }
181 
182         Stream<Optional<SafetySourceIssue>> nullCipherIssues =
183                 mNullCipherStates.entrySet().stream()
184                         .map(e -> getNullCipherIssue(context, e.getKey(), e.getValue()));
185         Stream<Optional<SafetySourceIssue>> identifierDisclosureIssues =
186                 mIdentifierDisclosures.entrySet().stream()
187                         .map(e -> getIdentifierDisclosureIssue(context, e.getKey(), e.getValue()));
188         SafetySourceIssue[] issues = Stream.concat(nullCipherIssues, identifierDisclosureIssues)
189                 .flatMap(Optional::stream)
190                 .toArray(SafetySourceIssue[]::new);
191 
192         SafetySourceData.Builder builder = new SafetySourceData.Builder();
193         int maxSeverity = SEVERITY_LEVEL_INFORMATION;
194         for (SafetySourceIssue issue : issues) {
195             builder.addIssue(issue);
196             maxSeverity = Math.max(maxSeverity, issue.getSeverityLevel());
197         }
198 
199         builder.setStatus(
200                 new SafetySourceStatus.Builder(
201                         context.getString(R.string.scCellularNetworkSecurityTitle),
202                         context.getString(R.string.scCellularNetworkSecuritySummary),
203                         maxSeverity)
204                     .setPendingIntent(mSafetyCenterManagerWrapper.getActivityPendingIntent(
205                             context, CELLULAR_NETWORK_SECURITY_SETTINGS_INTENT))
206                     .build());
207         return builder.build();
208     }
209 
210     /** Builds the null cipher issue if it's enabled and there are null ciphers to report. */
getNullCipherIssue( Context context, int subId, @NullCipherState int state)211     private Optional<SafetySourceIssue> getNullCipherIssue(
212             Context context, int subId, @NullCipherState int state) {
213         if (!mNullCipherStateIssuesEnabled) {
214             return Optional.empty();
215         }
216 
217         SubscriptionInfoInternal subInfo =
218                 mSubscriptionManagerService.getSubscriptionInfoInternal(subId);
219         final SafetySourceIssue.Builder builder;
220         final SafetySourceIssue.Notification customNotification;
221         switch (state) {
222             case NULL_CIPHER_STATE_ENCRYPTED:
223                 return Optional.empty();
224             case NULL_CIPHER_STATE_NOTIFY_NON_ENCRYPTED:
225                 builder = new SafetySourceIssue.Builder(
226                         NULL_CIPHER_ISSUE_NON_ENCRYPTED_ID + "_" + subId,
227                         context.getString(
228                                 R.string.scNullCipherIssueNonEncryptedTitle),
229                         context.getString(
230                               R.string.scNullCipherIssueNonEncryptedSummary,
231                               subInfo.getDisplayName()),
232                         SEVERITY_LEVEL_RECOMMENDATION,
233                         NULL_CIPHER_ISSUE_NON_ENCRYPTED_ID);
234                 customNotification =
235                          new SafetySourceIssue.Notification.Builder(
236                                 context.getString(R.string.scNullCipherIssueNonEncryptedTitle),
237                                 context.getString(
238                                         R.string.scNullCipherIssueNonEncryptedSummaryNotification,
239                                         subInfo.getDisplayName()))
240                         .build();
241                 break;
242             case NULL_CIPHER_STATE_NOTIFY_ENCRYPTED:
243                 builder = new SafetySourceIssue.Builder(
244                         NULL_CIPHER_ISSUE_NON_ENCRYPTED_ID + "_" + subId,
245                         context.getString(
246                                 R.string.scNullCipherIssueEncryptedTitle,
247                                 subInfo.getDisplayName()),
248                         context.getString(
249                                 R.string.scNullCipherIssueEncryptedSummary,
250                                 subInfo.getDisplayName()),
251                         SEVERITY_LEVEL_INFORMATION,
252                         NULL_CIPHER_ISSUE_ENCRYPTED_ID);
253                 customNotification =
254                         new SafetySourceIssue.Notification.Builder(
255                                 context.getString(
256                                       R.string.scNullCipherIssueEncryptedTitle,
257                                       subInfo.getDisplayName()),
258                                 context.getString(
259                                       R.string.scNullCipherIssueEncryptedSummary,
260                                       subInfo.getDisplayName()))
261                         .build();
262                 break;
263             default:
264                 throw new AssertionError();
265         }
266         builder
267                 .setNotificationBehavior(
268                         SafetySourceIssue.NOTIFICATION_BEHAVIOR_IMMEDIATELY)
269                 .setIssueCategory(SafetySourceIssue.ISSUE_CATEGORY_DATA)
270                 .setCustomNotification(customNotification)
271                 .addAction(
272                         new SafetySourceIssue.Action.Builder(
273                                 NULL_CIPHER_ACTION_SETTINGS_ID,
274                                 context.getString(R.string.scNullCipherIssueActionSettings),
275                                 mSafetyCenterManagerWrapper.getActivityPendingIntent(
276                                         context, CELLULAR_NETWORK_SECURITY_SETTINGS_INTENT))
277                                 .build());
278 
279         Intent learnMoreIntent = getLearnMoreIntent(context);
280         if (learnMoreIntent != null) {
281             builder.addAction(
282                     new SafetySourceIssue.Action.Builder(
283                             NULL_CIPHER_ACTION_LEARN_MORE_ID,
284                             context.getString(
285                                     R.string.scNullCipherIssueActionLearnMore),
286                             mSafetyCenterManagerWrapper.getActivityPendingIntent(
287                                     context, learnMoreIntent))
288                             .build());
289         }
290 
291         return Optional.of(builder.build());
292     }
293 
294     /** Builds the identity disclosure issue if it's enabled and there are disclosures to report. */
getIdentifierDisclosureIssue( Context context, int subId, IdentifierDisclosure disclosure)295     private Optional<SafetySourceIssue> getIdentifierDisclosureIssue(
296             Context context, int subId, IdentifierDisclosure disclosure) {
297         if (!mIdentifierDisclosureIssuesEnabled || disclosure.getDisclosureCount() == 0) {
298             return Optional.empty();
299         }
300 
301         SubscriptionInfoInternal subInfo =
302                 mSubscriptionManagerService.getSubscriptionInfoInternal(subId);
303 
304         // Notifications have no buttons
305         final SafetySourceIssue.Notification customNotification =
306                 new SafetySourceIssue.Notification.Builder(
307                         context.getString(R.string.scIdentifierDisclosureIssueTitle),
308                         context.getString(
309                                 R.string.scIdentifierDisclosureIssueSummaryNotification,
310                                 getCurrentTime(),
311                                 subInfo.getDisplayName())).build();
312         SafetySourceIssue.Builder builder =
313                 new SafetySourceIssue.Builder(
314                         IDENTIFIER_DISCLOSURE_ISSUE_ID + "_" + subId,
315                         context.getString(R.string.scIdentifierDisclosureIssueTitle),
316                         context.getString(
317                                 R.string.scIdentifierDisclosureIssueSummary,
318                                 getCurrentTime(),
319                                 subInfo.getDisplayName()),
320                         SEVERITY_LEVEL_RECOMMENDATION,
321                         IDENTIFIER_DISCLOSURE_ISSUE_ID)
322                         .setNotificationBehavior(
323                                 SafetySourceIssue.NOTIFICATION_BEHAVIOR_IMMEDIATELY)
324                         .setIssueCategory(SafetySourceIssue.ISSUE_CATEGORY_DATA)
325                         .setCustomNotification(customNotification)
326                         .addAction(
327                                 new SafetySourceIssue.Action.Builder(
328                                         NULL_CIPHER_ACTION_SETTINGS_ID,
329                                         context.getString(
330                                                 R.string.scNullCipherIssueActionSettings),
331                                         mSafetyCenterManagerWrapper.getActivityPendingIntent(
332                                                 context,
333                                                 CELLULAR_NETWORK_SECURITY_SETTINGS_INTENT))
334                                         .build());
335 
336         Intent learnMoreIntent = getLearnMoreIntent(context);
337         if (learnMoreIntent != null) {
338             builder.addAction(
339                     new SafetySourceIssue.Action.Builder(
340                             NULL_CIPHER_ACTION_LEARN_MORE_ID,
341                             context.getString(R.string.scNullCipherIssueActionLearnMore),
342                             mSafetyCenterManagerWrapper.getActivityPendingIntent(
343                                     context, learnMoreIntent)).build()
344             );
345         }
346 
347         return Optional.of(builder.build());
348     }
349 
getCurrentTime()350     private String getCurrentTime() {
351         String pattern = DateFormat.getBestDateTimePattern(Locale.getDefault(), "hh:mm");
352         return Instant.now().atZone(ZoneId.systemDefault())
353               .format(DateTimeFormatter.ofPattern(pattern)).toString();
354     }
355 
356     /**
357      * Return Intent for learn more action, or null if resource associated with the Intent
358      * uri is
359      * missing or empty.
360      */
getLearnMoreIntent(Context context)361     private Intent getLearnMoreIntent(Context context) {
362         String learnMoreUri;
363         try {
364             learnMoreUri = context.getString(R.string.scCellularNetworkSecurityLearnMore);
365         } catch (Resources.NotFoundException e) {
366             return null;
367         }
368 
369         if (learnMoreUri.isEmpty()) {
370             return null;
371         }
372 
373         return new Intent(Intent.ACTION_VIEW, Uri.parse(learnMoreUri));
374     }
375 
376     /** A wrapper around {@link SafetyCenterManager} that can be instrumented in tests. */
377     @VisibleForTesting
378     public static class SafetyCenterManagerWrapper {
379         private final SafetyCenterManager mSafetyCenterManager;
380 
SafetyCenterManagerWrapper(Context context)381         public SafetyCenterManagerWrapper(Context context) {
382             mSafetyCenterManager = context.getSystemService(SafetyCenterManager.class);
383         }
384 
385         /** Retrieve a {@link PendingIntent} that will start a new activity. */
getActivityPendingIntent(Context context, Intent intent)386         public PendingIntent getActivityPendingIntent(Context context, Intent intent) {
387             return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE);
388         }
389 
390         /** Set the {@link SafetySourceData} for this safety source. */
setSafetySourceData(SafetySourceData safetySourceData)391         public void setSafetySourceData(SafetySourceData safetySourceData) {
392             mSafetyCenterManager.setSafetySourceData(
393                     SAFETY_SOURCE_ID,
394                     safetySourceData,
395                     new SafetyEvent.Builder(SAFETY_EVENT_TYPE_SOURCE_STATE_CHANGED).build());
396         }
397 
398         /** Sets the {@link SafetySourceData} in response to a refresh request. */
setRefreshedSafetySourceData( String refreshBroadcastId, SafetySourceData safetySourceData)399         public void setRefreshedSafetySourceData(
400                 String refreshBroadcastId, SafetySourceData safetySourceData) {
401             mSafetyCenterManager.setSafetySourceData(
402                     SAFETY_SOURCE_ID,
403                     safetySourceData,
404                     new SafetyEvent.Builder(SAFETY_EVENT_TYPE_REFRESH_REQUESTED)
405                             .setRefreshBroadcastId(refreshBroadcastId)
406                             .build());
407         }
408     }
409 
410     private static class IdentifierDisclosure {
411         private final int mDisclosureCount;
412         private final Instant mWindowStart;
413         private final Instant mWindowEnd;
414 
IdentifierDisclosure(int count, Instant start, Instant end)415         private IdentifierDisclosure(int count, Instant start, Instant end) {
416             mDisclosureCount = count;
417             mWindowStart = start;
418             mWindowEnd = end;
419         }
420 
getDisclosureCount()421         private int getDisclosureCount() {
422             return mDisclosureCount;
423         }
424 
getWindowStart()425         private Instant getWindowStart() {
426             return mWindowStart;
427         }
428 
getWindowEnd()429         private Instant getWindowEnd() {
430             return mWindowEnd;
431         }
432 
433         @Override
equals(Object o)434         public boolean equals(Object o) {
435             if (!(o instanceof IdentifierDisclosure)) {
436                 return false;
437             }
438             IdentifierDisclosure other = (IdentifierDisclosure) o;
439             return mDisclosureCount == other.mDisclosureCount
440                     && Objects.equals(mWindowStart, other.mWindowStart)
441                     && Objects.equals(mWindowEnd, other.mWindowEnd);
442         }
443 
444         @Override
hashCode()445         public int hashCode() {
446             return Objects.hash(mDisclosureCount, mWindowStart, mWindowEnd);
447         }
448     }
449 }
450