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