From 79de06e6b80301fd336a339199014da82d45bbfc Mon Sep 17 00:00:00 2001 From: Guojing Yuan Date: Thu, 5 May 2022 22:11:31 +0000 Subject: [PATCH] [CDM perm sync] Send and receive messages integration Send message flow (will remain unchanged): 1. A companion app calls startSystemDataTransfer 2. SystemDataTransferProcessor creates the permission backup file and send it CompanionMessageProcessor 3. CompanionMessageProcessor paginate the message and send them to CompanionSecureCommsManager 4. CompanionSecureCommsManager encrypts the messages and send them to the app. This CL changes the logic to bind to the app if it's unbind. Previously CompanionSecureCommsManager would throw an exception if it's unbind. Receive message flow (newly integrated in this CL): 1. A companion app calls dispatchMessageToSystem 2. CompanionSecureCommsManager receives the message and decrypts it. 3. CompanionMessageProcessor receives the message via listener and may return the complete message to SystemDataTransferProcessor via listener. 4. SystemDataTransferProcessor processes the decrypted complete message. Bug: 231474219 Test: manually tested the sender and receiver flows. Change-Id: I0eb920103080fcb708614e636c6afec4fc69d92c --- .../CompanionApplicationController.java | 17 ++- .../CompanionDeviceManagerService.java | 10 +- .../CompanionDeviceServiceConnector.java | 11 +- .../CompanionDeviceShellCommand.java | 3 +- .../CompanionMessageProcessor.java | 103 ++++++++++++++---- .../SystemDataTransferProcessor.java | 62 +++++------ .../CompanionSecureCommunicationsManager.java | 44 +++++++- 7 files changed, 176 insertions(+), 74 deletions(-) diff --git a/services/companion/java/com/android/server/companion/CompanionApplicationController.java b/services/companion/java/com/android/server/companion/CompanionApplicationController.java index 943599cd83c73..0deb03923663e 100644 --- a/services/companion/java/com/android/server/companion/CompanionApplicationController.java +++ b/services/companion/java/com/android/server/companion/CompanionApplicationController.java @@ -103,7 +103,10 @@ public class CompanionApplicationController { mCompanionServicesRegister.invalidate(userId); } - void bindCompanionApplication(@UserIdInt int userId, @NonNull String packageName, + /** + * CDM binds to the companion app. + */ + public void bindCompanionApplication(@UserIdInt int userId, @NonNull String packageName, boolean bindImportant) { if (DEBUG) { Log.i(TAG, "bind() u" + userId + "/" + packageName @@ -143,7 +146,10 @@ public class CompanionApplicationController { } } - void unbindCompanionApplication(@UserIdInt int userId, @NonNull String packageName) { + /** + * CDM unbinds the companion app. + */ + public void unbindCompanionApplication(@UserIdInt int userId, @NonNull String packageName) { if (DEBUG) Log.i(TAG, "unbind() u" + userId + "/" + packageName); final List serviceConnectors; @@ -237,9 +243,9 @@ public class CompanionApplicationController { primaryServiceConnector.postOnDeviceDisappeared(association); } - /** Pass an encryped secure message to the companion application for transporting. */ + /** Pass an encrypted secure message to the companion application for transporting. */ public void dispatchMessage(@UserIdInt int userId, @NonNull String packageName, - int associationId, @NonNull byte[] message) { + int associationId, int messageId, @NonNull byte[] message) { if (DEBUG) { Log.i(TAG, "dispatchMessage() u" + userId + "/" + packageName + " associationId=" + associationId); @@ -256,7 +262,8 @@ public class CompanionApplicationController { return; } - primaryServiceConnector.postOnMessageDispatchedFromSystem(associationId, message); + primaryServiceConnector.postOnMessageDispatchedFromSystem(associationId, messageId, + message); } private void onPrimaryServiceBindingDied(@UserIdInt int userId, @NonNull String packageName) { diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java index fc3a12dd5fb74..ca4f76d8f4166 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java @@ -626,7 +626,15 @@ public class CompanionDeviceManagerService extends SystemService { + " message(Base64)=" + Base64.encodeToString(message, 0)); } - mSecureCommsManager.receiveSecureMessage(associationId, message); + AssociationInfo association = getAssociationWithCallerChecks(associationId); + if (association == null) { + throw new IllegalArgumentException("Association with ID " + associationId + " " + + "does not exist " + + "or belongs to a different package " + + "or belongs to a different user"); + } + + mSecureCommsManager.receiveSecureMessage(messageId, associationId, message); } @Override diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceServiceConnector.java b/services/companion/java/com/android/server/companion/CompanionDeviceServiceConnector.java index ef39b4b13f221..b9b29ffb4e867 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceServiceConnector.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceServiceConnector.java @@ -98,15 +98,10 @@ class CompanionDeviceServiceConnector extends ServiceConnector.Impl companionService.onDeviceDisappeared(associationInfo)); } - void postOnMessageDispatchedFromSystem(int associationId, @NonNull byte[] message) { - // We always use messageId 0 (at least for now). - // Unlike the message itself, the messageId is not encoded, which means that the CDM on the - // other (receiving) end CAN NOT and MUST NOT trust this messageId. - // If CDM needs to pass messageId around to the other side - it should embed it in the - // message body. + void postOnMessageDispatchedFromSystem(int associationId, int messageId, + @NonNull byte[] message) { post(companionService -> - companionService.onMessageDispatchedFromSystem( - /* messageId*/ 0, associationId, message)); + companionService.onMessageDispatchedFromSystem(messageId, associationId, message)); } /** diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java index c071bf640bd84..3d8e87ba63788 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java @@ -107,7 +107,8 @@ class CompanionDeviceShellCommand extends ShellCommand { message = sb.toString().getBytes(UTF_8); } - mSecureCommsManager.sendSecureMessage(associationId, message); + mSecureCommsManager.sendSecureMessage(associationId, /* messageId */ 0, + message); break; default: diff --git a/services/companion/java/com/android/server/companion/datatransfer/CompanionMessageProcessor.java b/services/companion/java/com/android/server/companion/datatransfer/CompanionMessageProcessor.java index 826bafa054411..e83e634ea859f 100644 --- a/services/companion/java/com/android/server/companion/datatransfer/CompanionMessageProcessor.java +++ b/services/companion/java/com/android/server/companion/datatransfer/CompanionMessageProcessor.java @@ -16,6 +16,8 @@ package com.android.server.companion.datatransfer; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.util.Slog; import android.util.proto.ProtoInputStream; import android.util.proto.ProtoOutputStream; @@ -39,6 +41,12 @@ public class CompanionMessageProcessor { private static final String LOG_TAG = CompanionMessageProcessor.class.getSimpleName(); + /** Listener for incoming complete messages. */ + interface Listener { + /** When a complete message is received from the companion app. */ + void onCompleteMessageReceived(@NonNull CompanionMessageInfo message); + } + // Rough size for each CompanionMessage, each message can exceed 50K for a little, but not // too much. Hard limit is 100K, WCS data processing limit. Closer to 100K, less stable at // the WCS data processing layer. Refer to @@ -48,6 +56,9 @@ public class CompanionMessageProcessor { private final CompanionSecureCommunicationsManager mSecureCommsManager; + @Nullable + private Listener mListener; + // Association id -> (parent id -> received messages) private final Map>> mAssociationsMessagesMap = new HashMap<>(); @@ -56,6 +67,11 @@ public class CompanionMessageProcessor { public CompanionMessageProcessor(CompanionSecureCommunicationsManager secureCommsManager) { mSecureCommsManager = secureCommsManager; + mSecureCommsManager.setListener(this::onDecryptedMessageReceived); + } + + public void setListener(@NonNull Listener listener) { + mListener = listener; } /** @@ -72,7 +88,8 @@ public class CompanionMessageProcessor { for (int i = 0; i < totalMessageCount; i++) { ProtoOutputStream proto = new ProtoOutputStream(); - proto.write(CompanionMessage.ID, parentMessageId + i + 1); + int messageId = parentMessageId + i + 1; + proto.write(CompanionMessage.ID, messageId); long paginationInfoToken = proto.start(CompanionMessage.PAGINATION_INFO); proto.write(CompanionMessage.PaginationInfo.PARENT_ID, parentMessageId); @@ -87,29 +104,68 @@ public class CompanionMessageProcessor { Slog.i(LOG_TAG, "Sending " + currentData.length + " bytes to " + packageName); - mSecureCommsManager.sendSecureMessage(associationId, proto.getBytes()); + mSecureCommsManager.sendSecureMessage(associationId, messageId, proto.getBytes()); } } /** - * Process message and store it. If all the messages with the same parent id have been received, - * return the message with combined message data. Otherwise, return null if there's still data - * parts missing. + * Process the message and store it. If all the messages with the same parent id have been + * received, return the message with combined message data. Otherwise, return null if there's + * still data parts missing. */ - public CompanionMessageInfo processMessage(int messageId, int associationId, byte[] message) { + public CompanionMessageInfo onDecryptedMessageReceived(int messageId, int associationId, + byte[] message) { ProtoInputStream proto = new ProtoInputStream(message); try { - int id = proto.readInt(CompanionMessage.ID); - if (id == messageId) { - // Read proto data - long paginationToken = proto.start(CompanionMessage.PAGINATION_INFO); - int parentId = proto.readInt(CompanionMessage.PaginationInfo.PARENT_ID); - int page = proto.readInt(CompanionMessage.PaginationInfo.PAGE); - int total = proto.readInt(CompanionMessage.PaginationInfo.TOTAL); - proto.end(paginationToken); - int type = proto.readInt(CompanionMessage.TYPE); - byte[] data = proto.readBytes(CompanionMessage.DATA); + int id = 0; + int parentId = 0; + int page = 0; + int total = 0; + int type = CompanionMessage.UNKNOWN; + byte[] data = null; + // Read proto data + while (proto.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (proto.getFieldNumber()) { + case (int) CompanionMessage.ID: + id = proto.readInt(CompanionMessage.ID); + break; + case (int) CompanionMessage.PAGINATION_INFO: + long paginationToken = proto.start(CompanionMessage.PAGINATION_INFO); + while (proto.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (proto.getFieldNumber()) { + case (int) CompanionMessage.PaginationInfo.PARENT_ID: + parentId = proto.readInt( + CompanionMessage.PaginationInfo.PARENT_ID); + break; + case (int) CompanionMessage.PaginationInfo.PAGE: + page = proto.readInt(CompanionMessage.PaginationInfo.PAGE); + break; + case (int) CompanionMessage.PaginationInfo.TOTAL: + total = proto.readInt(CompanionMessage.PaginationInfo.TOTAL); + break; + default: + Slog.e(LOG_TAG, "Unexpected field id " + + proto.getFieldNumber() + " for PaginationInfo."); + break; + } + } + proto.end(paginationToken); + break; + case (int) CompanionMessage.TYPE: + type = proto.readInt(CompanionMessage.TYPE); + break; + case (int) CompanionMessage.DATA: + data = proto.readBytes(CompanionMessage.DATA); + break; + default: + Slog.e(LOG_TAG, "Unexpected field id " + proto.getFieldNumber() + + " for CompanionMessage."); + break; + } + } + + if (id == messageId) { CompanionMessageInfo messageInfo = new CompanionMessageInfo(id, page, total, type, data); // Add the message into mAssociationsMessagesMap @@ -122,28 +178,35 @@ public class CompanionMessageProcessor { mAssociationsMessagesMap.put(associationId, associationMessages); // Check if all the messages with the same parentId are received. if (childMessages.size() == total) { + Slog.i(LOG_TAG, "All [" + total + "] messages are received. Processing."); + childMessages.sort(Comparator.comparing(CompanionMessageInfo::getPage)); ByteArrayOutputStream stream = new ByteArrayOutputStream(); for (int i = 0; i < childMessages.size(); i++) { stream.write(childMessages.get(i).getData()); } mAssociationsMessagesMap.remove(parentId); - return new CompanionMessageInfo(parentId, 0, total, type, stream.toByteArray()); + mListener.onCompleteMessageReceived( + new CompanionMessageInfo(parentId, 0, total, type, + stream.toByteArray())); + } else { + Slog.i(LOG_TAG, "[" + childMessages.size() + "/" + total + + "] messages are received for parentId [" + parentId + "]"); } } else { Slog.e(LOG_TAG, "Message id mismatch."); return null; } } catch (IOException e) { - Slog.e(LOG_TAG, "Can't read proto message id: " + messageId + ", message: " - + new String(message) + "."); + Slog.e(LOG_TAG, "Can't read proto from the message."); return null; } return null; } /** - * Find the next parent id. The parent and child ids are incremental. + * Find the next parent id from [1, Integer.MAX_VALUE]. + * The parent and child ids are incremental. */ private int findNextParentId(int associationId, int totalMessageCount) { int nextParentId = mNextParentId.getOrDefault(associationId, 1); diff --git a/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java b/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java index cafa78f7e2ab2..d39fa2acd978f 100644 --- a/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java +++ b/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java @@ -26,6 +26,7 @@ import static com.android.server.companion.Utils.prepareForIpc; import static java.nio.charset.StandardCharsets.UTF_8; +import android.annotation.NonNull; import android.annotation.UserIdInt; import android.app.PendingIntent; import android.companion.AssociationInfo; @@ -91,6 +92,7 @@ public class SystemDataTransferProcessor { mAssociationStore = associationStore; mSystemDataTransferRequestStore = systemDataTransferRequestStore; mCompanionMessageProcessor = companionMessageProcessor; + mCompanionMessageProcessor.setListener(this::onCompleteMessageReceived); } /** @@ -201,44 +203,36 @@ public class SystemDataTransferProcessor { } /** - * Process message reported by the companion app. + * Process a complete decrypted message reported by the companion app. */ - public void processMessage(String packageName, int userId, int associationId, - int messageId, byte[] message) { - Slog.i(LOG_TAG, "Start processing message [" + messageId + "] from package [" - + packageName + "] userId [" + userId + "] associationId [" + associationId + "]"); - - AssociationInfo association = mAssociationStore.getAssociationById(associationId); - association = PermissionsUtils.sanitizeWithCallerChecks(mContext, association); - if (association == null) { - throw new DeviceNotAssociatedException("Association " - + associationId + " is not associated with the app " + packageName - + " for user " + userId); + public void onCompleteMessageReceived(@NonNull CompanionMessageInfo completeMessage) { + switch (completeMessage.getType()) { + case CompanionMessage.PERMISSION_SYNC: + processPermissionSyncMessage(completeMessage); + break; + default: + Slog.e(LOG_TAG, "Unknown message type [" + completeMessage.getType() + + "]. Unable to process."); } + } - PermissionsUtils.enforceCallerIsSystemOr(userId, packageName); + private void processPermissionSyncMessage(CompanionMessageInfo messageInfo) { + Slog.i(LOG_TAG, "Applying permissions."); + // Start applying permissions + BackupHelper backupHelper = new BackupHelper(mContext, mContext.getUser()); + try { + XmlPullParser parser = Xml.newPullParser(); + ByteArrayInputStream stream = new ByteArrayInputStream( + messageInfo.getData()); + parser.setInput(stream, UTF_8.name()); - CompanionMessageInfo completeMessage = mCompanionMessageProcessor.processMessage(messageId, - associationId, message); - if (completeMessage != null) { - if (completeMessage.getType() == CompanionMessage.PERMISSION_SYNC) { - // Start applying permissions - BackupHelper backupHelper = new BackupHelper(mContext, UserHandle.of(userId)); - try { - XmlPullParser parser = Xml.newPullParser(); - ByteArrayInputStream stream = new ByteArrayInputStream( - completeMessage.getData()); - parser.setInput(stream, UTF_8.name()); - - backupHelper.restoreState(parser); - } catch (IOException e) { - Slog.e(LOG_TAG, "IOException reading message: " - + new String(completeMessage.getData())); - } catch (XmlPullParserException e) { - Slog.e(LOG_TAG, "Error parsing message: " - + new String(completeMessage.getData())); - } - } + backupHelper.restoreState(parser); + } catch (IOException e) { + Slog.e(LOG_TAG, "IOException reading message: " + + new String(messageInfo.getData())); + } catch (XmlPullParserException e) { + Slog.e(LOG_TAG, "Error parsing message: " + + new String(messageInfo.getData())); } } diff --git a/services/companion/java/com/android/server/companion/securechannel/CompanionSecureCommunicationsManager.java b/services/companion/java/com/android/server/companion/securechannel/CompanionSecureCommunicationsManager.java index 625360ebd417c..16a74ecd2068f 100644 --- a/services/companion/java/com/android/server/companion/securechannel/CompanionSecureCommunicationsManager.java +++ b/services/companion/java/com/android/server/companion/securechannel/CompanionSecureCommunicationsManager.java @@ -17,10 +17,12 @@ package com.android.server.companion.securechannel; import android.annotation.NonNull; +import android.annotation.Nullable; import android.annotation.SuppressLint; import android.companion.AssociationInfo; import android.util.Base64; import android.util.Log; +import android.util.Slog; import com.android.server.companion.AssociationStore; import com.android.server.companion.CompanionApplicationController; @@ -31,9 +33,18 @@ public class CompanionSecureCommunicationsManager { static final String TAG = "CompanionDevice_SecureComms"; static final boolean DEBUG = false; + /** Listener for incoming decrypted messages. */ + public interface Listener { + /** When an incoming message is decrypted. */ + void onDecryptedMessageReceived(int messageId, int associationId, byte[] message); + } + private final AssociationStore mAssociationStore; private final CompanionApplicationController mCompanionAppController; + @Nullable + private Listener mListener; + /** Constructor */ public CompanionSecureCommunicationsManager(AssociationStore associationStore, CompanionApplicationController companionApplicationController) { @@ -41,13 +52,18 @@ public class CompanionSecureCommunicationsManager { mCompanionAppController = companionApplicationController; } + public void setListener(@NonNull Listener listener) { + mListener = listener; + } + /** * Send a data to the associated companion device via secure channel (establishing one if * needed). * @param associationId associationId of the "recipient" companion device. + * @param messageId id of the message * @param message data to be sent securely. */ - public void sendSecureMessage(int associationId, @NonNull byte[] message) { + public void sendSecureMessage(int associationId, int messageId, @NonNull byte[] message) { if (DEBUG) { Log.d(TAG, "sendSecureMessage() associationId=" + associationId + "\n" + " message (Base64)=\"" + Base64.encodeToString(message, 0) + "\""); @@ -62,13 +78,28 @@ public class CompanionSecureCommunicationsManager { final int userId = association.getUserId(); final String packageName = association.getPackageName(); + + // Bind to the app if it hasn't been bound. if (!mCompanionAppController.isCompanionApplicationBound(userId, packageName)) { - throw new IllegalStateException("u" + userId + "\\" + packageName + " is NOT bound"); + Slog.d(TAG, "userId [" + userId + "] packageName [" + packageName + + "] is not bound. Binding it now to send a secure message."); + mCompanionAppController.bindCompanionApplication(userId, packageName, + association.isSelfManaged()); + + // TODO(b/202926196): implement: encrypt and pass on the companion application for + // transporting + mCompanionAppController.dispatchMessage(userId, packageName, associationId, messageId, + message); + + Slog.d(TAG, "Unbinding userId [" + userId + "] packageName [" + packageName + + "]"); + mCompanionAppController.unbindCompanionApplication(userId, packageName); } // TODO(b/202926196): implement: encrypt and pass on the companion application for // transporting - mCompanionAppController.dispatchMessage(userId, packageName, associationId, message); + mCompanionAppController.dispatchMessage(userId, packageName, associationId, messageId, + message); } /** @@ -76,12 +107,15 @@ public class CompanionSecureCommunicationsManager { * @param associationId associationId of the "sender" companion device. * @param encryptedMessage data. */ - public void receiveSecureMessage(int associationId, @NonNull byte[] encryptedMessage) { + public void receiveSecureMessage(int messageId, int associationId, + @NonNull byte[] encryptedMessage) { if (DEBUG) { Log.d(TAG, "sendSecureMessage() associationId=" + associationId + "\n" + " message (Base64)=\"" + Base64.encodeToString(encryptedMessage, 0) + "\""); } - // TODO(b/202926196): implement: decrypt and dispatch. + // TODO(b/202926196): implement: decrypt and dispatch + + mListener.onDecryptedMessageReceived(messageId, associationId, encryptedMessage); } }