blob: 02cf971a807685da90e96aa024e31e4d3ea2db08 [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.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.debug.AdbManager;
import android.debug.IAdbManager;
import android.os.ServiceManager;
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);
dropShellPermissionIdentity();
}
/**
* 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));
}
@Test
public void testClearAuthorizationsBeforeAdbEnabled() throws Exception {
// The adb key store is not instantiated until adb is enabled; however if the user attempts
// to clear the adb authorizations when adb is disabled after a boot a NullPointerException
// was thrown as deleteKeyStore is invoked against the key store. This test ensures the
// key store can be successfully cleared when adb is disabled.
mHandler = mManager.new AdbDebuggingHandler(FgThread.get().getLooper());
clearKeyStore();
}
@Test
public void testClearAuthorizationsDeletesKeyFiles() throws Exception {
mAdbKeyFile.createNewFile();
mAdbKeyXmlFile.createNewFile();
clearKeyStore();
assertFalse("The adb key file should have been deleted after revocation of the grants",
mAdbKeyFile.exists());
assertFalse("The adb xml key file should have been deleted after revocation of the grants",
mAdbKeyXmlFile.exists());
}
@Test
public void testAdbKeyStore_removeKey() throws Exception {
// Accept the test key with the 'Always allow' option selected.
runAdbTest(TEST_KEY_1, true, true, false);
runAdbTest(TEST_KEY_2, true, true, false);
// Set the connection time to 0 to restore the original behavior.
setAllowedConnectionTime(0);
// 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));
assertTrue("The key was not in the adb_keys file after persisting the keystore",
isKeyInFile(TEST_KEY_2, mAdbKeyFile));
// Now remove one of the keys and make sure the other key is still there
mKeyStore.removeKey(TEST_KEY_1);
assertFalse("The key was still in the adb_keys file after removing the key",
isKeyInFile(TEST_KEY_1, mAdbKeyFile));
assertTrue("The key was not in the adb_keys file after removing a different key",
isKeyInFile(TEST_KEY_2, mAdbKeyFile));
}
@Test
public void testIsValidMdnsServiceName() {
// Longer than 15 characters
assertFalse(isValidMdnsServiceName("abcd1234abcd1234"));
// Contains invalid characters
assertFalse(isValidMdnsServiceName("a*a"));
assertFalse(isValidMdnsServiceName("a_a"));
assertFalse(isValidMdnsServiceName("_a"));
// Does not begin or end with letter or digit
assertFalse(isValidMdnsServiceName(""));
assertFalse(isValidMdnsServiceName("-"));
assertFalse(isValidMdnsServiceName("-a"));
assertFalse(isValidMdnsServiceName("-1"));
assertFalse(isValidMdnsServiceName("a-"));
assertFalse(isValidMdnsServiceName("1-"));
// Contains consecutive hyphens
assertFalse(isValidMdnsServiceName("a--a"));
// Does not contain at least one letter
assertFalse(isValidMdnsServiceName("1"));
assertFalse(isValidMdnsServiceName("12"));
assertFalse(isValidMdnsServiceName("1-2"));
// letter not within [a-zA-Z]
assertFalse(isValidMdnsServiceName("aés"));
// Some valid names
assertTrue(isValidMdnsServiceName("a"));
assertTrue(isValidMdnsServiceName("a1"));
assertTrue(isValidMdnsServiceName("1A"));
assertTrue(isValidMdnsServiceName("aZ"));
assertTrue(isValidMdnsServiceName("a-Z"));
assertTrue(isValidMdnsServiceName("a-b-Z"));
assertTrue(isValidMdnsServiceName("abc-def-123-456"));
}
@Test
public void testPairingThread_MdnsServiceName_RFC6335() {
assertTrue(isValidMdnsServiceName(AdbDebuggingManager.PairingThread.SERVICE_PROTOCOL));
}
private boolean isValidMdnsServiceName(String name) {
// The rules for Service Names [RFC6335] state that they may be no more
// than fifteen characters long (not counting the mandatory underscore),
// consisting of only letters, digits, and hyphens, must begin and end
// with a letter or digit, must not contain consecutive hyphens, and
// must contain at least one letter.
// No more than 15 characters long
final int len = name.length();
if (name.isEmpty() || len > 15) {
return false;
}
boolean hasAtLeastOneLetter = false;
boolean sawHyphen = false;
for (int i = 0; i < len; ++i) {
// Must contain at least one letter
// Only contains letters, digits and hyphens
char c = name.charAt(i);
if (c == '-') {
// Cannot be at beginning or end
if (i == 0 || i == len - 1) {
return false;
}
if (sawHyphen) {
// Consecutive hyphen found
return false;
}
sawHyphen = true;
continue;
}
sawHyphen = false;
if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) {
hasAtLeastOneLetter = true;
continue;
}
if (c >= '0' && c <= '9') {
continue;
}
// Invalid character
return false;
}
return hasAtLeastOneLetter;
}
CountDownLatch mAdbActionLatch = new CountDownLatch(1);
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
Log.i(TAG, "Received intent action=" + action);
if (AdbManager.WIRELESS_DEBUG_PAIRED_DEVICES_ACTION.equals(action)) {
assertEquals("Received broadcast without MANAGE_DEBUGGING permission.",
context.checkSelfPermission(android.Manifest.permission.MANAGE_DEBUGGING),
PackageManager.PERMISSION_GRANTED);
Log.i(TAG, "action=" + action + " paired_device=" + intent.getSerializableExtra(
AdbManager.WIRELESS_DEVICES_EXTRA).toString());
mAdbActionLatch.countDown();
} else if (AdbManager.WIRELESS_DEBUG_STATE_CHANGED_ACTION.equals(action)) {
assertEquals("Received broadcast without MANAGE_DEBUGGING permission.",
context.checkSelfPermission(android.Manifest.permission.MANAGE_DEBUGGING),
PackageManager.PERMISSION_GRANTED);
int status = intent.getIntExtra(AdbManager.WIRELESS_STATUS_EXTRA,
AdbManager.WIRELESS_STATUS_DISCONNECTED);
Log.i(TAG, "action=" + action + " status=" + status);
mAdbActionLatch.countDown();
} else if (AdbManager.WIRELESS_DEBUG_PAIRING_RESULT_ACTION.equals(action)) {
assertEquals("Received broadcast without MANAGE_DEBUGGING permission.",
context.checkSelfPermission(android.Manifest.permission.MANAGE_DEBUGGING),
PackageManager.PERMISSION_GRANTED);
Integer res = intent.getIntExtra(
AdbManager.WIRELESS_STATUS_EXTRA,
AdbManager.WIRELESS_STATUS_FAIL);
Log.i(TAG, "action=" + action + " result=" + res);
if (res.equals(AdbManager.WIRELESS_STATUS_PAIRING_CODE)) {
String pairingCode = intent.getStringExtra(
AdbManager.WIRELESS_PAIRING_CODE_EXTRA);
Log.i(TAG, "pairingCode=" + pairingCode);
} else if (res.equals(AdbManager.WIRELESS_STATUS_CONNECTED)) {
int port = intent.getIntExtra(AdbManager.WIRELESS_DEBUG_PORT_EXTRA, 0);
Log.i(TAG, "port=" + port);
}
mAdbActionLatch.countDown();
}
}
};
private void adoptShellPermissionIdentity() {
InstrumentationRegistry.getInstrumentation().getUiAutomation()
.adoptShellPermissionIdentity(android.Manifest.permission.MANAGE_DEBUGGING);
}
private void dropShellPermissionIdentity() {
InstrumentationRegistry.getInstrumentation().getUiAutomation()
.dropShellPermissionIdentity();
}
@Test
public void testBroadcastReceiverWithPermissions() throws Exception {
adoptShellPermissionIdentity();
final IAdbManager mAdbManager = IAdbManager.Stub.asInterface(
ServiceManager.getService(Context.ADB_SERVICE));
IntentFilter intentFilter =
new IntentFilter(AdbManager.WIRELESS_DEBUG_PAIRED_DEVICES_ACTION);
intentFilter.addAction(AdbManager.WIRELESS_DEBUG_STATE_CHANGED_ACTION);
intentFilter.addAction(AdbManager.WIRELESS_DEBUG_PAIRING_RESULT_ACTION);
assertEquals("Context does not have MANAGE_DEBUGGING permission.",
mContext.checkSelfPermission(android.Manifest.permission.MANAGE_DEBUGGING),
PackageManager.PERMISSION_GRANTED);
try {
mContext.registerReceiver(mReceiver, intentFilter);
mAdbManager.enablePairingByPairingCode();
if (!mAdbActionLatch.await(TIMEOUT, TIMEOUT_TIME_UNIT)) {
fail("Receiver did not receive adb intent action within the timeout duration");
}
} finally {
mContext.unregisterReceiver(mReceiver);
}
}
@Test
public void testBroadcastReceiverWithoutPermissions() throws Exception {
adoptShellPermissionIdentity();
final IAdbManager mAdbManager = IAdbManager.Stub.asInterface(
ServiceManager.getService(Context.ADB_SERVICE));
IntentFilter intentFilter =
new IntentFilter(AdbManager.WIRELESS_DEBUG_PAIRED_DEVICES_ACTION);
intentFilter.addAction(AdbManager.WIRELESS_DEBUG_STATE_CHANGED_ACTION);
intentFilter.addAction(AdbManager.WIRELESS_DEBUG_PAIRING_RESULT_ACTION);
mAdbManager.enablePairingByPairingCode();
dropShellPermissionIdentity();
assertEquals("Context has MANAGE_DEBUGGING permission.",
mContext.checkSelfPermission(android.Manifest.permission.MANAGE_DEBUGGING),
PackageManager.PERMISSION_DENIED);
try {
mContext.registerReceiver(mReceiver, intentFilter);
if (mAdbActionLatch.await(TIMEOUT, TIMEOUT_TIME_UNIT)) {
fail("Broadcast receiver received adb action intent without debug permissions");
}
} finally {
mContext.unregisterReceiver(mReceiver);
}
}
/**
* 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 + "}";
}
}
}