blob: d4182f3d31e2d5c1906b025848447b8e344cc094 [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.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
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= test@android.com";
private static final String TEST_KEY_2 = "dGVzdCBrZXkgMgo= test@android.com";
// 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 mAdbKeyXmlFile;
private File mAdbKeyFile;
@Before
public void setUp() throws Exception {
mContext = InstrumentationRegistry.getContext();
mAdbKeyFile = new File(mContext.getFilesDir(), "adb_keys");
if (mAdbKeyFile.exists()) {
mAdbKeyFile.delete();
}
mManager = new AdbDebuggingManager(mContext, ADB_CONFIRM_COMPONENT, mAdbKeyFile);
mAdbKeyXmlFile = new File(mContext.getFilesDir(), "test_adb_keys.xml");
if (mAdbKeyXmlFile.exists()) {
mAdbKeyXmlFile.delete();
}
mThread = new AdbDebuggingThreadTest();
mKeyStore = mManager.new AdbKeyStore(mAdbKeyXmlFile);
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);
// Persist the keystore to ensure that the key is not written to the adb_keys file.
persistKeyStore();
assertFalse(
"A key for which the 'always allow' option is not selected must not be written "
+ "to the adb_keys file",
isKeyInFile(TEST_KEY_1, mAdbKeyFile));
}
@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.
disconnectKey(TEST_KEY_1);
assertNotEquals(
"The last connection time was not updated after the disconnect",
lastConnectionTime,
mKeyStore.getLastConnectionTime(TEST_KEY_1));
}
@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.
disconnectKey(TEST_KEY_1);
// 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);
// Send a persist keystore message to force the key to be written to the adb_keys file
persistKeyStore();
// Verify the key is in the adb_keys file to ensure subsequent connections are allowed by
// adbd.
assertTrue("The key was not in the adb_keys file after persisting the keystore",
isKeyInFile(TEST_KEY_1, mAdbKeyFile));
}
@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);
// Verify that the key is in the adb_keys file to ensure subsequent connections are
// automatically allowed by adbd.
persistKeyStore();
assertTrue("The key was not in the adb_keys file after persisting the keystore",
isKeyInFile(TEST_KEY_1, mAdbKeyFile));
}
@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
updateKeyStore();
assertNotEquals(
"The last connection time of the key was not updated after the update key "
+ "connection time message",
lastConnectionTime, mKeyStore.getLastConnectionTime(TEST_KEY_1));
}
@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 and verify a new key store
// backed by the XML file contains the key.
persistKeyStore();
assertTrue(
"The key with the 'Always allow' option selected was not persisted in the keystore",
mManager.new AdbKeyStore(mAdbKeyXmlFile).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.
updateKeyStore();
// Persist the updated last connection time and verify a new key store backed by the XML
// file contains the updated connection time.
persistKeyStore();
assertNotEquals(
"The last connection time in the key file was not updated after the update "
+ "connection time message", lastConnectionTime,
mManager.new AdbKeyStore(mAdbKeyXmlFile).getLastConnectionTime(TEST_KEY_1));
// Verify that the key is in the adb_keys file
assertTrue("The key was not in the adb_keys file after persisting the keystore",
isKeyInFile(TEST_KEY_1, mAdbKeyFile));
}
@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.
clearKeyStore();
// Send a message to disconnect the currently connected key
disconnectKey(TEST_KEY_1);
assertFalse(
"The currently connected 'always allow' key must not be authorized after an adb"
+ " clear message.",
mKeyStore.isKeyAuthorized(TEST_KEY_1));
// The key should not be in the adb_keys file after clearing the authorizations.
assertFalse("The key must not be in the adb_keys file after clearing authorizations",
isKeyInFile(TEST_KEY_1, mAdbKeyFile));
}
@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);
// The AdbKeyStore has a method to get the time of the next key expiration to ensure the
// scheduled job runs at the time of the next expiration or after 24 hours, whichever occurs
// first.
assertEquals("The time of the next key expiration must be 0.", 0,
mKeyStore.getNextExpirationTime());
// Persist the key store and verify that the key is no longer in the adb_keys file.
persistKeyStore();
assertFalse(
"The key must not be in the adb_keys file after the allowed time has elapsed.",
isKeyInFile(TEST_KEY_1, mAdbKeyFile));
}
@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 must 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 must not be set to a value less "
+ "than the previous connection time",
lastConnectionTime, mKeyStore.getLastConnectionTime(TEST_KEY_1));
}
@Test
public void testAdbKeyRemovedByScheduledJob() throws Exception {
// When a key is automatically allowed it should be stored in the adb_keys file. A job is
// then scheduled daily to update the connection time of the currently connected key, and if
// no connected key exists the key store is updated to purge expired keys. This test
// verifies that after a key's expiration time has been reached that it is no longer
// in the key store nor the adb_keys file
// Set the allowed time to the default to ensure that any modification to this value do not
// impact this test.
setAllowedConnectionTime(Settings.Global.DEFAULT_ADB_ALLOWED_CONNECTION_TIME);
// Allow both test keys to connect with the 'always allow' option selected.
runAdbTest(TEST_KEY_1, true, true, false);
runAdbTest(TEST_KEY_2, true, true, false);
disconnectKey(TEST_KEY_1);
disconnectKey(TEST_KEY_2);
// Persist the key store and verify that both keys are in the key store and adb_keys file.
persistKeyStore();
assertTrue(
"Test key 1 must be in the adb_keys file after selecting the 'always allow' "
+ "option",
isKeyInFile(TEST_KEY_1, mAdbKeyFile));
assertTrue(
"Test key 1 must be in the adb key store after selecting the 'always allow' "
+ "option",
mKeyStore.isKeyAuthorized(TEST_KEY_1));
assertTrue(
"Test key 2 must be in the adb_keys file after selecting the 'always allow' "
+ "option",
isKeyInFile(TEST_KEY_2, mAdbKeyFile));
assertTrue(
"Test key 2 must be in the adb key store after selecting the 'always allow' option",
mKeyStore.isKeyAuthorized(TEST_KEY_2));
// Set test key 1's last connection time to a small value and persist the keystore to ensure
// it is cleared out after the next key store update.
mKeyStore.setLastConnectionTime(TEST_KEY_1, 1, true);
updateKeyStore();
assertFalse(
"Test key 1 must no longer be in the adb_keys file after its timeout period is "
+ "reached",
isKeyInFile(TEST_KEY_1, mAdbKeyFile));
assertFalse(
"Test key 1 must no longer be in the adb key store after its timeout period is "
+ "reached",
mKeyStore.isKeyAuthorized(TEST_KEY_1));
assertTrue(
"Test key 2 must still be in the adb_keys file after test key 1's timeout "
+ "period is reached",
isKeyInFile(TEST_KEY_2, mAdbKeyFile));
assertTrue(
"Test key 2 must still be in the adb key store after test key 1's timeout period "
+ "is reached",
mKeyStore.isKeyAuthorized(TEST_KEY_2));
}
@Test
public void testKeystoreExpirationTimes() throws Exception {
// When one or more keys are always allowed a daily job is scheduled to update the
// connection time of the connected key and to purge any expired keys. The keystore provides
// a method to obtain the expiration time of the next key to expire to ensure that a
// scheduled job can run at the time of the next expiration if it is before the daily job
// would run. This test verifies that this method returns the expected values depending on
// when the key should expire and also verifies that the method to schedule the next job to
// update the keystore is the expected value based on the time of the next expiration.
final long epsilon = 5000;
// Ensure the allowed time is set to the default.
setAllowedConnectionTime(Settings.Global.DEFAULT_ADB_ALLOWED_CONNECTION_TIME);
// If there are no keys in the keystore the expiration time should be -1.
assertEquals("The expiration time must be -1 when there are no keys in the keystore", -1,
mKeyStore.getNextExpirationTime());
// Allow the test key to connect with the 'always allow' option.
runAdbTest(TEST_KEY_1, true, true, false);
// Verify that the current expiration time is within a small value of the default time.
long expirationTime = mKeyStore.getNextExpirationTime();
if (Math.abs(expirationTime - Settings.Global.DEFAULT_ADB_ALLOWED_CONNECTION_TIME)
> epsilon) {
fail("The expiration time for a new key, " + expirationTime
+ ", is outside the expected value of "
+ Settings.Global.DEFAULT_ADB_ALLOWED_CONNECTION_TIME);
}
// The delay until the next job should be the lesser of the default expiration time and the
// AdbDebuggingHandler's job interval.
long expectedValue = Math.min(
AdbDebuggingManager.AdbDebuggingHandler.UPDATE_KEYSTORE_JOB_INTERVAL,
Settings.Global.DEFAULT_ADB_ALLOWED_CONNECTION_TIME);
long delay = mHandler.scheduleJobToUpdateAdbKeyStore();
if (Math.abs(delay - expectedValue) > epsilon) {
fail("The delay before the next scheduled job, " + delay
+ ", is outside the expected value of " + expectedValue);
}
// Set the current expiration time to a minute from expiration and verify this new value is
// returned.
final long newExpirationTime = 60000;
mKeyStore.setLastConnectionTime(TEST_KEY_1,
System.currentTimeMillis() - Settings.Global.DEFAULT_ADB_ALLOWED_CONNECTION_TIME
+ newExpirationTime, true);
expirationTime = mKeyStore.getNextExpirationTime();
if (Math.abs(expirationTime - newExpirationTime) > epsilon) {
fail("The expiration time for a key about to expire, " + expirationTime
+ ", is outside the expected value of " + newExpirationTime);
}
delay = mHandler.scheduleJobToUpdateAdbKeyStore();
if (Math.abs(delay - newExpirationTime) > epsilon) {
fail("The delay before the next scheduled job, " + delay
+ ", is outside the expected value of " + newExpirationTime);
}
// If a key is already expired the expiration time and delay before the next job runs should
// be 0.
mKeyStore.setLastConnectionTime(TEST_KEY_1, 1, true);
assertEquals("The expiration time for a key that is already expired must be 0", 0,
mKeyStore.getNextExpirationTime());
assertEquals(
"The delay before the next scheduled job for a key that is already expired must"
+ " be 0", 0, mHandler.scheduleJobToUpdateAdbKeyStore());
// If the previous behavior of never removing old keys is set then the expiration time
// should be -1 to indicate the job does not need to run.
setAllowedConnectionTime(0);
assertEquals("The expiration time must be -1 when the keys are set to never expire", -1,
mKeyStore.getNextExpirationTime());
}
@Test
public void testConnectionTimeUpdatedWithConnectedKeyMessage() throws Exception {
// When a system successfully passes the SIGNATURE challenge adbd sends a connected key
// message to the framework to notify of the newly connected key. This message should
// trigger the AdbDebuggingManager to update the last connection time for this key and mark
// it as the currently connected key so that its time can be updated during subsequent
// keystore update jobs as well as when the disconnected message is received.
// Allow the test key to connect with the 'always allow' option selected.
runAdbTest(TEST_KEY_1, true, true, false);
// Simulate disconnecting the key before a subsequent connection without user interaction.
disconnectKey(TEST_KEY_1);
// Get the last connection time for the key to verify that it is updated when the connected
// key message is sent.
long connectionTime = mKeyStore.getLastConnectionTime(TEST_KEY_1);
Thread.sleep(10);
mHandler.obtainMessage(AdbDebuggingManager.AdbDebuggingHandler.MESSAGE_ADB_CONNECTED_KEY,
TEST_KEY_1).sendToTarget();
flushHandlerQueue();
assertNotEquals(
"The connection time for the key must be updated when the connected key message "
+ "is received",
connectionTime, mKeyStore.getLastConnectionTime(TEST_KEY_1));
// Verify that the scheduled job updates the connection time of the key.
connectionTime = mKeyStore.getLastConnectionTime(TEST_KEY_1);
Thread.sleep(10);
updateKeyStore();
assertNotEquals(
"The connection time for the key must be updated when the update keystore message"
+ " is sent",
connectionTime, mKeyStore.getLastConnectionTime(TEST_KEY_1));
// Verify that the connection time is updated when the key is disconnected.
connectionTime = mKeyStore.getLastConnectionTime(TEST_KEY_1);
Thread.sleep(10);
disconnectKey(TEST_KEY_1);
assertNotEquals(
"The connection time for the key must be updated when the disconnected message is"
+ " received",
connectionTime, mKeyStore.getLastConnectionTime(TEST_KEY_1));
}
@Test
public void testClearAuthorizations() throws Exception {
// When the user selects the 'Revoke USB debugging authorizations' all previously 'always
// allow' keys should be deleted.
// Set the allowed connection time to the default value to ensure tests do not fail due to
// a small value.
setAllowedConnectionTime(Settings.Global.DEFAULT_ADB_ALLOWED_CONNECTION_TIME);
// Allow the test key to connect with the 'always allow' option selected.
runAdbTest(TEST_KEY_1, true, true, false);
persistKeyStore();
// Verify that the key is authorized and in the adb_keys file
assertTrue(
"The test key must be in the keystore after the 'always allow' option is selected",
mKeyStore.isKeyAuthorized(TEST_KEY_1));
assertTrue(
"The test key must be in the adb_keys file after the 'always allow option is "
+ "selected",
isKeyInFile(TEST_KEY_1, mAdbKeyFile));
// Send the message to clear the adb authorizations and verify that the keys are no longer
// authorized.
clearKeyStore();
assertFalse(
"The test key must not be in the keystore after clearing the authorizations",
mKeyStore.isKeyAuthorized(TEST_KEY_1));
assertFalse(
"The test key must not be in the adb_keys file after clearing the authorizations",
isKeyInFile(TEST_KEY_1, mAdbKeyFile));
}
@Test
public void testClearKeystoreAfterDisablingAdb() throws Exception {
// When the user disables adb they should still be able to clear the authorized keys.
// Allow the test key to connect with the 'always allow' option selected and persist the
// keystore.
runAdbTest(TEST_KEY_1, true, true, false);
persistKeyStore();
// Disable adb and verify that the keystore can be cleared without throwing an exception.
disableAdb();
clearKeyStore();
assertFalse(
"The test key must not be in the adb_keys file after clearing the authorizations",
isKeyInFile(TEST_KEY_1, mAdbKeyFile));
}
@Test
public void testUntrackedUserKeysAddedToKeystore() throws Exception {
// When a device is first updated to a build that tracks the connection time of adb keys
// the keys in the user key file will not have a connection time. To prevent immediately
// deleting keys that the user is actively using these untracked keys should be added to the
// keystore with the current system time; this gives the user time to reconnect
// automatically with an active key while inactive keys are deleted after the expiration
// time.
final long epsilon = 5000;
final String[] testKeys = {TEST_KEY_1, TEST_KEY_2};
// Add the test keys to the user key file.
FileOutputStream fo = new FileOutputStream(mAdbKeyFile);
for (String key : testKeys) {
fo.write(key.getBytes());
fo.write('\n');
}
fo.close();
// Set the expiration time to the default and use this value to verify the expiration time
// of the previously untracked keys.
setAllowedConnectionTime(Settings.Global.DEFAULT_ADB_ALLOWED_CONNECTION_TIME);
// The untracked keys should be added to the keystore as part of the constructor.
AdbDebuggingManager.AdbKeyStore adbKeyStore = mManager.new AdbKeyStore(mAdbKeyXmlFile);
// Verify that the connection time for each test key is within a small value of the current
// time.
long time = System.currentTimeMillis();
for (String key : testKeys) {
long connectionTime = adbKeyStore.getLastConnectionTime(key);
if (Math.abs(connectionTime - connectionTime) > epsilon) {
fail("The connection time for a previously untracked key, " + connectionTime
+ ", is beyond the current time of " + time);
}
}
}
@Test
public void testConnectionTimeUpdatedForMultipleConnectedKeys() throws Exception {
// Since ADB supports multiple simultaneous connections verify that the connection time of
// each key is updated by the scheduled job as long as it is connected.
// Allow both test keys to connect with the 'always allow' option selected.
runAdbTest(TEST_KEY_1, true, true, false);
runAdbTest(TEST_KEY_2, true, true, false);
// Sleep a small amount of time to ensure the connection time is updated by the scheduled
// job.
long connectionTime1 = mKeyStore.getLastConnectionTime(TEST_KEY_1);
long connectionTime2 = mKeyStore.getLastConnectionTime(TEST_KEY_2);
Thread.sleep(10);
updateKeyStore();
assertNotEquals(
"The connection time for test key 1 must be updated after the scheduled job runs",
connectionTime1, mKeyStore.getLastConnectionTime(TEST_KEY_1));
assertNotEquals(
"The connection time for test key 2 must be updated after the scheduled job runs",
connectionTime2, mKeyStore.getLastConnectionTime(TEST_KEY_2));
// Disconnect the second test key and verify that the last connection time of the first key
// is the only one updated.
disconnectKey(TEST_KEY_2);
connectionTime1 = mKeyStore.getLastConnectionTime(TEST_KEY_1);
connectionTime2 = mKeyStore.getLastConnectionTime(TEST_KEY_2);
Thread.sleep(10);
updateKeyStore();
assertNotEquals(
"The connection time for test key 1 must be updated after another key is "
+ "disconnected and the scheduled job runs",
connectionTime1, mKeyStore.getLastConnectionTime(TEST_KEY_1));
assertEquals(
"The connection time for test key 2 must not be updated after it is disconnected",
connectionTime2, mKeyStore.getLastConnectionTime(TEST_KEY_2));
}
/**
* 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 must not be authorized in the key store",
mKeyStore.isKeyAuthorized(key));
assertFalse(
"The key must not be stored in the adb_keys file",
isKeyInFile(key, mAdbKeyFile));
}
flushHandlerQueue();
}
private void persistKeyStore() throws Exception {
// Send a message to the handler to persist the key store.
mHandler.obtainMessage(
AdbDebuggingManager.AdbDebuggingHandler.MESSAGE_ADB_PERSIST_KEYSTORE)
.sendToTarget();
flushHandlerQueue();
}
private void disconnectKey(String key) throws Exception {
// Send a message to the handler to disconnect the currently connected key.
mHandler.obtainMessage(AdbDebuggingManager.AdbDebuggingHandler.MESSAGE_ADB_DISCONNECT,
key).sendToTarget();
flushHandlerQueue();
}
private void updateKeyStore() throws Exception {
// Send a message to the handler to run the update keystore job.
mHandler.obtainMessage(
AdbDebuggingManager.AdbDebuggingHandler.MESSAGE_ADB_UPDATE_KEYSTORE).sendToTarget();
flushHandlerQueue();
}
private void clearKeyStore() throws Exception {
// Send a message to the handler to clear all previously authorized keys.
mHandler.obtainMessage(
AdbDebuggingManager.AdbDebuggingHandler.MESSAGE_ADB_CLEAR).sendToTarget();
flushHandlerQueue();
}
private void disableAdb() throws Exception {
// Send a message to the handler to disable adb.
mHandler.obtainMessage(
AdbDebuggingManager.AdbDebuggingHandler.MESSAGE_ADB_DISABLED).sendToTarget();
flushHandlerQueue();
}
private void flushHandlerQueue() throws Exception {
// Post a Runnable to ensure that all of the current messages in the queue are flushed.
CountDownLatch latch = new CountDownLatch(1);
mHandler.post(() -> {
latch.countDown();
});
if (!latch.await(TIMEOUT, TIMEOUT_TIME_UNIT)) {
fail("The Runnable to flush the handler's queue did not complete within the timeout "
+ "period");
}
}
private boolean isKeyInFile(String key, File keyFile) throws Exception {
if (key == null) {
return false;
}
if (keyFile.exists()) {
try (BufferedReader in = new BufferedReader(new FileReader(keyFile))) {
String currKey;
while ((currKey = in.readLine()) != null) {
if (key.equals(currKey)) {
return true;
}
}
}
}
return false;
}
/**
* 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 + "}";
}
}
}