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