Merge "Remove keys from adb_keys after period of inactivity" into qt-dev
diff --git a/core/proto/android/service/adb.proto b/core/proto/android/service/adb.proto
index 493f9b8..549d30c 100644
--- a/core/proto/android/service/adb.proto
+++ b/core/proto/android/service/adb.proto
@@ -35,4 +35,5 @@
optional string last_key_recevied = 2 [ (android.privacy).dest = DEST_EXPLICIT ];
optional string user_keys = 3 [ (android.privacy).dest = DEST_LOCAL ];
optional string system_keys = 4 [ (android.privacy).dest = DEST_LOCAL ];
+ optional string keystore = 5 [ (android.privacy).dest = DEST_LOCAL ];
}
diff --git a/services/core/java/com/android/server/adb/AdbDebuggingManager.java b/services/core/java/com/android/server/adb/AdbDebuggingManager.java
index 9325d25..bdbff3d 100644
--- a/services/core/java/com/android/server/adb/AdbDebuggingManager.java
+++ b/services/core/java/com/android/server/adb/AdbDebuggingManager.java
@@ -27,9 +27,11 @@
import android.content.pm.PackageManager;
import android.content.pm.UserInfo;
import android.content.res.Resources;
+import android.database.ContentObserver;
import android.debug.AdbProtoEnums;
import android.net.LocalSocket;
import android.net.LocalSocketAddress;
+import android.net.Uri;
import android.os.Environment;
import android.os.FileUtils;
import android.os.Handler;
@@ -48,6 +50,7 @@
import android.util.Xml;
import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.FastXmlSerializer;
import com.android.internal.util.XmlUtils;
import com.android.internal.util.dump.DualDumpOutputStream;
@@ -57,18 +60,24 @@
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlSerializer;
+import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
+import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.Iterator;
+import java.util.List;
import java.util.Map;
+import java.util.Set;
/**
* Provides communication to the Android Debug Bridge daemon to allow, deny, or clear public keysi
@@ -85,19 +94,22 @@
// This file contains keys that will be allowed to connect without user interaction as long
// as a subsequent connection occurs within the allowed duration.
private static final String ADB_TEMP_KEYS_FILE = "adb_temp_keys.xml";
- private static final int BUFFER_SIZE = 4096;
+ private static final int BUFFER_SIZE = 65536;
private final Context mContext;
private final Handler mHandler;
private AdbDebuggingThread mThread;
private boolean mAdbEnabled = false;
private String mFingerprints;
- private String mConnectedKey;
+ private final List<String> mConnectedKeys;
private String mConfirmComponent;
+ private final File mTestUserKeyFile;
public AdbDebuggingManager(Context context) {
mHandler = new AdbDebuggingHandler(FgThread.get().getLooper());
mContext = context;
+ mTestUserKeyFile = null;
+ mConnectedKeys = new ArrayList<>(1);
}
/**
@@ -105,10 +117,12 @@
* an adb connection from the key.
*/
@TestApi
- protected AdbDebuggingManager(Context context, String confirmComponent) {
+ protected AdbDebuggingManager(Context context, String confirmComponent, File testUserKeyFile) {
mHandler = new AdbDebuggingHandler(FgThread.get().getLooper());
mContext = context;
mConfirmComponent = confirmComponent;
+ mTestUserKeyFile = testUserKeyFile;
+ mConnectedKeys = new ArrayList<>();
}
class AdbDebuggingThread extends Thread {
@@ -153,12 +167,13 @@
mInputStream = null;
if (DEBUG) Slog.d(TAG, "Creating socket");
- mSocket = new LocalSocket();
+ mSocket = new LocalSocket(LocalSocket.SOCKET_SEQPACKET);
mSocket.connect(address);
mOutputStream = mSocket.getOutputStream();
mInputStream = mSocket.getInputStream();
} catch (IOException ioe) {
+ Slog.e(TAG, "Caught an exception opening the socket: " + ioe);
closeSocketLocked();
throw ioe;
}
@@ -172,6 +187,7 @@
// if less than 2 bytes are read the if statements below will throw an
// IndexOutOfBoundsException.
if (count < 2) {
+ Slog.w(TAG, "Read failed with count " + count);
break;
}
@@ -183,9 +199,18 @@
msg.obj = key;
mHandler.sendMessage(msg);
} else if (buffer[0] == 'D' && buffer[1] == 'C') {
- Slog.d(TAG, "Received disconnected message");
+ String key = new String(Arrays.copyOfRange(buffer, 2, count));
+ Slog.d(TAG, "Received disconnected message: " + key);
Message msg = mHandler.obtainMessage(
AdbDebuggingHandler.MESSAGE_ADB_DISCONNECT);
+ msg.obj = key;
+ mHandler.sendMessage(msg);
+ } else if (buffer[0] == 'C' && buffer[1] == 'K') {
+ String key = new String(Arrays.copyOfRange(buffer, 2, count));
+ Slog.d(TAG, "Received connected key message: " + key);
+ Message msg = mHandler.obtainMessage(
+ AdbDebuggingHandler.MESSAGE_ADB_CONNECTED_KEY);
+ msg.obj = key;
mHandler.sendMessage(msg);
} else {
Slog.e(TAG, "Wrong message: "
@@ -243,14 +268,13 @@
}
class AdbDebuggingHandler extends Handler {
- // The time to schedule the job to keep the key store updated with a currently connected
- // key. This job is required because a deveoper could keep a device connected to their
- // system beyond the time within which a subsequent connection is allowed. But since the
- // last connection time is only written when a device is connected and disconnected then
- // if the device is rebooted while connected to the development system it would appear as
- // though the adb grant for the system is no longer authorized and the developer would need
- // to manually allow the connection again.
- private static final long UPDATE_KEY_STORE_JOB_INTERVAL = 86400000;
+ // The default time to schedule the job to keep the keystore updated with a currently
+ // connected key as well as to removed expired keys.
+ static final long UPDATE_KEYSTORE_JOB_INTERVAL = 86400000;
+ // The minimum interval at which the job should run to update the keystore. This is intended
+ // to prevent the job from running too often if the allowed connection time for adb grants
+ // is set to an extremely small value.
+ static final long UPDATE_KEYSTORE_MIN_JOB_INTERVAL = 60000;
static final int MESSAGE_ADB_ENABLED = 1;
static final int MESSAGE_ADB_DISABLED = 2;
@@ -259,11 +283,21 @@
static final int MESSAGE_ADB_CONFIRM = 5;
static final int MESSAGE_ADB_CLEAR = 6;
static final int MESSAGE_ADB_DISCONNECT = 7;
- static final int MESSAGE_ADB_PERSIST_KEY_STORE = 8;
- static final int MESSAGE_ADB_UPDATE_KEY_CONNECTION_TIME = 9;
+ static final int MESSAGE_ADB_PERSIST_KEYSTORE = 8;
+ static final int MESSAGE_ADB_UPDATE_KEYSTORE = 9;
+ static final int MESSAGE_ADB_CONNECTED_KEY = 10;
private AdbKeyStore mAdbKeyStore;
+ private ContentObserver mAuthTimeObserver = new ContentObserver(this) {
+ @Override
+ public void onChange(boolean selfChange, Uri uri) {
+ Slog.d(TAG, "Received notification that uri " + uri
+ + " was modified; rescheduling keystore job");
+ scheduleJobToUpdateAdbKeyStore();
+ }
+ };
+
AdbDebuggingHandler(Looper looper) {
super(looper);
}
@@ -285,13 +319,15 @@
if (mAdbEnabled) {
break;
}
-
+ registerForAuthTimeChanges();
mAdbEnabled = true;
mThread = new AdbDebuggingThread();
mThread.start();
mAdbKeyStore = new AdbKeyStore();
+ mAdbKeyStore.updateKeyStore();
+ scheduleJobToUpdateAdbKeyStore();
break;
case MESSAGE_ADB_DISABLED:
@@ -306,14 +342,20 @@
mThread = null;
}
- cancelJobToUpdateAdbKeyStore();
- mConnectedKey = null;
+ if (!mConnectedKeys.isEmpty()) {
+ for (String connectedKey : mConnectedKeys) {
+ mAdbKeyStore.setLastConnectionTime(connectedKey,
+ System.currentTimeMillis());
+ }
+ sendPersistKeyStoreMessage();
+ mConnectedKeys.clear();
+ }
+ scheduleJobToUpdateAdbKeyStore();
break;
case MESSAGE_ADB_ALLOW: {
String key = (String) msg.obj;
String fingerprints = getFingerprints(key);
-
if (!fingerprints.equals(mFingerprints)) {
Slog.e(TAG, "Fingerprints do not match. Got "
+ fingerprints + ", expected " + mFingerprints);
@@ -324,12 +366,12 @@
if (mThread != null) {
mThread.sendResponse("OK");
if (alwaysAllow) {
- mConnectedKey = key;
+ if (!mConnectedKeys.contains(key)) {
+ mConnectedKeys.add(key);
+ }
mAdbKeyStore.setLastConnectionTime(key, System.currentTimeMillis());
+ sendPersistKeyStoreMessage();
scheduleJobToUpdateAdbKeyStore();
- // write this key to adb_keys as well so that subsequent connections can
- // go through the expected SIGNATURE interaction.
- writeKey(key);
}
logAdbConnectionChanged(key, AdbProtoEnums.USER_ALLOWED, alwaysAllow);
}
@@ -369,47 +411,129 @@
}
case MESSAGE_ADB_CLEAR: {
- deleteKeyFile();
- mConnectedKey = null;
+ Slog.d(TAG, "Received a request to clear the adb authorizations");
+ mConnectedKeys.clear();
mAdbKeyStore.deleteKeyStore();
cancelJobToUpdateAdbKeyStore();
break;
}
case MESSAGE_ADB_DISCONNECT: {
- if (mConnectedKey != null) {
- mAdbKeyStore.setLastConnectionTime(mConnectedKey,
- System.currentTimeMillis());
- cancelJobToUpdateAdbKeyStore();
+ String key = (String) msg.obj;
+ boolean alwaysAllow = false;
+ if (key != null && key.length() > 0) {
+ if (mConnectedKeys.contains(key)) {
+ alwaysAllow = true;
+ mAdbKeyStore.setLastConnectionTime(key, System.currentTimeMillis());
+ sendPersistKeyStoreMessage();
+ scheduleJobToUpdateAdbKeyStore();
+ mConnectedKeys.remove(key);
+ }
+ } else {
+ Slog.w(TAG, "Received a disconnected key message with an empty key");
}
- logAdbConnectionChanged(mConnectedKey, AdbProtoEnums.DISCONNECTED,
- (mConnectedKey != null));
- mConnectedKey = null;
+ logAdbConnectionChanged(key, AdbProtoEnums.DISCONNECTED, alwaysAllow);
break;
}
- case MESSAGE_ADB_PERSIST_KEY_STORE: {
- mAdbKeyStore.persistKeyStore();
+ case MESSAGE_ADB_PERSIST_KEYSTORE: {
+ if (mAdbKeyStore != null) {
+ mAdbKeyStore.persistKeyStore();
+ }
break;
}
- case MESSAGE_ADB_UPDATE_KEY_CONNECTION_TIME: {
- if (mConnectedKey != null) {
- mAdbKeyStore.setLastConnectionTime(mConnectedKey,
- System.currentTimeMillis());
+ case MESSAGE_ADB_UPDATE_KEYSTORE: {
+ if (!mConnectedKeys.isEmpty()) {
+ for (String connectedKey : mConnectedKeys) {
+ mAdbKeyStore.setLastConnectionTime(connectedKey,
+ System.currentTimeMillis());
+ }
+ sendPersistKeyStoreMessage();
scheduleJobToUpdateAdbKeyStore();
+ } else if (!mAdbKeyStore.isEmpty()) {
+ mAdbKeyStore.updateKeyStore();
+ scheduleJobToUpdateAdbKeyStore();
+ }
+ break;
+ }
+
+ case MESSAGE_ADB_CONNECTED_KEY: {
+ String key = (String) msg.obj;
+ if (key == null || key.length() == 0) {
+ Slog.w(TAG, "Received a connected key message with an empty key");
+ } else {
+ if (!mConnectedKeys.contains(key)) {
+ mConnectedKeys.add(key);
+ }
+ mAdbKeyStore.setLastConnectionTime(key, System.currentTimeMillis());
+ sendPersistKeyStoreMessage();
+ scheduleJobToUpdateAdbKeyStore();
+ logAdbConnectionChanged(key, AdbProtoEnums.AUTOMATICALLY_ALLOWED, true);
}
break;
}
}
}
+ void registerForAuthTimeChanges() {
+ Uri uri = Settings.Global.getUriFor(Settings.Global.ADB_ALLOWED_CONNECTION_TIME);
+ mContext.getContentResolver().registerContentObserver(uri, false, mAuthTimeObserver);
+ }
+
private void logAdbConnectionChanged(String key, int state, boolean alwaysAllow) {
long lastConnectionTime = mAdbKeyStore.getLastConnectionTime(key);
long authWindow = mAdbKeyStore.getAllowedConnectionTime();
+ Slog.d(TAG,
+ "Logging key " + key + ", state = " + state + ", alwaysAllow = " + alwaysAllow
+ + ", lastConnectionTime = " + lastConnectionTime + ", authWindow = "
+ + authWindow);
StatsLog.write(StatsLog.ADB_CONNECTION_CHANGED, lastConnectionTime, authWindow, state,
alwaysAllow);
}
+
+
+ /**
+ * Schedules a job to update the connection time of the currently connected key and filter
+ * out any keys that are beyond their expiration time.
+ *
+ * @return the time in ms when the next job will run or -1 if the job should not be
+ * scheduled to run.
+ */
+ @VisibleForTesting
+ long scheduleJobToUpdateAdbKeyStore() {
+ cancelJobToUpdateAdbKeyStore();
+ long keyExpiration = mAdbKeyStore.getNextExpirationTime();
+ // if the keyExpiration time is -1 then either the keys are set to never expire or
+ // there are no keys in the keystore, just return for now as a new job will be
+ // scheduled on the next connection or when the auth time changes.
+ if (keyExpiration == -1) {
+ return -1;
+ }
+ long delay;
+ // if the keyExpiration is 0 this indicates a key has already expired; schedule the job
+ // to run now to ensure the key is removed immediately from adb_keys.
+ if (keyExpiration == 0) {
+ delay = 0;
+ } else {
+ // else the next job should be run either daily or when the next key is set to
+ // expire with a min job interval to ensure this job does not run too often if a
+ // small value is set for the key expiration.
+ delay = Math.max(Math.min(UPDATE_KEYSTORE_JOB_INTERVAL, keyExpiration),
+ UPDATE_KEYSTORE_MIN_JOB_INTERVAL);
+ }
+ Message message = obtainMessage(MESSAGE_ADB_UPDATE_KEYSTORE);
+ sendMessageDelayed(message, delay);
+ return delay;
+ }
+
+ /**
+ * Cancels the scheduled job to update the connection time of the currently connected key
+ * and to remove any expired keys.
+ */
+ private void cancelJobToUpdateAdbKeyStore() {
+ removeMessages(AdbDebuggingHandler.MESSAGE_ADB_UPDATE_KEYSTORE);
+ }
}
private String getFingerprints(String key) {
@@ -534,7 +658,13 @@
}
File getUserKeyFile() {
- return getAdbFile(ADB_KEYS_FILE);
+ return mTestUserKeyFile == null ? getAdbFile(ADB_KEYS_FILE) : mTestUserKeyFile;
+ }
+
+ private void createKeyFile(File keyFile) throws IOException {
+ keyFile.createNewFile();
+ FileUtils.setPermissions(keyFile.toString(),
+ FileUtils.S_IRUSR | FileUtils.S_IWUSR | FileUtils.S_IRGRP, -1, -1);
}
private void writeKey(String key) {
@@ -546,9 +676,7 @@
}
if (!keyFile.exists()) {
- keyFile.createNewFile();
- FileUtils.setPermissions(keyFile.toString(),
- FileUtils.S_IRUSR | FileUtils.S_IWUSR | FileUtils.S_IRGRP, -1, -1);
+ createKeyFile(keyFile);
}
FileOutputStream fo = new FileOutputStream(keyFile, true);
@@ -560,6 +688,35 @@
}
}
+ private void writeKeys(Iterable<String> keys) {
+ AtomicFile atomicKeyFile = null;
+ FileOutputStream fo = null;
+ try {
+ File keyFile = getUserKeyFile();
+
+ if (keyFile == null) {
+ return;
+ }
+
+ if (!keyFile.exists()) {
+ createKeyFile(keyFile);
+ }
+
+ atomicKeyFile = new AtomicFile(keyFile);
+ fo = atomicKeyFile.startWrite();
+ for (String key : keys) {
+ fo.write(key.getBytes());
+ fo.write('\n');
+ }
+ atomicKeyFile.finishWrite(fo);
+ } catch (IOException ex) {
+ Slog.e(TAG, "Error writing keys: " + ex);
+ if (atomicKeyFile != null) {
+ atomicKeyFile.failWrite(fo);
+ }
+ }
+ }
+
private void deleteKeyFile() {
File keyFile = getUserKeyFile();
if (keyFile != null) {
@@ -604,36 +761,14 @@
}
/**
- * Sends a message to the handler to persist the key store.
+ * Sends a message to the handler to persist the keystore.
*/
private void sendPersistKeyStoreMessage() {
- Message msg = mHandler.obtainMessage(AdbDebuggingHandler.MESSAGE_ADB_PERSIST_KEY_STORE);
+ Message msg = mHandler.obtainMessage(AdbDebuggingHandler.MESSAGE_ADB_PERSIST_KEYSTORE);
mHandler.sendMessage(msg);
}
/**
- * Schedules a job to update the connection time of the currently connected key. This is
- * intended for cases such as development devices that are left connected to a user's
- * system beyond the window within which a connection is allowed without user interaction.
- * A job should be rescheduled daily so that if the device is rebooted while connected to
- * the user's system the last time in the key store will show within 24 hours which should
- * be within the allowed window.
- */
- private void scheduleJobToUpdateAdbKeyStore() {
- Message message = mHandler.obtainMessage(
- AdbDebuggingHandler.MESSAGE_ADB_UPDATE_KEY_CONNECTION_TIME);
- mHandler.sendMessageDelayed(message, AdbDebuggingHandler.UPDATE_KEY_STORE_JOB_INTERVAL);
- }
-
- /**
- * Cancels the scheduled job to update the connection time of the currently connected key.
- * This should be invoked once the adb session is disconnected.
- */
- private void cancelJobToUpdateAdbKeyStore() {
- mHandler.removeMessages(AdbDebuggingHandler.MESSAGE_ADB_UPDATE_KEY_CONNECTION_TIME);
- }
-
- /**
* Dump the USB debugging state.
*/
public void dump(DualDumpOutputStream dump, String idName, long id) {
@@ -657,6 +792,13 @@
Slog.e(TAG, "Cannot read system keys", e);
}
+ try {
+ dump.write("keystore", AdbDebuggingManagerProto.KEYSTORE,
+ FileUtils.readTextFile(getAdbTempKeysFile(), 0, null));
+ } catch (IOException e) {
+ Slog.e(TAG, "Cannot read keystore: ", e);
+ }
+
dump.end(token);
}
@@ -667,11 +809,14 @@
*/
class AdbKeyStore {
private Map<String, Long> mKeyMap;
+ private Set<String> mSystemKeys;
private File mKeyFile;
private AtomicFile mAtomicKeyFile;
+
private static final String XML_TAG_ADB_KEY = "adbKey";
private static final String XML_ATTRIBUTE_KEY = "key";
private static final String XML_ATTRIBUTE_LAST_CONNECTION = "lastConnection";
+ private static final String SYSTEM_KEY_FILE = "/adb_keys";
/**
* Value returned by {@code getLastConnectionTime} when there is no previously saved
@@ -680,21 +825,25 @@
public static final long NO_PREVIOUS_CONNECTION = 0;
/**
- * Constructor that uses the default location for the persistent adb key store.
+ * Constructor that uses the default location for the persistent adb keystore.
*/
AdbKeyStore() {
- initKeyFile();
- mKeyMap = getKeyMapFromFile();
+ init();
}
/**
- * Constructor that uses the specified file as the location for the persistent adb key
- * store.
+ * Constructor that uses the specified file as the location for the persistent adb keystore.
*/
AdbKeyStore(File keyFile) {
mKeyFile = keyFile;
+ init();
+ }
+
+ private void init() {
initKeyFile();
- mKeyMap = getKeyMapFromFile();
+ mKeyMap = getKeyMap();
+ mSystemKeys = getSystemKeysFromFile(SYSTEM_KEY_FILE);
+ addUserKeysToKeyStore();
}
/**
@@ -710,10 +859,46 @@
}
}
+ private Set<String> getSystemKeysFromFile(String fileName) {
+ Set<String> systemKeys = new HashSet<>();
+ File systemKeyFile = new File(fileName);
+ if (systemKeyFile.exists()) {
+ try (BufferedReader in = new BufferedReader(new FileReader(systemKeyFile))) {
+ String key;
+ while ((key = in.readLine()) != null) {
+ key = key.trim();
+ if (key.length() > 0) {
+ systemKeys.add(key);
+ }
+ }
+ } catch (IOException e) {
+ Slog.e(TAG, "Caught an exception reading " + fileName + ": " + e);
+ }
+ }
+ return systemKeys;
+ }
+
+ /**
+ * Returns whether there are any 'always allowed' keys in the keystore.
+ */
+ public boolean isEmpty() {
+ return mKeyMap.isEmpty();
+ }
+
+ /**
+ * Iterates through the keys in the keystore and removes any that are beyond the window
+ * within which connections are automatically allowed without user interaction.
+ */
+ public void updateKeyStore() {
+ if (filterOutOldKeys()) {
+ sendPersistKeyStoreMessage();
+ }
+ }
+
/**
* Returns the key map with the keys and last connection times from the key file.
*/
- private Map<String, Long> getKeyMapFromFile() {
+ private Map<String, Long> getKeyMap() {
Map<String, Long> keyMap = new HashMap<String, Long>();
// if the AtomicFile could not be instantiated before attempt again; if it still fails
// return an empty key map.
@@ -760,10 +945,39 @@
}
/**
+ * Updates the keystore with keys that were previously set to be always allowed before the
+ * connection time of keys was tracked.
+ */
+ private void addUserKeysToKeyStore() {
+ File userKeyFile = getUserKeyFile();
+ boolean mapUpdated = false;
+ if (userKeyFile != null && userKeyFile.exists()) {
+ try (BufferedReader in = new BufferedReader(new FileReader(userKeyFile))) {
+ long time = System.currentTimeMillis();
+ String key;
+ while ((key = in.readLine()) != null) {
+ // if the keystore does not contain the key from the user key file then add
+ // it to the Map with the current system time to prevent it from expiring
+ // immediately if the user is actively using this key.
+ if (!mKeyMap.containsKey(key)) {
+ mKeyMap.put(key, time);
+ mapUpdated = true;
+ }
+ }
+ } catch (IOException e) {
+ Slog.e(TAG, "Caught an exception reading " + userKeyFile + ": " + e);
+ }
+ }
+ if (mapUpdated) {
+ sendPersistKeyStoreMessage();
+ }
+ }
+
+ /**
* Writes the key map to the key file.
*/
public void persistKeyStore() {
- // if there is nothing in the key map then ensure any keys left in the key store files
+ // if there is nothing in the key map then ensure any keys left in the keystore files
// are deleted as well.
filterOutOldKeys();
if (mKeyMap.isEmpty()) {
@@ -800,7 +1014,8 @@
}
}
- private void filterOutOldKeys() {
+ private boolean filterOutOldKeys() {
+ boolean keysDeleted = false;
long allowedTime = getAllowedConnectionTime();
long systemTime = System.currentTimeMillis();
Iterator<Map.Entry<String, Long>> keyMapIterator = mKeyMap.entrySet().iterator();
@@ -809,8 +1024,41 @@
long connectionTime = keyEntry.getValue();
if (allowedTime != 0 && systemTime > (connectionTime + allowedTime)) {
keyMapIterator.remove();
+ keysDeleted = true;
}
}
+ // if any keys were deleted then the key file should be rewritten with the active keys
+ // to prevent authorizing a key that is now beyond the allowed window.
+ if (keysDeleted) {
+ writeKeys(mKeyMap.keySet());
+ }
+ return keysDeleted;
+ }
+
+ /**
+ * Returns the time in ms that the next key will expire or -1 if there are no keys or the
+ * keys will not expire.
+ */
+ public long getNextExpirationTime() {
+ long minExpiration = -1;
+ long allowedTime = getAllowedConnectionTime();
+ // if the allowedTime is 0 then keys never expire; return -1 to indicate this
+ if (allowedTime == 0) {
+ return minExpiration;
+ }
+ long systemTime = System.currentTimeMillis();
+ Iterator<Map.Entry<String, Long>> keyMapIterator = mKeyMap.entrySet().iterator();
+ while (keyMapIterator.hasNext()) {
+ Map.Entry<String, Long> keyEntry = keyMapIterator.next();
+ long connectionTime = keyEntry.getValue();
+ // if the key has already expired then ensure that the result is set to 0 so that
+ // any scheduled jobs to clean up the keystore can run right away.
+ long keyExpiration = Math.max(0, (connectionTime + allowedTime) - systemTime);
+ if (minExpiration == -1 || keyExpiration < minExpiration) {
+ minExpiration = keyExpiration;
+ }
+ }
+ return minExpiration;
}
/**
@@ -818,6 +1066,7 @@
*/
public void deleteKeyStore() {
mKeyMap.clear();
+ deleteKeyFile();
if (mAtomicKeyFile == null) {
return;
}
@@ -836,37 +1085,31 @@
* Sets the time of the last connection for the specified key to the provided time.
*/
public void setLastConnectionTime(String key, long connectionTime) {
- // Do not set the connection time to a value that is earlier than what was previously
- // stored as the last connection time.
- if (mKeyMap.containsKey(key) && mKeyMap.get(key) >= connectionTime) {
- return;
- }
- mKeyMap.put(key, connectionTime);
- sendPersistKeyStoreMessage();
+ setLastConnectionTime(key, connectionTime, false);
}
/**
- * Returns whether the specified key should be authroized to connect without user
- * interaction. This requires that the user previously connected this device and selected
- * the option to 'Always allow', and the time since the last connection is within the
- * allowed window.
+ * Sets the time of the last connection for the specified key to the provided time. If force
+ * is set to true the time will be set even if it is older than the previously written
+ * connection time.
*/
- public boolean isKeyAuthorized(String key) {
- long lastConnectionTime = getLastConnectionTime(key);
- if (lastConnectionTime == NO_PREVIOUS_CONNECTION) {
- return false;
+ public void setLastConnectionTime(String key, long connectionTime, boolean force) {
+ // Do not set the connection time to a value that is earlier than what was previously
+ // stored as the last connection time unless force is set.
+ if (mKeyMap.containsKey(key) && mKeyMap.get(key) >= connectionTime && !force) {
+ return;
}
- long allowedConnectionTime = getAllowedConnectionTime();
- // if the allowed connection time is 0 then revert to the previous behavior of always
- // allowing previously granted adb grants.
- if (allowedConnectionTime == 0 || (System.currentTimeMillis() < (lastConnectionTime
- + allowedConnectionTime))) {
- return true;
- } else {
- // since this key is no longer auhorized remove it from the Map
- removeKey(key);
- return false;
+ // System keys are always allowed so there's no need to keep track of their connection
+ // time.
+ if (mSystemKeys.contains(key)) {
+ return;
}
+ // if this is the first time the key is being added then write it to the key file as
+ // well.
+ if (!mKeyMap.containsKey(key)) {
+ writeKey(key);
+ }
+ mKeyMap.put(key, connectionTime);
}
/**
@@ -880,14 +1123,29 @@
}
/**
- * Removes the specified key from the key store.
+ * Returns whether the specified key should be authroized to connect without user
+ * interaction. This requires that the user previously connected this device and selected
+ * the option to 'Always allow', and the time since the last connection is within the
+ * allowed window.
*/
- public void removeKey(String key) {
- if (!mKeyMap.containsKey(key)) {
- return;
+ public boolean isKeyAuthorized(String key) {
+ // A system key is always authorized to connect.
+ if (mSystemKeys.contains(key)) {
+ return true;
}
- mKeyMap.remove(key);
- sendPersistKeyStoreMessage();
+ long lastConnectionTime = getLastConnectionTime(key);
+ if (lastConnectionTime == NO_PREVIOUS_CONNECTION) {
+ return false;
+ }
+ long allowedConnectionTime = getAllowedConnectionTime();
+ // if the allowed connection time is 0 then revert to the previous behavior of always
+ // allowing previously granted adb grants.
+ if (allowedConnectionTime == 0 || (System.currentTimeMillis() < (lastConnectionTime
+ + allowedConnectionTime))) {
+ return true;
+ } else {
+ return false;
+ }
}
}
}
diff --git a/services/tests/servicestests/src/com/android/server/adb/AdbDebuggingManagerTest.java b/services/tests/servicestests/src/com/android/server/adb/AdbDebuggingManagerTest.java
index 65af677..d4182f3 100644
--- a/services/tests/servicestests/src/com/android/server/adb/AdbDebuggingManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/adb/AdbDebuggingManagerTest.java
@@ -37,7 +37,10 @@
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
+import java.io.BufferedReader;
import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FileReader;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CountDownLatch;
@@ -57,8 +60,8 @@
+ "com.android.server.adb.AdbDebuggingManagerTestActivity";
// The base64 encoding of the values 'test key 1' and 'test key 2'.
- private static final String TEST_KEY_1 = "dGVzdCBrZXkgMQo=";
- private static final String TEST_KEY_2 = "dGVzdCBrZXkgMgo=";
+ private static final String TEST_KEY_1 = "dGVzdCBrZXkgMQo= test@android.com";
+ private static final String TEST_KEY_2 = "dGVzdCBrZXkgMgo= test@android.com";
// This response is received from the AdbDebuggingHandler when the key is allowed to connect
private static final String RESPONSE_KEY_ALLOWED = "OK";
@@ -76,22 +79,26 @@
private AdbDebuggingManager.AdbKeyStore mKeyStore;
private BlockingQueue<TestResult> mBlockingQueue;
private long mOriginalAllowedConnectionTime;
- private File mKeyFile;
+ private File mAdbKeyXmlFile;
+ private File mAdbKeyFile;
@Before
public void setUp() throws Exception {
mContext = InstrumentationRegistry.getContext();
- mManager = new AdbDebuggingManager(mContext, ADB_CONFIRM_COMPONENT);
- mKeyFile = new File(mContext.getFilesDir(), "test_adb_keys.xml");
- if (mKeyFile.exists()) {
- mKeyFile.delete();
+ mAdbKeyFile = new File(mContext.getFilesDir(), "adb_keys");
+ if (mAdbKeyFile.exists()) {
+ mAdbKeyFile.delete();
+ }
+ mManager = new AdbDebuggingManager(mContext, ADB_CONFIRM_COMPONENT, mAdbKeyFile);
+ mAdbKeyXmlFile = new File(mContext.getFilesDir(), "test_adb_keys.xml");
+ if (mAdbKeyXmlFile.exists()) {
+ mAdbKeyXmlFile.delete();
}
mThread = new AdbDebuggingThreadTest();
- mKeyStore = mManager.new AdbKeyStore(mKeyFile);
+ mKeyStore = mManager.new AdbKeyStore(mAdbKeyXmlFile);
mHandler = mManager.new AdbDebuggingHandler(FgThread.get().getLooper(), mThread, mKeyStore);
mOriginalAllowedConnectionTime = mKeyStore.getAllowedConnectionTime();
mBlockingQueue = new ArrayBlockingQueue<>(1);
-
}
@After
@@ -118,6 +125,13 @@
// Verify if the user allows the key but does not select the option to 'always
// allow' that the connection is allowed but the key is not stored.
runAdbTest(TEST_KEY_1, true, false, false);
+
+ // Persist the keystore to ensure that the key is not written to the adb_keys file.
+ persistKeyStore();
+ assertFalse(
+ "A key for which the 'always allow' option is not selected must not be written "
+ + "to the adb_keys file",
+ isKeyInFile(TEST_KEY_1, mAdbKeyFile));
}
@Test
@@ -146,25 +160,11 @@
// Send the disconnect message for the currently connected key to trigger an update of the
// last connection time.
- mHandler.obtainMessage(
- AdbDebuggingManager.AdbDebuggingHandler.MESSAGE_ADB_DISCONNECT).sendToTarget();
-
- // Use a latch to ensure the test does not exit untill the Runnable has been processed.
- CountDownLatch latch = new CountDownLatch(1);
-
- // Post a new Runnable to the handler to ensure it runs after the disconnect message is
- // processed.
- mHandler.post(() -> {
- assertNotEquals(
- "The last connection time was not updated after the disconnect",
- lastConnectionTime,
- mKeyStore.getLastConnectionTime(TEST_KEY_1));
- latch.countDown();
- });
- if (!latch.await(TIMEOUT, TIMEOUT_TIME_UNIT)) {
- fail("The Runnable to verify the last connection time was updated did not complete "
- + "within the timeout period");
- }
+ disconnectKey(TEST_KEY_1);
+ assertNotEquals(
+ "The last connection time was not updated after the disconnect",
+ lastConnectionTime,
+ mKeyStore.getLastConnectionTime(TEST_KEY_1));
}
@Test
@@ -177,8 +177,7 @@
runAdbTest(TEST_KEY_1, true, false, false);
// Send the disconnect message for the currently connected key.
- mHandler.obtainMessage(
- AdbDebuggingManager.AdbDebuggingHandler.MESSAGE_ADB_DISCONNECT).sendToTarget();
+ disconnectKey(TEST_KEY_1);
// Verify that the disconnected key is not automatically allowed on a subsequent connection.
runAdbTest(TEST_KEY_1, true, false, false);
@@ -192,12 +191,13 @@
// Allow a connection from a new key with the 'Always allow' option selected.
runAdbTest(TEST_KEY_1, true, true, false);
- // Next attempt another connection with the same key and verify that the activity to prompt
- // the user to accept the key is not launched.
- runAdbTest(TEST_KEY_1, true, true, true);
+ // Send a persist keystore message to force the key to be written to the adb_keys file
+ persistKeyStore();
- // Verify that a different key is not automatically allowed.
- runAdbTest(TEST_KEY_2, false, false, false);
+ // Verify the key is in the adb_keys file to ensure subsequent connections are allowed by
+ // adbd.
+ assertTrue("The key was not in the adb_keys file after persisting the keystore",
+ isKeyInFile(TEST_KEY_1, mAdbKeyFile));
}
@Test
@@ -215,8 +215,11 @@
// fail the new test but would be allowed with the original behavior.
mKeyStore.setLastConnectionTime(TEST_KEY_1, 1);
- // Run the test with the key and verify that the connection is automatically allowed.
- runAdbTest(TEST_KEY_1, true, true, true);
+ // Verify that the key is in the adb_keys file to ensure subsequent connections are
+ // automatically allowed by adbd.
+ persistKeyStore();
+ assertTrue("The key was not in the adb_keys file after persisting the keystore",
+ isKeyInFile(TEST_KEY_1, mAdbKeyFile));
}
@Test
@@ -237,24 +240,11 @@
Thread.sleep(10);
// Send a message to the handler to update the last connection time for the active key
- mHandler.obtainMessage(
- AdbDebuggingManager.AdbDebuggingHandler.MESSAGE_ADB_UPDATE_KEY_CONNECTION_TIME)
- .sendToTarget();
-
- // Post a Runnable to the handler to ensure it runs after the update key connection time
- // message is processed.
- CountDownLatch latch = new CountDownLatch(1);
- mHandler.post(() -> {
- assertNotEquals(
- "The last connection time of the key was not updated after the update key "
- + "connection time message",
- lastConnectionTime, mKeyStore.getLastConnectionTime(TEST_KEY_1));
- latch.countDown();
- });
- if (!latch.await(TIMEOUT, TIMEOUT_TIME_UNIT)) {
- fail("The Runnable to verify the last connection time was updated did not complete "
- + "within the timeout period");
- }
+ updateKeyStore();
+ assertNotEquals(
+ "The last connection time of the key was not updated after the update key "
+ + "connection time message",
+ lastConnectionTime, mKeyStore.getLastConnectionTime(TEST_KEY_1));
}
@Test
@@ -266,16 +256,12 @@
// Allow the key to connect with the 'Always allow' option selected
runAdbTest(TEST_KEY_1, true, true, false);
- // Send a message to the handler to persist the updated keystore.
- mHandler.obtainMessage(
- AdbDebuggingManager.AdbDebuggingHandler.MESSAGE_ADB_PERSIST_KEY_STORE)
- .sendToTarget();
-
- // Post a Runnable to the handler to ensure the persist key store message has been processed
- // using a new AdbKeyStore backed by the key file.
- mHandler.post(() -> assertTrue(
+ // Send a message to the handler to persist the updated keystore and verify a new key store
+ // backed by the XML file contains the key.
+ persistKeyStore();
+ assertTrue(
"The key with the 'Always allow' option selected was not persisted in the keystore",
- mManager.new AdbKeyStore(mKeyFile).isKeyAuthorized(TEST_KEY_1)));
+ mManager.new AdbKeyStore(mAdbKeyXmlFile).isKeyAuthorized(TEST_KEY_1));
// Get the current last connection time to ensure it is updated in the persisted keystore.
long lastConnectionTime = mKeyStore.getLastConnectionTime(TEST_KEY_1);
@@ -284,29 +270,18 @@
Thread.sleep(10);
// Send a message to the handler to update the last connection time for the active key.
- mHandler.obtainMessage(
- AdbDebuggingManager.AdbDebuggingHandler.MESSAGE_ADB_UPDATE_KEY_CONNECTION_TIME)
- .sendToTarget();
+ updateKeyStore();
- // Persist the updated last connection time.
- mHandler.obtainMessage(
- AdbDebuggingManager.AdbDebuggingHandler.MESSAGE_ADB_PERSIST_KEY_STORE)
- .sendToTarget();
-
- // Post a Runnable with a new key store backed by the key file to verify that the last
- // connection time obtained above is different from the persisted updated value.
- CountDownLatch latch = new CountDownLatch(1);
- mHandler.post(() -> {
- assertNotEquals(
- "The last connection time in the key file was not updated after the update "
- + "connection time message", lastConnectionTime,
- mManager.new AdbKeyStore(mKeyFile).getLastConnectionTime(TEST_KEY_1));
- latch.countDown();
- });
- if (!latch.await(TIMEOUT, TIMEOUT_TIME_UNIT)) {
- fail("The Runnable to verify the last connection time was updated did not complete "
- + "within the timeout period");
- }
+ // Persist the updated last connection time and verify a new key store backed by the XML
+ // file contains the updated connection time.
+ persistKeyStore();
+ assertNotEquals(
+ "The last connection time in the key file was not updated after the update "
+ + "connection time message", lastConnectionTime,
+ mManager.new AdbKeyStore(mAdbKeyXmlFile).getLastConnectionTime(TEST_KEY_1));
+ // Verify that the key is in the adb_keys file
+ assertTrue("The key was not in the adb_keys file after persisting the keystore",
+ isKeyInFile(TEST_KEY_1, mAdbKeyFile));
}
@Test
@@ -318,28 +293,18 @@
runAdbTest(TEST_KEY_1, true, true, false);
// Send a message to the handler to clear the adb authorizations.
- mHandler.obtainMessage(
- AdbDebuggingManager.AdbDebuggingHandler.MESSAGE_ADB_CLEAR).sendToTarget();
+ clearKeyStore();
// Send a message to disconnect the currently connected key
- mHandler.obtainMessage(
- AdbDebuggingManager.AdbDebuggingHandler.MESSAGE_ADB_DISCONNECT).sendToTarget();
+ disconnectKey(TEST_KEY_1);
+ assertFalse(
+ "The currently connected 'always allow' key must not be authorized after an adb"
+ + " clear message.",
+ mKeyStore.isKeyAuthorized(TEST_KEY_1));
- // Post a Runnable to ensure the disconnect has completed to verify the 'Always allow' key
- // that was connected when the clear was sent requires authorization.
- CountDownLatch latch = new CountDownLatch(1);
- mHandler.post(() -> {
- assertFalse(
- "The currently connected 'always allow' key should not be authorized after an"
- + " adb"
- + " clear message.",
- mKeyStore.isKeyAuthorized(TEST_KEY_1));
- latch.countDown();
- });
- if (!latch.await(TIMEOUT, TIMEOUT_TIME_UNIT)) {
- fail("The Runnable to verify the key is not authorized did not complete within the "
- + "timeout period");
- }
+ // The key should not be in the adb_keys file after clearing the authorizations.
+ assertFalse("The key must not be in the adb_keys file after clearing authorizations",
+ isKeyInFile(TEST_KEY_1, mAdbKeyFile));
}
@Test
@@ -357,8 +322,17 @@
// Sleep for a small amount of time to exceed the allowed window.
Thread.sleep(10);
- // A new connection from this key should prompt the user again.
- runAdbTest(TEST_KEY_1, true, true, false);
+ // The AdbKeyStore has a method to get the time of the next key expiration to ensure the
+ // scheduled job runs at the time of the next expiration or after 24 hours, whichever occurs
+ // first.
+ assertEquals("The time of the next key expiration must be 0.", 0,
+ mKeyStore.getNextExpirationTime());
+
+ // Persist the key store and verify that the key is no longer in the adb_keys file.
+ persistKeyStore();
+ assertFalse(
+ "The key must not be in the adb_keys file after the allowed time has elapsed.",
+ isKeyInFile(TEST_KEY_1, mAdbKeyFile));
}
@Test
@@ -381,7 +355,7 @@
// Attempt to set the last connection time to 1970
mKeyStore.setLastConnectionTime(TEST_KEY_1, 0);
assertEquals(
- "The last connection time in the adb key store should not be set to a value less "
+ "The last connection time in the adb key store must not be set to a value less "
+ "than the previous connection time",
lastConnectionTime, mKeyStore.getLastConnectionTime(TEST_KEY_1));
@@ -389,11 +363,315 @@
mKeyStore.setLastConnectionTime(TEST_KEY_1,
Math.max(0, lastConnectionTime - (mKeyStore.getAllowedConnectionTime() + 1)));
assertEquals(
- "The last connection time in the adb key store should not be set to a value less "
+ "The last connection time in the adb key store must not be set to a value less "
+ "than the previous connection time",
lastConnectionTime, mKeyStore.getLastConnectionTime(TEST_KEY_1));
}
+ @Test
+ public void testAdbKeyRemovedByScheduledJob() throws Exception {
+ // When a key is automatically allowed it should be stored in the adb_keys file. A job is
+ // then scheduled daily to update the connection time of the currently connected key, and if
+ // no connected key exists the key store is updated to purge expired keys. This test
+ // verifies that after a key's expiration time has been reached that it is no longer
+ // in the key store nor the adb_keys file
+
+ // Set the allowed time to the default to ensure that any modification to this value do not
+ // impact this test.
+ setAllowedConnectionTime(Settings.Global.DEFAULT_ADB_ALLOWED_CONNECTION_TIME);
+
+ // Allow both test keys to connect with the 'always allow' option selected.
+ runAdbTest(TEST_KEY_1, true, true, false);
+ runAdbTest(TEST_KEY_2, true, true, false);
+ disconnectKey(TEST_KEY_1);
+ disconnectKey(TEST_KEY_2);
+
+ // Persist the key store and verify that both keys are in the key store and adb_keys file.
+ persistKeyStore();
+ assertTrue(
+ "Test key 1 must be in the adb_keys file after selecting the 'always allow' "
+ + "option",
+ isKeyInFile(TEST_KEY_1, mAdbKeyFile));
+ assertTrue(
+ "Test key 1 must be in the adb key store after selecting the 'always allow' "
+ + "option",
+ mKeyStore.isKeyAuthorized(TEST_KEY_1));
+ assertTrue(
+ "Test key 2 must be in the adb_keys file after selecting the 'always allow' "
+ + "option",
+ isKeyInFile(TEST_KEY_2, mAdbKeyFile));
+ assertTrue(
+ "Test key 2 must be in the adb key store after selecting the 'always allow' option",
+ mKeyStore.isKeyAuthorized(TEST_KEY_2));
+
+ // Set test key 1's last connection time to a small value and persist the keystore to ensure
+ // it is cleared out after the next key store update.
+ mKeyStore.setLastConnectionTime(TEST_KEY_1, 1, true);
+ updateKeyStore();
+ assertFalse(
+ "Test key 1 must no longer be in the adb_keys file after its timeout period is "
+ + "reached",
+ isKeyInFile(TEST_KEY_1, mAdbKeyFile));
+ assertFalse(
+ "Test key 1 must no longer be in the adb key store after its timeout period is "
+ + "reached",
+ mKeyStore.isKeyAuthorized(TEST_KEY_1));
+ assertTrue(
+ "Test key 2 must still be in the adb_keys file after test key 1's timeout "
+ + "period is reached",
+ isKeyInFile(TEST_KEY_2, mAdbKeyFile));
+ assertTrue(
+ "Test key 2 must still be in the adb key store after test key 1's timeout period "
+ + "is reached",
+ mKeyStore.isKeyAuthorized(TEST_KEY_2));
+ }
+
+ @Test
+ public void testKeystoreExpirationTimes() throws Exception {
+ // When one or more keys are always allowed a daily job is scheduled to update the
+ // connection time of the connected key and to purge any expired keys. The keystore provides
+ // a method to obtain the expiration time of the next key to expire to ensure that a
+ // scheduled job can run at the time of the next expiration if it is before the daily job
+ // would run. This test verifies that this method returns the expected values depending on
+ // when the key should expire and also verifies that the method to schedule the next job to
+ // update the keystore is the expected value based on the time of the next expiration.
+
+ final long epsilon = 5000;
+
+ // Ensure the allowed time is set to the default.
+ setAllowedConnectionTime(Settings.Global.DEFAULT_ADB_ALLOWED_CONNECTION_TIME);
+
+ // If there are no keys in the keystore the expiration time should be -1.
+ assertEquals("The expiration time must be -1 when there are no keys in the keystore", -1,
+ mKeyStore.getNextExpirationTime());
+
+ // Allow the test key to connect with the 'always allow' option.
+ runAdbTest(TEST_KEY_1, true, true, false);
+
+ // Verify that the current expiration time is within a small value of the default time.
+ long expirationTime = mKeyStore.getNextExpirationTime();
+ if (Math.abs(expirationTime - Settings.Global.DEFAULT_ADB_ALLOWED_CONNECTION_TIME)
+ > epsilon) {
+ fail("The expiration time for a new key, " + expirationTime
+ + ", is outside the expected value of "
+ + Settings.Global.DEFAULT_ADB_ALLOWED_CONNECTION_TIME);
+ }
+ // The delay until the next job should be the lesser of the default expiration time and the
+ // AdbDebuggingHandler's job interval.
+ long expectedValue = Math.min(
+ AdbDebuggingManager.AdbDebuggingHandler.UPDATE_KEYSTORE_JOB_INTERVAL,
+ Settings.Global.DEFAULT_ADB_ALLOWED_CONNECTION_TIME);
+ long delay = mHandler.scheduleJobToUpdateAdbKeyStore();
+ if (Math.abs(delay - expectedValue) > epsilon) {
+ fail("The delay before the next scheduled job, " + delay
+ + ", is outside the expected value of " + expectedValue);
+ }
+
+ // Set the current expiration time to a minute from expiration and verify this new value is
+ // returned.
+ final long newExpirationTime = 60000;
+ mKeyStore.setLastConnectionTime(TEST_KEY_1,
+ System.currentTimeMillis() - Settings.Global.DEFAULT_ADB_ALLOWED_CONNECTION_TIME
+ + newExpirationTime, true);
+ expirationTime = mKeyStore.getNextExpirationTime();
+ if (Math.abs(expirationTime - newExpirationTime) > epsilon) {
+ fail("The expiration time for a key about to expire, " + expirationTime
+ + ", is outside the expected value of " + newExpirationTime);
+ }
+ delay = mHandler.scheduleJobToUpdateAdbKeyStore();
+ if (Math.abs(delay - newExpirationTime) > epsilon) {
+ fail("The delay before the next scheduled job, " + delay
+ + ", is outside the expected value of " + newExpirationTime);
+ }
+
+ // If a key is already expired the expiration time and delay before the next job runs should
+ // be 0.
+ mKeyStore.setLastConnectionTime(TEST_KEY_1, 1, true);
+ assertEquals("The expiration time for a key that is already expired must be 0", 0,
+ mKeyStore.getNextExpirationTime());
+ assertEquals(
+ "The delay before the next scheduled job for a key that is already expired must"
+ + " be 0", 0, mHandler.scheduleJobToUpdateAdbKeyStore());
+
+ // If the previous behavior of never removing old keys is set then the expiration time
+ // should be -1 to indicate the job does not need to run.
+ setAllowedConnectionTime(0);
+ assertEquals("The expiration time must be -1 when the keys are set to never expire", -1,
+ mKeyStore.getNextExpirationTime());
+ }
+
+ @Test
+ public void testConnectionTimeUpdatedWithConnectedKeyMessage() throws Exception {
+ // When a system successfully passes the SIGNATURE challenge adbd sends a connected key
+ // message to the framework to notify of the newly connected key. This message should
+ // trigger the AdbDebuggingManager to update the last connection time for this key and mark
+ // it as the currently connected key so that its time can be updated during subsequent
+ // keystore update jobs as well as when the disconnected message is received.
+
+ // Allow the test key to connect with the 'always allow' option selected.
+ runAdbTest(TEST_KEY_1, true, true, false);
+
+ // Simulate disconnecting the key before a subsequent connection without user interaction.
+ disconnectKey(TEST_KEY_1);
+
+ // Get the last connection time for the key to verify that it is updated when the connected
+ // key message is sent.
+ long connectionTime = mKeyStore.getLastConnectionTime(TEST_KEY_1);
+ Thread.sleep(10);
+ mHandler.obtainMessage(AdbDebuggingManager.AdbDebuggingHandler.MESSAGE_ADB_CONNECTED_KEY,
+ TEST_KEY_1).sendToTarget();
+ flushHandlerQueue();
+ assertNotEquals(
+ "The connection time for the key must be updated when the connected key message "
+ + "is received",
+ connectionTime, mKeyStore.getLastConnectionTime(TEST_KEY_1));
+
+ // Verify that the scheduled job updates the connection time of the key.
+ connectionTime = mKeyStore.getLastConnectionTime(TEST_KEY_1);
+ Thread.sleep(10);
+ updateKeyStore();
+ assertNotEquals(
+ "The connection time for the key must be updated when the update keystore message"
+ + " is sent",
+ connectionTime, mKeyStore.getLastConnectionTime(TEST_KEY_1));
+
+ // Verify that the connection time is updated when the key is disconnected.
+ connectionTime = mKeyStore.getLastConnectionTime(TEST_KEY_1);
+ Thread.sleep(10);
+ disconnectKey(TEST_KEY_1);
+ assertNotEquals(
+ "The connection time for the key must be updated when the disconnected message is"
+ + " received",
+ connectionTime, mKeyStore.getLastConnectionTime(TEST_KEY_1));
+ }
+
+ @Test
+ public void testClearAuthorizations() throws Exception {
+ // When the user selects the 'Revoke USB debugging authorizations' all previously 'always
+ // allow' keys should be deleted.
+
+ // Set the allowed connection time to the default value to ensure tests do not fail due to
+ // a small value.
+ setAllowedConnectionTime(Settings.Global.DEFAULT_ADB_ALLOWED_CONNECTION_TIME);
+
+ // Allow the test key to connect with the 'always allow' option selected.
+ runAdbTest(TEST_KEY_1, true, true, false);
+ persistKeyStore();
+
+ // Verify that the key is authorized and in the adb_keys file
+ assertTrue(
+ "The test key must be in the keystore after the 'always allow' option is selected",
+ mKeyStore.isKeyAuthorized(TEST_KEY_1));
+ assertTrue(
+ "The test key must be in the adb_keys file after the 'always allow option is "
+ + "selected",
+ isKeyInFile(TEST_KEY_1, mAdbKeyFile));
+
+ // Send the message to clear the adb authorizations and verify that the keys are no longer
+ // authorized.
+ clearKeyStore();
+ assertFalse(
+ "The test key must not be in the keystore after clearing the authorizations",
+ mKeyStore.isKeyAuthorized(TEST_KEY_1));
+ assertFalse(
+ "The test key must not be in the adb_keys file after clearing the authorizations",
+ isKeyInFile(TEST_KEY_1, mAdbKeyFile));
+ }
+
+ @Test
+ public void testClearKeystoreAfterDisablingAdb() throws Exception {
+ // When the user disables adb they should still be able to clear the authorized keys.
+
+ // Allow the test key to connect with the 'always allow' option selected and persist the
+ // keystore.
+ runAdbTest(TEST_KEY_1, true, true, false);
+ persistKeyStore();
+
+ // Disable adb and verify that the keystore can be cleared without throwing an exception.
+ disableAdb();
+ clearKeyStore();
+ assertFalse(
+ "The test key must not be in the adb_keys file after clearing the authorizations",
+ isKeyInFile(TEST_KEY_1, mAdbKeyFile));
+ }
+
+ @Test
+ public void testUntrackedUserKeysAddedToKeystore() throws Exception {
+ // When a device is first updated to a build that tracks the connection time of adb keys
+ // the keys in the user key file will not have a connection time. To prevent immediately
+ // deleting keys that the user is actively using these untracked keys should be added to the
+ // keystore with the current system time; this gives the user time to reconnect
+ // automatically with an active key while inactive keys are deleted after the expiration
+ // time.
+
+ final long epsilon = 5000;
+ final String[] testKeys = {TEST_KEY_1, TEST_KEY_2};
+
+ // Add the test keys to the user key file.
+ FileOutputStream fo = new FileOutputStream(mAdbKeyFile);
+ for (String key : testKeys) {
+ fo.write(key.getBytes());
+ fo.write('\n');
+ }
+ fo.close();
+
+ // Set the expiration time to the default and use this value to verify the expiration time
+ // of the previously untracked keys.
+ setAllowedConnectionTime(Settings.Global.DEFAULT_ADB_ALLOWED_CONNECTION_TIME);
+
+ // The untracked keys should be added to the keystore as part of the constructor.
+ AdbDebuggingManager.AdbKeyStore adbKeyStore = mManager.new AdbKeyStore(mAdbKeyXmlFile);
+
+ // Verify that the connection time for each test key is within a small value of the current
+ // time.
+ long time = System.currentTimeMillis();
+ for (String key : testKeys) {
+ long connectionTime = adbKeyStore.getLastConnectionTime(key);
+ if (Math.abs(connectionTime - connectionTime) > epsilon) {
+ fail("The connection time for a previously untracked key, " + connectionTime
+ + ", is beyond the current time of " + time);
+ }
+ }
+ }
+
+ @Test
+ public void testConnectionTimeUpdatedForMultipleConnectedKeys() throws Exception {
+ // Since ADB supports multiple simultaneous connections verify that the connection time of
+ // each key is updated by the scheduled job as long as it is connected.
+
+ // Allow both test keys to connect with the 'always allow' option selected.
+ runAdbTest(TEST_KEY_1, true, true, false);
+ runAdbTest(TEST_KEY_2, true, true, false);
+
+ // Sleep a small amount of time to ensure the connection time is updated by the scheduled
+ // job.
+ long connectionTime1 = mKeyStore.getLastConnectionTime(TEST_KEY_1);
+ long connectionTime2 = mKeyStore.getLastConnectionTime(TEST_KEY_2);
+ Thread.sleep(10);
+ updateKeyStore();
+ assertNotEquals(
+ "The connection time for test key 1 must be updated after the scheduled job runs",
+ connectionTime1, mKeyStore.getLastConnectionTime(TEST_KEY_1));
+ assertNotEquals(
+ "The connection time for test key 2 must be updated after the scheduled job runs",
+ connectionTime2, mKeyStore.getLastConnectionTime(TEST_KEY_2));
+
+ // Disconnect the second test key and verify that the last connection time of the first key
+ // is the only one updated.
+ disconnectKey(TEST_KEY_2);
+ connectionTime1 = mKeyStore.getLastConnectionTime(TEST_KEY_1);
+ connectionTime2 = mKeyStore.getLastConnectionTime(TEST_KEY_2);
+ Thread.sleep(10);
+ updateKeyStore();
+ assertNotEquals(
+ "The connection time for test key 1 must be updated after another key is "
+ + "disconnected and the scheduled job runs",
+ connectionTime1, mKeyStore.getLastConnectionTime(TEST_KEY_1));
+ assertEquals(
+ "The connection time for test key 2 must not be updated after it is disconnected",
+ connectionTime2, mKeyStore.getLastConnectionTime(TEST_KEY_2));
+ }
+
/**
* Runs an adb test with the provided configuration.
*
@@ -440,10 +718,78 @@
allowKey ? RESPONSE_KEY_ALLOWED : RESPONSE_KEY_DENIED, threadResult.mMessage);
// if the key is not allowed or not always allowed verify it is not in the key store
if (!allowKey || !alwaysAllow) {
- assertFalse(
- "The key should not be allowed automatically on subsequent connection attempts",
+ assertFalse("The key must not be authorized in the key store",
mKeyStore.isKeyAuthorized(key));
+ assertFalse(
+ "The key must not be stored in the adb_keys file",
+ isKeyInFile(key, mAdbKeyFile));
}
+ flushHandlerQueue();
+ }
+
+ private void persistKeyStore() throws Exception {
+ // Send a message to the handler to persist the key store.
+ mHandler.obtainMessage(
+ AdbDebuggingManager.AdbDebuggingHandler.MESSAGE_ADB_PERSIST_KEYSTORE)
+ .sendToTarget();
+ flushHandlerQueue();
+ }
+
+ private void disconnectKey(String key) throws Exception {
+ // Send a message to the handler to disconnect the currently connected key.
+ mHandler.obtainMessage(AdbDebuggingManager.AdbDebuggingHandler.MESSAGE_ADB_DISCONNECT,
+ key).sendToTarget();
+ flushHandlerQueue();
+ }
+
+ private void updateKeyStore() throws Exception {
+ // Send a message to the handler to run the update keystore job.
+ mHandler.obtainMessage(
+ AdbDebuggingManager.AdbDebuggingHandler.MESSAGE_ADB_UPDATE_KEYSTORE).sendToTarget();
+ flushHandlerQueue();
+ }
+
+ private void clearKeyStore() throws Exception {
+ // Send a message to the handler to clear all previously authorized keys.
+ mHandler.obtainMessage(
+ AdbDebuggingManager.AdbDebuggingHandler.MESSAGE_ADB_CLEAR).sendToTarget();
+ flushHandlerQueue();
+ }
+
+ private void disableAdb() throws Exception {
+ // Send a message to the handler to disable adb.
+ mHandler.obtainMessage(
+ AdbDebuggingManager.AdbDebuggingHandler.MESSAGE_ADB_DISABLED).sendToTarget();
+ flushHandlerQueue();
+ }
+
+ private void flushHandlerQueue() throws Exception {
+ // Post a Runnable to ensure that all of the current messages in the queue are flushed.
+ CountDownLatch latch = new CountDownLatch(1);
+ mHandler.post(() -> {
+ latch.countDown();
+ });
+ if (!latch.await(TIMEOUT, TIMEOUT_TIME_UNIT)) {
+ fail("The Runnable to flush the handler's queue did not complete within the timeout "
+ + "period");
+ }
+ }
+
+ private boolean isKeyInFile(String key, File keyFile) throws Exception {
+ if (key == null) {
+ return false;
+ }
+ if (keyFile.exists()) {
+ try (BufferedReader in = new BufferedReader(new FileReader(keyFile))) {
+ String currKey;
+ while ((currKey = in.readLine()) != null) {
+ if (key.equals(currKey)) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
}
/**