• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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