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