1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.adb; 18 19 import static org.junit.Assert.assertEquals; 20 import static org.junit.Assert.assertFalse; 21 import static org.junit.Assert.assertNotEquals; 22 import static org.junit.Assert.assertNotNull; 23 import static org.junit.Assert.assertTrue; 24 import static org.junit.Assert.fail; 25 26 import android.content.Context; 27 import android.provider.Settings; 28 import android.util.Log; 29 30 import androidx.test.InstrumentationRegistry; 31 32 import com.android.server.FgThread; 33 34 import org.junit.After; 35 import org.junit.Before; 36 import org.junit.Test; 37 import org.junit.runner.RunWith; 38 import org.junit.runners.JUnit4; 39 40 import java.io.BufferedReader; 41 import java.io.File; 42 import java.io.FileOutputStream; 43 import java.io.FileReader; 44 import java.util.concurrent.ArrayBlockingQueue; 45 import java.util.concurrent.BlockingQueue; 46 import java.util.concurrent.CountDownLatch; 47 import java.util.concurrent.TimeUnit; 48 49 @RunWith(JUnit4.class) 50 public final class AdbDebuggingManagerTest { 51 52 private static final String TAG = "AdbDebuggingManagerTest"; 53 54 // This component is passed to the AdbDebuggingManager to act as the activity that can confirm 55 // unknown adb keys. An overlay package was first attempted to override the 56 // config_customAdbPublicKeyConfirmationComponent config, but the value from that package was 57 // not being read. 58 private static final String ADB_CONFIRM_COMPONENT = 59 "com.android.frameworks.servicestests/" 60 + "com.android.server.adb.AdbDebuggingManagerTestActivity"; 61 62 // The base64 encoding of the values 'test key 1' and 'test key 2'. 63 private static final String TEST_KEY_1 = "dGVzdCBrZXkgMQo= test@android.com"; 64 private static final String TEST_KEY_2 = "dGVzdCBrZXkgMgo= test@android.com"; 65 66 // This response is received from the AdbDebuggingHandler when the key is allowed to connect 67 private static final String RESPONSE_KEY_ALLOWED = "OK"; 68 // This response is received from the AdbDebuggingHandler when the key is not allowed to connect 69 private static final String RESPONSE_KEY_DENIED = "NO"; 70 71 // wait up to 5 seconds for any blocking queries 72 private static final long TIMEOUT = 5000; 73 private static final TimeUnit TIMEOUT_TIME_UNIT = TimeUnit.MILLISECONDS; 74 75 private Context mContext; 76 private AdbDebuggingManager mManager; 77 private AdbDebuggingManager.AdbDebuggingThread mThread; 78 private AdbDebuggingManager.AdbDebuggingHandler mHandler; 79 private AdbDebuggingManager.AdbKeyStore mKeyStore; 80 private BlockingQueue<TestResult> mBlockingQueue; 81 private long mOriginalAllowedConnectionTime; 82 private File mAdbKeyXmlFile; 83 private File mAdbKeyFile; 84 85 @Before setUp()86 public void setUp() throws Exception { 87 mContext = InstrumentationRegistry.getContext(); 88 mAdbKeyFile = new File(mContext.getFilesDir(), "adb_keys"); 89 if (mAdbKeyFile.exists()) { 90 mAdbKeyFile.delete(); 91 } 92 mManager = new AdbDebuggingManager(mContext, ADB_CONFIRM_COMPONENT, mAdbKeyFile); 93 mAdbKeyXmlFile = new File(mContext.getFilesDir(), "test_adb_keys.xml"); 94 if (mAdbKeyXmlFile.exists()) { 95 mAdbKeyXmlFile.delete(); 96 } 97 mThread = new AdbDebuggingThreadTest(); 98 mKeyStore = mManager.new AdbKeyStore(mAdbKeyXmlFile); 99 mHandler = mManager.new AdbDebuggingHandler(FgThread.get().getLooper(), mThread, mKeyStore); 100 mOriginalAllowedConnectionTime = mKeyStore.getAllowedConnectionTime(); 101 mBlockingQueue = new ArrayBlockingQueue<>(1); 102 } 103 104 @After tearDown()105 public void tearDown() throws Exception { 106 mKeyStore.deleteKeyStore(); 107 setAllowedConnectionTime(mOriginalAllowedConnectionTime); 108 } 109 110 /** 111 * Sets the allowed connection time within which a subsequent connection from a key for which 112 * the user selected the 'Always allow' option will be allowed without user interaction. 113 */ setAllowedConnectionTime(long connectionTime)114 private void setAllowedConnectionTime(long connectionTime) { 115 Settings.Global.putLong(mContext.getContentResolver(), 116 Settings.Global.ADB_ALLOWED_CONNECTION_TIME, connectionTime); 117 }; 118 119 @Test testAllowNewKeyOnce()120 public void testAllowNewKeyOnce() throws Exception { 121 // Verifies the behavior when a new key first attempts to connect to a device. During the 122 // first connection the ADB confirmation activity should be launched to prompt the user to 123 // allow the connection with an option to always allow connections from this key. 124 125 // Verify if the user allows the key but does not select the option to 'always 126 // allow' that the connection is allowed but the key is not stored. 127 runAdbTest(TEST_KEY_1, true, false, false); 128 129 // Persist the keystore to ensure that the key is not written to the adb_keys file. 130 persistKeyStore(); 131 assertFalse( 132 "A key for which the 'always allow' option is not selected must not be written " 133 + "to the adb_keys file", 134 isKeyInFile(TEST_KEY_1, mAdbKeyFile)); 135 } 136 137 @Test testDenyNewKey()138 public void testDenyNewKey() throws Exception { 139 // Verifies if the user does not allow the key then the connection is not allowed and the 140 // key is not stored. 141 runAdbTest(TEST_KEY_1, false, false, false); 142 } 143 144 @Test testDisconnectAlwaysAllowKey()145 public void testDisconnectAlwaysAllowKey() throws Exception { 146 // When a key is disconnected from a device ADB should send a disconnect message; this 147 // message should trigger an update of the last connection time for the currently connected 148 // key. 149 150 // Allow a connection from a new key with the 'Always allow' option selected. 151 runAdbTest(TEST_KEY_1, true, true, false); 152 153 // Get the last connection time for the currently connected key to verify that it is updated 154 // after the disconnect. 155 long lastConnectionTime = mKeyStore.getLastConnectionTime(TEST_KEY_1); 156 157 // Sleep for a small amount of time to ensure a difference can be observed in the last 158 // connection time after a disconnect. 159 Thread.sleep(10); 160 161 // Send the disconnect message for the currently connected key to trigger an update of the 162 // last connection time. 163 disconnectKey(TEST_KEY_1); 164 assertNotEquals( 165 "The last connection time was not updated after the disconnect", 166 lastConnectionTime, 167 mKeyStore.getLastConnectionTime(TEST_KEY_1)); 168 } 169 170 @Test testDisconnectAllowedOnceKey()171 public void testDisconnectAllowedOnceKey() throws Exception { 172 // When a key is disconnected ADB should send a disconnect message; this message should 173 // essentially result in a noop for keys that the user only allows once since the last 174 // connection time is not maintained for these keys. 175 176 // Allow a connection from a new key with the 'Always allow' option set to false 177 runAdbTest(TEST_KEY_1, true, false, false); 178 179 // Send the disconnect message for the currently connected key. 180 disconnectKey(TEST_KEY_1); 181 182 // Verify that the disconnected key is not automatically allowed on a subsequent connection. 183 runAdbTest(TEST_KEY_1, true, false, false); 184 } 185 186 @Test testAlwaysAllowConnectionFromKey()187 public void testAlwaysAllowConnectionFromKey() throws Exception { 188 // Verifies when the user selects the 'Always allow' option for the current key that 189 // subsequent connection attempts from that key are allowed. 190 191 // Allow a connection from a new key with the 'Always allow' option selected. 192 runAdbTest(TEST_KEY_1, true, true, false); 193 194 // Send a persist keystore message to force the key to be written to the adb_keys file 195 persistKeyStore(); 196 197 // Verify the key is in the adb_keys file to ensure subsequent connections are allowed by 198 // adbd. 199 assertTrue("The key was not in the adb_keys file after persisting the keystore", 200 isKeyInFile(TEST_KEY_1, mAdbKeyFile)); 201 } 202 203 @Test testOriginalAlwaysAllowBehavior()204 public void testOriginalAlwaysAllowBehavior() throws Exception { 205 // If the Settings.Global.ADB_ALLOWED_CONNECTION_TIME setting is set to 0 then the original 206 // behavior of 'Always allow' should be restored. 207 208 // Accept the test key with the 'Always allow' option selected. 209 runAdbTest(TEST_KEY_1, true, true, false); 210 211 // Set the connection time to 0 to restore the original behavior. 212 setAllowedConnectionTime(0); 213 214 // Set the last connection time to the test key to a very small value to ensure it would 215 // fail the new test but would be allowed with the original behavior. 216 mKeyStore.setLastConnectionTime(TEST_KEY_1, 1); 217 218 // Verify that the key is in the adb_keys file to ensure subsequent connections are 219 // automatically allowed by adbd. 220 persistKeyStore(); 221 assertTrue("The key was not in the adb_keys file after persisting the keystore", 222 isKeyInFile(TEST_KEY_1, mAdbKeyFile)); 223 } 224 225 @Test testLastConnectionTimeUpdatedByScheduledJob()226 public void testLastConnectionTimeUpdatedByScheduledJob() throws Exception { 227 // If a development device is left connected to a system beyond the allowed connection time 228 // a reboot of the device while connected could make it appear as though the last connection 229 // time is beyond the allowed window. A scheduled job runs daily while a key is connected 230 // to update the last connection time to the current time; this ensures if the device is 231 // rebooted while connected to a system the last connection time should be within 24 hours. 232 233 // Allow the key to connect with the 'Always allow' option selected 234 runAdbTest(TEST_KEY_1, true, true, false); 235 236 // Get the current last connection time for comparison after the scheduled job is run 237 long lastConnectionTime = mKeyStore.getLastConnectionTime(TEST_KEY_1); 238 239 // Sleep a small amount of time to ensure that the updated connection time changes 240 Thread.sleep(10); 241 242 // Send a message to the handler to update the last connection time for the active key 243 updateKeyStore(); 244 assertNotEquals( 245 "The last connection time of the key was not updated after the update key " 246 + "connection time message", 247 lastConnectionTime, mKeyStore.getLastConnectionTime(TEST_KEY_1)); 248 } 249 250 @Test testKeystorePersisted()251 public void testKeystorePersisted() throws Exception { 252 // After any updates are made to the key store a message should be sent to persist the 253 // key store. This test verifies that a key that is always allowed is persisted in the key 254 // store along with its last connection time. 255 256 // Allow the key to connect with the 'Always allow' option selected 257 runAdbTest(TEST_KEY_1, true, true, false); 258 259 // Send a message to the handler to persist the updated keystore and verify a new key store 260 // backed by the XML file contains the key. 261 persistKeyStore(); 262 assertTrue( 263 "The key with the 'Always allow' option selected was not persisted in the keystore", 264 mManager.new AdbKeyStore(mAdbKeyXmlFile).isKeyAuthorized(TEST_KEY_1)); 265 266 // Get the current last connection time to ensure it is updated in the persisted keystore. 267 long lastConnectionTime = mKeyStore.getLastConnectionTime(TEST_KEY_1); 268 269 // Sleep a small amount of time to ensure the last connection time is updated. 270 Thread.sleep(10); 271 272 // Send a message to the handler to update the last connection time for the active key. 273 updateKeyStore(); 274 275 // Persist the updated last connection time and verify a new key store backed by the XML 276 // file contains the updated connection time. 277 persistKeyStore(); 278 assertNotEquals( 279 "The last connection time in the key file was not updated after the update " 280 + "connection time message", lastConnectionTime, 281 mManager.new AdbKeyStore(mAdbKeyXmlFile).getLastConnectionTime(TEST_KEY_1)); 282 // Verify that the key is in the adb_keys file 283 assertTrue("The key was not in the adb_keys file after persisting the keystore", 284 isKeyInFile(TEST_KEY_1, mAdbKeyFile)); 285 } 286 287 @Test testAdbClearRemovesActiveKey()288 public void testAdbClearRemovesActiveKey() throws Exception { 289 // If the user selects the option to 'Revoke USB debugging authorizations' while an 'Always 290 // allow' key is connected that key should be deleted as well. 291 292 // Allow the key to connect with the 'Always allow' option selected 293 runAdbTest(TEST_KEY_1, true, true, false); 294 295 // Send a message to the handler to clear the adb authorizations. 296 clearKeyStore(); 297 298 // Send a message to disconnect the currently connected key 299 disconnectKey(TEST_KEY_1); 300 assertFalse( 301 "The currently connected 'always allow' key must not be authorized after an adb" 302 + " clear message.", 303 mKeyStore.isKeyAuthorized(TEST_KEY_1)); 304 305 // The key should not be in the adb_keys file after clearing the authorizations. 306 assertFalse("The key must not be in the adb_keys file after clearing authorizations", 307 isKeyInFile(TEST_KEY_1, mAdbKeyFile)); 308 } 309 310 @Test testAdbGrantRevokedIfLastConnectionBeyondAllowedTime()311 public void testAdbGrantRevokedIfLastConnectionBeyondAllowedTime() throws Exception { 312 // If the user selects the 'Always allow' option then subsequent connections from the key 313 // will be allowed as long as the connection is within the allowed window. Once the last 314 // connection time is beyond this window the user should be prompted to allow the key again. 315 316 // Allow the key to connect with the 'Always allow' option selected 317 runAdbTest(TEST_KEY_1, true, true, false); 318 319 // Set the allowed window to a small value to ensure the time is beyond the allowed window. 320 setAllowedConnectionTime(1); 321 322 // Sleep for a small amount of time to exceed the allowed window. 323 Thread.sleep(10); 324 325 // The AdbKeyStore has a method to get the time of the next key expiration to ensure the 326 // scheduled job runs at the time of the next expiration or after 24 hours, whichever occurs 327 // first. 328 assertEquals("The time of the next key expiration must be 0.", 0, 329 mKeyStore.getNextExpirationTime()); 330 331 // Persist the key store and verify that the key is no longer in the adb_keys file. 332 persistKeyStore(); 333 assertFalse( 334 "The key must not be in the adb_keys file after the allowed time has elapsed.", 335 isKeyInFile(TEST_KEY_1, mAdbKeyFile)); 336 } 337 338 @Test testLastConnectionTimeCannotBeSetBack()339 public void testLastConnectionTimeCannotBeSetBack() throws Exception { 340 // When a device is first booted there is a possibility that the system time will be set to 341 // the build time of the system image. If a device is connected to a system during a reboot 342 // this could cause the connection time to be set in the past; if the device time is not 343 // corrected before the device is disconnected then a subsequent connection with the time 344 // corrected would appear as though the last connection time was beyond the allowed window, 345 // and the user would be required to authorize the connection again. This test verifies that 346 // the AdbKeyStore does not update the last connection time if it is less than the 347 // previously written connection time. 348 349 // Allow the key to connect with the 'Always allow' option selected 350 runAdbTest(TEST_KEY_1, true, true, false); 351 352 // Get the last connection time that was written to the key store. 353 long lastConnectionTime = mKeyStore.getLastConnectionTime(TEST_KEY_1); 354 355 // Attempt to set the last connection time to 1970 356 mKeyStore.setLastConnectionTime(TEST_KEY_1, 0); 357 assertEquals( 358 "The last connection time in the adb key store must not be set to a value less " 359 + "than the previous connection time", 360 lastConnectionTime, mKeyStore.getLastConnectionTime(TEST_KEY_1)); 361 362 // Attempt to set the last connection time just beyond the allowed window. 363 mKeyStore.setLastConnectionTime(TEST_KEY_1, 364 Math.max(0, lastConnectionTime - (mKeyStore.getAllowedConnectionTime() + 1))); 365 assertEquals( 366 "The last connection time in the adb key store must not be set to a value less " 367 + "than the previous connection time", 368 lastConnectionTime, mKeyStore.getLastConnectionTime(TEST_KEY_1)); 369 } 370 371 @Test testAdbKeyRemovedByScheduledJob()372 public void testAdbKeyRemovedByScheduledJob() throws Exception { 373 // When a key is automatically allowed it should be stored in the adb_keys file. A job is 374 // then scheduled daily to update the connection time of the currently connected key, and if 375 // no connected key exists the key store is updated to purge expired keys. This test 376 // verifies that after a key's expiration time has been reached that it is no longer 377 // in the key store nor the adb_keys file 378 379 // Set the allowed time to the default to ensure that any modification to this value do not 380 // impact this test. 381 setAllowedConnectionTime(Settings.Global.DEFAULT_ADB_ALLOWED_CONNECTION_TIME); 382 383 // Allow both test keys to connect with the 'always allow' option selected. 384 runAdbTest(TEST_KEY_1, true, true, false); 385 runAdbTest(TEST_KEY_2, true, true, false); 386 disconnectKey(TEST_KEY_1); 387 disconnectKey(TEST_KEY_2); 388 389 // Persist the key store and verify that both keys are in the key store and adb_keys file. 390 persistKeyStore(); 391 assertTrue( 392 "Test key 1 must be in the adb_keys file after selecting the 'always allow' " 393 + "option", 394 isKeyInFile(TEST_KEY_1, mAdbKeyFile)); 395 assertTrue( 396 "Test key 1 must be in the adb key store after selecting the 'always allow' " 397 + "option", 398 mKeyStore.isKeyAuthorized(TEST_KEY_1)); 399 assertTrue( 400 "Test key 2 must be in the adb_keys file after selecting the 'always allow' " 401 + "option", 402 isKeyInFile(TEST_KEY_2, mAdbKeyFile)); 403 assertTrue( 404 "Test key 2 must be in the adb key store after selecting the 'always allow' option", 405 mKeyStore.isKeyAuthorized(TEST_KEY_2)); 406 407 // Set test key 1's last connection time to a small value and persist the keystore to ensure 408 // it is cleared out after the next key store update. 409 mKeyStore.setLastConnectionTime(TEST_KEY_1, 1, true); 410 updateKeyStore(); 411 assertFalse( 412 "Test key 1 must no longer be in the adb_keys file after its timeout period is " 413 + "reached", 414 isKeyInFile(TEST_KEY_1, mAdbKeyFile)); 415 assertFalse( 416 "Test key 1 must no longer be in the adb key store after its timeout period is " 417 + "reached", 418 mKeyStore.isKeyAuthorized(TEST_KEY_1)); 419 assertTrue( 420 "Test key 2 must still be in the adb_keys file after test key 1's timeout " 421 + "period is reached", 422 isKeyInFile(TEST_KEY_2, mAdbKeyFile)); 423 assertTrue( 424 "Test key 2 must still be in the adb key store after test key 1's timeout period " 425 + "is reached", 426 mKeyStore.isKeyAuthorized(TEST_KEY_2)); 427 } 428 429 @Test testKeystoreExpirationTimes()430 public void testKeystoreExpirationTimes() throws Exception { 431 // When one or more keys are always allowed a daily job is scheduled to update the 432 // connection time of the connected key and to purge any expired keys. The keystore provides 433 // a method to obtain the expiration time of the next key to expire to ensure that a 434 // scheduled job can run at the time of the next expiration if it is before the daily job 435 // would run. This test verifies that this method returns the expected values depending on 436 // when the key should expire and also verifies that the method to schedule the next job to 437 // update the keystore is the expected value based on the time of the next expiration. 438 439 final long epsilon = 5000; 440 441 // Ensure the allowed time is set to the default. 442 setAllowedConnectionTime(Settings.Global.DEFAULT_ADB_ALLOWED_CONNECTION_TIME); 443 444 // If there are no keys in the keystore the expiration time should be -1. 445 assertEquals("The expiration time must be -1 when there are no keys in the keystore", -1, 446 mKeyStore.getNextExpirationTime()); 447 448 // Allow the test key to connect with the 'always allow' option. 449 runAdbTest(TEST_KEY_1, true, true, false); 450 451 // Verify that the current expiration time is within a small value of the default time. 452 long expirationTime = mKeyStore.getNextExpirationTime(); 453 if (Math.abs(expirationTime - Settings.Global.DEFAULT_ADB_ALLOWED_CONNECTION_TIME) 454 > epsilon) { 455 fail("The expiration time for a new key, " + expirationTime 456 + ", is outside the expected value of " 457 + Settings.Global.DEFAULT_ADB_ALLOWED_CONNECTION_TIME); 458 } 459 // The delay until the next job should be the lesser of the default expiration time and the 460 // AdbDebuggingHandler's job interval. 461 long expectedValue = Math.min( 462 AdbDebuggingManager.AdbDebuggingHandler.UPDATE_KEYSTORE_JOB_INTERVAL, 463 Settings.Global.DEFAULT_ADB_ALLOWED_CONNECTION_TIME); 464 long delay = mHandler.scheduleJobToUpdateAdbKeyStore(); 465 if (Math.abs(delay - expectedValue) > epsilon) { 466 fail("The delay before the next scheduled job, " + delay 467 + ", is outside the expected value of " + expectedValue); 468 } 469 470 // Set the current expiration time to a minute from expiration and verify this new value is 471 // returned. 472 final long newExpirationTime = 60000; 473 mKeyStore.setLastConnectionTime(TEST_KEY_1, 474 System.currentTimeMillis() - Settings.Global.DEFAULT_ADB_ALLOWED_CONNECTION_TIME 475 + newExpirationTime, true); 476 expirationTime = mKeyStore.getNextExpirationTime(); 477 if (Math.abs(expirationTime - newExpirationTime) > epsilon) { 478 fail("The expiration time for a key about to expire, " + expirationTime 479 + ", is outside the expected value of " + newExpirationTime); 480 } 481 delay = mHandler.scheduleJobToUpdateAdbKeyStore(); 482 if (Math.abs(delay - newExpirationTime) > epsilon) { 483 fail("The delay before the next scheduled job, " + delay 484 + ", is outside the expected value of " + newExpirationTime); 485 } 486 487 // If a key is already expired the expiration time and delay before the next job runs should 488 // be 0. 489 mKeyStore.setLastConnectionTime(TEST_KEY_1, 1, true); 490 assertEquals("The expiration time for a key that is already expired must be 0", 0, 491 mKeyStore.getNextExpirationTime()); 492 assertEquals( 493 "The delay before the next scheduled job for a key that is already expired must" 494 + " be 0", 0, mHandler.scheduleJobToUpdateAdbKeyStore()); 495 496 // If the previous behavior of never removing old keys is set then the expiration time 497 // should be -1 to indicate the job does not need to run. 498 setAllowedConnectionTime(0); 499 assertEquals("The expiration time must be -1 when the keys are set to never expire", -1, 500 mKeyStore.getNextExpirationTime()); 501 } 502 503 @Test testConnectionTimeUpdatedWithConnectedKeyMessage()504 public void testConnectionTimeUpdatedWithConnectedKeyMessage() throws Exception { 505 // When a system successfully passes the SIGNATURE challenge adbd sends a connected key 506 // message to the framework to notify of the newly connected key. This message should 507 // trigger the AdbDebuggingManager to update the last connection time for this key and mark 508 // it as the currently connected key so that its time can be updated during subsequent 509 // keystore update jobs as well as when the disconnected message is received. 510 511 // Allow the test key to connect with the 'always allow' option selected. 512 runAdbTest(TEST_KEY_1, true, true, false); 513 514 // Simulate disconnecting the key before a subsequent connection without user interaction. 515 disconnectKey(TEST_KEY_1); 516 517 // Get the last connection time for the key to verify that it is updated when the connected 518 // key message is sent. 519 long connectionTime = mKeyStore.getLastConnectionTime(TEST_KEY_1); 520 Thread.sleep(10); 521 mHandler.obtainMessage(AdbDebuggingManager.AdbDebuggingHandler.MESSAGE_ADB_CONNECTED_KEY, 522 TEST_KEY_1).sendToTarget(); 523 flushHandlerQueue(); 524 assertNotEquals( 525 "The connection time for the key must be updated when the connected key message " 526 + "is received", 527 connectionTime, mKeyStore.getLastConnectionTime(TEST_KEY_1)); 528 529 // Verify that the scheduled job updates the connection time of the key. 530 connectionTime = mKeyStore.getLastConnectionTime(TEST_KEY_1); 531 Thread.sleep(10); 532 updateKeyStore(); 533 assertNotEquals( 534 "The connection time for the key must be updated when the update keystore message" 535 + " is sent", 536 connectionTime, mKeyStore.getLastConnectionTime(TEST_KEY_1)); 537 538 // Verify that the connection time is updated when the key is disconnected. 539 connectionTime = mKeyStore.getLastConnectionTime(TEST_KEY_1); 540 Thread.sleep(10); 541 disconnectKey(TEST_KEY_1); 542 assertNotEquals( 543 "The connection time for the key must be updated when the disconnected message is" 544 + " received", 545 connectionTime, mKeyStore.getLastConnectionTime(TEST_KEY_1)); 546 } 547 548 @Test testClearAuthorizations()549 public void testClearAuthorizations() throws Exception { 550 // When the user selects the 'Revoke USB debugging authorizations' all previously 'always 551 // allow' keys should be deleted. 552 553 // Set the allowed connection time to the default value to ensure tests do not fail due to 554 // a small value. 555 setAllowedConnectionTime(Settings.Global.DEFAULT_ADB_ALLOWED_CONNECTION_TIME); 556 557 // Allow the test key to connect with the 'always allow' option selected. 558 runAdbTest(TEST_KEY_1, true, true, false); 559 persistKeyStore(); 560 561 // Verify that the key is authorized and in the adb_keys file 562 assertTrue( 563 "The test key must be in the keystore after the 'always allow' option is selected", 564 mKeyStore.isKeyAuthorized(TEST_KEY_1)); 565 assertTrue( 566 "The test key must be in the adb_keys file after the 'always allow option is " 567 + "selected", 568 isKeyInFile(TEST_KEY_1, mAdbKeyFile)); 569 570 // Send the message to clear the adb authorizations and verify that the keys are no longer 571 // authorized. 572 clearKeyStore(); 573 assertFalse( 574 "The test key must not be in the keystore after clearing the authorizations", 575 mKeyStore.isKeyAuthorized(TEST_KEY_1)); 576 assertFalse( 577 "The test key must not be in the adb_keys file after clearing the authorizations", 578 isKeyInFile(TEST_KEY_1, mAdbKeyFile)); 579 } 580 581 @Test testClearKeystoreAfterDisablingAdb()582 public void testClearKeystoreAfterDisablingAdb() throws Exception { 583 // When the user disables adb they should still be able to clear the authorized keys. 584 585 // Allow the test key to connect with the 'always allow' option selected and persist the 586 // keystore. 587 runAdbTest(TEST_KEY_1, true, true, false); 588 persistKeyStore(); 589 590 // Disable adb and verify that the keystore can be cleared without throwing an exception. 591 disableAdb(); 592 clearKeyStore(); 593 assertFalse( 594 "The test key must not be in the adb_keys file after clearing the authorizations", 595 isKeyInFile(TEST_KEY_1, mAdbKeyFile)); 596 } 597 598 @Test testUntrackedUserKeysAddedToKeystore()599 public void testUntrackedUserKeysAddedToKeystore() throws Exception { 600 // When a device is first updated to a build that tracks the connection time of adb keys 601 // the keys in the user key file will not have a connection time. To prevent immediately 602 // deleting keys that the user is actively using these untracked keys should be added to the 603 // keystore with the current system time; this gives the user time to reconnect 604 // automatically with an active key while inactive keys are deleted after the expiration 605 // time. 606 607 final long epsilon = 5000; 608 final String[] testKeys = {TEST_KEY_1, TEST_KEY_2}; 609 610 // Add the test keys to the user key file. 611 FileOutputStream fo = new FileOutputStream(mAdbKeyFile); 612 for (String key : testKeys) { 613 fo.write(key.getBytes()); 614 fo.write('\n'); 615 } 616 fo.close(); 617 618 // Set the expiration time to the default and use this value to verify the expiration time 619 // of the previously untracked keys. 620 setAllowedConnectionTime(Settings.Global.DEFAULT_ADB_ALLOWED_CONNECTION_TIME); 621 622 // The untracked keys should be added to the keystore as part of the constructor. 623 AdbDebuggingManager.AdbKeyStore adbKeyStore = mManager.new AdbKeyStore(mAdbKeyXmlFile); 624 625 // Verify that the connection time for each test key is within a small value of the current 626 // time. 627 long time = System.currentTimeMillis(); 628 for (String key : testKeys) { 629 long connectionTime = adbKeyStore.getLastConnectionTime(key); 630 if (Math.abs(connectionTime - connectionTime) > epsilon) { 631 fail("The connection time for a previously untracked key, " + connectionTime 632 + ", is beyond the current time of " + time); 633 } 634 } 635 } 636 637 @Test testConnectionTimeUpdatedForMultipleConnectedKeys()638 public void testConnectionTimeUpdatedForMultipleConnectedKeys() throws Exception { 639 // Since ADB supports multiple simultaneous connections verify that the connection time of 640 // each key is updated by the scheduled job as long as it is connected. 641 642 // Allow both test keys to connect with the 'always allow' option selected. 643 runAdbTest(TEST_KEY_1, true, true, false); 644 runAdbTest(TEST_KEY_2, true, true, false); 645 646 // Sleep a small amount of time to ensure the connection time is updated by the scheduled 647 // job. 648 long connectionTime1 = mKeyStore.getLastConnectionTime(TEST_KEY_1); 649 long connectionTime2 = mKeyStore.getLastConnectionTime(TEST_KEY_2); 650 Thread.sleep(10); 651 updateKeyStore(); 652 assertNotEquals( 653 "The connection time for test key 1 must be updated after the scheduled job runs", 654 connectionTime1, mKeyStore.getLastConnectionTime(TEST_KEY_1)); 655 assertNotEquals( 656 "The connection time for test key 2 must be updated after the scheduled job runs", 657 connectionTime2, mKeyStore.getLastConnectionTime(TEST_KEY_2)); 658 659 // Disconnect the second test key and verify that the last connection time of the first key 660 // is the only one updated. 661 disconnectKey(TEST_KEY_2); 662 connectionTime1 = mKeyStore.getLastConnectionTime(TEST_KEY_1); 663 connectionTime2 = mKeyStore.getLastConnectionTime(TEST_KEY_2); 664 Thread.sleep(10); 665 updateKeyStore(); 666 assertNotEquals( 667 "The connection time for test key 1 must be updated after another key is " 668 + "disconnected and the scheduled job runs", 669 connectionTime1, mKeyStore.getLastConnectionTime(TEST_KEY_1)); 670 assertEquals( 671 "The connection time for test key 2 must not be updated after it is disconnected", 672 connectionTime2, mKeyStore.getLastConnectionTime(TEST_KEY_2)); 673 } 674 675 @Test testClearAuthorizationsBeforeAdbEnabled()676 public void testClearAuthorizationsBeforeAdbEnabled() throws Exception { 677 // The adb key store is not instantiated until adb is enabled; however if the user attempts 678 // to clear the adb authorizations when adb is disabled after a boot a NullPointerException 679 // was thrown as deleteKeyStore is invoked against the key store. This test ensures the 680 // key store can be successfully cleared when adb is disabled. 681 mHandler = mManager.new AdbDebuggingHandler(FgThread.get().getLooper()); 682 683 clearKeyStore(); 684 } 685 686 @Test testClearAuthorizationsDeletesKeyFiles()687 public void testClearAuthorizationsDeletesKeyFiles() throws Exception { 688 mAdbKeyFile.createNewFile(); 689 mAdbKeyXmlFile.createNewFile(); 690 691 clearKeyStore(); 692 693 assertFalse("The adb key file should have been deleted after revocation of the grants", 694 mAdbKeyFile.exists()); 695 assertFalse("The adb xml key file should have been deleted after revocation of the grants", 696 mAdbKeyXmlFile.exists()); 697 } 698 699 @Test testAdbKeyStore_removeKey()700 public void testAdbKeyStore_removeKey() throws Exception { 701 // Accept the test key with the 'Always allow' option selected. 702 runAdbTest(TEST_KEY_1, true, true, false); 703 runAdbTest(TEST_KEY_2, true, true, false); 704 705 // Set the connection time to 0 to restore the original behavior. 706 setAllowedConnectionTime(0); 707 708 // Verify that the key is in the adb_keys file to ensure subsequent connections are 709 // automatically allowed by adbd. 710 persistKeyStore(); 711 assertTrue("The key was not in the adb_keys file after persisting the keystore", 712 isKeyInFile(TEST_KEY_1, mAdbKeyFile)); 713 assertTrue("The key was not in the adb_keys file after persisting the keystore", 714 isKeyInFile(TEST_KEY_2, mAdbKeyFile)); 715 716 // Now remove one of the keys and make sure the other key is still there 717 mKeyStore.removeKey(TEST_KEY_1); 718 assertFalse("The key was still in the adb_keys file after removing the key", 719 isKeyInFile(TEST_KEY_1, mAdbKeyFile)); 720 assertTrue("The key was not in the adb_keys file after removing a different key", 721 isKeyInFile(TEST_KEY_2, mAdbKeyFile)); 722 } 723 724 @Test testIsValidMdnsServiceName()725 public void testIsValidMdnsServiceName() { 726 // Longer than 15 characters 727 assertFalse(isValidMdnsServiceName("abcd1234abcd1234")); 728 729 // Contains invalid characters 730 assertFalse(isValidMdnsServiceName("a*a")); 731 assertFalse(isValidMdnsServiceName("a_a")); 732 assertFalse(isValidMdnsServiceName("_a")); 733 734 // Does not begin or end with letter or digit 735 assertFalse(isValidMdnsServiceName("")); 736 assertFalse(isValidMdnsServiceName("-")); 737 assertFalse(isValidMdnsServiceName("-a")); 738 assertFalse(isValidMdnsServiceName("-1")); 739 assertFalse(isValidMdnsServiceName("a-")); 740 assertFalse(isValidMdnsServiceName("1-")); 741 742 // Contains consecutive hyphens 743 assertFalse(isValidMdnsServiceName("a--a")); 744 745 // Does not contain at least one letter 746 assertFalse(isValidMdnsServiceName("1")); 747 assertFalse(isValidMdnsServiceName("12")); 748 assertFalse(isValidMdnsServiceName("1-2")); 749 750 // letter not within [a-zA-Z] 751 assertFalse(isValidMdnsServiceName("aés")); 752 753 // Some valid names 754 assertTrue(isValidMdnsServiceName("a")); 755 assertTrue(isValidMdnsServiceName("a1")); 756 assertTrue(isValidMdnsServiceName("1A")); 757 assertTrue(isValidMdnsServiceName("aZ")); 758 assertTrue(isValidMdnsServiceName("a-Z")); 759 assertTrue(isValidMdnsServiceName("a-b-Z")); 760 assertTrue(isValidMdnsServiceName("abc-def-123-456")); 761 } 762 763 @Test testPairingThread_MdnsServiceName_RFC6335()764 public void testPairingThread_MdnsServiceName_RFC6335() { 765 assertTrue(isValidMdnsServiceName(AdbDebuggingManager.PairingThread.SERVICE_PROTOCOL)); 766 } 767 isValidMdnsServiceName(String name)768 private boolean isValidMdnsServiceName(String name) { 769 // The rules for Service Names [RFC6335] state that they may be no more 770 // than fifteen characters long (not counting the mandatory underscore), 771 // consisting of only letters, digits, and hyphens, must begin and end 772 // with a letter or digit, must not contain consecutive hyphens, and 773 // must contain at least one letter. 774 // No more than 15 characters long 775 final int len = name.length(); 776 if (name.isEmpty() || len > 15) { 777 return false; 778 } 779 780 boolean hasAtLeastOneLetter = false; 781 boolean sawHyphen = false; 782 for (int i = 0; i < len; ++i) { 783 // Must contain at least one letter 784 // Only contains letters, digits and hyphens 785 char c = name.charAt(i); 786 if (c == '-') { 787 // Cannot be at beginning or end 788 if (i == 0 || i == len - 1) { 789 return false; 790 } 791 if (sawHyphen) { 792 // Consecutive hyphen found 793 return false; 794 } 795 sawHyphen = true; 796 continue; 797 } 798 799 sawHyphen = false; 800 if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) { 801 hasAtLeastOneLetter = true; 802 continue; 803 } 804 805 if (c >= '0' && c <= '9') { 806 continue; 807 } 808 809 // Invalid character 810 return false; 811 } 812 813 return hasAtLeastOneLetter; 814 } 815 816 /** 817 * Runs an adb test with the provided configuration. 818 * 819 * @param key The base64 encoding of the key to be used during the test. 820 * @param allowKey boolean indicating whether the key should be allowed to connect. 821 * @param alwaysAllow boolean indicating whether the 'Always allow' option should be selected. 822 * @param autoAllowExpected boolean indicating whether the key is expected to be automatically 823 * allowed without user interaction. 824 */ runAdbTest(String key, boolean allowKey, boolean alwaysAllow, boolean autoAllowExpected)825 private void runAdbTest(String key, boolean allowKey, boolean alwaysAllow, 826 boolean autoAllowExpected) throws Exception { 827 // if the key should not be automatically allowed then set up the activity 828 if (!autoAllowExpected) { 829 new AdbDebuggingManagerTestActivity.Configurator() 830 .setExpectedKey(key) 831 .setAllowKey(allowKey) 832 .setAlwaysAllow(alwaysAllow) 833 .setHandler(mHandler) 834 .setBlockingQueue(mBlockingQueue); 835 } 836 // send the message indicating a new key is attempting to connect 837 mHandler.obtainMessage(AdbDebuggingManager.AdbDebuggingHandler.MESSAGE_ADB_CONFIRM, 838 key).sendToTarget(); 839 // if the key should not be automatically allowed then the ADB public key confirmation 840 // activity should be launched 841 if (!autoAllowExpected) { 842 TestResult activityResult = mBlockingQueue.poll(TIMEOUT, TIMEOUT_TIME_UNIT); 843 assertNotNull( 844 "The ADB public key confirmation activity did not complete within the timeout" 845 + " period", activityResult); 846 assertEquals("The ADB public key activity failed with result: " + activityResult, 847 TestResult.RESULT_ACTIVITY_LAUNCHED, activityResult.mReturnCode); 848 } 849 // If the activity was launched it should send a response back to the manager that would 850 // trigger a response to the thread, or if the key is a known valid key then a response 851 // should be sent back without requiring interaction with the activity. 852 TestResult threadResult = mBlockingQueue.poll(TIMEOUT, TIMEOUT_TIME_UNIT); 853 assertNotNull("A response was not sent to the thread within the timeout period", 854 threadResult); 855 // verify that the result is an expected message from the thread 856 assertEquals("An unexpected result was received: " + threadResult, 857 TestResult.RESULT_RESPONSE_RECEIVED, threadResult.mReturnCode); 858 assertEquals("The manager did not send the proper response for allowKey = " + allowKey, 859 allowKey ? RESPONSE_KEY_ALLOWED : RESPONSE_KEY_DENIED, threadResult.mMessage); 860 // if the key is not allowed or not always allowed verify it is not in the key store 861 if (!allowKey || !alwaysAllow) { 862 assertFalse("The key must not be authorized in the key store", 863 mKeyStore.isKeyAuthorized(key)); 864 assertFalse( 865 "The key must not be stored in the adb_keys file", 866 isKeyInFile(key, mAdbKeyFile)); 867 } 868 flushHandlerQueue(); 869 } 870 persistKeyStore()871 private void persistKeyStore() throws Exception { 872 // Send a message to the handler to persist the key store. 873 mHandler.obtainMessage( 874 AdbDebuggingManager.AdbDebuggingHandler.MESSAGE_ADB_PERSIST_KEYSTORE) 875 .sendToTarget(); 876 flushHandlerQueue(); 877 } 878 disconnectKey(String key)879 private void disconnectKey(String key) throws Exception { 880 // Send a message to the handler to disconnect the currently connected key. 881 mHandler.obtainMessage(AdbDebuggingManager.AdbDebuggingHandler.MESSAGE_ADB_DISCONNECT, 882 key).sendToTarget(); 883 flushHandlerQueue(); 884 } 885 updateKeyStore()886 private void updateKeyStore() throws Exception { 887 // Send a message to the handler to run the update keystore job. 888 mHandler.obtainMessage( 889 AdbDebuggingManager.AdbDebuggingHandler.MESSAGE_ADB_UPDATE_KEYSTORE).sendToTarget(); 890 flushHandlerQueue(); 891 } 892 clearKeyStore()893 private void clearKeyStore() throws Exception { 894 // Send a message to the handler to clear all previously authorized keys. 895 mHandler.obtainMessage( 896 AdbDebuggingManager.AdbDebuggingHandler.MESSAGE_ADB_CLEAR).sendToTarget(); 897 flushHandlerQueue(); 898 } 899 disableAdb()900 private void disableAdb() throws Exception { 901 // Send a message to the handler to disable adb. 902 mHandler.obtainMessage( 903 AdbDebuggingManager.AdbDebuggingHandler.MESSAGE_ADB_DISABLED).sendToTarget(); 904 flushHandlerQueue(); 905 } 906 flushHandlerQueue()907 private void flushHandlerQueue() throws Exception { 908 // Post a Runnable to ensure that all of the current messages in the queue are flushed. 909 CountDownLatch latch = new CountDownLatch(1); 910 mHandler.post(() -> { 911 latch.countDown(); 912 }); 913 if (!latch.await(TIMEOUT, TIMEOUT_TIME_UNIT)) { 914 fail("The Runnable to flush the handler's queue did not complete within the timeout " 915 + "period"); 916 } 917 } 918 isKeyInFile(String key, File keyFile)919 private boolean isKeyInFile(String key, File keyFile) throws Exception { 920 if (key == null) { 921 return false; 922 } 923 if (keyFile.exists()) { 924 try (BufferedReader in = new BufferedReader(new FileReader(keyFile))) { 925 String currKey; 926 while ((currKey = in.readLine()) != null) { 927 if (key.equals(currKey)) { 928 return true; 929 } 930 } 931 } 932 } 933 return false; 934 } 935 936 /** 937 * Helper class that extends AdbDebuggingThread to receive the response from AdbDebuggingManager 938 * indicating whether the key should be allowed to connect. 939 */ 940 class AdbDebuggingThreadTest extends AdbDebuggingManager.AdbDebuggingThread { AdbDebuggingThreadTest()941 AdbDebuggingThreadTest() { 942 mManager.super(); 943 } 944 945 @Override sendResponse(String msg)946 public void sendResponse(String msg) { 947 TestResult result = new TestResult(TestResult.RESULT_RESPONSE_RECEIVED, msg); 948 try { 949 mBlockingQueue.put(result); 950 } catch (InterruptedException e) { 951 Log.e(TAG, 952 "Caught an InterruptedException putting the result in the queue: " + result, 953 e); 954 } 955 } 956 } 957 958 /** 959 * Contains the result for the current portion of the test along with any corresponding 960 * messages. 961 */ 962 public static class TestResult { 963 public int mReturnCode; 964 public String mMessage; 965 966 public static final int RESULT_ACTIVITY_LAUNCHED = 1; 967 public static final int RESULT_UNEXPECTED_KEY = 2; 968 public static final int RESULT_RESPONSE_RECEIVED = 3; 969 TestResult(int returnCode)970 public TestResult(int returnCode) { 971 this(returnCode, null); 972 } 973 TestResult(int returnCode, String message)974 public TestResult(int returnCode, String message) { 975 mReturnCode = returnCode; 976 mMessage = message; 977 } 978 979 @Override toString()980 public String toString() { 981 return "{mReturnCode = " + mReturnCode + ", mMessage = " + mMessage + "}"; 982 } 983 } 984 } 985