blob: 65af677182b499fc8789eb9184475dc6d57fb9dc [file] [log] [blame]
/*
* Copyright (C) 2018 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.android.server.adb;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import android.content.Context;
import android.provider.Settings;
import android.util.Log;
import androidx.test.InstrumentationRegistry;
import com.android.server.FgThread;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.io.File;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@RunWith(JUnit4.class)
public final class AdbDebuggingManagerTest {
private static final String TAG = "AdbDebuggingManagerTest";
// This component is passed to the AdbDebuggingManager to act as the activity that can confirm
// unknown adb keys. An overlay package was first attempted to override the
// config_customAdbPublicKeyConfirmationComponent config, but the value from that package was
// not being read.
private static final String ADB_CONFIRM_COMPONENT =
"com.android.frameworks.servicestests/"
+ "com.android.server.adb.AdbDebuggingManagerTestActivity";
// The base64 encoding of the values 'test key 1' and 'test key 2'.
private static final String TEST_KEY_1 = "dGVzdCBrZXkgMQo=";
private static final String TEST_KEY_2 = "dGVzdCBrZXkgMgo=";
// This response is received from the AdbDebuggingHandler when the key is allowed to connect
private static final String RESPONSE_KEY_ALLOWED = "OK";
// This response is received from the AdbDebuggingHandler when the key is not allowed to connect
private static final String RESPONSE_KEY_DENIED = "NO";
// wait up to 5 seconds for any blocking queries
private static final long TIMEOUT = 5000;
private static final TimeUnit TIMEOUT_TIME_UNIT = TimeUnit.MILLISECONDS;
private Context mContext;
private AdbDebuggingManager mManager;
private AdbDebuggingManager.AdbDebuggingThread mThread;
private AdbDebuggingManager.AdbDebuggingHandler mHandler;
private AdbDebuggingManager.AdbKeyStore mKeyStore;
private BlockingQueue<TestResult> mBlockingQueue;
private long mOriginalAllowedConnectionTime;
private File mKeyFile;
@Before
public void setUp() throws Exception {
mContext = InstrumentationRegistry.getContext();
mManager = new AdbDebuggingManager(mContext, ADB_CONFIRM_COMPONENT);
mKeyFile = new File(mContext.getFilesDir(), "test_adb_keys.xml");
if (mKeyFile.exists()) {
mKeyFile.delete();
}
mThread = new AdbDebuggingThreadTest();
mKeyStore = mManager.new AdbKeyStore(mKeyFile);
mHandler = mManager.new AdbDebuggingHandler(FgThread.get().getLooper(), mThread, mKeyStore);
mOriginalAllowedConnectionTime = mKeyStore.getAllowedConnectionTime();
mBlockingQueue = new ArrayBlockingQueue<>(1);
}
@After
public void tearDown() throws Exception {
mKeyStore.deleteKeyStore();
setAllowedConnectionTime(mOriginalAllowedConnectionTime);
}
/**
* Sets the allowed connection time within which a subsequent connection from a key for which
* the user selected the 'Always allow' option will be allowed without user interaction.
*/
private void setAllowedConnectionTime(long connectionTime) {
Settings.Global.putLong(mContext.getContentResolver(),
Settings.Global.ADB_ALLOWED_CONNECTION_TIME, connectionTime);
};
@Test
public void testAllowNewKeyOnce() throws Exception {
// Verifies the behavior when a new key first attempts to connect to a device. During the
// first connection the ADB confirmation activity should be launched to prompt the user to
// allow the connection with an option to always allow connections from this key.
// Verify if the user allows the key but does not select the option to 'always
// allow' that the connection is allowed but the key is not stored.
runAdbTest(TEST_KEY_1, true, false, false);
}
@Test
public void testDenyNewKey() throws Exception {
// Verifies if the user does not allow the key then the connection is not allowed and the
// key is not stored.
runAdbTest(TEST_KEY_1, false, false, false);
}
@Test
public void testDisconnectAlwaysAllowKey() throws Exception {
// When a key is disconnected from a device ADB should send a disconnect message; this
// message should trigger an update of the last connection time for the currently connected
// key.
// Allow a connection from a new key with the 'Always allow' option selected.
runAdbTest(TEST_KEY_1, true, true, false);
// Get the last connection time for the currently connected key to verify that it is updated
// after the disconnect.
long lastConnectionTime = mKeyStore.getLastConnectionTime(TEST_KEY_1);
// Sleep for a small amount of time to ensure a difference can be observed in the last
// connection time after a disconnect.
Thread.sleep(10);
// Send the disconnect message for the currently connected key to trigger an update of the
// last connection time.
mHandler.obtainMessage(
AdbDebuggingManager.AdbDebuggingHandler.MESSAGE_ADB_DISCONNECT).sendToTarget();
// Use a latch to ensure the test does not exit untill the Runnable has been processed.
CountDownLatch latch = new CountDownLatch(1);
// Post a new Runnable to the handler to ensure it runs after the disconnect message is
// processed.
mHandler.post(() -> {
assertNotEquals(
"The last connection time was not updated after the disconnect",
lastConnectionTime,
mKeyStore.getLastConnectionTime(TEST_KEY_1));
latch.countDown();
});
if (!latch.await(TIMEOUT, TIMEOUT_TIME_UNIT)) {
fail("The Runnable to verify the last connection time was updated did not complete "
+ "within the timeout period");
}
}
@Test
public void testDisconnectAllowedOnceKey() throws Exception {
// When a key is disconnected ADB should send a disconnect message; this message should
// essentially result in a noop for keys that the user only allows once since the last
// connection time is not maintained for these keys.
// Allow a connection from a new key with the 'Always allow' option set to false
runAdbTest(TEST_KEY_1, true, false, false);
// Send the disconnect message for the currently connected key.
mHandler.obtainMessage(
AdbDebuggingManager.AdbDebuggingHandler.MESSAGE_ADB_DISCONNECT).sendToTarget();
// Verify that the disconnected key is not automatically allowed on a subsequent connection.
runAdbTest(TEST_KEY_1, true, false, false);
}
@Test
public void testAlwaysAllowConnectionFromKey() throws Exception {
// Verifies when the user selects the 'Always allow' option for the current key that
// subsequent connection attempts from that key are allowed.
// Allow a connection from a new key with the 'Always allow' option selected.
runAdbTest(TEST_KEY_1, true, true, false);
// Next attempt another connection with the same key and verify that the activity to prompt
// the user to accept the key is not launched.
runAdbTest(TEST_KEY_1, true, true, true);
// Verify that a different key is not automatically allowed.
runAdbTest(TEST_KEY_2, false, false, false);
}
@Test
public void testOriginalAlwaysAllowBehavior() throws Exception {
// If the Settings.Global.ADB_ALLOWED_CONNECTION_TIME setting is set to 0 then the original
// behavior of 'Always allow' should be restored.
// Accept the test key with the 'Always allow' option selected.
runAdbTest(TEST_KEY_1, true, true, false);
// Set the connection time to 0 to restore the original behavior.
setAllowedConnectionTime(0);
// Set the last connection time to the test key to a very small value to ensure it would
// fail the new test but would be allowed with the original behavior.
mKeyStore.setLastConnectionTime(TEST_KEY_1, 1);
// Run the test with the key and verify that the connection is automatically allowed.
runAdbTest(TEST_KEY_1, true, true, true);
}
@Test
public void testLastConnectionTimeUpdatedByScheduledJob() throws Exception {
// If a development device is left connected to a system beyond the allowed connection time
// a reboot of the device while connected could make it appear as though the last connection
// time is beyond the allowed window. A scheduled job runs daily while a key is connected
// to update the last connection time to the current time; this ensures if the device is
// rebooted while connected to a system the last connection time should be within 24 hours.
// Allow the key to connect with the 'Always allow' option selected
runAdbTest(TEST_KEY_1, true, true, false);
// Get the current last connection time for comparison after the scheduled job is run
long lastConnectionTime = mKeyStore.getLastConnectionTime(TEST_KEY_1);
// Sleep a small amount of time to ensure that the updated connection time changes
Thread.sleep(10);
// Send a message to the handler to update the last connection time for the active key
mHandler.obtainMessage(
AdbDebuggingManager.AdbDebuggingHandler.MESSAGE_ADB_UPDATE_KEY_CONNECTION_TIME)
.sendToTarget();
// Post a Runnable to the handler to ensure it runs after the update key connection time
// message is processed.
CountDownLatch latch = new CountDownLatch(1);
mHandler.post(() -> {
assertNotEquals(
"The last connection time of the key was not updated after the update key "
+ "connection time message",
lastConnectionTime, mKeyStore.getLastConnectionTime(TEST_KEY_1));
latch.countDown();
});
if (!latch.await(TIMEOUT, TIMEOUT_TIME_UNIT)) {
fail("The Runnable to verify the last connection time was updated did not complete "
+ "within the timeout period");
}
}
@Test
public void testKeystorePersisted() throws Exception {
// After any updates are made to the key store a message should be sent to persist the
// key store. This test verifies that a key that is always allowed is persisted in the key
// store along with its last connection time.
// Allow the key to connect with the 'Always allow' option selected
runAdbTest(TEST_KEY_1, true, true, false);
// Send a message to the handler to persist the updated keystore.
mHandler.obtainMessage(
AdbDebuggingManager.AdbDebuggingHandler.MESSAGE_ADB_PERSIST_KEY_STORE)
.sendToTarget();
// Post a Runnable to the handler to ensure the persist key store message has been processed
// using a new AdbKeyStore backed by the key file.
mHandler.post(() -> assertTrue(
"The key with the 'Always allow' option selected was not persisted in the keystore",
mManager.new AdbKeyStore(mKeyFile).isKeyAuthorized(TEST_KEY_1)));
// Get the current last connection time to ensure it is updated in the persisted keystore.
long lastConnectionTime = mKeyStore.getLastConnectionTime(TEST_KEY_1);
// Sleep a small amount of time to ensure the last connection time is updated.
Thread.sleep(10);
// Send a message to the handler to update the last connection time for the active key.
mHandler.obtainMessage(
AdbDebuggingManager.AdbDebuggingHandler.MESSAGE_ADB_UPDATE_KEY_CONNECTION_TIME)
.sendToTarget();
// Persist the updated last connection time.
mHandler.obtainMessage(
AdbDebuggingManager.AdbDebuggingHandler.MESSAGE_ADB_PERSIST_KEY_STORE)
.sendToTarget();
// Post a Runnable with a new key store backed by the key file to verify that the last
// connection time obtained above is different from the persisted updated value.
CountDownLatch latch = new CountDownLatch(1);
mHandler.post(() -> {
assertNotEquals(
"The last connection time in the key file was not updated after the update "
+ "connection time message", lastConnectionTime,
mManager.new AdbKeyStore(mKeyFile).getLastConnectionTime(TEST_KEY_1));
latch.countDown();
});
if (!latch.await(TIMEOUT, TIMEOUT_TIME_UNIT)) {
fail("The Runnable to verify the last connection time was updated did not complete "
+ "within the timeout period");
}
}
@Test
public void testAdbClearRemovesActiveKey() throws Exception {
// If the user selects the option to 'Revoke USB debugging authorizations' while an 'Always
// allow' key is connected that key should be deleted as well.
// Allow the key to connect with the 'Always allow' option selected
runAdbTest(TEST_KEY_1, true, true, false);
// Send a message to the handler to clear the adb authorizations.
mHandler.obtainMessage(
AdbDebuggingManager.AdbDebuggingHandler.MESSAGE_ADB_CLEAR).sendToTarget();
// Send a message to disconnect the currently connected key
mHandler.obtainMessage(
AdbDebuggingManager.AdbDebuggingHandler.MESSAGE_ADB_DISCONNECT).sendToTarget();
// Post a Runnable to ensure the disconnect has completed to verify the 'Always allow' key
// that was connected when the clear was sent requires authorization.
CountDownLatch latch = new CountDownLatch(1);
mHandler.post(() -> {
assertFalse(
"The currently connected 'always allow' key should not be authorized after an"
+ " adb"
+ " clear message.",
mKeyStore.isKeyAuthorized(TEST_KEY_1));
latch.countDown();
});
if (!latch.await(TIMEOUT, TIMEOUT_TIME_UNIT)) {
fail("The Runnable to verify the key is not authorized did not complete within the "
+ "timeout period");
}
}
@Test
public void testAdbGrantRevokedIfLastConnectionBeyondAllowedTime() throws Exception {
// If the user selects the 'Always allow' option then subsequent connections from the key
// will be allowed as long as the connection is within the allowed window. Once the last
// connection time is beyond this window the user should be prompted to allow the key again.
// Allow the key to connect with the 'Always allow' option selected
runAdbTest(TEST_KEY_1, true, true, false);
// Set the allowed window to a small value to ensure the time is beyond the allowed window.
setAllowedConnectionTime(1);
// Sleep for a small amount of time to exceed the allowed window.
Thread.sleep(10);
// A new connection from this key should prompt the user again.
runAdbTest(TEST_KEY_1, true, true, false);
}
@Test
public void testLastConnectionTimeCannotBeSetBack() throws Exception {
// When a device is first booted there is a possibility that the system time will be set to
// the build time of the system image. If a device is connected to a system during a reboot
// this could cause the connection time to be set in the past; if the device time is not
// corrected before the device is disconnected then a subsequent connection with the time
// corrected would appear as though the last connection time was beyond the allowed window,
// and the user would be required to authorize the connection again. This test verifies that
// the AdbKeyStore does not update the last connection time if it is less than the
// previously written connection time.
// Allow the key to connect with the 'Always allow' option selected
runAdbTest(TEST_KEY_1, true, true, false);
// Get the last connection time that was written to the key store.
long lastConnectionTime = mKeyStore.getLastConnectionTime(TEST_KEY_1);
// Attempt to set the last connection time to 1970
mKeyStore.setLastConnectionTime(TEST_KEY_1, 0);
assertEquals(
"The last connection time in the adb key store should not be set to a value less "
+ "than the previous connection time",
lastConnectionTime, mKeyStore.getLastConnectionTime(TEST_KEY_1));
// Attempt to set the last connection time just beyond the allowed window.
mKeyStore.setLastConnectionTime(TEST_KEY_1,
Math.max(0, lastConnectionTime - (mKeyStore.getAllowedConnectionTime() + 1)));
assertEquals(
"The last connection time in the adb key store should not be set to a value less "
+ "than the previous connection time",
lastConnectionTime, mKeyStore.getLastConnectionTime(TEST_KEY_1));
}
/**
* Runs an adb test with the provided configuration.
*
* @param key The base64 encoding of the key to be used during the test.
* @param allowKey boolean indicating whether the key should be allowed to connect.
* @param alwaysAllow boolean indicating whether the 'Always allow' option should be selected.
* @param autoAllowExpected boolean indicating whether the key is expected to be automatically
* allowed without user interaction.
*/
private void runAdbTest(String key, boolean allowKey, boolean alwaysAllow,
boolean autoAllowExpected) throws Exception {
// if the key should not be automatically allowed then set up the activity
if (!autoAllowExpected) {
new AdbDebuggingManagerTestActivity.Configurator()
.setExpectedKey(key)
.setAllowKey(allowKey)
.setAlwaysAllow(alwaysAllow)
.setHandler(mHandler)
.setBlockingQueue(mBlockingQueue);
}
// send the message indicating a new key is attempting to connect
mHandler.obtainMessage(AdbDebuggingManager.AdbDebuggingHandler.MESSAGE_ADB_CONFIRM,
key).sendToTarget();
// if the key should not be automatically allowed then the ADB public key confirmation
// activity should be launched
if (!autoAllowExpected) {
TestResult activityResult = mBlockingQueue.poll(TIMEOUT, TIMEOUT_TIME_UNIT);
assertNotNull(
"The ADB public key confirmation activity did not complete within the timeout"
+ " period", activityResult);
assertEquals("The ADB public key activity failed with result: " + activityResult,
TestResult.RESULT_ACTIVITY_LAUNCHED, activityResult.mReturnCode);
}
// If the activity was launched it should send a response back to the manager that would
// trigger a response to the thread, or if the key is a known valid key then a response
// should be sent back without requiring interaction with the activity.
TestResult threadResult = mBlockingQueue.poll(TIMEOUT, TIMEOUT_TIME_UNIT);
assertNotNull("A response was not sent to the thread within the timeout period",
threadResult);
// verify that the result is an expected message from the thread
assertEquals("An unexpected result was received: " + threadResult,
TestResult.RESULT_RESPONSE_RECEIVED, threadResult.mReturnCode);
assertEquals("The manager did not send the proper response for allowKey = " + allowKey,
allowKey ? RESPONSE_KEY_ALLOWED : RESPONSE_KEY_DENIED, threadResult.mMessage);
// if the key is not allowed or not always allowed verify it is not in the key store
if (!allowKey || !alwaysAllow) {
assertFalse(
"The key should not be allowed automatically on subsequent connection attempts",
mKeyStore.isKeyAuthorized(key));
}
}
/**
* Helper class that extends AdbDebuggingThread to receive the response from AdbDebuggingManager
* indicating whether the key should be allowed to connect.
*/
class AdbDebuggingThreadTest extends AdbDebuggingManager.AdbDebuggingThread {
AdbDebuggingThreadTest() {
mManager.super();
}
@Override
public void sendResponse(String msg) {
TestResult result = new TestResult(TestResult.RESULT_RESPONSE_RECEIVED, msg);
try {
mBlockingQueue.put(result);
} catch (InterruptedException e) {
Log.e(TAG,
"Caught an InterruptedException putting the result in the queue: " + result,
e);
}
}
}
/**
* Contains the result for the current portion of the test along with any corresponding
* messages.
*/
public static class TestResult {
public int mReturnCode;
public String mMessage;
public static final int RESULT_ACTIVITY_LAUNCHED = 1;
public static final int RESULT_UNEXPECTED_KEY = 2;
public static final int RESULT_RESPONSE_RECEIVED = 3;
public TestResult(int returnCode) {
this(returnCode, null);
}
public TestResult(int returnCode, String message) {
mReturnCode = returnCode;
mMessage = message;
}
@Override
public String toString() {
return "{mReturnCode = " + mReturnCode + ", mMessage = " + mMessage + "}";
}
}
}