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 // Schedule incrementAndNotify from within the lock because we're sure at this point 124 // that we're enabled. This allows incrementAndNotify to avoid re-checking mEnabled 125 // because we know that any actions taken on disabled will be scheduled after this 126 // incrementAndNotify call. 127 try { 128 mSerializedWorkQueue.execute(incrementAndNotify(context, subId)); 129 } catch (RejectedExecutionException e) { 130 Rlog.e(TAG, "Failed to schedule incrementAndNotify: " + e.getMessage()); 131 } 132 } // end mEnabledLock 133 } 134 logDisclosure(int subId, CellularIdentifierDisclosure disclosure)135 private void logDisclosure(int subId, CellularIdentifierDisclosure disclosure) { 136 try { 137 mSerializedWorkQueue.execute(runLogDisclosure(subId, disclosure)); 138 } catch (RejectedExecutionException e) { 139 Rlog.e(TAG, "Failed to schedule runLogDisclosure: " + e.getMessage()); 140 } 141 } 142 runLogDisclosure(int subId, CellularIdentifierDisclosure disclosure)143 private Runnable runLogDisclosure(int subId, 144 CellularIdentifierDisclosure disclosure) { 145 return () -> { 146 SubscriptionInfoInternal subInfo = 147 mSubscriptionManagerService.getSubscriptionInfoInternal(subId); 148 String mcc = null; 149 String mnc = null; 150 if (subInfo != null) { 151 mcc = subInfo.getMcc(); 152 mnc = subInfo.getMnc(); 153 } 154 155 mCellularSecurityTransparencyStats.logIdentifierDisclosure(disclosure, mcc, mnc, 156 isEnabled()); 157 }; 158 } 159 160 /** 161 * Re-enable if previously disabled. This means that {@code addDisclsoure} will start tracking 162 * disclosures again and potentially emitting notifications. 163 */ enable(Context context)164 public void enable(Context context) { 165 synchronized (mEnabledLock) { 166 Rlog.d(TAG, "enabled"); 167 mEnabled = true; 168 try { 169 mSerializedWorkQueue.execute(onEnableNotifier(context)); 170 } catch (RejectedExecutionException e) { 171 Rlog.e(TAG, "Failed to schedule onEnableNotifier: " + e.getMessage()); 172 } 173 } 174 } 175 176 /** 177 * Clear all internal state and prevent further notifications until optionally re-enabled. 178 * This can be used to in response to a user disabling the feature to emit notifications. 179 * If {@code addDisclosure} is called while in a disabled state, disclosures will be dropped. 180 */ disable(Context context)181 public void disable(Context context) { 182 Rlog.d(TAG, "disabled"); 183 synchronized (mEnabledLock) { 184 mEnabled = false; 185 try { 186 mSerializedWorkQueue.execute(onDisableNotifier(context)); 187 } catch (RejectedExecutionException e) { 188 Rlog.e(TAG, "Failed to schedule onDisableNotifier: " + e.getMessage()); 189 } 190 } 191 } 192 isEnabled()193 public boolean isEnabled() { 194 synchronized (mEnabledLock) { 195 return mEnabled; 196 } 197 } 198 199 /** Get a singleton CellularIdentifierDisclosureNotifier. */ getInstance( CellularNetworkSecuritySafetySource safetySource)200 public static synchronized CellularIdentifierDisclosureNotifier getInstance( 201 CellularNetworkSecuritySafetySource safetySource) { 202 if (sInstance == null) { 203 sInstance = new CellularIdentifierDisclosureNotifier(safetySource); 204 } 205 206 return sInstance; 207 } 208 incrementAndNotify(Context context, int subId)209 private Runnable incrementAndNotify(Context context, int subId) { 210 return () -> { 211 DisclosureWindow window = mWindows.get(subId); 212 if (window == null) { 213 window = new DisclosureWindow(subId); 214 mWindows.put(subId, window); 215 } 216 217 window.increment(context, this); 218 219 int disclosureCount = window.getDisclosureCount(); 220 221 Rlog.d( 222 TAG, 223 "Emitting notification for subId: " 224 + subId 225 + ". New disclosure count " 226 + disclosureCount); 227 228 mSafetySource.setIdentifierDisclosure( 229 context, 230 subId, 231 disclosureCount, 232 window.getFirstOpen(), 233 window.getCurrentEnd()); 234 }; 235 } 236 237 private Runnable onDisableNotifier(Context context) { 238 return () -> { 239 Rlog.d(TAG, "On disable notifier"); 240 for (DisclosureWindow window : mWindows.values()) { 241 window.close(); 242 } 243 mSafetySource.setIdentifierDisclosureIssueEnabled(context, false); 244 }; 245 } 246 247 private Runnable onEnableNotifier(Context context) { 248 return () -> { 249 Rlog.i(TAG, "On enable notifier"); 250 mSafetySource.setIdentifierDisclosureIssueEnabled(context, true); 251 }; 252 } 253 254 /** 255 * Get the disclosure count for a given subId. NOTE: This method is not thread safe. Without 256 * external synchronization, one should only call it if there are no pending tasks on the 257 * Executor passed into this class. 258 */ 259 @VisibleForTesting 260 public int getCurrentDisclosureCount(int subId) { 261 DisclosureWindow window = mWindows.get(subId); 262 if (window != null) { 263 return window.getDisclosureCount(); 264 } 265 266 return 0; 267 } 268 269 /** 270 * Get the open time for a given subId. NOTE: This method is not thread safe. Without 271 * external synchronization, one should only call it if there are no pending tasks on the 272 * Executor passed into this class. 273 */ 274 @VisibleForTesting 275 public Instant getFirstOpen(int subId) { 276 DisclosureWindow window = mWindows.get(subId); 277 if (window != null) { 278 return window.getFirstOpen(); 279 } 280 281 return null; 282 } 283 284 /** 285 * Get the current end time for a given subId. NOTE: This method is not thread safe. Without 286 * external synchronization, one should only call it if there are no pending tasks on the 287 * Executor passed into this class. 288 */ 289 @VisibleForTesting 290 public Instant getCurrentEnd(int subId) { 291 DisclosureWindow window = mWindows.get(subId); 292 if (window != null) { 293 return window.getCurrentEnd(); 294 } 295 296 return null; 297 } 298 299 /** 300 * A helper class that maintains all state associated with the disclosure window for a single 301 * subId. No methods are thread safe. Callers must implement all synchronization. 302 */ 303 private class DisclosureWindow { 304 private int mDisclosureCount; 305 private Instant mWindowFirstOpen; 306 private Instant mLastEvent; 307 private ScheduledFuture<?> mWhenWindowCloses; 308 309 private int mSubId; 310 311 DisclosureWindow(int subId) { 312 mDisclosureCount = 0; 313 mWindowFirstOpen = null; 314 mLastEvent = null; 315 mSubId = subId; 316 mWhenWindowCloses = null; 317 } 318 319 void increment(Context context, CellularIdentifierDisclosureNotifier notifier) { 320 321 mDisclosureCount++; 322 323 Instant now = Instant.now(); 324 if (mDisclosureCount == 1) { 325 // Our window was opened for the first time 326 mWindowFirstOpen = now; 327 } 328 329 mLastEvent = now; 330 331 cancelWindowCloseFuture(); 332 333 try { 334 mWhenWindowCloses = 335 notifier.mSerializedWorkQueue.schedule( 336 closeWindowRunnable(context), 337 notifier.mWindowCloseDuration, 338 notifier.mWindowCloseUnit); 339 } catch (RejectedExecutionException e) { 340 Rlog.e( 341 TAG, 342 "Failed to schedule closeWindow for subId " 343 + mSubId 344 + " : " 345 + e.getMessage()); 346 } 347 } 348 349 int getDisclosureCount() { 350 return mDisclosureCount; 351 } 352 353 Instant getFirstOpen() { 354 return mWindowFirstOpen; 355 } 356 357 Instant getCurrentEnd() { 358 return mLastEvent; 359 } 360 361 void close() { 362 mDisclosureCount = 0; 363 mWindowFirstOpen = null; 364 mLastEvent = null; 365 366 if (mWhenWindowCloses == null) { 367 return; 368 } 369 mWhenWindowCloses = null; 370 } 371 372 private Runnable closeWindowRunnable(Context context) { 373 return () -> { 374 Rlog.i( 375 TAG, 376 "Disclosure window closing for subId " 377 + mSubId 378 + ". Disclosure count was " 379 + getDisclosureCount()); 380 close(); 381 mSafetySource.clearIdentifierDisclosure(context, mSubId); 382 }; 383 } 384 385 private boolean cancelWindowCloseFuture() { 386 if (mWhenWindowCloses == null) { 387 return false; 388 } 389 390 // Pass false to not interrupt a running Future. Nothing about our notifier is ready 391 // for this type of preemption. 392 return mWhenWindowCloses.cancel(false); 393 } 394 395 } 396 } 397 398