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