1 /* 2 * Copyright (C) 2023 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 android.content.Context; 20 import android.telephony.CellularIdentifierDisclosure; 21 22 import com.android.internal.annotations.GuardedBy; 23 import com.android.internal.annotations.VisibleForTesting; 24 import com.android.internal.telephony.metrics.CellularSecurityTransparencyStats; 25 import com.android.internal.telephony.subscription.SubscriptionInfoInternal; 26 import com.android.internal.telephony.subscription.SubscriptionManagerService; 27 import com.android.telephony.Rlog; 28 29 import java.time.Instant; 30 import java.util.HashMap; 31 import java.util.Map; 32 import java.util.concurrent.Executors; 33 import java.util.concurrent.RejectedExecutionException; 34 import java.util.concurrent.ScheduledExecutorService; 35 import java.util.concurrent.ScheduledFuture; 36 import java.util.concurrent.TimeUnit; 37 38 /** 39 * Encapsulates logic to emit notifications to the user that their cellular identifiers were 40 * disclosed in the clear. Callers add CellularIdentifierDisclosure instances by calling 41 * addDisclosure. 42 * 43 * <p>This class is thread safe and is designed to do costly work on worker threads. The intention 44 * is to allow callers to add disclosures from a Looper thread without worrying about blocking for 45 * IPC. 46 * 47 * @hide 48 */ 49 public class CellularIdentifierDisclosureNotifier { 50 51 private static final String TAG = "CellularIdentifierDisclosureNotifier"; 52 private static final long DEFAULT_WINDOW_CLOSE_DURATION_IN_MINUTES = 15; 53 private static CellularIdentifierDisclosureNotifier sInstance = null; 54 private final long mWindowCloseDuration; 55 private final TimeUnit mWindowCloseUnit; 56 private final CellularNetworkSecuritySafetySource mSafetySource; 57 private final Object mEnabledLock = new Object(); 58 59 @GuardedBy("mEnabledLock") 60 private boolean mEnabled = false; 61 // This is a single threaded executor. This is important because we want to ensure certain 62 // events are strictly serialized. 63 private ScheduledExecutorService mSerializedWorkQueue; 64 65 // This object should only be accessed from within the thread of mSerializedWorkQueue. Access 66 // outside of that thread would require additional synchronization. 67 private Map<Integer, DisclosureWindow> mWindows; 68 private SubscriptionManagerService mSubscriptionManagerService; 69 private CellularSecurityTransparencyStats mCellularSecurityTransparencyStats; 70 CellularIdentifierDisclosureNotifier(CellularNetworkSecuritySafetySource safetySource)71 public CellularIdentifierDisclosureNotifier(CellularNetworkSecuritySafetySource safetySource) { 72 this(Executors.newSingleThreadScheduledExecutor(), DEFAULT_WINDOW_CLOSE_DURATION_IN_MINUTES, 73 TimeUnit.MINUTES, safetySource, SubscriptionManagerService.getInstance(), 74 new CellularSecurityTransparencyStats()); 75 } 76 77 /** 78 * Construct a CellularIdentifierDisclosureNotifier by injection. This should only be used for 79 * testing. 80 * 81 * @param notificationQueue a ScheduledExecutorService that should only execute on a single 82 * thread. 83 */ 84 @VisibleForTesting CellularIdentifierDisclosureNotifier( ScheduledExecutorService notificationQueue, long windowCloseDuration, TimeUnit windowCloseUnit, CellularNetworkSecuritySafetySource safetySource, SubscriptionManagerService subscriptionManagerService, CellularSecurityTransparencyStats cellularSecurityTransparencyStats)85 public CellularIdentifierDisclosureNotifier( 86 ScheduledExecutorService notificationQueue, 87 long windowCloseDuration, 88 TimeUnit windowCloseUnit, 89 CellularNetworkSecuritySafetySource safetySource, 90 SubscriptionManagerService subscriptionManagerService, 91 CellularSecurityTransparencyStats cellularSecurityTransparencyStats) { 92 mSerializedWorkQueue = notificationQueue; 93 mWindowCloseDuration = windowCloseDuration; 94 mWindowCloseUnit = windowCloseUnit; 95 mWindows = new HashMap<>(); 96 mSafetySource = safetySource; 97 mSubscriptionManagerService = subscriptionManagerService; 98 mCellularSecurityTransparencyStats = cellularSecurityTransparencyStats; 99 } 100 101 /** 102 * Add a CellularIdentifierDisclosure to be tracked by this instance. If appropriate, this will 103 * trigger a user notification. 104 */ addDisclosure(Context context, int subId, CellularIdentifierDisclosure disclosure)105 public void addDisclosure(Context context, int subId, CellularIdentifierDisclosure disclosure) { 106 Rlog.d(TAG, "Identifier disclosure reported: " + disclosure); 107 108 logDisclosure(subId, disclosure); 109 110 synchronized (mEnabledLock) { 111 if (!mEnabled) { 112 Rlog.d(TAG, "Skipping disclosure because notifier was disabled."); 113 return; 114 } 115 116 // Don't notify if this disclosure happened in service of an emergency. That's a user 117 // initiated action that we don't want to interfere with. 118 if (disclosure.isEmergency()) { 119 Rlog.i(TAG, "Ignoring identifier disclosure associated with an emergency."); 120 return; 121 } 122 123 // Don't notify if the modem vendor indicates this is a benign disclosure. 124 if (disclosure.isBenign()) { 125 Rlog.i(TAG, "Ignoring identifier disclosure that is claimed to be benign."); 126 return; 127 } 128 129 // Schedule incrementAndNotify from within the lock because we're sure at this point 130 // that we're enabled. This allows incrementAndNotify to avoid re-checking mEnabled 131 // because we know that any actions taken on disabled will be scheduled after this 132 // incrementAndNotify call. 133 try { 134 mSerializedWorkQueue.execute(incrementAndNotify(context, subId)); 135 } catch (RejectedExecutionException e) { 136 Rlog.e(TAG, "Failed to schedule incrementAndNotify: " + e.getMessage()); 137 } 138 } // end mEnabledLock 139 } 140 logDisclosure(int subId, CellularIdentifierDisclosure disclosure)141 private void logDisclosure(int subId, CellularIdentifierDisclosure disclosure) { 142 try { 143 mSerializedWorkQueue.execute(runLogDisclosure(subId, disclosure)); 144 } catch (RejectedExecutionException e) { 145 Rlog.e(TAG, "Failed to schedule runLogDisclosure: " + e.getMessage()); 146 } 147 } 148 runLogDisclosure(int subId, CellularIdentifierDisclosure disclosure)149 private Runnable runLogDisclosure(int subId, 150 CellularIdentifierDisclosure disclosure) { 151 return () -> { 152 SubscriptionInfoInternal subInfo = 153 mSubscriptionManagerService.getSubscriptionInfoInternal(subId); 154 String mcc = null; 155 String mnc = null; 156 if (subInfo != null) { 157 mcc = subInfo.getMcc(); 158 mnc = subInfo.getMnc(); 159 } 160 161 mCellularSecurityTransparencyStats.logIdentifierDisclosure(disclosure, mcc, mnc, 162 isEnabled()); 163 }; 164 } 165 166 /** 167 * Re-enable if previously disabled. This means that {@code addDisclsoure} will start tracking 168 * disclosures again and potentially emitting notifications. 169 */ enable(Context context)170 public void enable(Context context) { 171 synchronized (mEnabledLock) { 172 Rlog.d(TAG, "enabled"); 173 mEnabled = true; 174 try { 175 mSerializedWorkQueue.execute(onEnableNotifier(context)); 176 } catch (RejectedExecutionException e) { 177 Rlog.e(TAG, "Failed to schedule onEnableNotifier: " + e.getMessage()); 178 } 179 } 180 } 181 182 /** 183 * Clear all internal state and prevent further notifications until optionally re-enabled. 184 * This can be used to in response to a user disabling the feature to emit notifications. 185 * If {@code addDisclosure} is called while in a disabled state, disclosures will be dropped. 186 */ disable(Context context)187 public void disable(Context context) { 188 Rlog.d(TAG, "disabled"); 189 synchronized (mEnabledLock) { 190 mEnabled = false; 191 try { 192 mSerializedWorkQueue.execute(onDisableNotifier(context)); 193 } catch (RejectedExecutionException e) { 194 Rlog.e(TAG, "Failed to schedule onDisableNotifier: " + e.getMessage()); 195 } 196 } 197 } 198 isEnabled()199 public boolean isEnabled() { 200 synchronized (mEnabledLock) { 201 return mEnabled; 202 } 203 } 204 205 /** Get a singleton CellularIdentifierDisclosureNotifier. */ getInstance( CellularNetworkSecuritySafetySource safetySource)206 public static synchronized CellularIdentifierDisclosureNotifier getInstance( 207 CellularNetworkSecuritySafetySource safetySource) { 208 if (sInstance == null) { 209 sInstance = new CellularIdentifierDisclosureNotifier(safetySource); 210 } 211 212 return sInstance; 213 } 214 incrementAndNotify(Context context, int subId)215 private Runnable incrementAndNotify(Context context, int subId) { 216 return () -> { 217 DisclosureWindow window = mWindows.get(subId); 218 if (window == null) { 219 window = new DisclosureWindow(subId); 220 mWindows.put(subId, window); 221 } 222 223 window.increment(context, this); 224 225 int disclosureCount = window.getDisclosureCount(); 226 227 Rlog.d( 228 TAG, 229 "Emitting notification for subId: " 230 + subId 231 + ". New disclosure count " 232 + disclosureCount); 233 234 mSafetySource.setIdentifierDisclosure( 235 context, 236 subId, 237 disclosureCount, 238 window.getFirstOpen(), 239 window.getCurrentEnd()); 240 }; 241 } 242 243 private Runnable onDisableNotifier(Context context) { 244 return () -> { 245 Rlog.d(TAG, "On disable notifier"); 246 for (DisclosureWindow window : mWindows.values()) { 247 window.close(); 248 } 249 mSafetySource.setIdentifierDisclosureIssueEnabled(context, false); 250 }; 251 } 252 253 private Runnable onEnableNotifier(Context context) { 254 return () -> { 255 Rlog.i(TAG, "On enable notifier"); 256 mSafetySource.setIdentifierDisclosureIssueEnabled(context, true); 257 }; 258 } 259 260 /** 261 * Get the disclosure count for a given subId. NOTE: This method is not thread safe. Without 262 * external synchronization, one should only call it if there are no pending tasks on the 263 * Executor passed into this class. 264 */ 265 @VisibleForTesting 266 public int getCurrentDisclosureCount(int subId) { 267 DisclosureWindow window = mWindows.get(subId); 268 if (window != null) { 269 return window.getDisclosureCount(); 270 } 271 272 return 0; 273 } 274 275 /** 276 * Get the open time for a given subId. NOTE: This method is not thread safe. Without 277 * external synchronization, one should only call it if there are no pending tasks on the 278 * Executor passed into this class. 279 */ 280 @VisibleForTesting 281 public Instant getFirstOpen(int subId) { 282 DisclosureWindow window = mWindows.get(subId); 283 if (window != null) { 284 return window.getFirstOpen(); 285 } 286 287 return null; 288 } 289 290 /** 291 * Get the current end time for a given subId. NOTE: This method is not thread safe. Without 292 * external synchronization, one should only call it if there are no pending tasks on the 293 * Executor passed into this class. 294 */ 295 @VisibleForTesting 296 public Instant getCurrentEnd(int subId) { 297 DisclosureWindow window = mWindows.get(subId); 298 if (window != null) { 299 return window.getCurrentEnd(); 300 } 301 302 return null; 303 } 304 305 /** 306 * A helper class that maintains all state associated with the disclosure window for a single 307 * subId. No methods are thread safe. Callers must implement all synchronization. 308 */ 309 private class DisclosureWindow { 310 private int mDisclosureCount; 311 private Instant mWindowFirstOpen; 312 private Instant mLastEvent; 313 private ScheduledFuture<?> mWhenWindowCloses; 314 315 private int mSubId; 316 317 DisclosureWindow(int subId) { 318 mDisclosureCount = 0; 319 mWindowFirstOpen = null; 320 mLastEvent = null; 321 mSubId = subId; 322 mWhenWindowCloses = null; 323 } 324 325 void increment(Context context, CellularIdentifierDisclosureNotifier notifier) { 326 327 mDisclosureCount++; 328 329 Instant now = Instant.now(); 330 if (mDisclosureCount == 1) { 331 // Our window was opened for the first time 332 mWindowFirstOpen = now; 333 } 334 335 mLastEvent = now; 336 337 cancelWindowCloseFuture(); 338 339 try { 340 mWhenWindowCloses = 341 notifier.mSerializedWorkQueue.schedule( 342 closeWindowRunnable(context), 343 notifier.mWindowCloseDuration, 344 notifier.mWindowCloseUnit); 345 } catch (RejectedExecutionException e) { 346 Rlog.e( 347 TAG, 348 "Failed to schedule closeWindow for subId " 349 + mSubId 350 + " : " 351 + e.getMessage()); 352 } 353 } 354 355 int getDisclosureCount() { 356 return mDisclosureCount; 357 } 358 359 Instant getFirstOpen() { 360 return mWindowFirstOpen; 361 } 362 363 Instant getCurrentEnd() { 364 return mLastEvent; 365 } 366 367 void close() { 368 mDisclosureCount = 0; 369 mWindowFirstOpen = null; 370 mLastEvent = null; 371 372 if (mWhenWindowCloses == null) { 373 return; 374 } 375 mWhenWindowCloses = null; 376 } 377 378 private Runnable closeWindowRunnable(Context context) { 379 return () -> { 380 Rlog.i( 381 TAG, 382 "Disclosure window closing for subId " 383 + mSubId 384 + ". Disclosure count was " 385 + getDisclosureCount()); 386 close(); 387 mSafetySource.clearIdentifierDisclosure(context, mSubId); 388 }; 389 } 390 391 private boolean cancelWindowCloseFuture() { 392 if (mWhenWindowCloses == null) { 393 return false; 394 } 395 396 // Pass false to not interrupt a running Future. Nothing about our notifier is ready 397 // for this type of preemption. 398 return mWhenWindowCloses.cancel(false); 399 } 400 401 } 402 } 403 404