• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 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 package com.google.android.car.kitchensink;
17 
18 import android.annotation.Nullable;
19 import android.app.admin.DevicePolicyManager;
20 import android.content.Context;
21 import android.os.Handler;
22 import android.os.HandlerThread;
23 import android.security.AttestedKeyPair;
24 import android.security.keystore.KeyGenParameterSpec;
25 import android.security.keystore.KeyProperties;
26 import android.util.IndentingPrintWriter;
27 import android.util.Log;
28 
29 import java.io.PrintWriter;
30 import java.util.Arrays;
31 import java.util.Collection;
32 import java.util.List;
33 
34 /**
35  * {@code KitchenSink}'s own {@code cmd} implementation.
36  *
37  * <p>Usage: {$code adb shell dumpsys activity
38  * com.google.android.car.kitchensink/.KitchenSinkActivity cmd <CMD>}
39  *
40  * <p><p>Note</p>: you must launch {@code KitchenSink} first. Example: {@code
41  * adb shell am start com.google.android.car.kitchensink/.KitchenSinkActivity}
42  */
43 final class KitchenSinkShellCommand {
44 
45     private static final String TAG = "KitchenSinkCmd";
46 
47     private static final String CMD_HELP = "help";
48     private static final String CMD_GET_DELEGATED_SCOPES = "get-delegated-scopes";
49     private static final String CMD_IS_UNINSTALL_BLOCKED = "is-uninstall-blocked";
50     private static final String CMD_SET_UNINSTALL_BLOCKED = "set-uninstall-blocked";
51     private static final String CMD_GENERATE_DEVICE_ATTESTATION_KEY_PAIR =
52             "generate-device-attestation-key-pair";
53 
54     private final Context mContext;
55     private final @Nullable DevicePolicyManager mDpm;
56     private final IndentingPrintWriter mWriter;
57     private final String[] mArgs;
58 
59     @Nullable // dynamically created on post() method
60     private Handler mHandler;
61 
62     private int mNextArgIndex;
63 
KitchenSinkShellCommand(Context context, PrintWriter writer, String[] args)64     KitchenSinkShellCommand(Context context, PrintWriter writer, String[] args) {
65         mContext = context;
66         mDpm = context.getSystemService(DevicePolicyManager.class);
67         mWriter = new IndentingPrintWriter(writer);
68         mArgs = args;
69     }
70 
run()71     void run() {
72         if (mArgs.length == 0) {
73             showHelp("Error: must pass an argument");
74             return;
75         }
76         String cmd = mArgs[0];
77         switch (cmd) {
78             case CMD_HELP:
79                 showHelp("KitchenSink Command-Line Interface");
80                 break;
81             case CMD_GET_DELEGATED_SCOPES:
82                 getDelegatedScopes();
83                 break;
84             case CMD_IS_UNINSTALL_BLOCKED:
85                 isUninstallBlocked();
86                 break;
87             case CMD_SET_UNINSTALL_BLOCKED:
88                 setUninstallBlocked();
89                 break;
90             case CMD_GENERATE_DEVICE_ATTESTATION_KEY_PAIR:
91                 generateDeviceAttestationKeyPair();
92                 break;
93             default:
94                 showHelp("Invalid command: %s", cmd);
95         }
96     }
97 
showHelp(String headerMessage, Object... headerArgs)98     private void showHelp(String headerMessage, Object... headerArgs) {
99         if (headerMessage != null) {
100             mWriter.printf(headerMessage, headerArgs);
101             mWriter.print(". ");
102         }
103         mWriter.println("Available commands:\n");
104 
105         mWriter.increaseIndent();
106         showCommandHelp("Shows this help message.",
107                 CMD_HELP);
108         showCommandHelp("Lists delegated scopes set by the device admin.",
109                 CMD_GET_DELEGATED_SCOPES);
110         showCommandHelp("Checks whether uninstalling the given app is blocked.",
111                 CMD_IS_UNINSTALL_BLOCKED, "<PKG>");
112         showCommandHelp("Blocks / unblocks uninstalling the given app.",
113                 CMD_SET_UNINSTALL_BLOCKED, "<PKG>", "<true|false>");
114         showCommandHelp("Generates a device attestation key.",
115                 CMD_GENERATE_DEVICE_ATTESTATION_KEY_PAIR, "<ALIAS>", "[FLAGS]");
116         mWriter.decreaseIndent();
117     }
118 
showCommandHelp(String description, String cmd, String... args)119     private void showCommandHelp(String description, String cmd, String... args) {
120         mWriter.printf("%s", cmd);
121         if (args != null) {
122             for (String arg : args) {
123                 mWriter.printf(" %s", arg);
124             }
125         }
126         mWriter.println(":");
127         mWriter.increaseIndent();
128         mWriter.printf("%s\n\n", description);
129         mWriter.decreaseIndent();
130     }
131 
getDelegatedScopes()132     private void getDelegatedScopes() {
133         if (!supportDevicePolicyManagement()) return;
134 
135         List<String> scopes = mDpm.getDelegatedScopes(/* admin= */ null, mContext.getPackageName());
136         printCollection("delegated scope", scopes);
137     }
138 
isUninstallBlocked()139     private void isUninstallBlocked() {
140         if (!supportDevicePolicyManagement()) return;
141 
142         String packageName = getNextArg();
143         boolean isIt = mDpm.isUninstallBlocked(/* admin= */ null, packageName);
144         mWriter.println(isIt);
145     }
146 
setUninstallBlocked()147     private void setUninstallBlocked() {
148         if (!supportDevicePolicyManagement()) return;
149 
150         String packageName = getNextArg();
151         boolean blocked = getNextBooleanArg();
152 
153         Log.i(TAG, "Calling dpm.setUninstallBlocked(" + packageName + ", " + blocked + ")");
154         mDpm.setUninstallBlocked(/* admin= */ null, packageName, blocked);
155     }
156 
generateDeviceAttestationKeyPair()157     private void generateDeviceAttestationKeyPair() {
158         if (!supportDevicePolicyManagement()) return;
159 
160         String alias = getNextArg();
161         int flags = getNextOptionalIntArg(/* defaultValue= */ 0);
162         // Cannot call dpm.generateKeyPair() on main thread
163         warnAboutAsyncCall();
164         post(() -> handleDeviceAttestationKeyPair(alias, flags));
165     }
166 
handleDeviceAttestationKeyPair(String alias, int flags)167     private void handleDeviceAttestationKeyPair(String alias, int flags) {
168         KeyGenParameterSpec keySpec = buildRsaKeySpecWithKeyAttestation(alias);
169         String algorithm = "RSA";
170         Log.i(TAG, "calling dpm.generateKeyPair(alg=" + algorithm + ", spec=" + keySpec
171                 + ", flags=" + flags + ")");
172         AttestedKeyPair kp = mDpm.generateKeyPair(/* admin= */ null, algorithm, keySpec, flags);
173         Log.i(TAG, "key: " + kp);
174     }
175 
warnAboutAsyncCall()176     private void warnAboutAsyncCall() {
177         mWriter.printf("Command will be executed asynchronally; use `adb logcat %s *:s` for result"
178                 + "\n", TAG);
179     }
180 
post(Runnable r)181     private void post(Runnable r) {
182         if (mHandler == null) {
183             HandlerThread handlerThread = new HandlerThread("KitchenSinkShellCommandThread");
184             Log.i(TAG, "Starting " + handlerThread);
185             handlerThread.start();
186             mHandler = new Handler(handlerThread.getLooper());
187         }
188         Log.d(TAG, "posting runnable");
189         mHandler.post(r);
190     }
191 
supportDevicePolicyManagement()192     private boolean supportDevicePolicyManagement() {
193         if (mDpm == null) {
194             mWriter.println("Device Policy Management not supported by device");
195             return false;
196         }
197         return true;
198     }
199 
getNextArgAndIncrementCounter()200     private String getNextArgAndIncrementCounter() {
201         return mArgs[++mNextArgIndex];
202     }
203 
getNextArg()204     private String getNextArg() {
205         try {
206             return getNextArgAndIncrementCounter();
207         } catch (Exception e) {
208             mWriter.println("Error: missing argument");
209             mWriter.flush();
210             throw new IllegalArgumentException(
211                     "Missing argument. Args=" + Arrays.toString(mArgs));
212         }
213     }
214 
getNextOptionalIntArg(int defaultValue)215     private int getNextOptionalIntArg(int defaultValue) {
216         try {
217             return Integer.parseInt(getNextArgAndIncrementCounter());
218         } catch (Exception e) {
219             Log.d(TAG, "Exception getting optional arg: " + e);
220             return defaultValue;
221         }
222     }
223 
getNextBooleanArg()224     private boolean getNextBooleanArg() {
225         String arg = getNextArg();
226         return Boolean.parseBoolean(arg);
227     }
228 
printCollection(String nameOnSingular, Collection<String> collection)229     private void printCollection(String nameOnSingular, Collection<String> collection) {
230         if (collection.isEmpty()) {
231             mWriter.printf("No %ss\n", nameOnSingular);
232             return;
233         }
234         int size = collection.size();
235         mWriter.printf("%d %s%s:\n", size, nameOnSingular, size == 1 ? "" : "s");
236         collection.forEach((s) -> mWriter.printf("  %s\n", s));
237     }
238 
239     // Copied from CTS' KeyGenerationUtils
buildRsaKeySpecWithKeyAttestation(String alias)240     private static KeyGenParameterSpec buildRsaKeySpecWithKeyAttestation(String alias) {
241         return new KeyGenParameterSpec.Builder(alias,
242                 KeyProperties.PURPOSE_SIGN | KeyProperties.PURPOSE_VERIFY)
243                         .setKeySize(2048)
244                         .setDigests(KeyProperties.DIGEST_SHA256)
245                         .setSignaturePaddings(KeyProperties.SIGNATURE_PADDING_RSA_PSS,
246                                 KeyProperties.SIGNATURE_PADDING_RSA_PKCS1)
247                         .setIsStrongBoxBacked(false)
248                         .setAttestationChallenge(new byte[] {
249                                 'a', 'b', 'c'
250                         })
251                         .build();
252     }
253 }
254