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.server.sdksandbox.verifier;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.os.Handler;
22 import android.os.HandlerThread;
23 import android.os.OutcomeReceiver;
24 import android.os.Process;
25 import android.os.SystemClock;
26 import android.util.Log;
27 
28 import com.android.internal.annotations.GuardedBy;
29 import com.android.internal.annotations.VisibleForTesting;
30 import com.android.server.sdksandbox.proto.Verifier.AllowedApi;
31 import com.android.server.sdksandbox.proto.Verifier.AllowedApisList;
32 import com.android.server.sdksandbox.verifier.SerialDexLoader.DexLoadResult;
33 import com.android.server.sdksandbox.verifier.SerialDexLoader.VerificationHandler;
34 
35 import com.google.protobuf.InvalidProtocolBufferException;
36 
37 import java.io.File;
38 import java.io.FileNotFoundException;
39 import java.io.IOException;
40 import java.util.ArrayList;
41 import java.util.Arrays;
42 import java.util.HashMap;
43 import java.util.List;
44 import java.util.Map;
45 
46 /**
47  * Verifies the SDK being installed against the APIs allowlist. Verification runs on DEX files of
48  * the SDK package.
49  *
50  * @hide
51  */
52 public class SdkDexVerifier {
53 
54     private final Object mPlatformApiAllowlistsLock = new Object();
55     private final Object mVerificationLock = new Object();
56 
57     private static final String TAG = "SdkSandboxVerifier";
58 
59     private static final String WILDCARD = "*";
60     private static final String EMPTY_STRING = "";
61 
62     private static final AllowedApi[] DEFAULT_RULES = {
63         AllowedApi.newBuilder().setClassName("Landroid/*").setAllow(false).build(),
64         AllowedApi.newBuilder().setClassName("Ljava/*").setAllow(false).build(),
65         AllowedApi.newBuilder().setClassName("Lcom/google/android/*").setAllow(false).build(),
66         AllowedApi.newBuilder().setClassName("Lcom/android/*").setAllow(false).build(),
67         AllowedApi.newBuilder().setClassName("Landroidx/*").setAllow(false).build(),
68     };
69 
70     private static SdkDexVerifier sSdkDexVerifier;
71     private ApiAllowlistProvider mApiAllowlistProvider;
72     private SerialDexLoader mDexLoader;
73 
74     // Maps targetSdkVersion to its allowlist
75     @GuardedBy("mPlatformApiAllowlistsLock")
76     private Map<Long, AllowedApisList> mPlatformApiAllowlists;
77 
78     @GuardedBy("mPlatformApiAllowlistsLock")
79     private Map<Long, StringTrie> mPlatformApiAllowTries = new HashMap<>();
80 
81     @GuardedBy("mVerificationLock")
82     private Map<String, Long> mVerificationTimes = new HashMap<>();
83 
84     /** Returns a singleton instance of {@link SdkDexVerifier} */
85     @NonNull
getInstance()86     public static SdkDexVerifier getInstance() {
87         synchronized (SdkDexVerifier.class) {
88             if (sSdkDexVerifier == null) {
89                 sSdkDexVerifier = new SdkDexVerifier(new Injector());
90             }
91         }
92         return sSdkDexVerifier;
93     }
94 
95     @VisibleForTesting
SdkDexVerifier(Injector injector)96     SdkDexVerifier(Injector injector) {
97         mApiAllowlistProvider = injector.getApiAllowlistProvider();
98         mDexLoader = injector.getDexLoader();
99     }
100 
101     /**
102      * Starts verification of the requested sdk
103      *
104      * @param sdkPath path to the sdk package to be verified
105      * @param targetSdkVersion Android SDK target version of the package being verified, declared in
106      *     the package manifest.
107      * @param callback to client for communication of parsing/verification results.
108      */
startDexVerification( String sdkPath, String packagename, long targetSdkVersion, OutcomeReceiver<Void, Exception> callback)109     public void startDexVerification(
110             String sdkPath,
111             String packagename,
112             long targetSdkVersion,
113             OutcomeReceiver<Void, Exception> callback) {
114         long startTime = SystemClock.elapsedRealtime();
115         synchronized (mVerificationLock) {
116             mVerificationTimes.put(packagename, startTime);
117         }
118 
119         try {
120             initAllowlist(targetSdkVersion);
121         } catch (Exception e) {
122             callback.onError(e);
123             return;
124         }
125 
126         File sdkFile = new File(sdkPath);
127 
128         if (!sdkFile.exists()) {
129             callback.onError(new FileNotFoundException("Apk to verify not found: " + sdkPath));
130             return;
131         }
132 
133         mDexLoader.queueApkToLoad(
134                 sdkFile,
135                 packagename,
136                 new VerificationHandler() {
137                     @Override
138                     public boolean verify(DexLoadResult result) {
139                         // TODO(b/231441674): verify against allowtrie.
140                         return true;
141                     }
142 
143                     @Override
144                     public void onVerificationCompleteForPackage(boolean passed) {
145                         if (passed) {
146                             Log.d(TAG, packagename + " verified.");
147                         } else {
148                             Log.d(TAG, packagename + " rejected");
149                         }
150                         synchronized (mVerificationLock) {
151                             long verificationTime =
152                                     SystemClock.elapsedRealtime()
153                                             - mVerificationTimes.remove(packagename);
154                             Log.d(TAG, "Verification time (ms): " + verificationTime);
155                         }
156                         callback.onResult(null);
157 
158                         // TODO(b/231441674): cache and log verification result
159                     }
160 
161                     @Override
162                     public void onVerificationErrorForPackage(Exception e) {
163                         callback.onError(e);
164                     }
165                 });
166     }
167 
168     /*
169      * Converts an AllowedApi object into an array of keys that will be added to the verification
170      * trie. The AllowedApi rules should follow TypeDescriptors semantics from the DEX format.
171      *
172      * The list of tokens is generated splitting the class name at '/' and adding the
173      * subsequent fields to be matched for equality or wildcard. The parameters list specifies
174      * all of the parameter types, its order is preserved from the method signature in
175      * the source file and there is no distinction between input and return parameters.
176      * Therefore, the order of the parameters in the rules matters when computing a rule match.
177      * Omitted fields will add a wildcard to match all possibilities.
178      *
179      * A fully qualified rule will match an exact method, an example:
180      * {
181      *      allow : false
182      *      class_name : "Landroid/view/inputmethod/InputMethodManager"
183      *      method_name : "getCurrentInputMethodSubtype"
184      *      parameters: ["V"]
185      *      return_type: "Landroid/view/inputmethod/InputMethodSubtype"
186      * }
187      * This rule will produce the list of tokens:
188      * ["Landroid", "view", "inputmethod", "InputMethodManager", "getCurrentInputMethodSubtype",
189      *      "V", "Landroid/view/inputmethod/InputMethodSubtype"]
190      *
191      * A generalized rule, that matches all methods in the InputMethodManager class that
192      * return an InputMethodSubtype object, will look like this:
193      * {
194      *      allow : false
195      *      class_name : "Landroid/view/inputmethod/InputMethodManager"
196      *      return_type: "Landroid/view/inputmethod/InputMethodSubtype"
197      * }
198      * This rule produces the list of tokens:
199      * ["Landroid", "view", "inputmethod", "InputMethodManager",
200      *      null, "Landroid/view/inputmethod/InputMethodSubtype"]
201      *
202      * Wildcards can be included in the class name to generalize to packages, for example:
203      * "Landroid/view/inputmethod/*" matches all classes within the android.view.inputmethod.
204      */
205     @VisibleForTesting
206     @Nullable
getApiTokens(AllowedApi apiRule)207     String[] getApiTokens(AllowedApi apiRule) {
208         ArrayList<String> tokens = new ArrayList<>();
209 
210         if (apiRule.getClassName().equals(EMPTY_STRING)) {
211             // match unspecified class name
212             tokens.add(WILDCARD);
213         } else {
214             List<String> classTokens = Arrays.asList(apiRule.getClassName().toString().split("/"));
215             tokens.addAll(classTokens);
216         }
217 
218         if (!apiRule.getMethodName().equals(EMPTY_STRING)) {
219             tokens.add(apiRule.getMethodName());
220         } else if (!WILDCARD.equals(tokens.get(tokens.size() - 1))) {
221             // match unspecified method name
222             tokens.add(WILDCARD);
223         }
224 
225         if (apiRule.getParametersCount() != 0) {
226             tokens.addAll(apiRule.getParametersList());
227         } else if (!WILDCARD.equals(tokens.get(tokens.size() - 1))) {
228             // match unspecified params
229             tokens.add(WILDCARD);
230         }
231 
232         if (!apiRule.getReturnType().equals(EMPTY_STRING)) {
233             tokens.add(apiRule.getReturnType());
234         } else if (!WILDCARD.equals(tokens.get(tokens.size() - 1))) {
235             // match unspecified return type
236             tokens.add(WILDCARD);
237         }
238 
239         // To catch a malformed rule like "Landroid//com"
240         if (tokens.contains(EMPTY_STRING)) {
241             return null;
242         }
243 
244         tokens.replaceAll(token -> token.equals(WILDCARD) ? null : token);
245 
246         return tokens.toArray(new String[tokens.size()]);
247     }
248 
249     /**
250      * Initializes the allowlist for a given target sandbox sdk version
251      *
252      * @param targetSdkVersion declared in the manifest of the installed package, different from
253      *     effectiveTargetSdkVersion.
254      */
initAllowlist(long targetSdkVersion)255     private void initAllowlist(long targetSdkVersion)
256             throws FileNotFoundException, InvalidProtocolBufferException, IOException {
257         synchronized (mPlatformApiAllowlistsLock) {
258             if (mPlatformApiAllowlists == null) {
259                 mPlatformApiAllowlists = mApiAllowlistProvider.loadPlatformApiAllowlist();
260             }
261 
262             if (!mPlatformApiAllowTries.containsKey(targetSdkVersion)) {
263                 buildAllowTrie(targetSdkVersion, mPlatformApiAllowlists.get(targetSdkVersion));
264             }
265         }
266     }
267 
268     @GuardedBy("mPlatformApiAllowlistsLock")
buildAllowTrie(long targetSdkVersion, AllowedApisList allowedApisList)269     private void buildAllowTrie(long targetSdkVersion, AllowedApisList allowedApisList) {
270         if (allowedApisList == null) {
271             Log.w(TAG, "No allowlist found for targetSdk " + targetSdkVersion);
272             return;
273         }
274 
275         StringTrie<AllowedApi> allowTrie = getBaseRuleTrie();
276 
277         for (AllowedApi apiRule : allowedApisList.getAllowedApisList()) {
278             String[] apiTokens = getApiTokens(apiRule);
279             if (apiTokens != null) {
280                 AllowedApi oldRule = allowTrie.put(apiRule, apiTokens);
281                 if (oldRule != null && oldRule.getAllow() != apiRule.getAllow()) {
282                     Log.w(
283                             TAG,
284                             "Rule was replaced for class "
285                                     + oldRule.getClassName()
286                                     + ". New rule value is: "
287                                     + apiRule.getAllow());
288                 }
289             } else {
290                 Log.w(TAG, "API Rule was malformed for rule with class " + apiRule.getClassName());
291                 return;
292             }
293         }
294 
295         mPlatformApiAllowTries.put(targetSdkVersion, allowTrie);
296     }
297 
getBaseRuleTrie()298     private StringTrie<AllowedApi> getBaseRuleTrie() {
299         StringTrie<AllowedApi> allowTrie = new StringTrie();
300         for (int i = 0; i < DEFAULT_RULES.length; i++) {
301             allowTrie.put(DEFAULT_RULES[i], getApiTokens(DEFAULT_RULES[i]));
302         }
303         return allowTrie;
304     }
305 
306     static class Injector {
307         private ApiAllowlistProvider mAllowlistProvider;
308         private SerialDexLoader mDexLoader;
309 
Injector()310         Injector() {
311             mAllowlistProvider = new ApiAllowlistProvider();
312             HandlerThread handlerThread =
313                     new HandlerThread("DexParsingThread", Process.THREAD_PRIORITY_BACKGROUND);
314             handlerThread.start();
315             DexParser dexParser = new DexParserImpl();
316             mDexLoader = new SerialDexLoader(dexParser, new Handler(handlerThread.getLooper()));
317         }
318 
Injector(ApiAllowlistProvider apiAllowlistProvider, SerialDexLoader serialDexLoader)319         Injector(ApiAllowlistProvider apiAllowlistProvider, SerialDexLoader serialDexLoader) {
320             mAllowlistProvider = apiAllowlistProvider;
321             mDexLoader = serialDexLoader;
322         }
323 
getApiAllowlistProvider()324         ApiAllowlistProvider getApiAllowlistProvider() {
325             return mAllowlistProvider;
326         }
327 
getDexLoader()328         SerialDexLoader getDexLoader() {
329             return mDexLoader;
330         }
331     }
332 }
333