• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.server.appop;
18 
19 import static android.app.AppOpsManager.OP_ACCESS_ACCESSIBILITY;
20 import static android.app.AppOpsManager.OP_ACCESS_NOTIFICATIONS;
21 import static android.app.AppOpsManager.OP_BIND_ACCESSIBILITY_SERVICE;
22 import static android.app.AppOpsManager.OP_CAMERA;
23 import static android.app.AppOpsManager.OP_COARSE_LOCATION;
24 import static android.app.AppOpsManager.OP_EMERGENCY_LOCATION;
25 import static android.app.AppOpsManager.OP_FINE_LOCATION;
26 import static android.app.AppOpsManager.OP_FLAG_SELF;
27 import static android.app.AppOpsManager.OP_FLAG_TRUSTED_PROXIED;
28 import static android.app.AppOpsManager.OP_FLAG_TRUSTED_PROXY;
29 import static android.app.AppOpsManager.OP_GPS;
30 import static android.app.AppOpsManager.OP_MONITOR_HIGH_POWER_LOCATION;
31 import static android.app.AppOpsManager.OP_MONITOR_LOCATION;
32 import static android.app.AppOpsManager.OP_PHONE_CALL_CAMERA;
33 import static android.app.AppOpsManager.OP_PHONE_CALL_MICROPHONE;
34 import static android.app.AppOpsManager.OP_READ_DEVICE_IDENTIFIERS;
35 import static android.app.AppOpsManager.OP_READ_HEART_RATE;
36 import static android.app.AppOpsManager.OP_READ_OXYGEN_SATURATION;
37 import static android.app.AppOpsManager.OP_READ_SKIN_TEMPERATURE;
38 import static android.app.AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO;
39 import static android.app.AppOpsManager.OP_RECEIVE_SANDBOX_TRIGGER_AUDIO;
40 import static android.app.AppOpsManager.OP_RECORD_AUDIO;
41 import static android.app.AppOpsManager.OP_RESERVED_FOR_TESTING;
42 import static android.app.AppOpsManager.OP_RUN_IN_BACKGROUND;
43 
44 import static java.lang.Long.min;
45 import static java.lang.Math.max;
46 
47 import android.annotation.NonNull;
48 import android.annotation.Nullable;
49 import android.app.AppOpsManager;
50 import android.os.AsyncTask;
51 import android.os.Build;
52 import android.permission.flags.Flags;
53 import android.provider.DeviceConfig;
54 import android.util.IntArray;
55 import android.util.Slog;
56 
57 import java.io.PrintWriter;
58 import java.text.SimpleDateFormat;
59 import java.time.Duration;
60 import java.util.Arrays;
61 import java.util.Date;
62 import java.util.Set;
63 
64 /**
65  * This class provides interface for xml and sqlite implementation. Implementation manages
66  * information about recent accesses to ops for permission usage timeline.
67  * <p>
68  * The discrete history is kept for limited time (initial default is 24 hours, set in
69  * {@link DiscreteOpsRegistry#sDiscreteHistoryCutoff} and discarded after that.
70  * <p>
71  * Discrete history is quantized to reduce resources footprint. By default, quantization is set to
72  * one minute in {@link DiscreteOpsRegistry#sDiscreteHistoryQuantization}. All access times are
73  * aligned to the closest quantized time. All durations (except -1, meaning no duration) are
74  * rounded up to the closest quantized interval.
75  * <p>
76  * When data is queried through API, events are deduplicated and for every time quant there can
77  * be only one {@link AppOpsManager.AttributedOpEntry}. Each entry contains information about
78  * different accesses which happened in specified time quant - across dimensions of
79  * {@link AppOpsManager.UidState} and {@link AppOpsManager.OpFlags}. For each dimension
80  * it is only possible to know if at least one access happened in the time quant.
81  * <p>
82  * INITIALIZATION: We can initialize persistence only after the system is ready
83  * as we need to check the optional configuration override from the settings
84  * database which is not initialized at the time the app ops service is created. This class
85  * relies on {@link LegacyHistoricalRegistry} for controlling that no calls are allowed until then.
86  * All outside calls are going through {@link LegacyHistoricalRegistry}.
87  */
88 abstract class DiscreteOpsRegistry {
89     private static final String TAG = DiscreteOpsRegistry.class.getSimpleName();
90 
91     static final boolean DEBUG_LOG = false;
92     static final String PROPERTY_DISCRETE_HISTORY_CUTOFF = "discrete_history_cutoff_millis";
93     static final String PROPERTY_DISCRETE_HISTORY_QUANTIZATION =
94             "discrete_history_quantization_millis";
95     static final String PROPERTY_DISCRETE_FLAGS = "discrete_history_op_flags";
96     // Comma separated app ops list config for testing i.e. "1,2,3,4"
97     static final String PROPERTY_DISCRETE_OPS_LIST = "discrete_history_ops_cslist";
98     // These ops are deemed important for detecting a malicious app, and are recorded.
99     static final int[] IMPORTANT_OPS_FOR_SECURITY = new int[] {
100             OP_GPS,
101             OP_ACCESS_NOTIFICATIONS,
102             OP_RUN_IN_BACKGROUND,
103             OP_BIND_ACCESSIBILITY_SERVICE,
104             OP_ACCESS_ACCESSIBILITY,
105             OP_READ_DEVICE_IDENTIFIERS,
106             OP_MONITOR_HIGH_POWER_LOCATION,
107             OP_MONITOR_LOCATION
108     };
109 
110     // These are additional ops, which are not backed by runtime permissions, but are recorded.
111     static final int[] ADDITIONAL_DISCRETE_OPS = new int[] {
112             OP_PHONE_CALL_MICROPHONE,
113             OP_RECEIVE_AMBIENT_TRIGGER_AUDIO,
114             OP_RECEIVE_SANDBOX_TRIGGER_AUDIO,
115             OP_PHONE_CALL_CAMERA,
116             OP_EMERGENCY_LOCATION,
117             OP_RESERVED_FOR_TESTING
118     };
119 
120     // Legacy ops captured in discrete database.
121     private static final String LEGACY_OPS = OP_FINE_LOCATION + "," + OP_COARSE_LOCATION
122             + "," + OP_EMERGENCY_LOCATION + "," + OP_CAMERA + "," + OP_RECORD_AUDIO + ","
123             + OP_PHONE_CALL_MICROPHONE + "," + OP_PHONE_CALL_CAMERA + ","
124             + OP_RECEIVE_AMBIENT_TRIGGER_AUDIO + "," + OP_RECEIVE_SANDBOX_TRIGGER_AUDIO
125             + "," + OP_READ_HEART_RATE + "," + OP_READ_OXYGEN_SATURATION + ","
126             + OP_READ_SKIN_TEMPERATURE + "," + OP_RESERVED_FOR_TESTING;
127 
128     static final long DEFAULT_DISCRETE_HISTORY_CUTOFF = Duration.ofDays(7).toMillis();
129     static final long MAXIMUM_DISCRETE_HISTORY_CUTOFF = Duration.ofDays(30).toMillis();
130     // The duration for which the data is kept, default is 7 days and max 30 days enforced.
131     static long sDiscreteHistoryCutoff;
132 
133     static final long DEFAULT_DISCRETE_HISTORY_QUANTIZATION = Duration.ofMinutes(1).toMillis();
134     // discrete ops are rounded up to quantization time, meaning we record one op per time bucket
135     // in case of duplicate op events.
136     static long sDiscreteHistoryQuantization;
137 
138     static int[] sDiscreteOps = new int[0];
139     static int sDiscreteFlags;
140 
141     static final int OP_FLAGS_DISCRETE = OP_FLAG_SELF | OP_FLAG_TRUSTED_PROXIED
142             | OP_FLAG_TRUSTED_PROXY;
143 
144     boolean mDebugMode = false;
145 
systemReady()146     void systemReady() {
147         DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_PRIVACY,
148                 AsyncTask.THREAD_POOL_EXECUTOR, (DeviceConfig.Properties p) -> {
149                     setDiscreteHistoryParameters(p);
150                 });
151         setDiscreteHistoryParameters(DeviceConfig.getProperties(DeviceConfig.NAMESPACE_PRIVACY));
152     }
153 
recordDiscreteAccess(int uid, String packageName, @NonNull String deviceId, int op, @Nullable String attributionTag, @AppOpsManager.OpFlags int flags, @AppOpsManager.UidState int uidState, long accessTime, long accessDuration, @AppOpsManager.AttributionFlags int attributionFlags, int attributionChainId)154     abstract void recordDiscreteAccess(int uid, String packageName, @NonNull String deviceId,
155             int op, @Nullable String attributionTag, @AppOpsManager.OpFlags int flags,
156             @AppOpsManager.UidState int uidState, long accessTime, long accessDuration,
157             @AppOpsManager.AttributionFlags int attributionFlags, int attributionChainId);
158 
159     /**
160      * A periodic callback from {@link AppOpsService} to flush the in memory events to disk.
161      * The shutdown callback is also plugged into it.
162      * <p>
163      * This method flushes in memory records to disk, and also clears old records from disk.
164      */
writeAndClearOldAccessHistory()165     abstract void writeAndClearOldAccessHistory();
166 
shutdown()167     void shutdown() {}
168 
169     /** Remove all discrete op events. */
clearHistory()170     abstract void clearHistory();
171 
172     /** Remove all discrete op events for given UID and package. */
clearHistory(int uid, String packageName)173     abstract void clearHistory(int uid, String packageName);
174 
175     /**
176      * Offset access time by given timestamp, new access time would be accessTime - offsetMillis.
177      */
offsetHistory(long offset)178     abstract void offsetHistory(long offset);
179 
addFilteredDiscreteOpsToHistoricalOps(AppOpsManager.HistoricalOps result, long beginTimeMillis, long endTimeMillis, @AppOpsManager.HistoricalOpsRequestFilter int filter, int uidFilter, @Nullable String packageNameFilter, @Nullable String[] opNamesFilter, @Nullable String attributionTagFilter, @AppOpsManager.OpFlags int flagsFilter, Set<String> attributionExemptPkgs)180     abstract  void addFilteredDiscreteOpsToHistoricalOps(AppOpsManager.HistoricalOps result,
181             long beginTimeMillis, long endTimeMillis,
182             @AppOpsManager.HistoricalOpsRequestFilter int filter, int uidFilter,
183             @Nullable String packageNameFilter, @Nullable String[] opNamesFilter,
184             @Nullable String attributionTagFilter, @AppOpsManager.OpFlags int flagsFilter,
185             Set<String> attributionExemptPkgs);
186 
dump(@onNull PrintWriter pw, int uidFilter, @Nullable String packageNameFilter, @Nullable String attributionTagFilter, @AppOpsManager.HistoricalOpsRequestFilter int filter, int dumpOp, @NonNull SimpleDateFormat sdf, @NonNull Date date, @NonNull String prefix, int nDiscreteOps)187     abstract void dump(@NonNull PrintWriter pw, int uidFilter, @Nullable String packageNameFilter,
188             @Nullable String attributionTagFilter,
189             @AppOpsManager.HistoricalOpsRequestFilter int filter, int dumpOp,
190             @NonNull SimpleDateFormat sdf, @NonNull Date date, @NonNull String prefix,
191             int nDiscreteOps);
192 
setDebugMode(boolean debugMode)193     void setDebugMode(boolean debugMode) {
194         this.mDebugMode = debugMode;
195     }
196 
discretizeTimeStamp(long timeStamp)197     static long discretizeTimeStamp(long timeStamp) {
198         return timeStamp / sDiscreteHistoryQuantization * sDiscreteHistoryQuantization;
199 
200     }
201 
discretizeDuration(long duration)202     static long discretizeDuration(long duration) {
203         return duration == -1 ? -1 : (duration + sDiscreteHistoryQuantization - 1)
204                 / sDiscreteHistoryQuantization * sDiscreteHistoryQuantization;
205     }
206 
isDiscreteOp(int op, @AppOpsManager.OpFlags int flags)207     static boolean isDiscreteOp(int op, @AppOpsManager.OpFlags int flags) {
208         if (Arrays.binarySearch(sDiscreteOps, op) < 0) {
209             return false;
210         }
211         if ((flags & (sDiscreteFlags)) == 0) {
212             return false;
213         }
214         return true;
215     }
216 
setDiscreteHistoryParameters(DeviceConfig.Properties p)217     private void setDiscreteHistoryParameters(DeviceConfig.Properties p) {
218         if (p.getKeyset().contains(PROPERTY_DISCRETE_HISTORY_CUTOFF)) {
219             sDiscreteHistoryCutoff = p.getLong(PROPERTY_DISCRETE_HISTORY_CUTOFF,
220                     DEFAULT_DISCRETE_HISTORY_CUTOFF);
221             if (!Build.IS_DEBUGGABLE && !mDebugMode) {
222                 sDiscreteHistoryCutoff = min(MAXIMUM_DISCRETE_HISTORY_CUTOFF,
223                         sDiscreteHistoryCutoff);
224             }
225         } else {
226             sDiscreteHistoryCutoff = DEFAULT_DISCRETE_HISTORY_CUTOFF;
227         }
228         if (p.getKeyset().contains(PROPERTY_DISCRETE_HISTORY_QUANTIZATION)) {
229             sDiscreteHistoryQuantization = p.getLong(PROPERTY_DISCRETE_HISTORY_QUANTIZATION,
230                     DEFAULT_DISCRETE_HISTORY_QUANTIZATION);
231             if (!Build.IS_DEBUGGABLE && !mDebugMode) {
232                 sDiscreteHistoryQuantization = max(DEFAULT_DISCRETE_HISTORY_QUANTIZATION,
233                         sDiscreteHistoryQuantization);
234             }
235         } else {
236             sDiscreteHistoryQuantization = DEFAULT_DISCRETE_HISTORY_QUANTIZATION;
237         }
238         sDiscreteFlags = p.getKeyset().contains(PROPERTY_DISCRETE_FLAGS)
239                 ? p.getInt(PROPERTY_DISCRETE_FLAGS, OP_FLAGS_DISCRETE) : OP_FLAGS_DISCRETE;
240         String opsListConfig = p.getString(PROPERTY_DISCRETE_OPS_LIST, null);
241         sDiscreteOps = opsListConfig == null ? getDefaultOpsList() : parseOpsList(opsListConfig);
242 
243         Arrays.sort(sDiscreteOps);
244     }
245 
246     // App ops backed by runtime/dangerous permissions.
getRuntimePermissionOps()247     private static IntArray getRuntimePermissionOps() {
248         IntArray runtimeOps = new IntArray();
249         for (int op = 0; op < AppOpsManager._NUM_OP; op++) {
250             if (AppOpsManager.opIsRuntimePermission(op)) {
251                 runtimeOps.add(op);
252             }
253         }
254         return runtimeOps;
255     }
256 
257     /**
258      * @return an array of app ops captured into discrete database.
259      */
getDefaultOpsList()260     private static int[] getDefaultOpsList() {
261         if (!(Flags.recordAllRuntimeAppopsSqlite() && Flags.enableSqliteAppopsAccesses())) {
262             return getDefaultLegacyOps();
263         }
264 
265         IntArray discreteOpsArray = getRuntimePermissionOps();
266         discreteOpsArray.addAll(IMPORTANT_OPS_FOR_SECURITY);
267         discreteOpsArray.addAll(ADDITIONAL_DISCRETE_OPS);
268 
269         return discreteOpsArray.toArray();
270     }
271 
getDefaultLegacyOps()272     private static int[] getDefaultLegacyOps() {
273         return parseOpsList(LEGACY_OPS);
274     }
275 
parseOpsList(String opsList)276     private static int[] parseOpsList(String opsList) {
277         String[] strArr;
278         if (opsList.isEmpty()) {
279             strArr = new String[0];
280         } else {
281             strArr = opsList.split(",");
282         }
283         int nOps = strArr.length;
284         int[] result = new int[nOps];
285         try {
286             for (int i = 0; i < nOps; i++) {
287                 result[i] = Integer.parseInt(strArr[i]);
288             }
289         } catch (NumberFormatException e) {
290             Slog.e(TAG, "Failed to parse Discrete ops list: " + e.getMessage());
291             return getDefaultOpsList();
292         }
293         return result;
294     }
295 }
296