blob: 4b48ef917744b32c0b809717e5e6e8a43f6c21e3 [file] [log] [blame]
/*
* 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<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);
}
/**
* 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<String> 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<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
* 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<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> 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.
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<String, Long> 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<Map.Entry<String, Long>> keyMapIterator = mKeyMap.entrySet().iterator();
while (keyMapIterator.hasNext()) {
Map.Entry<String, Long> 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<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;
}
/**
* 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;
}
}
}
}