/* * Copyright (C) 2012 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server.adb; import static com.android.internal.util.dump.DumpUtils.writeStringIfNotNull; import android.annotation.TestApi; import android.app.ActivityManager; import android.content.ActivityNotFoundException; import android.content.ComponentName; import android.content.Context; import android.content.Intent; 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; import android.os.Looper; import android.os.Message; import android.os.SystemClock; import android.os.SystemProperties; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; import android.service.adb.AdbDebuggingManagerProto; import android.util.AtomicFile; import android.util.Base64; import android.util.Slog; import android.util.StatsLog; 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; import com.android.server.FgThread; import org.xmlpull.v1.XmlPullParser; 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 * that are authorized to connect to the ADB service itself. */ public class AdbDebuggingManager { private static final String TAG = "AdbDebuggingManager"; private static final boolean DEBUG = false; private static final String ADBD_SOCKET = "adbd"; private static final String ADB_DIRECTORY = "misc/adb"; // This file contains keys that will always be allowed to connect to the device via adb. private static final String ADB_KEYS_FILE = "adb_keys"; // 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 = 65536; private final Context mContext; private final Handler mHandler; private AdbDebuggingThread mThread; private boolean mAdbEnabled = false; private String mFingerprints; private final List 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); } /** * Constructor that accepts the component to be invoked to confirm if the user wants to allow * an adb connection from the key. */ @TestApi 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 { private boolean mStopped; private LocalSocket mSocket; private OutputStream mOutputStream; private InputStream mInputStream; AdbDebuggingThread() { super(TAG); } @Override public void run() { if (DEBUG) Slog.d(TAG, "Entering thread"); while (true) { synchronized (this) { if (mStopped) { if (DEBUG) Slog.d(TAG, "Exiting thread"); return; } try { openSocketLocked(); } catch (Exception e) { /* Don't loop too fast if adbd dies, before init restarts it */ SystemClock.sleep(1000); } } try { listenToSocket(); } catch (Exception e) { /* Don't loop too fast if adbd dies, before init restarts it */ SystemClock.sleep(1000); } } } private void openSocketLocked() throws IOException { try { LocalSocketAddress address = new LocalSocketAddress(ADBD_SOCKET, LocalSocketAddress.Namespace.RESERVED); mInputStream = null; if (DEBUG) Slog.d(TAG, "Creating socket"); 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; } } private void listenToSocket() throws IOException { try { byte[] buffer = new byte[BUFFER_SIZE]; while (true) { int count = mInputStream.read(buffer); // 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; } if (buffer[0] == 'P' && buffer[1] == 'K') { String key = new String(Arrays.copyOfRange(buffer, 2, count)); Slog.d(TAG, "Received public key: " + key); Message msg = mHandler.obtainMessage( AdbDebuggingHandler.MESSAGE_ADB_CONFIRM); msg.obj = key; mHandler.sendMessage(msg); } else if (buffer[0] == 'D' && buffer[1] == 'C') { 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: " + (new String(Arrays.copyOfRange(buffer, 0, 2)))); break; } } } finally { synchronized (this) { closeSocketLocked(); } } } private void closeSocketLocked() { if (DEBUG) Slog.d(TAG, "Closing socket"); try { if (mOutputStream != null) { mOutputStream.close(); mOutputStream = null; } } catch (IOException e) { Slog.e(TAG, "Failed closing output stream: " + e); } try { if (mSocket != null) { mSocket.close(); mSocket = null; } } catch (IOException ex) { Slog.e(TAG, "Failed closing socket: " + ex); } } /** Call to stop listening on the socket and exit the thread. */ void stopListening() { synchronized (this) { mStopped = true; closeSocketLocked(); } } void sendResponse(String msg) { synchronized (this) { if (!mStopped && mOutputStream != null) { try { mOutputStream.write(msg.getBytes()); } catch (IOException ex) { Slog.e(TAG, "Failed to write response:", ex); } } } } } class AdbDebuggingHandler extends Handler { // 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; static final int MESSAGE_ADB_ALLOW = 3; static final int MESSAGE_ADB_DENY = 4; 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_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); } /** * Constructor that accepts the AdbDebuggingThread to which responses should be sent * and the AdbKeyStore to be used to store the temporary grants. */ @TestApi AdbDebuggingHandler(Looper looper, AdbDebuggingThread thread, AdbKeyStore adbKeyStore) { super(looper); mThread = thread; mAdbKeyStore = adbKeyStore; } public void handleMessage(Message msg) { switch (msg.what) { case MESSAGE_ADB_ENABLED: if (mAdbEnabled) { break; } registerForAuthTimeChanges(); mAdbEnabled = true; mThread = new AdbDebuggingThread(); mThread.start(); mAdbKeyStore = new AdbKeyStore(); mAdbKeyStore.updateKeyStore(); scheduleJobToUpdateAdbKeyStore(); break; case MESSAGE_ADB_DISABLED: if (!mAdbEnabled) { break; } mAdbEnabled = false; if (mThread != null) { mThread.stopListening(); mThread = 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); break; } boolean alwaysAllow = msg.arg1 == 1; if (mThread != null) { mThread.sendResponse("OK"); if (alwaysAllow) { if (!mConnectedKeys.contains(key)) { mConnectedKeys.add(key); } mAdbKeyStore.setLastConnectionTime(key, System.currentTimeMillis()); sendPersistKeyStoreMessage(); scheduleJobToUpdateAdbKeyStore(); } logAdbConnectionChanged(key, AdbProtoEnums.USER_ALLOWED, alwaysAllow); } break; } case MESSAGE_ADB_DENY: if (mThread != null) { mThread.sendResponse("NO"); logAdbConnectionChanged(null, AdbProtoEnums.USER_DENIED, false); } break; case MESSAGE_ADB_CONFIRM: { String key = (String) msg.obj; if ("trigger_restart_min_framework".equals( SystemProperties.get("vold.decrypt"))) { Slog.d(TAG, "Deferring adb confirmation until after vold decrypt"); if (mThread != null) { mThread.sendResponse("NO"); logAdbConnectionChanged(key, AdbProtoEnums.DENIED_VOLD_DECRYPT, false); } break; } String fingerprints = getFingerprints(key); if ("".equals(fingerprints)) { if (mThread != null) { mThread.sendResponse("NO"); logAdbConnectionChanged(key, AdbProtoEnums.DENIED_INVALID_KEY, false); } break; } logAdbConnectionChanged(key, AdbProtoEnums.AWAITING_USER_APPROVAL, false); mFingerprints = fingerprints; startConfirmation(key, mFingerprints); break; } case MESSAGE_ADB_CLEAR: { Slog.d(TAG, "Received a request to clear the adb authorizations"); mConnectedKeys.clear(); mAdbKeyStore.deleteKeyStore(); cancelJobToUpdateAdbKeyStore(); break; } case MESSAGE_ADB_DISCONNECT: { 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(key, AdbProtoEnums.DISCONNECTED, alwaysAllow); break; } case MESSAGE_ADB_PERSIST_KEYSTORE: { if (mAdbKeyStore != null) { mAdbKeyStore.persistKeyStore(); } break; } 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) { String hex = "0123456789ABCDEF"; StringBuilder sb = new StringBuilder(); MessageDigest digester; if (key == null) { return ""; } try { digester = MessageDigest.getInstance("MD5"); } catch (Exception ex) { Slog.e(TAG, "Error getting digester", ex); return ""; } byte[] base64_data = key.split("\\s+")[0].getBytes(); byte[] digest; try { digest = digester.digest(Base64.decode(base64_data, Base64.DEFAULT)); } catch (IllegalArgumentException e) { Slog.e(TAG, "error doing base64 decoding", e); return ""; } for (int i = 0; i < digest.length; i++) { sb.append(hex.charAt((digest[i] >> 4) & 0xf)); sb.append(hex.charAt(digest[i] & 0xf)); if (i < digest.length - 1) { sb.append(":"); } } return sb.toString(); } private void startConfirmation(String key, String fingerprints) { int currentUserId = ActivityManager.getCurrentUser(); UserInfo userInfo = UserManager.get(mContext).getUserInfo(currentUserId); String componentString; if (userInfo.isAdmin()) { componentString = mConfirmComponent != null ? mConfirmComponent : Resources.getSystem().getString( com.android.internal.R.string.config_customAdbPublicKeyConfirmationComponent); } else { // If the current foreground user is not the admin user we send a different // notification specific to secondary users. componentString = Resources.getSystem().getString( R.string.config_customAdbPublicKeyConfirmationSecondaryUserComponent); } ComponentName componentName = ComponentName.unflattenFromString(componentString); if (startConfirmationActivity(componentName, userInfo.getUserHandle(), key, fingerprints) || startConfirmationService(componentName, userInfo.getUserHandle(), key, fingerprints)) { return; } Slog.e(TAG, "unable to start customAdbPublicKeyConfirmation[SecondaryUser]Component " + componentString + " as an Activity or a Service"); } /** * @return true if the componentName led to an Activity that was started. */ private boolean startConfirmationActivity(ComponentName componentName, UserHandle userHandle, String key, String fingerprints) { PackageManager packageManager = mContext.getPackageManager(); Intent intent = createConfirmationIntent(componentName, key, fingerprints); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); if (packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) != null) { try { mContext.startActivityAsUser(intent, userHandle); return true; } catch (ActivityNotFoundException e) { Slog.e(TAG, "unable to start adb whitelist activity: " + componentName, e); } } return false; } /** * @return true if the componentName led to a Service that was started. */ private boolean startConfirmationService(ComponentName componentName, UserHandle userHandle, String key, String fingerprints) { Intent intent = createConfirmationIntent(componentName, key, fingerprints); try { if (mContext.startServiceAsUser(intent, userHandle) != null) { return true; } } catch (SecurityException e) { Slog.e(TAG, "unable to start adb whitelist service: " + componentName, e); } return false; } private Intent createConfirmationIntent(ComponentName componentName, String key, String fingerprints) { Intent intent = new Intent(); intent.setClassName(componentName.getPackageName(), componentName.getClassName()); intent.putExtra("key", key); intent.putExtra("fingerprints", fingerprints); return intent; } /** * Returns a new File with the specified name in the adb directory. */ private File getAdbFile(String fileName) { File dataDir = Environment.getDataDirectory(); File adbDir = new File(dataDir, ADB_DIRECTORY); if (!adbDir.exists()) { Slog.e(TAG, "ADB data directory does not exist"); return null; } return new File(adbDir, fileName); } File getAdbTempKeysFile() { return getAdbFile(ADB_TEMP_KEYS_FILE); } File getUserKeyFile() { return mTestUserKeyFile == null ? getAdbFile(ADB_KEYS_FILE) : mTestUserKeyFile; } private void writeKey(String key) { try { File keyFile = getUserKeyFile(); if (keyFile == null) { return; } FileOutputStream fo = new FileOutputStream(keyFile, true); fo.write(key.getBytes()); fo.write('\n'); fo.close(); FileUtils.setPermissions(keyFile.toString(), FileUtils.S_IRUSR | FileUtils.S_IWUSR | FileUtils.S_IRGRP, -1, -1); } catch (IOException ex) { Slog.e(TAG, "Error writing key:" + ex); } } private void writeKeys(Iterable keys) { AtomicFile atomicKeyFile = null; FileOutputStream fo = null; try { File keyFile = getUserKeyFile(); if (keyFile == null) { return; } atomicKeyFile = new AtomicFile(keyFile); fo = atomicKeyFile.startWrite(); for (String key : keys) { fo.write(key.getBytes()); fo.write('\n'); } atomicKeyFile.finishWrite(fo); FileUtils.setPermissions(keyFile.toString(), FileUtils.S_IRUSR | FileUtils.S_IWUSR | FileUtils.S_IRGRP, -1, -1); } 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) { keyFile.delete(); } } /** * When {@code enabled} is {@code true}, this allows ADB debugging and starts the ADB hanler * thread. When {@code enabled} is {@code false}, this disallows ADB debugging and shuts * down the handler thread. */ public void setAdbEnabled(boolean enabled) { mHandler.sendEmptyMessage(enabled ? AdbDebuggingHandler.MESSAGE_ADB_ENABLED : AdbDebuggingHandler.MESSAGE_ADB_DISABLED); } /** * Allows the debugging from the endpoint identified by {@code publicKey} either once or * always if {@code alwaysAllow} is {@code true}. */ public void allowDebugging(boolean alwaysAllow, String publicKey) { Message msg = mHandler.obtainMessage(AdbDebuggingHandler.MESSAGE_ADB_ALLOW); msg.arg1 = alwaysAllow ? 1 : 0; msg.obj = publicKey; mHandler.sendMessage(msg); } /** * Denies debugging connection from the device that last requested to connect. */ public void denyDebugging() { mHandler.sendEmptyMessage(AdbDebuggingHandler.MESSAGE_ADB_DENY); } /** * Clears all previously accepted ADB debugging public keys. Any subsequent request will need * to pass through {@link #allowUsbDebugging(boolean, String)} again. */ public void clearDebuggingKeys() { mHandler.sendEmptyMessage(AdbDebuggingHandler.MESSAGE_ADB_CLEAR); } /** * Sends a message to the handler to persist the keystore. */ private void sendPersistKeyStoreMessage() { Message msg = mHandler.obtainMessage(AdbDebuggingHandler.MESSAGE_ADB_PERSIST_KEYSTORE); mHandler.sendMessage(msg); } /** * Dump the USB debugging state. */ public void dump(DualDumpOutputStream dump, String idName, long id) { long token = dump.start(idName, id); dump.write("connected_to_adb", AdbDebuggingManagerProto.CONNECTED_TO_ADB, mThread != null); writeStringIfNotNull(dump, "last_key_received", AdbDebuggingManagerProto.LAST_KEY_RECEVIED, mFingerprints); try { dump.write("user_keys", AdbDebuggingManagerProto.USER_KEYS, FileUtils.readTextFile(new File("/data/misc/adb/adb_keys"), 0, null)); } catch (IOException e) { Slog.e(TAG, "Cannot read user keys", e); } try { dump.write("system_keys", AdbDebuggingManagerProto.SYSTEM_KEYS, FileUtils.readTextFile(new File("/adb_keys"), 0, null)); } catch (IOException e) { 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); } /** * Handles adb keys for which the user has granted the 'always allow' option. This class ensures * these grants are revoked after a period of inactivity as specified in the * ADB_ALLOWED_CONNECTION_TIME setting. */ class AdbKeyStore { private Map mKeyMap; private Set 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 * connection time for the specified key. */ public static final long NO_PREVIOUS_CONNECTION = 0; /** * Constructor that uses the default location for the persistent adb keystore. */ AdbKeyStore() { init(); } /** * 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 = getKeyMap(); mSystemKeys = getSystemKeysFromFile(SYSTEM_KEY_FILE); addUserKeysToKeyStore(); } /** * Initializes the key file that will be used to persist the adb grants. */ private void initKeyFile() { if (mKeyFile == null) { mKeyFile = getAdbTempKeysFile(); } // getAdbTempKeysFile can return null if the adb file cannot be obtained if (mKeyFile != null) { mAtomicKeyFile = new AtomicFile(mKeyFile); } } private Set getSystemKeysFromFile(String fileName) { Set 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 getKeyMap() { Map keyMap = new HashMap(); // if the AtomicFile could not be instantiated before attempt again; if it still fails // return an empty key map. if (mAtomicKeyFile == null) { initKeyFile(); if (mAtomicKeyFile == null) { Slog.e(TAG, "Unable to obtain the key file, " + mKeyFile + ", for reading"); return keyMap; } } if (!mAtomicKeyFile.exists()) { return keyMap; } try (FileInputStream keyStream = mAtomicKeyFile.openRead()) { XmlPullParser parser = Xml.newPullParser(); parser.setInput(keyStream, StandardCharsets.UTF_8.name()); XmlUtils.beginDocument(parser, XML_TAG_ADB_KEY); while (parser.next() != XmlPullParser.END_DOCUMENT) { String tagName = parser.getName(); if (tagName == null) { break; } else if (!tagName.equals(XML_TAG_ADB_KEY)) { XmlUtils.skipCurrentTag(parser); continue; } String key = parser.getAttributeValue(null, XML_ATTRIBUTE_KEY); long connectionTime; try { connectionTime = Long.valueOf( parser.getAttributeValue(null, XML_ATTRIBUTE_LAST_CONNECTION)); } catch (NumberFormatException e) { Slog.e(TAG, "Caught a NumberFormatException parsing the last connection time: " + e); XmlUtils.skipCurrentTag(parser); continue; } keyMap.put(key, connectionTime); } } catch (IOException | XmlPullParserException e) { Slog.e(TAG, "Caught an exception parsing the XML key file: ", e); } return keyMap; } /** * 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 keystore files // are deleted as well. filterOutOldKeys(); if (mKeyMap.isEmpty()) { deleteKeyStore(); return; } if (mAtomicKeyFile == null) { initKeyFile(); if (mAtomicKeyFile == null) { Slog.e(TAG, "Unable to obtain the key file, " + mKeyFile + ", for writing"); return; } } FileOutputStream keyStream = null; try { XmlSerializer serializer = new FastXmlSerializer(); keyStream = mAtomicKeyFile.startWrite(); serializer.setOutput(keyStream, StandardCharsets.UTF_8.name()); serializer.startDocument(null, true); for (Map.Entry keyEntry : mKeyMap.entrySet()) { serializer.startTag(null, XML_TAG_ADB_KEY); serializer.attribute(null, XML_ATTRIBUTE_KEY, keyEntry.getKey()); serializer.attribute(null, XML_ATTRIBUTE_LAST_CONNECTION, String.valueOf(keyEntry.getValue())); serializer.endTag(null, XML_TAG_ADB_KEY); } serializer.endDocument(); mAtomicKeyFile.finishWrite(keyStream); } catch (IOException e) { Slog.e(TAG, "Caught an exception writing the key map: ", e); mAtomicKeyFile.failWrite(keyStream); } } private boolean filterOutOldKeys() { boolean keysDeleted = false; long allowedTime = getAllowedConnectionTime(); long systemTime = System.currentTimeMillis(); Iterator> keyMapIterator = mKeyMap.entrySet().iterator(); while (keyMapIterator.hasNext()) { Map.Entry keyEntry = keyMapIterator.next(); 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> keyMapIterator = mKeyMap.entrySet().iterator(); while (keyMapIterator.hasNext()) { Map.Entry 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; } /** * Removes all of the entries in the key map and deletes the key file. */ public void deleteKeyStore() { mKeyMap.clear(); deleteKeyFile(); if (mAtomicKeyFile == null) { return; } mAtomicKeyFile.delete(); } /** * Returns the time of the last connection from the specified key, or {@code * NO_PREVIOUS_CONNECTION} if the specified key does not have an active adb grant. */ public long getLastConnectionTime(String key) { return mKeyMap.getOrDefault(key, NO_PREVIOUS_CONNECTION); } /** * Sets the time of the last connection for the specified key to the provided time. */ public void setLastConnectionTime(String key, long connectionTime) { setLastConnectionTime(key, connectionTime, false); } /** * 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 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; } // 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); } /** * Returns the connection time within which a connection from an allowed key is * automatically allowed without user interaction. */ public long getAllowedConnectionTime() { return Settings.Global.getLong(mContext.getContentResolver(), Settings.Global.ADB_ALLOWED_CONNECTION_TIME, Settings.Global.DEFAULT_ADB_ALLOWED_CONNECTION_TIME); } /** * 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 boolean isKeyAuthorized(String key) { // A system key is always authorized to connect. if (mSystemKeys.contains(key)) { return true; } 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; } } } }