blob: d72765e0c734ec12f705dd11073130960f20c94f [file] [log] [blame]
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.car.kitchensink;
import android.annotation.Nullable;
import android.app.admin.DevicePolicyManager;
import android.content.Context;
import android.os.Handler;
import android.os.HandlerThread;
import android.security.AttestedKeyPair;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
import android.util.IndentingPrintWriter;
import android.util.Log;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
/**
* {@code KitchenSink}'s own {@code cmd} implementation.
*
* <p>Usage: {$code adb shell dumpsys activity
* com.google.android.car.kitchensink/.KitchenSinkActivity cmd <CMD>}
*
* <p><p>Note</p>: you must launch {@code KitchenSink} first. Example: {@code
* adb shell am start com.google.android.car.kitchensink/.KitchenSinkActivity}
*/
final class KitchenSinkShellCommand {
private static final String TAG = "KitchenSinkCmd";
private static final String CMD_HELP = "help";
private static final String CMD_GET_DELEGATED_SCOPES = "get-delegated-scopes";
private static final String CMD_IS_UNINSTALL_BLOCKED = "is-uninstall-blocked";
private static final String CMD_SET_UNINSTALL_BLOCKED = "set-uninstall-blocked";
private static final String CMD_GENERATE_DEVICE_ATTESTATION_KEY_PAIR =
"generate-device-attestation-key-pair";
private final Context mContext;
private final @Nullable DevicePolicyManager mDpm;
private final IndentingPrintWriter mWriter;
private final String[] mArgs;
@Nullable // dynamically created on post() method
private Handler mHandler;
private int mNextArgIndex;
KitchenSinkShellCommand(Context context, PrintWriter writer, String[] args) {
mContext = context;
mDpm = context.getSystemService(DevicePolicyManager.class);
mWriter = new IndentingPrintWriter(writer);
mArgs = args;
}
void run() {
if (mArgs.length == 0) {
showHelp("Error: must pass an argument");
return;
}
String cmd = mArgs[0];
switch (cmd) {
case CMD_HELP:
showHelp("KitchenSink Command-Line Interface");
break;
case CMD_GET_DELEGATED_SCOPES:
getDelegatedScopes();
break;
case CMD_IS_UNINSTALL_BLOCKED:
isUninstallBlocked();
break;
case CMD_SET_UNINSTALL_BLOCKED:
setUninstallBlocked();
break;
case CMD_GENERATE_DEVICE_ATTESTATION_KEY_PAIR:
generateDeviceAttestationKeyPair();
break;
default:
showHelp("Invalid command: %s", cmd);
}
}
private void showHelp(String headerMessage, Object... headerArgs) {
if (headerMessage != null) {
mWriter.printf(headerMessage, headerArgs);
mWriter.print(". ");
}
mWriter.println("Available commands:\n");
mWriter.increaseIndent();
showCommandHelp("Shows this help message.",
CMD_HELP);
showCommandHelp("Lists delegated scopes set by the device admin.",
CMD_GET_DELEGATED_SCOPES);
showCommandHelp("Checks whether uninstalling the given app is blocked.",
CMD_IS_UNINSTALL_BLOCKED, "<PKG>");
showCommandHelp("Blocks / unblocks uninstalling the given app.",
CMD_SET_UNINSTALL_BLOCKED, "<PKG>", "<true|false>");
showCommandHelp("Generates a device attestation key.",
CMD_GENERATE_DEVICE_ATTESTATION_KEY_PAIR, "<ALIAS>", "[FLAGS]");
mWriter.decreaseIndent();
}
private void showCommandHelp(String description, String cmd, String... args) {
mWriter.printf("%s", cmd);
if (args != null) {
for (String arg : args) {
mWriter.printf(" %s", arg);
}
}
mWriter.println(":");
mWriter.increaseIndent();
mWriter.printf("%s\n\n", description);
mWriter.decreaseIndent();
}
private void getDelegatedScopes() {
if (!supportDevicePolicyManagement()) return;
List<String> scopes = mDpm.getDelegatedScopes(/* admin= */ null, mContext.getPackageName());
printCollection("delegated scope", scopes);
}
private void isUninstallBlocked() {
if (!supportDevicePolicyManagement()) return;
String packageName = getNextArg();
boolean isIt = mDpm.isUninstallBlocked(/* admin= */ null, packageName);
mWriter.println(isIt);
}
private void setUninstallBlocked() {
if (!supportDevicePolicyManagement()) return;
String packageName = getNextArg();
boolean blocked = getNextBooleanArg();
Log.i(TAG, "Calling dpm.setUninstallBlocked(" + packageName + ", " + blocked + ")");
mDpm.setUninstallBlocked(/* admin= */ null, packageName, blocked);
}
private void generateDeviceAttestationKeyPair() {
if (!supportDevicePolicyManagement()) return;
String alias = getNextArg();
int flags = getNextOptionalIntArg(/* defaultValue= */ 0);
// Cannot call dpm.generateKeyPair() on main thread
warnAboutAsyncCall();
post(() -> handleDeviceAttestationKeyPair(alias, flags));
}
private void handleDeviceAttestationKeyPair(String alias, int flags) {
KeyGenParameterSpec keySpec = buildRsaKeySpecWithKeyAttestation(alias);
String algorithm = "RSA";
Log.i(TAG, "calling dpm.generateKeyPair(alg=" + algorithm + ", spec=" + keySpec
+ ", flags=" + flags + ")");
AttestedKeyPair kp = mDpm.generateKeyPair(/* admin= */ null, algorithm, keySpec, flags);
Log.i(TAG, "key: " + kp);
}
private void warnAboutAsyncCall() {
mWriter.printf("Command will be executed asynchronally; use `adb logcat %s *:s` for result"
+ "\n", TAG);
}
private void post(Runnable r) {
if (mHandler == null) {
HandlerThread handlerThread = new HandlerThread("KitchenSinkShellCommandThread");
Log.i(TAG, "Starting " + handlerThread);
handlerThread.start();
mHandler = new Handler(handlerThread.getLooper());
}
Log.d(TAG, "posting runnable");
mHandler.post(r);
}
private boolean supportDevicePolicyManagement() {
if (mDpm == null) {
mWriter.println("Device Policy Management not supported by device");
return false;
}
return true;
}
private String getNextArgAndIncrementCounter() {
return mArgs[++mNextArgIndex];
}
private String getNextArg() {
try {
return getNextArgAndIncrementCounter();
} catch (Exception e) {
mWriter.println("Error: missing argument");
mWriter.flush();
throw new IllegalArgumentException(
"Missing argument. Args=" + Arrays.toString(mArgs));
}
}
private int getNextOptionalIntArg(int defaultValue) {
try {
return Integer.parseInt(getNextArgAndIncrementCounter());
} catch (Exception e) {
Log.d(TAG, "Exception getting optional arg: " + e);
return defaultValue;
}
}
private boolean getNextBooleanArg() {
String arg = getNextArg();
return Boolean.parseBoolean(arg);
}
private void printCollection(String nameOnSingular, Collection<String> collection) {
if (collection.isEmpty()) {
mWriter.printf("No %ss\n", nameOnSingular);
return;
}
int size = collection.size();
mWriter.printf("%d %s%s:\n", size, nameOnSingular, size == 1 ? "" : "s");
collection.forEach((s) -> mWriter.printf(" %s\n", s));
}
// Copied from CTS' KeyGenerationUtils
private static KeyGenParameterSpec buildRsaKeySpecWithKeyAttestation(String alias) {
return new KeyGenParameterSpec.Builder(alias,
KeyProperties.PURPOSE_SIGN | KeyProperties.PURPOSE_VERIFY)
.setKeySize(2048)
.setDigests(KeyProperties.DIGEST_SHA256)
.setSignaturePaddings(KeyProperties.SIGNATURE_PADDING_RSA_PSS,
KeyProperties.SIGNATURE_PADDING_RSA_PKCS1)
.setIsStrongBoxBacked(false)
.setAttestationChallenge(new byte[] {
'a', 'b', 'c'
})
.build();
}
}